scummvm/engines/vcruise/runtime_scriptexec.cpp
2025-01-24 08:23:54 +02:00

2614 lines
74 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#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<StackInt_t>(_direction)) ? 1 : 0));
}
void Runtime::scriptOpAngleGGet(ScriptArg_t arg) {
TAKE_STACK_INT(1);
if (stackArgs[0] < 0 || stackArgs[0] >= static_cast<StackInt_t>(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<uint32>(_roomNumber) << 16) | static_cast<uint32>(stackArgs[0]);
Common::HashMap<uint32, int32>::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<uint32>(_roomNumber) << 16) | static_cast<uint32>(stackArgs[1]);
_variables[varID] = stackArgs[0];
}
void Runtime::scriptOpVarAddAndStore(ScriptArg_t arg) {
TAKE_STACK_INT(2);
uint32 varID = (static_cast<uint32>(_roomNumber) << 16) | static_cast<uint32>(stackArgs[0]);
Common::HashMap<uint32, int32>::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<uint32>(stackArgs[0]);
Common::HashMap<uint32, int32>::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<uint32>(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<uint>(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<uint>(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<uint>(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<uint>(stackArgs[0]) >= _cursors.size())
error("Invalid cursor ID");
uint resolvedCursorID = stackArgs[0];
Common::HashMap<StackInt_t, uint>::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<uint32>(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<int32>(duration);
_musicVolumeRampStartTime = timestamp;
_musicVolumeRampStartVolume = _musicVolume;
_musicVolumeRampEnd = newVolume;
}
}
}
void Runtime::scriptOpParm0(ScriptArg_t arg) {
TAKE_STACK_INT(4);
if (stackArgs[0] < 0 || static_cast<uint>(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<uint>(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<uint>(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<uint>(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<uint>(sndParamArgs[0]) * 100u;
if (sndIDArgs[0].type == StackValue::kNumber && sndIDArgs[0].value.i == 0) {
// Apply to all sounds
for (const Common::SharedPtr<SoundInstance> &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<uint32>(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<char>((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<uint>(stackArgs[0])] = g_system->getMillis() + static_cast<uint32>(stackArgs[1]) * 1000u;
}
void Runtime::scriptOpGetTimer(ScriptArg_t arg) {
TAKE_STACK_INT(1);
bool isCompleted = true;
Common::HashMap<uint, uint32>::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<uint>(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<uint>(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<Script> function = _scriptSet->functions[arg];
if (function) {
CallStackFrame newFrame;
newFrame._script = function;
newFrame._nextInstruction = 0;
_scriptCallStack.push_back(newFrame);
_gameState = kGameStateScriptReset;
} else {
error("Unknown function '%s'", _scriptSet->functionNames[arg].c_str());
}
}
void Runtime::scriptOpReturn(ScriptArg_t arg) {
_scriptCallStack.pop_back();
_gameState = kGameStateScriptReset;
}
void Runtime::scriptOpSaveAs(ScriptArg_t arg) {
TAKE_STACK_INT(4);
// Just ignore this op, it looks like it's for save room remapping of some sort but we allow
// saves at any idle screen.
(void)stackArgs;
}
void Runtime::scriptOpSave0(ScriptArg_t arg) {
warning("save0 op not implemented");
}
void Runtime::scriptOpExit(ScriptArg_t arg) {
_isInGame = false;
_mostRecentlyRecordedSaveState.reset();
_mostRecentValidSaveState.reset();
if (_gameID == GID_REAH) {
_havePendingScreenChange = true;
_forceScreenChange = true;
_roomNumber = 40;
_screenNumber = 0xa1;
terminateScript();
changeMusicTrack(0);
if (_musicWavePlayer)
_musicWavePlayer->setVolumeAndBalance(applyVolumeScale(getDefaultSoundVolume()), 0);
} else {
error("Don't know what screen to go to on exit");
}
}
void Runtime::scriptOpNot(ScriptArg_t arg) {
TAKE_STACK_INT(1);
_scriptStack.push_back(StackValue((stackArgs[0] == 0) ? 1 : 0));
}
void Runtime::scriptOpAnd(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] != 0 && stackArgs[1] != 0) ? 1 : 0));
}
void Runtime::scriptOpOr(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] != 0 || stackArgs[1] != 0) ? 1 : 0));
}
void Runtime::scriptOpAdd(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(stackArgs[0] + stackArgs[1]));
}
void Runtime::scriptOpSub(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(stackArgs[0] - stackArgs[1]));
}
void Runtime::scriptOpNegate(ScriptArg_t arg) {
TAKE_STACK_INT(1);
_scriptStack.push_back(StackValue(-stackArgs[0]));
}
void Runtime::scriptOpCmpEq(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] == stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpCmpNE(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] != stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpCmpLt(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] < stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpCmpLE(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] <= stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpCmpGt(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] > stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpCmpGE(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] >= stackArgs[1]) ? 1 : 0));
}
void Runtime::scriptOpBitLoad(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue((stackArgs[0] >> stackArgs[1]) & 1));
}
void Runtime::scriptOpBitSet0(ScriptArg_t arg) {
TAKE_STACK_INT(2);
ScriptArg_t bitMask = static_cast<ScriptArg_t>(1) << stackArgs[1];
_scriptStack.push_back(StackValue(stackArgs[0] & ~bitMask));
}
void Runtime::scriptOpBitSet1(ScriptArg_t arg) {
TAKE_STACK_INT(2);
ScriptArg_t bitMask = static_cast<ScriptArg_t>(1) << stackArgs[1];
_scriptStack.push_back(StackValue(stackArgs[0] | bitMask));
}
void Runtime::scriptOpDisc1(ScriptArg_t arg) {
// Disc check, always pass
TAKE_STACK_INT(1);
(void)stackArgs;
_scriptStack.push_back(StackValue(1));
}
void Runtime::scriptOpDisc2(ScriptArg_t arg) {
// Disc check, always pass
TAKE_STACK_INT(2);
(void)stackArgs;
_scriptStack.push_back(StackValue(1));
}
void Runtime::scriptOpDisc3(ScriptArg_t arg) {
// Disc check, always pass
TAKE_STACK_INT(3);
(void)stackArgs;
_scriptStack.push_back(StackValue(1));
}
void Runtime::scriptOpGoto(ScriptArg_t arg) {
TAKE_STACK_INT(1);
uint newInteraction = static_cast<uint>(stackArgs[0]);
Common::SharedPtr<Script> newScript = nullptr;
if (_scriptSet) {
RoomScriptSet *roomScriptSet = getRoomScriptSetForCurrentRoom();
if (roomScriptSet) {
const ScreenScriptSetMap_t &screenScriptsMap = roomScriptSet->screenScripts;
ScreenScriptSetMap_t::const_iterator screenScriptIt = screenScriptsMap.find(_screenNumber);
if (screenScriptIt != screenScriptsMap.end()) {
const ScreenScriptSet &screenScriptSet = *screenScriptIt->_value;
ScriptMap_t::const_iterator interactionScriptIt = screenScriptSet.interactionScripts.find(newInteraction);
if (interactionScriptIt != screenScriptSet.interactionScripts.end())
newScript = interactionScriptIt->_value;
}
}
}
if (newScript) {
// This only happens in Reah so we don't have to worry about what to do about frames on the callstack in Schizm
_gameState = kGameStateScriptReset;
CallStackFrame frame;
frame._script = newScript;
frame._nextInstruction = 0;
_scriptCallStack.resize(1);
_scriptCallStack[0] = frame;
} else {
error("Goto target %u couldn't be resolved", newInteraction);
}
}
void Runtime::scriptOpEscOn(ScriptArg_t arg) {
TAKE_STACK_INT(1);
_escOn = (stackArgs[0] != 0);
}
void Runtime::scriptOpEscOff(ScriptArg_t arg) {
_escOn = false;
}
void Runtime::scriptOpEscGet(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_scriptEnv.esc ? 1 : 0));
_scriptEnv.esc = false;
}
void Runtime::scriptOpBackStart(ScriptArg_t arg) {
_scriptEnv.exitToMenu = true;
}
void Runtime::scriptOpAllowSaves(ScriptArg_t arg) {
_forceAllowSaves = true;
}
void Runtime::scriptOpAnimName(ScriptArg_t arg) {
if (_roomNumber >= _roomDefs.size())
error("Can't resolve animation for room, room number was invalid");
Common::String &animName = _scriptSet->strings[arg];
// In Reah, animations are mapped to rooms.
//
// In Schizm this can get very complicated: It supports overlapping room logics which in some cases
// have animation ranges mapped to a different animation.
//
// For example, in Schizm, rooms 25-28 all share one logic file and their corresponding animations are
// largely duplicates of each other with different skies.
//
// It appears that the animation to select is based on the remapped room if the animation can't be
// found in its primary room.
//
// For example, PRZYCUMIE_KRZESELKO is mapped to an animation range in room 25 and another range in
// room 26, and there is no mapping for room 28. In this case, the animation frame range from room 25
// is used, but it is remapped to animation 28.
Common::SharedPtr<RoomDef> roomDef = _roomDefs[_roomNumber];
if (roomDef) {
Common::HashMap<Common::String, AnimationDef>::const_iterator it = roomDef->animations.find(animName);
if (it != roomDef->animations.end()) {
pushAnimDef(it->_value);
return;
}
}
if (_roomNumber < _roomDuplicationOffsets.size() && _roomDuplicationOffsets[_roomNumber] != 0) {
int roomToUse = _roomNumber - _roomDuplicationOffsets[_roomNumber];
roomDef = _roomDefs[roomToUse];
Common::HashMap<Common::String, AnimationDef>::const_iterator it = roomDef->animations.find(animName);
if (it != roomDef->animations.end()) {
AnimationDef animDef = it->_value;
if (animDef.animNum == roomToUse)
animDef.animNum = _roomNumber;
else if (animDef.animNum == -roomToUse)
animDef.animNum = -static_cast<int>(_roomNumber);
pushAnimDef(animDef);
return;
}
}
error("Can't resolve animation for room, couldn't find animation '%s'", animName.c_str());
}
void Runtime::scriptOpValueName(ScriptArg_t arg) {
if (_roomNumber >= _roomDefs.size())
error("Invalid room number for var name op");
const RoomDef *roomDef = _roomDefs[_roomNumber].get();
if (!roomDef)
error("Room def doesn't exist");
const Common::String &varName = _scriptSet->strings[arg];
Common::HashMap<Common::String, int>::const_iterator it = roomDef->values.find(varName);
if (it == roomDef->values.end())
error("Value '%s' doesn't exist in room %i", varName.c_str(), static_cast<int>(_roomNumber));
_scriptStack.push_back(StackValue(it->_value));
}
void Runtime::scriptOpVarName(ScriptArg_t arg) {
if (_roomNumber >= _roomDefs.size())
error("Invalid room number for var name op");
const RoomDef *roomDef = _roomDefs[_roomNumber].get();
if (!roomDef)
error("Room def doesn't exist");
const Common::String &varName = _scriptSet->strings[arg];
Common::HashMap<Common::String, uint>::const_iterator it = roomDef->vars.find(varName);
if (it == roomDef->vars.end())
error("Var '%s' doesn't exist in room %i", varName.c_str(), static_cast<int>(_roomNumber));
_scriptStack.push_back(StackValue(it->_value));
}
void Runtime::scriptOpSoundName(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
}
void Runtime::scriptOpCursorName(ScriptArg_t arg) {
const Common::String &cursorName = _scriptSet->strings[arg];
Common::HashMap<Common::String, StackInt_t>::const_iterator namedCursorIt = _namedCursors.find(cursorName);
if (namedCursorIt == _namedCursors.end()) {
error("Unimplemented cursor name '%s'", cursorName.c_str());
return;
}
_scriptStack.push_back(StackValue(namedCursorIt->_value));
}
void Runtime::scriptOpDubbing(ScriptArg_t arg) {
warning("Dubbing op not implemented");
}
void Runtime::scriptOpCheckValue(ScriptArg_t arg) {
PEEK_STACK(1);
if (stackArgs[0].type == StackValue::kNumber && stackArgs[0].value.i == arg)
_scriptStack.pop_back();
else
_scriptCallStack.back()._nextInstruction++;
}
void Runtime::scriptOpJump(ScriptArg_t arg) {
_scriptCallStack.back()._nextInstruction = arg;
}
void Runtime::scriptOpMusicStop(ScriptArg_t arg) {
_musicWavePlayer.reset();
if (_musicMidiPlayer) {
Common::StackLock lock(_midiPlayerMutex);
_musicMidiPlayer.reset();
}
_musicActive = false;
}
void Runtime::scriptOpMusicPlayScore(ScriptArg_t arg) {
TAKE_STACK_STR(2);
_scoreTrack = stackArgs[0];
_scoreSection = stackArgs[1];
_musicActive = true;
startScoreSection();
}
void Runtime::scriptOpScoreAlways(ScriptArg_t arg) {
assert(_gameID == GID_SCHIZM);
_musicMuteDisabled = true;
// We don't call startScoreSection here because ScoreAlways is always followed by a PlayScore
// that triggers the actual music, and we don't want to play any amount of the score that's about
// to be disabled. PlayScore will call startScoreSection after changing to the correct section.
}
void Runtime::scriptOpScoreNormal(ScriptArg_t arg) {
_musicMuteDisabled = false;
if (_musicMute) {
_musicWavePlayer.reset();
if (_musicMidiPlayer) {
Common::StackLock lock(_midiPlayerMutex);
_musicMidiPlayer.reset();
}
_scoreSectionEndTime = 0;
}
}
void Runtime::scriptOpSndPlay(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(kSoundLoopBehaviorAuto, *cachedSound, getSilentSoundVolume(), 0, false, false);
}
void Runtime::scriptOpSndPlayEx(ScriptArg_t arg) {
TAKE_STACK_INT_NAMED(2, sndParamArgs);
TAKE_STACK_VAR_NAMED(1, sndNameArgs);
Common::String soundName;
if (sndNameArgs[0].type == StackValue::kString)
soundName = sndNameArgs[0].value.s;
else if (sndNameArgs[0].type == StackValue::kNumber) {
// Sometimes the name is a string, such as the bell puzzle in the temple.
// In this case the number is the name, with no suffix.
soundName = Common::String::format("%i", static_cast<int>(sndNameArgs[0].value.i));
} else
error("Invalid sound name type for SndPlayEx");
StackInt_t soundID = 0;
SoundInstance *cachedSound = nullptr;
resolveSoundByName(soundName, true, soundID, cachedSound);
if (cachedSound)
triggerSound(kSoundLoopBehaviorAuto, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
}
void Runtime::scriptOpSndPlay3D(ScriptArg_t arg) {
TAKE_STACK_INT_NAMED(5, sndParamArgs);
TAKE_STACK_STR_NAMED(1, sndNameArgs);
StackInt_t soundID = 0;
SoundInstance *cachedSound = nullptr;
resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
SoundParams3D sndParams;
sndParams.minRange = sndParamArgs[2];
sndParams.maxRange = sndParamArgs[3];
sndParams.unknownRange = sndParamArgs[4]; // Doesn't appear to be the same thing as Reah. Usually 1000, sometimes 2000 or 3000.
if (cachedSound) {
setSound3DParameters(*cachedSound, sndParamArgs[0], sndParamArgs[1], sndParams);
triggerSound(kSoundLoopBehaviorAuto, *cachedSound, getSilentSoundVolume(), 0, true, false);
}
}
void Runtime::scriptOpSndPlaying(ScriptArg_t arg) {
TAKE_STACK_INT(1);
SoundInstance *snd = resolveSoundByID(stackArgs[0]);
if (!snd || !snd->cache) {
_scriptStack.push_back(StackValue(0));
return;
}
if (snd->cache->isLoopActive) {
_scriptStack.push_back(StackValue(1));
return;
}
bool hasEnded = (snd->endTime < g_system->getMillis());
_scriptStack.push_back(StackValue(hasEnded ? 1 : 0));
}
void Runtime::scriptOpSndWait(ScriptArg_t arg) {
TAKE_STACK_INT(1);
SoundInstance *snd = resolveSoundByID(stackArgs[0]);
if (snd) {
_delayCompletionTime = snd->endTime;
_gameState = kGameStateDelay;
}
}
void Runtime::scriptOpSndHalt(ScriptArg_t arg) {
TAKE_STACK_INT(1);
SoundInstance *snd = resolveSoundByID(stackArgs[0]);
if (snd) {
convertLoopingSoundToNonLooping(*snd);
_delayCompletionTime = snd->endTime;
_gameState = kGameStateDelay;
}
}
void Runtime::scriptOpSndToBack(ScriptArg_t arg) {
recordSounds(*_altState);
}
void Runtime::scriptOpSndStop(ScriptArg_t arg) {
TAKE_STACK_INT(1);
SoundInstance *cachedSound = resolveSoundByID(stackArgs[0]);
if (cachedSound)
stopSound(*cachedSound);
}
void Runtime::scriptOpSndStopAll(ScriptArg_t arg) {
for (const Common::SharedPtr<SoundInstance> &snd : _activeSounds)
stopSound(*snd);
}
void Runtime::scriptOpVolumeAdd(ScriptArg_t arg) {
TAKE_STACK_INT(3);
SoundInstance *cachedSound = resolveSoundByID(static_cast<uint>(stackArgs[0]));
if (cachedSound)
triggerSoundRamp(*cachedSound, stackArgs[1] * 100, cachedSound->volume + stackArgs[2], false);
}
void Runtime::scriptOpVolumeChange(ScriptArg_t arg) {
TAKE_STACK_INT(3);
SoundInstance *cachedSound = resolveSoundByID(static_cast<uint>(stackArgs[0]));
if (cachedSound)
triggerSoundRamp(*cachedSound, stackArgs[1] * 100, stackArgs[2], false);
}
void Runtime::scriptOpAnimVolume(ScriptArg_t arg) {
TAKE_STACK_INT(1);
_animVolume = stackArgs[0];
applyAnimationVolume();
}
void Runtime::scriptOpAnimChange(ScriptArg_t arg) {
TAKE_STACK_INT(2);
if (stackArgs[1] == 0)
error("animChange frame count shouldn't be zero");
_scriptEnv.animChangeSet = true;
_scriptEnv.animChangeFrameOffset = stackArgs[0];
_scriptEnv.animChangeNumFrames = stackArgs[1] - 1;
}
void Runtime::scriptOpScreenName(ScriptArg_t arg) {
const Common::String &scrName = _scriptSet->strings[arg];
uint roomNumber = 0;
if (_gameID == GID_SCHIZM) {
error("Screen numbers should be preprocessed in Schizm");
// ... absent that error, we would do roomNumber = _loadedRoomNumber probably.
// See the comment in optimizeScriptSet for explanation.
}
if (roomNumber < _roomDuplicationOffsets.size())
roomNumber -= _roomDuplicationOffsets[roomNumber];
RoomToScreenNameToRoomMap_t::const_iterator roomIt = _globalRoomScreenNameToScreenIDs.find(roomNumber);
if (roomIt != _globalRoomScreenNameToScreenIDs.end()) {
ScreenNameToRoomMap_t::const_iterator screenIt = roomIt->_value.find(scrName);
if (screenIt != roomIt->_value.end()) {
_scriptStack.push_back(StackValue(static_cast<StackInt_t>(screenIt->_value)));
return;
}
}
error("Couldn't resolve screen name '%s'", scrName.c_str());
}
void Runtime::scriptOpExtractByte(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(static_cast<StackInt_t>((stackArgs[0] >> (stackArgs[1] * 8) & 0xff))));
}
void Runtime::scriptOpInsertByte(ScriptArg_t arg) {
TAKE_STACK_INT(3);
StackInt_t value = stackArgs[0];
StackInt_t valueToInsert = (stackArgs[1] & 0xff);
int bytePos = stackArgs[2];
StackInt_t mask = static_cast<StackInt_t>(0xff) << (bytePos * 8);
value -= (value & mask);
value += (valueToInsert << (bytePos * 8));
_scriptStack.push_back(StackValue(value));
}
void Runtime::scriptOpString(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
}
void Runtime::scriptOpSpeechEx(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];
if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[1], 0, false, true);
_triggeredOneShots.push_back(oneShot);
triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
}
}
}
void Runtime::scriptOpSpeechTest(ScriptArg_t arg) {
TAKE_STACK_INT(1);
bool found = false;
for (const TriggeredOneShot &oneShot : _triggeredOneShots) {
if (oneShot.soundID == static_cast<uint>(stackArgs[0])) {
found = true;
break;
}
}
_scriptStack.push_back(StackValue(found ? 1 : 0));
}
void Runtime::scriptOpRandomInclusive(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])));
}
void Runtime::scriptOpHeroOut(ScriptArg_t arg) {
TAKE_STACK_INT(3);
_swapOutRoom = stackArgs[0];
_swapOutScreen = stackArgs[1];
_swapOutDirection = stackArgs[2];
}
void Runtime::scriptOpHeroGetPos(ScriptArg_t arg) {
TAKE_STACK_INT(1);
bool thisHero = false;
switch (stackArgs[0]) {
case 0:
thisHero = (_hero == 0);
break;
case 1:
thisHero = (_hero == 1);
break;
case 2:
thisHero = false;
break;
default:
error("Unhandled heroGetPos argument %i", static_cast<int>(stackArgs[0]));
return;
}
uint roomNumber = thisHero ? _roomNumber : _altState->roomNumber;
uint screenNumber = thisHero ? _screenNumber : _altState->screenNumber;
uint direction = thisHero ? _direction : _altState->direction;
uint combined = (roomNumber << 16) | (screenNumber << 8) | direction;
_scriptStack.push_back(StackValue(static_cast<StackInt_t>(combined)));
}
void Runtime::scriptOpHeroSetPos(ScriptArg_t arg) {
TAKE_STACK_INT(2);
bool thisHero = false;
switch (stackArgs[0]) {
case 0:
thisHero = (_hero == 0);
break;
case 1:
thisHero = (_hero == 1);
break;
case 2:
thisHero = false;
break;
default:
error("Unhandled heroSetPos argument %i", static_cast<int>(stackArgs[0]));
return;
}
if (thisHero) {
error("heroSetPos for the current hero isn't supported (and Schizm's game scripts shouldn't be doing it).");
return;
}
_altState->roomNumber = (stackArgs[1] >> 16) & 0xff;
_altState->screenNumber = (stackArgs[1] >> 8) & 0xff;
_altState->direction = stackArgs[1] & 0xff;
_altState->havePendingPostSwapScreenReset = true;
}
void Runtime::scriptOpHeroGet(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_hero));
}
void Runtime::scriptOpGetRoom(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_roomNumber));
}
void Runtime::scriptOpBitAnd(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(stackArgs[0] & stackArgs[1]));
}
void Runtime::scriptOpBitOr(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(stackArgs[0] | stackArgs[1]));
}
void Runtime::scriptOpAngleGet(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_direction));
}
void Runtime::scriptOpIsDVDVersion(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_isCDVariant ? 0 : 1));
}
void Runtime::scriptOpIsCDVersion(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_isCDVariant ? 1 : 0));
}
void Runtime::scriptOpDisc(ScriptArg_t arg) {
TAKE_STACK_INT(1);
(void)stackArgs;
// Always pass correct disc checks
_scriptStack.push_back(StackValue(1));
}
void Runtime::scriptOpHidePanel(ScriptArg_t arg) {
_isInGame = false;
clearTray();
}
void Runtime::scriptOpRotateUpdate(ScriptArg_t arg) {
warning("RotateUpdate op not implemented yet");
}
void Runtime::scriptOpMul(ScriptArg_t arg) {
TAKE_STACK_INT(2);
_scriptStack.push_back(StackValue(stackArgs[0] * stackArgs[1]));
}
void Runtime::scriptOpDiv(ScriptArg_t arg) {
TAKE_STACK_INT(2);
if (stackArgs[1] == 0) {
error("Division by zero");
return;
}
_scriptStack.push_back(StackValue(stackArgs[0] / stackArgs[1]));
}
void Runtime::scriptOpMod(ScriptArg_t arg) {
TAKE_STACK_INT(2);
if (stackArgs[1] == 0) {
error("Division by zero");
return;
}
_scriptStack.push_back(StackValue(stackArgs[0] % stackArgs[1]));
}
void Runtime::scriptOpGetDigit(ScriptArg_t arg) {
TAKE_STACK_INT(2);
StackInt_t digit = (stackArgs[0] >> (stackArgs[1] * 4)) & 0xf;
_scriptStack.push_back(StackValue(digit));
}
void Runtime::scriptOpPuzzleInit(ScriptArg_t arg) {
TAKE_STACK_INT(kAnimDefStackArgs * 2 + 3);
AnimationDef animDef1 = stackArgsToAnimDef(stackArgs + 0);
AnimationDef animDef2 = stackArgsToAnimDef(stackArgs + kAnimDefStackArgs);
int firstMover = stackArgs[kAnimDefStackArgs * 2 + 0];
int firstMover2 = stackArgs[kAnimDefStackArgs * 2 + 1];
int unknownParam = stackArgs[kAnimDefStackArgs * 2 + 2];
if (firstMover != firstMover2 || unknownParam != 0)
error("PuzzleInit had a weird parameter");
clearCircuitPuzzle();
_circuitPuzzle.reset(new CircuitPuzzle(firstMover));
_circuitPuzzleConnectAnimation = animDef1;
_circuitPuzzleBlockAnimation = animDef2;
_idleIsOnOpenCircuitPuzzleLink = false;
_scriptEnv.puzzleWasSet = true;
if (firstMover == 2)
scriptOpPuzzleDoMove2(0);
}
void Runtime::scriptOpPuzzleWhoWon(ScriptArg_t arg) {
StackInt_t winner = 2;
if (_circuitPuzzle) {
switch (_circuitPuzzle->checkConclusion()) {
case CircuitPuzzle::kConclusionNone:
winner = 0;
break;
case CircuitPuzzle::kConclusionPlayerWon:
winner = 1;
break;
case CircuitPuzzle::kConclusionPlayerLost:
winner = 2;
// Clear puzzle so circuit highlights stop appearing
_circuitPuzzle.reset();
break;
default:
error("Unhandled puzzle conclusion");
break;
}
}
_scriptStack.push_back(StackValue(winner));
}
void Runtime::scriptOpPuzzleCanPress(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_idleIsOnOpenCircuitPuzzleLink ? 1 : 0));
}
void Runtime::scriptOpPuzzleDoMove1(ScriptArg_t arg) {
if (!_idleIsOnOpenCircuitPuzzleLink)
error("Attempted puzzleDoMove1 but don't have a circuit point");
if (!_circuitPuzzle)
error("Attempted puzzleDoMove1 but the circuit puzzle is gone");
_circuitPuzzle->addLink(_idleCircuitPuzzleCoord, _idleIsCircuitPuzzleLinkDown ? CircuitPuzzle::kCellDirectionDown : CircuitPuzzle::kCellDirectionRight);
SoundInstance *snd = nullptr;
StackInt_t soundID = 0;
resolveSoundByName("85_connect", true, soundID, snd);
if (snd)
triggerSound(kSoundLoopBehaviorNo, *snd, 0, 0, false, false);
const CircuitPuzzle::CellRectSpec *rectSpec = _circuitPuzzle->getCellRectSpec(_idleCircuitPuzzleCoord);
if (rectSpec) {
AnimationDef animDef = _circuitPuzzleConnectAnimation;
animDef.constraintRect = _idleIsCircuitPuzzleLinkDown ? rectSpec->_downLinkRect : rectSpec->_rightLinkRect;
changeAnimation(animDef, false);
_gameState = kGameStateWaitingForAnimation;
}
clearCircuitHighlightRect(_idleCircuitPuzzleLinkHighlightRect);
_idleIsOnOpenCircuitPuzzleLink = false;
changeToCursor(_cursors[kCursorArrow]);
// Return to idle so the mouse changes to the correct cursor if it's moved over a new link while waiting for the animation
_havePendingReturnToIdleState = true;
}
void Runtime::scriptOpPuzzleDoMove2(ScriptArg_t arg) {
if (!_circuitPuzzle)
error("Attempted puzzleDoMove2 but the circuit puzzle is gone");
CircuitPuzzle::CellDirection actionDirection = CircuitPuzzle::kCellDirectionDown;
Common::Point actionCoord;
if (_circuitPuzzle->executeAIAction(*_rng, actionCoord, actionDirection)) {
SoundInstance *snd = nullptr;
StackInt_t soundID = 0;
resolveSoundByName("85_block", true, soundID, snd);
if (snd)
triggerSound(kSoundLoopBehaviorNo, *snd, 0, 0, false, false);
const CircuitPuzzle::CellRectSpec *rectSpec = _circuitPuzzle->getCellRectSpec(actionCoord);
if (rectSpec) {
AnimationDef animDef = _circuitPuzzleBlockAnimation;
animDef.constraintRect = (actionDirection == CircuitPuzzle::kCellDirectionDown) ? rectSpec->_downBarrierRect : rectSpec->_rightBarrierRect;
changeAnimation(animDef, false);
_gameState = kGameStateWaitingForAnimation;
}
}
}
void Runtime::scriptOpPuzzleDone(ScriptArg_t arg) {
_circuitPuzzle.reset();
}
// AD2044 ops
void Runtime::scriptOpAnimT(ScriptArg_t arg) {
TAKE_STACK_INT(1);
StackInt_t animationID = stackArgs[0];
Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _currentRoomAnimIDToFrameRange.find(animationID);
if (animRangeIt == _currentRoomAnimIDToFrameRange.end())
error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
AnimationDef animDef;
animDef.animNum = animRangeIt->_value.animationNum;
animDef.firstFrame = animRangeIt->_value.firstFrame;
animDef.lastFrame = animRangeIt->_value.lastFrame;
_keepStaticAnimationInIdle = true;
_haveIdleAnimations[0] = true;
StaticAnimation &outAnim = _idleAnimations[0];
outAnim = StaticAnimation();
outAnim.animDefs[0] = animDef;
outAnim.animDefs[1] = animDef;
}
void Runtime::scriptOpAnimAD2044(bool isForward) {
TAKE_STACK_INT(2);
int16 animationID = 0;
bool found = false;
for (const AD2044AnimationDef &def : _ad2044AnimationDefs) {
if (def.roomID == _roomNumber && static_cast<StackInt_t>(def.lookupID) == stackArgs[0]) {
animationID = isForward ? def.fwdAnimationID : def.revAnimationID;
found = true;
break;
}
}
if (!found)
error("Couldn't resolve animation lookup ID %i", static_cast<int>(stackArgs[0]));
Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _currentRoomAnimIDToFrameRange.find(animationID);
if (animRangeIt == _currentRoomAnimIDToFrameRange.end())
error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
AnimationDef animDef;
animDef.animNum = animRangeIt->_value.animationNum;
animDef.firstFrame = animRangeIt->_value.firstFrame;
animDef.lastFrame = animRangeIt->_value.lastFrame;
for (const AD2044UnusualAnimationRules &unusualAnimRule : g_unusualAnimationRules) {
if (static_cast<StackInt_t>(unusualAnimRule.animLookupID) == stackArgs[0] && unusualAnimRule.interactionID == _scriptEnv.clickInteractionID && unusualAnimRule.roomNumber == _roomNumber && unusualAnimRule.screenNumber == _screenNumber) {
switch (unusualAnimRule.ruleType) {
case AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly:
animDef.lastFrame = animDef.firstFrame;
break;
default:
error("Unknown unusual animation rule");
}
break;
}
}
changeAnimation(animDef, animDef.firstFrame, true, _animSpeedDefault);
_gameState = kGameStateWaitingForAnimation;
_screenNumber = stackArgs[1];
_havePendingScreenChange = true;
clearIdleAnimations();
if (_loadedAnimationHasSound)
changeToCursor(nullptr);
else {
changeToCursor(_cursors[kCursorWait]);
}
}
void Runtime::scriptOpAnimForward(ScriptArg_t arg) {
scriptOpAnimAD2044(true);
}
void Runtime::scriptOpAnimReverse(ScriptArg_t arg) {
scriptOpAnimAD2044(false);
}
OPCODE_STUB(AnimKForward)
void Runtime::scriptOpNoUpdate(ScriptArg_t arg) {
}
void Runtime::scriptOpNoClear(ScriptArg_t arg) {
}
void Runtime::scriptOpSayCycle_AD2044(const StackInt_t *values, uint numValues) {
// Checking the scripts, there don't appear to be any cycles that can't be tracked from
// the first value, so just use that.
uint &cyclePosRef = _sayCycles[static_cast<uint32>(values[0])];
Common::String soundName = Common::String::format("%02i-%08i", static_cast<int>(_disc * 10u + 1u), static_cast<int>(values[cyclePosRef]));
cyclePosRef = (cyclePosRef + 1u) % numValues;
StackInt_t soundID = 0;
SoundInstance *cachedSound = nullptr;
resolveSoundByName(soundName, true, soundID, cachedSound);
if (cachedSound) {
TriggeredOneShot oneShot;
oneShot.soundID = soundID;
oneShot.uniqueSlot = _disc;
triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
}
}
void Runtime::scriptOpSay2K(ScriptArg_t arg) {
TAKE_STACK_INT(2);
scriptOpSayCycle_AD2044(stackArgs, 2);
}
void Runtime::scriptOpSay3K(ScriptArg_t arg) {
TAKE_STACK_INT(3);
scriptOpSayCycle_AD2044(stackArgs, 3);
}
void Runtime::scriptOpSay1_AD2044(ScriptArg_t arg) {
TAKE_STACK_INT(1);
Common::String soundName = Common::String::format("%02i-%08i", static_cast<int>(_disc * 10u + 1u), static_cast<int>(stackArgs[0]));
StackInt_t soundID = 0;
SoundInstance *cachedSound = nullptr;
resolveSoundByName(soundName, true, soundID, cachedSound);
if (cachedSound) {
TriggeredOneShot oneShot;
oneShot.soundID = soundID;
oneShot.uniqueSlot = _disc;
if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
_triggeredOneShots.push_back(oneShot);
}
}
}
void Runtime::scriptOpSay1Rnd(ScriptArg_t arg) {
scriptOpSay1_AD2044(arg);
}
void Runtime::scriptOpSay2_AD2044(ScriptArg_t arg) {
scriptOpSay1_AD2044(arg);
}
void Runtime::scriptOpM(ScriptArg_t arg) {
// Looks like this is possibly support to present a mouse click prompt and end
// with the #EM instruction, but so far as best I can tell, it just stops
// execution.
scriptOpLMB(arg);
}
void Runtime::scriptOpEM(ScriptArg_t arg) {
}
void Runtime::scriptOpSE(ScriptArg_t arg) {
// English subtitle
if (_language == Common::PL_POL)
return;
_subtitleText = _scriptSet->strings[arg];
}
void Runtime::scriptOpSDot(ScriptArg_t arg) {
// Polish subtitle
if (_language == Common::PL_POL)
return;
_subtitleText = _scriptSet->strings[arg];
}
void Runtime::scriptOpE(ScriptArg_t arg) {
if (_language == Common::PL_POL)
return;
_tooltipText = _scriptSet->strings[arg];
redrawSubtitleSection();
}
void Runtime::scriptOpDot(ScriptArg_t arg) {
if (_language != Common::PL_POL)
return;
_tooltipText = _scriptSet->strings[arg];
redrawSubtitleSection();
}
void Runtime::scriptOpSound(ScriptArg_t arg) {
TAKE_STACK_INT(2);
debug(1, "TODO: %s() stackArgs[0]:%d stackArgs[1]:%d", __FUNCTION__, stackArgs[0], stackArgs[1]);
}
void Runtime::scriptOpISound(ScriptArg_t arg) {
TAKE_STACK_INT(2);
debug(1, "TODO: %s() stackArgs[0]:%d stackArgs[1]:%d", __FUNCTION__, stackArgs[0], stackArgs[1]);
}
OPCODE_STUB(USound)
void Runtime::scriptOpRGet(ScriptArg_t arg) {
StackInt_t itemID = 0x2000;
if (_inventoryActiveItem.itemID < kNumAD2044Items) {
itemID = g_ad2044ItemInfos[_inventoryActiveItem.itemID].scriptItemID;
if (itemID == 0 && _inventoryActiveItem.itemID != 0) {
warning("No script item ID for item type %i", static_cast<int>(_inventoryActiveItem.itemID));
itemID = 0x2000;
}
} else
error("Invalid item ID");
_scriptStack.push_back(StackValue(itemID));
}
void Runtime::scriptOpRSet(ScriptArg_t arg) {
TAKE_STACK_INT(1);
for (uint itemID = 0; itemID < kNumAD2044Items; itemID++) {
if (static_cast<StackInt_t>(g_ad2044ItemInfos[itemID].scriptItemID) == stackArgs[0]) {
if (_inventoryActiveItem.itemID != itemID) {
Common::String itemFileName;
Common::String alphaFileName;
_inventoryActiveItem.itemID = itemID;
getFileNamesForItemGraphic(itemID, itemFileName, alphaFileName);
_inventoryActiveItem.graphic = loadGraphic(itemFileName, false);
_inventoryActiveItem.mask = loadGraphic(alphaFileName, false);
clearActiveItemGraphic();
drawActiveItemGraphic();
}
return;
}
}
error("Couldn't resolve item ID for script item 0x%x", static_cast<int>(stackArgs[0]));
}
void Runtime::scriptOpEndRSet(ScriptArg_t arg) {
scriptOpRSet(arg);
returnFromExaminingItem();
}
void Runtime::scriptOpStop(ScriptArg_t arg) {
terminateScript();
}
// Unused Schizm ops
// Only used in fnRandomBirds and fnRandomMachines in Room 60, both of which are unused
OPCODE_STUB(SndAddRandom)
OPCODE_STUB(SndClearRandom)
// Only used in Room 02 (cheat room, which isn't supported)
OPCODE_STUB(Speech)
OPCODE_STUB(Say)
OPCODE_STUB(Garbage)
// Referenced in Room 30 screen 0a4 interaction 0a0, however there is no interaction with that ID,
// so this is unreachable.
OPCODE_STUB(Fn)
#undef TAKE_STACK_STR
#undef TAKE_STACK_STR_NAMED
#undef TAKE_STACK_INT
#undef TAKE_STACK_INT_NAMED
#undef TAKE_STACK_VAR
#undef TAKE_STACK_VAR_NAMED
#undef PEEK_STACK
#undef OPCODE_STUB
#ifdef DISPATCH_OP
#error "DISPATCH_OP already defined"
#endif
#define DISPATCH_OP(op) \
case ScriptOps::k##op: \
this->scriptOp##op(arg); \
break
bool Runtime::runScript() {
if (_scriptCallStack.empty()) {
terminateScript();
return true;
}
CallStackFrame &frame = _scriptCallStack.back();
const Common::Array<Instruction> &instrs = frame._script->instrs;
while (_gameState == kGameStateScript) {
uint instrNum = frame._nextInstruction;
if (instrNum >= instrs.size()) {
_scriptCallStack.pop_back();
return true;
}
frame._nextInstruction = instrNum + 1u;
const Instruction &instr = instrs[instrNum];
int32 arg = instr.arg;
switch (instr.op) {
DISPATCH_OP(Number);
DISPATCH_OP(Rotate);
DISPATCH_OP(Angle);
DISPATCH_OP(AngleGGet);
DISPATCH_OP(Speed);
DISPATCH_OP(SAnimL);
DISPATCH_OP(ChangeL);
DISPATCH_OP(AnimR);
DISPATCH_OP(AnimF);
DISPATCH_OP(AnimN);
DISPATCH_OP(AnimG);
DISPATCH_OP(AnimS);
DISPATCH_OP(AnimT);
DISPATCH_OP(Anim);
DISPATCH_OP(Static);
DISPATCH_OP(VarLoad);
DISPATCH_OP(VarStore);
DISPATCH_OP(VarAddAndStore);
DISPATCH_OP(VarGlobalLoad);
DISPATCH_OP(VarGlobalStore);
DISPATCH_OP(ItemCheck);
DISPATCH_OP(ItemRemove);
DISPATCH_OP(ItemHighlightSet);
DISPATCH_OP(ItemAdd);
DISPATCH_OP(ItemHaveSpace);
DISPATCH_OP(ItemClear);
DISPATCH_OP(SetCursor);
DISPATCH_OP(SetRoom);
DISPATCH_OP(LMB);
DISPATCH_OP(LMB1);
DISPATCH_OP(SoundS1);
DISPATCH_OP(SoundS2);
DISPATCH_OP(SoundS3);
DISPATCH_OP(SoundL1);
DISPATCH_OP(SoundL2);
DISPATCH_OP(SoundL3);
DISPATCH_OP(3DSoundS2);
DISPATCH_OP(3DSoundL2);
DISPATCH_OP(3DSoundL3);
DISPATCH_OP(StopAL);
DISPATCH_OP(Range);
DISPATCH_OP(AddXSound);
DISPATCH_OP(ClrXSound);
DISPATCH_OP(StopSndLA);
DISPATCH_OP(StopSndLO);
DISPATCH_OP(Music);
DISPATCH_OP(MusicVolRamp);
DISPATCH_OP(Parm0);
DISPATCH_OP(Parm1);
DISPATCH_OP(Parm2);
DISPATCH_OP(Parm3);
DISPATCH_OP(ParmG);
DISPATCH_OP(SParmX);
DISPATCH_OP(SAnimX);
DISPATCH_OP(VolumeDn2);
DISPATCH_OP(VolumeDn3);
DISPATCH_OP(VolumeDn4);
DISPATCH_OP(VolumeUp3);
DISPATCH_OP(Random);
DISPATCH_OP(Drop);
DISPATCH_OP(Dup);
DISPATCH_OP(Swap);
DISPATCH_OP(Say1);
DISPATCH_OP(Say2);
DISPATCH_OP(Say3);
DISPATCH_OP(Say3Get);
DISPATCH_OP(SetTimer);
DISPATCH_OP(GetTimer);
DISPATCH_OP(Delay);
DISPATCH_OP(LoSet);
DISPATCH_OP(LoGet);
DISPATCH_OP(HiSet);
DISPATCH_OP(HiGet);
DISPATCH_OP(Not);
DISPATCH_OP(And);
DISPATCH_OP(Or);
DISPATCH_OP(Add);
DISPATCH_OP(Sub);
DISPATCH_OP(Negate);
DISPATCH_OP(CmpEq);
DISPATCH_OP(CmpGt);
DISPATCH_OP(CmpLt);
DISPATCH_OP(BitLoad);
DISPATCH_OP(BitSet0);
DISPATCH_OP(BitSet1);
DISPATCH_OP(Disc1);
DISPATCH_OP(Disc2);
DISPATCH_OP(Disc3);
DISPATCH_OP(Goto);
DISPATCH_OP(EscOn);
DISPATCH_OP(EscOff);
DISPATCH_OP(EscGet);
DISPATCH_OP(BackStart);
DISPATCH_OP(SaveAs);
DISPATCH_OP(Save0);
DISPATCH_OP(Exit);
DISPATCH_OP(AllowSaves);
DISPATCH_OP(AnimName);
DISPATCH_OP(ValueName);
DISPATCH_OP(VarName);
DISPATCH_OP(SoundName);
DISPATCH_OP(CursorName);
DISPATCH_OP(Dubbing);
DISPATCH_OP(CheckValue);
DISPATCH_OP(Jump);
// Schizm ops
DISPATCH_OP(CallFunction);
DISPATCH_OP(Return);
DISPATCH_OP(MusicStop);
DISPATCH_OP(MusicPlayScore);
DISPATCH_OP(ScoreAlways);
DISPATCH_OP(ScoreNormal);
DISPATCH_OP(SndPlay);
DISPATCH_OP(SndPlayEx);
DISPATCH_OP(SndPlay3D);
DISPATCH_OP(SndPlaying);
DISPATCH_OP(SndWait);
DISPATCH_OP(SndHalt);
DISPATCH_OP(SndToBack);
DISPATCH_OP(SndStop);
DISPATCH_OP(SndStopAll);
DISPATCH_OP(SndAddRandom);
DISPATCH_OP(SndClearRandom);
DISPATCH_OP(VolumeAdd);
DISPATCH_OP(VolumeChange);
DISPATCH_OP(AnimVolume);
DISPATCH_OP(AnimChange);
DISPATCH_OP(ScreenName);
DISPATCH_OP(ExtractByte);
DISPATCH_OP(InsertByte);
DISPATCH_OP(String);
DISPATCH_OP(CmpNE);
DISPATCH_OP(CmpLE);
DISPATCH_OP(CmpGE);
DISPATCH_OP(Speech);
DISPATCH_OP(SpeechEx);
DISPATCH_OP(SpeechTest);
DISPATCH_OP(Say);
DISPATCH_OP(RandomInclusive);
DISPATCH_OP(HeroOut);
DISPATCH_OP(HeroGetPos);
DISPATCH_OP(HeroSetPos);
DISPATCH_OP(HeroGet);
DISPATCH_OP(Garbage);
DISPATCH_OP(GetRoom);
DISPATCH_OP(BitAnd);
DISPATCH_OP(BitOr);
DISPATCH_OP(AngleGet);
DISPATCH_OP(IsCDVersion);
DISPATCH_OP(IsDVDVersion);
DISPATCH_OP(Disc);
DISPATCH_OP(HidePanel);
DISPATCH_OP(RotateUpdate);
DISPATCH_OP(Mul);
DISPATCH_OP(Div);
DISPATCH_OP(Mod);
DISPATCH_OP(GetDigit);
DISPATCH_OP(PuzzleInit);
DISPATCH_OP(PuzzleCanPress);
DISPATCH_OP(PuzzleDoMove1);
DISPATCH_OP(PuzzleDoMove2);
DISPATCH_OP(PuzzleDone);
DISPATCH_OP(PuzzleWhoWon);
DISPATCH_OP(Fn);
DISPATCH_OP(ItemHighlightSetTrue);
DISPATCH_OP(AnimForward);
DISPATCH_OP(AnimReverse);
DISPATCH_OP(AnimKForward);
DISPATCH_OP(NoUpdate);
DISPATCH_OP(NoClear);
DISPATCH_OP(Say1_AD2044);
DISPATCH_OP(Say2_AD2044);
DISPATCH_OP(Say1Rnd);
DISPATCH_OP(M);
DISPATCH_OP(EM);
DISPATCH_OP(SE);
DISPATCH_OP(SDot);
DISPATCH_OP(E);
DISPATCH_OP(Dot);
DISPATCH_OP(Sound);
DISPATCH_OP(ISound);
DISPATCH_OP(RGet);
DISPATCH_OP(RSet);
DISPATCH_OP(EndRSet);
DISPATCH_OP(Say2K);
DISPATCH_OP(Say3K);
DISPATCH_OP(Stop);
default:
error("Unimplemented opcode %i", static_cast<int>(instr.op));
}
}
return true;
}
#undef DISPATCH_OP
} // End of namespace VCruise