[Scummvm-git-logs] scummvm master -> d7b2fee0c2965cc70ede83b341cd8fda323cafbf

elasota noreply at scummvm.org
Sat May 27 17:52:04 UTC 2023


This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
031e11dfcd VCRUISE: Fix subtitle checkbox.  Apply music volume even if music is disabled since Schizm transit scenes ignore mute.
c8473e44ca VCRUISE: Change Schizm to testing status
dde58a8e00 VCRUISE: Split up script stuff to another file
d7b2fee0c2 NEWS: Add Schizm: Mysterious Journey support


Commit: 031e11dfcdfb2ef69447b1ae202ca02f469dd4a7
    https://github.com/scummvm/scummvm/commit/031e11dfcdfb2ef69447b1ae202ca02f469dd4a7
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-27T13:39:01-04:00

Commit Message:
VCRUISE: Fix subtitle checkbox.  Apply music volume even if music is disabled since Schizm transit scenes ignore mute.

Changed paths:
    engines/vcruise/menu.cpp


diff --git a/engines/vcruise/menu.cpp b/engines/vcruise/menu.cpp
index 2a4ff60d76e..4f895f584f9 100644
--- a/engines/vcruise/menu.cpp
+++ b/engines/vcruise/menu.cpp
@@ -728,6 +728,7 @@ void ReahSoundMenuPage::addPageContents() {
 
 	_soundChecked = !soundMute;
 	_musicChecked = !musicMute;
+	_subtitleChecked = ConfMan.getBool("subtitles");
 
 	Graphics::Surface *soundGraphics = _menuInterface->getUIGraphic(17);
 	if (soundGraphics) {
@@ -872,7 +873,7 @@ void ReahSoundMenuPage::onCheckboxClicked(uint button, bool &outChangedState) {
 	}
 	if (button == kCheckboxSubtitle) {
 		_subtitleChecked = _checkboxes[button]._enabled;
-		//applySubtitles();
+		ConfMan.setBool("subtitles", _subtitleChecked);
 	}
 
 	outChangedState = false;
@@ -882,7 +883,7 @@ void ReahSoundMenuPage::onSliderMoved(uint slider) {
 	if (slider == kSliderSound && _soundChecked)
 		applySoundVolume();
 
-	if (slider == kSliderMusic && _musicChecked)
+	if (slider == kSliderMusic)
 		applyMusicVolume();
 }
 


Commit: c8473e44cae980cb8f1a9b03e446db9ea2237603
    https://github.com/scummvm/scummvm/commit/c8473e44cae980cb8f1a9b03e446db9ea2237603
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-27T13:39:50-04:00

Commit Message:
VCRUISE: Change Schizm to testing status

Changed paths:
    engines/vcruise/detection_tables.h


diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index 01169d22077..215c9795031 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -120,7 +120,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 			AD_ENTRY1s("Schizm.exe", "296edd26d951c3bdc4d303c4c88b27cd", 364544),
 			Common::UNK_LANG,
 			Common::kPlatformWindows,
-			ADGF_UNSTABLE | VCRUISE_GF_WANT_OGG_VORBIS | VCRUISE_GF_NEED_JPEG,
+			ADGF_TESTING | VCRUISE_GF_WANT_OGG_VORBIS | VCRUISE_GF_NEED_JPEG,
 			GUIO0()
 		},
 		GID_SCHIZM,


Commit: dde58a8e001f5f7be0b366a08c8c0ff196fca163
    https://github.com/scummvm/scummvm/commit/dde58a8e001f5f7be0b366a08c8c0ff196fca163
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-27T13:46:10-04:00

Commit Message:
VCRUISE: Split up script stuff to another file

Changed paths:
  A engines/vcruise/runtime_scriptexec.cpp
    engines/vcruise/module.mk
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 6ef199c9009..32086488026 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -6,6 +6,7 @@ MODULE_OBJS = \
 	metaengine.o \
 	menu.o \
 	runtime.o \
+	runtime_scriptexec.o \
 	sampleloop.o \
 	script.o \
 	textparser.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index ee5de303393..542a7bc22fd 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -2088,224 +2088,6 @@ void Runtime::commitSectionToScreen(const RenderSection &section, const Common::
 	_system->copyRectToScreen(section.surf->getBasePtr(rect.left, rect.top), section.surf->pitch, rect.left + section.rect.left, rect.top + section.rect.top, rect.width(), rect.height());
 }
 
-#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(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(BlockSaves);
-
-			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);
-
-		default:
-			error("Unimplemented opcode %i", static_cast<int>(instr.op));
-		}
-	}
-
-	return true;
-}
-
-#undef DISPATCH_OP
-
 bool Runtime::requireAvailableStack(uint n) {
 	if (_scriptStack.size() < n) {
 		error("Script stack underflow");
@@ -5712,2028 +5494,8 @@ LoadGameOutcome Runtime::loadGame(Common::ReadStream *stream) {
 	return outcome;
 }
 
-#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);
-		_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();
-			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::drawFrame() {
+	_system->updateScreen();
 }
 
-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 (_musicPlayer)
-			_musicPlayer->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 (_musicPlayer)
-			_musicPlayer->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::scriptOpBlockSaves(ScriptArg_t arg) {
-	warning("SAVES SHOULD BE BLOCKED ON THIS SCREEN");
-}
-
-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) {
-	_musicPlayer.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) {
-		_musicPlayer.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 = _roomNumber;
-	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;
-
-	_scriptEnv.puzzleWasSet = true;
-
-	if (firstMover == 2)
-		scriptOpPuzzleDoMove2(0);
-}
-
-void Runtime::scriptOpPuzzleWhoWon(ScriptArg_t arg) {
-	StackInt_t winner = 0;
-	if (_circuitPuzzle) {
-		switch (_circuitPuzzle->checkConclusion()) {
-		case CircuitPuzzle::kConclusionNone:
-			winner = 0;
-			break;
-		case CircuitPuzzle::kConclusionPlayerWon:
-			winner = 1;
-			break;
-		case CircuitPuzzle::kConclusionPlayerLost:
-			winner = 2;
-			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]);
-}
-
-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();
-}
-
-// 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
-
-
-void Runtime::drawFrame() {
-	_system->updateScreen();
-}
-
-
 } // End of namespace VCruise
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
new file mode 100644
index 00000000000..ae2446c7434
--- /dev/null
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -0,0 +1,2050 @@
+/* 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/audio_player.h"
+#include "vcruise/circuitpuzzle.h"
+#include "vcruise/runtime.h"
+#include "vcruise/script.h"
+
+
+namespace VCruise {
+
+#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);
+		_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();
+			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 (_musicPlayer)
+			_musicPlayer->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 (_musicPlayer)
+			_musicPlayer->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::scriptOpBlockSaves(ScriptArg_t arg) {
+	warning("SAVES SHOULD BE BLOCKED ON THIS SCREEN");
+}
+
+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) {
+	_musicPlayer.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) {
+		_musicPlayer.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 = _roomNumber;
+	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;
+
+	_scriptEnv.puzzleWasSet = true;
+
+	if (firstMover == 2)
+		scriptOpPuzzleDoMove2(0);
+}
+
+void Runtime::scriptOpPuzzleWhoWon(ScriptArg_t arg) {
+	StackInt_t winner = 0;
+	if (_circuitPuzzle) {
+		switch (_circuitPuzzle->checkConclusion()) {
+		case CircuitPuzzle::kConclusionNone:
+			winner = 0;
+			break;
+		case CircuitPuzzle::kConclusionPlayerWon:
+			winner = 1;
+			break;
+		case CircuitPuzzle::kConclusionPlayerLost:
+			winner = 2;
+			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]);
+}
+
+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();
+}
+
+// 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
+
+} // End of namespace VCruise


Commit: d7b2fee0c2965cc70ede83b341cd8fda323cafbf
    https://github.com/scummvm/scummvm/commit/d7b2fee0c2965cc70ede83b341cd8fda323cafbf
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-27T13:50:10-04:00

Commit Message:
NEWS: Add Schizm: Mysterious Journey support

Changed paths:
    NEWS.md


diff --git a/NEWS.md b/NEWS.md
index e97030c3048..702d18344b6 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -6,7 +6,7 @@ For a more comprehensive changelog of the latest experimental code, see:
  New games:
    - Added support for macOS versions of Syberia and Syberia II.
    - Added support for The Vampire Diaries and Nancy Drew: Secrets Can Kill.
-   - Added support for Reah: Face the Unknown.
+   - Added support for Reah: Face the Unknown and Schizm: Mysterious Journey.
 
  New platforms:
    - Added libretro new shiny port.




More information about the Scummvm-git-logs mailing list