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

elasota noreply at scummvm.org
Tue May 23 01:55:42 UTC 2023


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

Summary:
2b570b3605 VCRUISE: Increase gyro limit to 5, fixes crash in Schizm height meter puzzle.
fb1a286204 VCRUISE: Fix wrong animation playing after entering the statue.  Fix crash when solving bell puzzle.  Fix heroSetPos err
e4e5b2acfd VCRUISE: Add SndToBack opcode.
db396739fb VCRUISE: Remove obsolete comment
f2f4db29e5 VCRUISE: Adjust initial frame timing to absorb disk seeks without skipping as much.  Prune unused functions and strings 
c7b9bc1fc1 VCRUISE: Avoid reloading scripts from forced screen changes if the room didn't actually change
fee9e6e8a8 VCRUISE: Run one frame of the rotate animation after changing characters after heroSetPos if there are no idle animation
ecd3d1b71f VCRUISE: Fix animation skip not working.  Fix sounds started with SndPlayEx looping.
3b1246a4a4 VCRUISE: Correct changeL opcode behavior to fix the airship nav computer without also causing the infinite loop bug
e980e80e53 VCRUISE: Add parsing and handling of "smpl" chunks in Schizm WAV files, which determine sound loop behavior


Commit: 2b570b3605152ff3a1102c8d7bc30ed23d0fb565
    https://github.com/scummvm/scummvm/commit/2b570b3605152ff3a1102c8d7bc30ed23d0fb565
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:10-04:00

Commit Message:
VCRUISE: Increase gyro limit to 5, fixes crash in Schizm height meter puzzle.

Changed paths:
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 501cab947e9..abc08fea23a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -640,7 +640,7 @@ private:
 
 		void reset();
 
-		static const uint kNumGyros = 4;
+		static const uint kNumGyros = 5;
 
 		Gyro gyros[kNumGyros];
 


Commit: fb1a2862049ec5e2ddad953fd24b0824477bde33
    https://github.com/scummvm/scummvm/commit/fb1a2862049ec5e2ddad953fd24b0824477bde33
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:10-04:00

Commit Message:
VCRUISE: Fix wrong animation playing after entering the statue.  Fix crash when solving bell puzzle.  Fix heroSetPos error.  Fix GetDigit not working correctly.

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index c894bb615a2..a3838867c7c 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -5400,6 +5400,8 @@ void Runtime::scriptOpAnimF(ScriptArg_t arg) {
 		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;
 	}
@@ -6678,11 +6680,21 @@ void Runtime::scriptOpSndPlay(ScriptArg_t arg) {
 
 void Runtime::scriptOpSndPlayEx(ScriptArg_t arg) {
 	TAKE_STACK_INT_NAMED(2, sndParamArgs);
-	TAKE_STACK_STR_NAMED(1, sndNameArgs);
+	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(sndNameArgs[0], true, soundID, cachedSound);
+	resolveSoundByName(soundName, true, soundID, cachedSound);
 
 	if (cachedSound)
 		triggerSound(true, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
@@ -6702,8 +6714,10 @@ void Runtime::scriptOpSndPlay3D(ScriptArg_t arg) {
 	sndParams.unknownRange = sndParamArgs[4]; // Doesn't appear to be the same thing as Reah.  Usually 1000, sometimes 2000 or 3000.
 
 	if (cachedSound) {
+		// FIXME: Should this be looping?  In the prayer bell puzzle, the prayer sounds (such as 6511_prayer)
+		// don't have a SndHalt afterwards, so 
 		setSound3DParameters(*cachedSound, sndParamArgs[0], sndParamArgs[1], sndParams);
-		triggerSound(true, *cachedSound, getSilentSoundVolume(), 0, true, false);
+		triggerSound(false, *cachedSound, getSilentSoundVolume(), 0, true, false);
 	}
 }
 
@@ -6935,11 +6949,11 @@ void Runtime::scriptOpHeroSetPos(ScriptArg_t arg) {
 		thisHero = false;
 		break;
 	default:
-		error("Unhandled heroGetPos argument %i", static_cast<int>(stackArgs[0]));
+		error("Unhandled heroSetPos argument %i", static_cast<int>(stackArgs[0]));
 		return;
 	}
 
-	if (!thisHero) {
+	if (thisHero) {
 		error("heroSetPos for the current hero isn't supported (and Schizm's game scripts shouldn't be doing it).");
 		return;
 	}
@@ -7029,15 +7043,7 @@ void Runtime::scriptOpMod(ScriptArg_t arg) {
 void Runtime::scriptOpGetDigit(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
-	StackInt_t power = stackArgs[1];
-	StackInt_t divisor = 1;
-
-	while (power > 0) {
-		power--;
-		divisor *= 10;
-	}
-
-	StackInt_t digit = (stackArgs[0] / divisor) % 10;
+	StackInt_t digit = (stackArgs[0] >> (stackArgs[1] * 4)) & 0xf;
 
 	_scriptStack.push_back(StackValue(digit));
 }


Commit: e4e5b2acfdaba80452979f7ca8895f378acd85a0
    https://github.com/scummvm/scummvm/commit/e4e5b2acfdaba80452979f7ca8895f378acd85a0
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:10-04:00

Commit Message:
VCRUISE: Add SndToBack opcode.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index a3838867c7c..7ffacae223a 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -4987,6 +4987,23 @@ void Runtime::recordSaveGameSnapshot() {
 	mainState->loadedAnimation = _loadedAnimation;
 	mainState->animDisplayingFrame = _animDisplayingFrame;
 
+	recordSounds(*mainState);
+
+	snapshot->pendingSoundParams3D = _pendingSoundParams3D;
+
+	snapshot->triggeredOneShots = _triggeredOneShots;
+	snapshot->sayCycles = _sayCycles;
+
+	snapshot->listenerX = _listenerX;
+	snapshot->listenerY = _listenerY;
+	snapshot->listenerAngle = _listenerAngle;
+}
+
+void Runtime::recordSounds(SaveGameSwappableState &state) {
+	state.sounds.clear();
+
+	state.randomAmbientSounds = _randomAmbientSounds;
+
 	for (const Common::SharedPtr<SoundInstance> &soundPtr : _activeSounds) {
 		const SoundInstance &sound = *soundPtr;
 
@@ -5001,7 +5018,7 @@ void Runtime::recordSaveGameSnapshot() {
 		// Skip ramp
 		if (sound.rampRatePerMSec != 0) {
 			if (sound.rampTerminateOnCompletion)
-				continue;	// Don't even save this
+				continue; // Don't even save this
 
 			saveSound.volume = sound.rampEndVolume;
 		}
@@ -5014,19 +5031,8 @@ void Runtime::recordSaveGameSnapshot() {
 
 		saveSound.params3D = sound.params3D;
 
-		mainState->sounds.push_back(saveSound);
+		state.sounds.push_back(saveSound);
 	}
-
-	snapshot->pendingSoundParams3D = _pendingSoundParams3D;
-
-	snapshot->triggeredOneShots = _triggeredOneShots;
-	snapshot->sayCycles = _sayCycles;
-
-	snapshot->listenerX = _listenerX;
-	snapshot->listenerY = _listenerY;
-	snapshot->listenerAngle = _listenerAngle;
-
-	mainState->randomAmbientSounds = _randomAmbientSounds;
 }
 
 void Runtime::restoreSaveGameSnapshot() {
@@ -6745,7 +6751,9 @@ void Runtime::scriptOpSndHalt(ScriptArg_t arg) {
 	}
 }
 
-OPCODE_STUB(SndToBack)
+void Runtime::scriptOpSndToBack(ScriptArg_t arg) {
+	recordSounds(*_altState);
+}
 
 void Runtime::scriptOpSndStop(ScriptArg_t arg) {
 	TAKE_STACK_INT(1);
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index abc08fea23a..5d7e94a6bc5 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -576,6 +576,7 @@ public:
 	bool canLoad() const;
 
 	void recordSaveGameSnapshot();
+	void recordSounds(SaveGameSwappableState &state);
 	void restoreSaveGameSnapshot();
 	Common::SharedPtr<SaveGameSnapshot> generateNewGameSnapshot() const;
 


Commit: db396739fbdd043ef1734613ac6b3da68804afb0
    https://github.com/scummvm/scummvm/commit/db396739fbdd043ef1734613ac6b3da68804afb0
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Remove obsolete comment

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 7ffacae223a..3048291d744 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -5996,9 +5996,6 @@ void Runtime::scriptOpSParmX(ScriptArg_t arg) {
 	_pendingStaticAnimParams.initialDelay = stackArgs[0];
 	_pendingStaticAnimParams.repeatDelay = stackArgs[1];
 	_pendingStaticAnimParams.lockInteractions = (stackArgs[2] != 0);
-
-	//if (_pendingStaticAnimParams.lockInteractions)
-	//	error("Locking interactions for animation is not implemented yet");
 }
 
 void Runtime::scriptOpSAnimX(ScriptArg_t arg) {


Commit: f2f4db29e58d1dca647676ab9f12c628fc91a801
    https://github.com/scummvm/scummvm/commit/f2f4db29e58d1dca647676ab9f12c628fc91a801
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Adjust initial frame timing to absorb disk seeks without skipping as much.  Prune unused functions and strings from scripts.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp
    engines/vcruise/script.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 3048291d744..a2526140721 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1822,15 +1822,22 @@ void Runtime::continuePlayingAnimation(bool loop, bool useStopFrame, bool &outAn
 
 	for (;;) {
 		bool needNewFrame = false;
+		bool needInitialTimestamp = false;
 
 		if (needsFirstFrame) {
 			needNewFrame = true;
 			needsFirstFrame = false;
+			needInitialTimestamp = true;
 		} else {
 			if (_animFrameRateLock.numerator) {
-				// if ((millis - startTime) / 1000 * frameRate / frameRateDenominator) >= framesDecoded
-				if ((millis - _animStartTime) * static_cast<uint64>(_animFrameRateLock.numerator) >= (static_cast<uint64>(_animFramesDecoded) * static_cast<uint64>(_animFrameRateLock.denominator) * 1000u))
+				if (_animFramesDecoded == 0) {
 					needNewFrame = true;
+					needInitialTimestamp = true;
+				} else {
+					// if ((millis - startTime) / 1000 * frameRate / frameRateDenominator) >= framesDecoded
+					if ((millis - _animStartTime) * static_cast<uint64>(_animFrameRateLock.numerator) >= (static_cast<uint64>(_animFramesDecoded) * static_cast<uint64>(_animFrameRateLock.denominator) * 1000u))
+						needNewFrame = true;
+				}
 			} else {
 				if (_animDecoder->getTimeToNextFrame() == 0)
 					needNewFrame = true;
@@ -1859,6 +1866,13 @@ void Runtime::continuePlayingAnimation(bool loop, bool useStopFrame, bool &outAn
 		}
 
 		const Graphics::Surface *surface = _animDecoder->decodeNextFrame();
+
+		// Get the start timestamp when the first frame finishes decoding so disk seeks don't cause frame skips
+		if (needInitialTimestamp) {
+			millis = g_system->getMillis();
+			_animStartTime = millis;
+		}
+
 		if (!surface) {
 			outAnimationEnded = true;
 			return;
@@ -2650,9 +2664,9 @@ void Runtime::loadAllSchizmScreenNames() {
 			if (roomNumber == 1)
 				numRooms = 2;
 
-			compileSchizmLogicSet(roomSetToCompile, numRooms);
+			Common::SharedPtr<ScriptSet> scriptSet = compileSchizmLogicSet(roomSetToCompile, numRooms);
 
-			for (const RoomScriptSetMap_t::Node &rssNode : _scriptSet->roomScripts) {
+			for (const RoomScriptSetMap_t::Node &rssNode : scriptSet->roomScripts) {
 				if (rssNode._key != roomNumber)
 					continue;
 
@@ -2827,7 +2841,8 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 			if (roomNumber != 1 && roomNumber != 3)
 				roomsToCompile[numRoomsToCompile++] = roomNumber;
 
-			compileSchizmLogicSet(roomsToCompile, numRoomsToCompile);
+			_scriptSet.reset();
+			_scriptSet = compileSchizmLogicSet(roomsToCompile, numRoomsToCompile);
 		} else if (_gameID == GID_REAH) {
 			_scriptSet.reset();
 
@@ -3418,7 +3433,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 
 	if (_animFrameRateLock.numerator) {
 		_animFramesDecoded = 0;
-		_animStartTime = g_system->getMillis();
+		_animStartTime = 0;
 	}
 
 	debug(1, "Animation last frame set to %u", animDef.lastFrame);
@@ -4017,9 +4032,7 @@ void Runtime::activateScript(const Common::SharedPtr<Script> &script, const Scri
 	_gameState = kGameStateScript;
 }
 
-void Runtime::compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) {
-	_scriptSet.reset();
-
+Common::SharedPtr<ScriptSet> Runtime::compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) const {
 	Common::SharedPtr<IScriptCompilerGlobalState> gs = createScriptCompilerGlobalState();
 
 	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
@@ -4055,7 +4068,9 @@ void Runtime::compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) {
 			warning("Function '%s' was referenced but not defined", scriptSet->functionNames[i].c_str());
 	}
 
-	_scriptSet = scriptSet;
+	optimizeScriptSet(*scriptSet);
+
+	return scriptSet;
 }
 
 bool Runtime::parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value) {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 5d7e94a6bc5..00e954c0b16 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -850,7 +850,7 @@ private:
 	void pushAnimDef(const AnimationDef &animDef);
 
 	void activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars);
-	void compileSchizmLogicSet(const uint *roomNumbers, uint numRooms);
+	Common::SharedPtr<ScriptSet> compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) const;
 
 	bool parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value);
 	void allocateRoomsUpTo(uint roomNumber);
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 326bec2002c..22ed8881bef 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/debug.h"
 #include "common/stream.h"
 #include "common/hash-str.h"
 
@@ -1399,5 +1400,99 @@ bool checkSchizmLogicForDuplicatedRoom(Common::ReadStream &stream, uint streamSi
 	return true;
 }
 
+static bool opArgIsStringIndex(ScriptOps::ScriptOp op) {
+	switch (op) {
+		case ScriptOps::kDubbing:
+		case ScriptOps::kVarName:
+		case ScriptOps::kValueName:
+		case ScriptOps::kAnimName:
+		case ScriptOps::kSoundName:
+		case ScriptOps::kCursorName:
+		case ScriptOps::kString:
+		case ScriptOps::kScreenName:
+		case ScriptOps::kGarbage:
+			return true;
+		default:
+			return false;
+	}
+}
+
+void optimizeScriptSet(ScriptSet &scriptSet) {
+	Common::HashMap<uint, uint> functionIndexToUsedFunction;
+	Common::HashMap<uint, uint> stringIndexToUsedString;
+
+	Common::Array<Script *> scriptCheckQueue;
+
+	for (const RoomScriptSetMap_t::Node &rsNode : scriptSet.roomScripts) {
+		for (const ScreenScriptSetMap_t::Node &ssNode : rsNode._value->screenScripts) {
+			if (ssNode._value->entryScript)
+				scriptCheckQueue.push_back(ssNode._value->entryScript.get());
+
+			for (const ScriptMap_t::Node &isNode : ssNode._value->interactionScripts)
+				scriptCheckQueue.push_back(isNode._value.get());
+		}
+	}
+
+	// scriptCheckQueue.size() may grow during the loop
+	for (uint i = 0; i < scriptCheckQueue.size(); i++) {
+		Script *script = scriptCheckQueue[i];
+
+		for (Instruction &instr : script->instrs) {
+			if (instr.op == ScriptOps::kCallFunction) {
+				uint funcID = instr.arg;
+
+				Common::HashMap<uint, uint>::const_iterator funcIDIt = functionIndexToUsedFunction.find(funcID);
+
+				if (funcIDIt == functionIndexToUsedFunction.end()) {
+					uint newIndex = functionIndexToUsedFunction.size();
+					functionIndexToUsedFunction[funcID] = newIndex;
+
+					scriptCheckQueue.push_back(scriptSet.functions[funcID].get());
+					instr.arg = newIndex;
+				} else
+					instr.arg = funcIDIt->_value;
+			} else if (opArgIsStringIndex(instr.op)) {
+				uint strID = instr.arg;
+
+				Common::HashMap<uint, uint>::const_iterator strIndexIt = stringIndexToUsedString.find(strID);
+
+				if (strIndexIt == stringIndexToUsedString.end()) {
+					uint newIndex = stringIndexToUsedString.size();
+					stringIndexToUsedString[strID] = newIndex;
+
+					instr.arg = newIndex;
+				} else
+					instr.arg = strIndexIt->_value;
+			}
+		}
+	}
+
+	debug(1, "Optimize result: Fns: %u -> %u  Strs: %u -> %u", static_cast<uint>(scriptSet.functions.size()), static_cast<uint>(functionIndexToUsedFunction.size()), static_cast<uint>(scriptSet.strings.size()), static_cast<uint>(stringIndexToUsedString.size()));
+
+	Common::Array<Common::SharedPtr<Script> > functions;
+	Common::Array<Common::String> functionNames;
+
+	functions.resize(functionIndexToUsedFunction.size());
+
+	if (scriptSet.functionNames.size())
+		functionNames.resize(functionIndexToUsedFunction.size());
+
+	for (const Common::HashMap<uint, uint>::Node &fnRemapNode : functionIndexToUsedFunction) {
+		functions[fnRemapNode._value] = scriptSet.functions[fnRemapNode._key];
+		if (functionNames.size())
+			functionNames[fnRemapNode._value] = scriptSet.functionNames[fnRemapNode._key];
+	}
+
+	Common::Array<Common::String> strings;
+	strings.resize(stringIndexToUsedString.size());
+
+	for (const Common::HashMap<uint, uint>::Node &strRemapNode : stringIndexToUsedString)
+		strings[strRemapNode._value] = scriptSet.strings[strRemapNode._key];
+
+	scriptSet.functions = Common::move(functions);
+	scriptSet.functionNames = Common::move(functionNames);
+	scriptSet.strings = Common::move(strings);
+}
+
 
 } // namespace VCruise
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 2d62ec015ba..55710b64053 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -286,6 +286,7 @@ Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState();
 Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath);
 void compileSchizmLogicFile(ScriptSet &scriptSet, uint loadAsRoom, uint fileRoom, Common::ReadStream &stream, uint streamSize, const Common::String &blamePath, IScriptCompilerGlobalState *gs);
 bool checkSchizmLogicForDuplicatedRoom(Common::ReadStream &stream, uint streamSize);
+void optimizeScriptSet(ScriptSet &scriptSet);
 
 }
 


Commit: c7b9bc1fc18909e9a77193ac26586eeb72e41345
    https://github.com/scummvm/scummvm/commit/c7b9bc1fc18909e9a77193ac26586eeb72e41345
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Avoid reloading scripts from forced screen changes if the room didn't actually change

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index a2526140721..328cb6060f6 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -2819,8 +2819,8 @@ void Runtime::resolveSoundByNameOrID(const StackValue &stackValue, bool load, St
 }
 
 void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
-	bool changedRoom = (roomNumber != _loadedRoomNumber) || _forceScreenChange;
-	bool changedScreen = (screenNumber != _activeScreenNumber) || changedRoom;
+	bool changedRoom = (roomNumber != _loadedRoomNumber);
+	bool changedScreen = (screenNumber != _activeScreenNumber) || changedRoom || _forceScreenChange;
 
 	_forceScreenChange = false;
 
@@ -2856,7 +2856,6 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 		} else
 			error("Don't know how to compile scripts for this game");
 
-
 		_map.clear();
 
 		Common::String mapFileName = Common::String::format("Map/Room%02i.map", static_cast<int>(roomNumber));


Commit: fee9e6e8a84700b85f06b0a76cce82c948e2e143
    https://github.com/scummvm/scummvm/commit/fee9e6e8a84700b85f06b0a76cce82c948e2e143
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Run one frame of the rotate animation after changing characters after heroSetPos if there are no idle animations to hopefully fix the wrong frame displaying.  Also allow animations with sound to show the tray in Schizm.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 328cb6060f6..c29c9591913 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -720,8 +720,9 @@ void SaveGameSwappableState::Sound::read(Common::ReadStream *stream) {
 	params3D.read(stream);
 }
 
-SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), musicTrack(0), musicVolume(100), musicActive(true),
-												   animVolume(100), loadedAnimation(0), animDisplayingFrame(0) {
+SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), havePendingPostSwapScreenReset(false),
+												   musicTrack(0), musicVolume(100), musicActive(true), animVolume(100),
+												   loadedAnimation(0), animDisplayingFrame(0) {
 }
 
 SaveGameSnapshot::SaveGameSnapshot() : hero(0), swapOutRoom(0), swapOutScreen(0), swapOutDirection(0),
@@ -738,6 +739,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 		stream->writeUint32BE(states[sti]->roomNumber);
 		stream->writeUint32BE(states[sti]->screenNumber);
 		stream->writeUint32BE(states[sti]->direction);
+		stream->writeByte(states[sti]->havePendingPostSwapScreenReset ? 1 : 0);
 	}
 
 	stream->writeUint32BE(hero);
@@ -843,6 +845,9 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 		states[sti]->roomNumber = stream->readUint32BE();
 		states[sti]->screenNumber = stream->readUint32BE();
 		states[sti]->direction = stream->readUint32BE();
+
+		if (saveVersion >= 7)
+			states[sti]->havePendingPostSwapScreenReset = (stream->readByte() != 0);
 	}
 
 	if (saveVersion >= 6) {
@@ -989,7 +994,7 @@ FontCacheItem::FontCacheItem() : font(nullptr), size(0) {
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
 	  _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
-	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false),
+	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false), _havePendingPostSwapScreenReset(false),
 	  _havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
 	  _musicTrack(0), _musicActive(true), _scoreSectionEndTime(0), _musicVolume(getDefaultSoundVolume()), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
@@ -1156,6 +1161,9 @@ bool Runtime::runFrame() {
 		case kGameStateWaitingForAnimation:
 			moreActions = runWaitForAnimation();
 			break;
+		case kGameStateWaitingForAnimationToDelay:
+			moreActions = runWaitForAnimationToDelay();
+			break;
 		case kGameStateWaitingForFacing:
 			moreActions = runWaitForFacing();
 			break;
@@ -1370,7 +1378,8 @@ bool Runtime::runIdle() {
 	if (_havePendingPreIdleActions) {
 		_havePendingPreIdleActions = false;
 
-		triggerPreIdleActions();
+		if (triggerPreIdleActions())
+			return true;
 	}
 
 	if (_havePendingReturnToIdleState) {
@@ -1531,7 +1540,8 @@ bool Runtime::runDelay() {
 	if (_havePendingPreIdleActions) {
 		_havePendingPreIdleActions = false;
 
-		triggerPreIdleActions();
+		if (triggerPreIdleActions())
+			return true;
 	}
 
 	// Play static animations.  Try to keep this in sync with runIdle
@@ -1645,6 +1655,19 @@ bool Runtime::runWaitForAnimation() {
 	return false;
 }
 
+bool Runtime::runWaitForAnimationToDelay() {
+	bool animEnded = false;
+	continuePlayingAnimation(false, false, animEnded);
+
+	if (animEnded) {
+		_gameState = kGameStateDelay;
+		return true;
+	}
+
+	// Yield
+	return false;
+}
+
 bool Runtime::runWaitForFacingToAnim() {
 	bool animEnded = false;
 	continuePlayingAnimation(true, true, animEnded);
@@ -2930,7 +2953,8 @@ void Runtime::changeHero() {
 		// via the "heroOut" op.
 		currentState->roomNumber = _swapOutRoom;
 		currentState->screenNumber = _swapOutScreen;
-		currentState->direction = _direction;
+		currentState->direction = _swapOutDirection;
+		currentState->havePendingPostSwapScreenReset = true;
 	}
 
 	_saveGame->states[0] = alternateState;
@@ -2943,7 +2967,7 @@ void Runtime::changeHero() {
 	restoreSaveGameSnapshot();
 }
 
-void Runtime::triggerPreIdleActions() {
+bool Runtime::triggerPreIdleActions() {
 	debug(1, "Triggering pre-idle actions in room %u screen 0%x facing direction %u", _roomNumber, _screenNumber, _direction);
 
 	_havePendingReturnToIdleState = true;
@@ -2963,7 +2987,31 @@ void Runtime::triggerPreIdleActions() {
 			_animPlayWhileIdle = true;
 			sanim.currentAlternation = 1;
 		}
+
+		_havePendingPostSwapScreenReset = false;
+	} else if (_havePendingPostSwapScreenReset) {
+		_havePendingPostSwapScreenReset = false;
+
+		if (_haveHorizPanAnimations) {
+			AnimationDef animDef = _panRightAnimationDef;
+			uint rotationFrame = _direction * (animDef.lastFrame - animDef.firstFrame) / kNumDirections + animDef.firstFrame;
+			animDef.firstFrame = rotationFrame;
+			animDef.lastFrame = rotationFrame;
+
+			changeAnimation(animDef, false);
+
+			if (_gameState == kGameStateScript || _gameState == kGameStateIdle || _gameState == kGameStateScriptReset)
+				_gameState = kGameStateWaitingForAnimation;
+			else if (_gameState == kGameStateDelay)
+				_gameState = kGameStateWaitingForAnimationToDelay;
+			else
+				error("Triggered pre-idle actions from an unexpected game state");
+		}
+
+		return true;
 	}
+
+	return false;
 }
 
 void Runtime::returnToIdleState() {
@@ -3339,7 +3387,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 }
 
 void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bool consumeFPSOverride, const Fraction &defaultFrameRate) {
-	debug("changeAnimation: Anim: %u  Range: %u -> %u  Initial %u", animDef.animNum, animDef.firstFrame, animDef.lastFrame, initialFrame);
+	debug("changeAnimation: Anim: %i  Range: %u -> %u  Initial %u", animDef.animNum, animDef.firstFrame, animDef.lastFrame, initialFrame);
 
 	_animPlaylist.reset();
 
@@ -4473,7 +4521,23 @@ void Runtime::drawCompass() {
 }
 
 bool Runtime::isTrayVisible() const {
-	return _subtitleQueue.size() == 0 && !_loadedAnimationHasSound && _isInGame && (_gameState != kGameStateMenu);
+	if (_subtitleQueue.size() == 0 && _isInGame && (_gameState != kGameStateMenu)) {
+		// In Reah, animations with sounds are cutscenes that hide the tray.  In Schizm, the tray continues displaying.
+		//
+		// This is important in some situations, e.g. after "reuniting" with Hannah in the lower temple, if you go left,
+		// a ghost will give you a key.  Since that animation has sound, you'll return to idle in that animation,
+		// which will keep the tray hidden because it has sound.
+		if (_gameID == GID_REAH && _loadedAnimationHasSound)
+			return false;
+
+		// Don't display tray during the intro cinematic.
+		if (_gameID == GID_SCHIZM && _loadedAnimation == 200)
+			return false;
+
+		return true;
+	}
+
+	return false;
 }
 
 void Runtime::resetInventoryHighlights() {
@@ -4975,6 +5039,7 @@ void Runtime::recordSaveGameSnapshot() {
 	mainState->roomNumber = _roomNumber;
 	mainState->screenNumber = _screenNumber;
 	mainState->direction = _direction;
+	mainState->havePendingPostSwapScreenReset = false;
 	snapshot->hero = _hero;
 
 	snapshot->pendingStaticAnimParams = _pendingStaticAnimParams;
@@ -5073,6 +5138,7 @@ void Runtime::restoreSaveGameSnapshot() {
 	_roomNumber = mainState->roomNumber;
 	_screenNumber = mainState->screenNumber;
 	_direction = mainState->direction;
+	_havePendingPostSwapScreenReset = mainState->havePendingPostSwapScreenReset;
 	_hero = _saveGame->hero;
 	_swapOutRoom = _saveGame->swapOutRoom;
 	_swapOutScreen = _saveGame->swapOutScreen;
@@ -6980,6 +7046,7 @@ void Runtime::scriptOpHeroSetPos(ScriptArg_t arg) {
 	_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) {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 00e954c0b16..e0661e5a1dd 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -88,7 +88,8 @@ struct RoomScriptSet;
 
 enum GameState {
 	kGameStateBoot,							// Booting the game
-	kGameStateWaitingForAnimation,			// Waiting for a blocking animation with no stop frame complete, then resuming script
+	kGameStateWaitingForAnimation,			// Waiting for a blocking animation with no stop frame to complete, then resuming script
+	kGameStateWaitingForAnimationToDelay,	// Waiting for a blocking animation with no stop frame to complete, then going to delay
 	kGameStateWaitingForFacing,				// Waiting for a blocking animation with a stop frame to complete, then resuming script
 	kGameStateWaitingForFacingToAnim,		// Waiting for a blocking animation to complete, then playing _postFacingAnimDef and switching to kGameStateWaitingForAnimation
 	kGameStateQuit,							// Quitting
@@ -419,6 +420,7 @@ struct SaveGameSwappableState {
 	uint roomNumber;
 	uint screenNumber;
 	uint direction;
+	bool havePendingPostSwapScreenReset;
 
 	uint loadedAnimation;
 	uint animDisplayingFrame;
@@ -444,7 +446,7 @@ struct SaveGameSnapshot {
 	LoadGameOutcome read(Common::ReadStream *stream);
 
 	static const uint kSaveGameIdentifier = 0x53566372;
-	static const uint kSaveGameCurrentVersion = 6;
+	static const uint kSaveGameCurrentVersion = 7;
 	static const uint kSaveGameEarliestSupportedVersion = 2;
 	static const uint kMaxStates = 2;
 
@@ -774,6 +776,7 @@ private:
 	bool runScript();
 	bool requireAvailableStack(uint n);
 	bool runWaitForAnimation();
+	bool runWaitForAnimationToDelay();
 	bool runWaitForFacing();
 	bool runWaitForFacingToAnim();
 	bool runGyroIdle();
@@ -808,7 +811,7 @@ private:
 	void changeToScreen(uint roomNumber, uint screenNumber);
 	void clearIdleAnimations();
 	void changeHero();
-	void triggerPreIdleActions();
+	bool triggerPreIdleActions();
 	void returnToIdleState();
 	void changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor);
 	bool dischargeIdleMouseMove();
@@ -1137,6 +1140,7 @@ private:
 	// Pre-idle actions are executed once upon either entering Idle OR Delay state.
 	bool _havePendingPreIdleActions;
 	bool _havePendingReturnToIdleState;
+	bool _havePendingPostSwapScreenReset;
 
 	bool _havePendingCompletionCheck;
 	GameState _gameState;


Commit: ecd3d1b71f6cc8208dd792223d8f0702bcca320f
    https://github.com/scummvm/scummvm/commit/ecd3d1b71f6cc8208dd792223d8f0702bcca320f
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Fix animation skip not working.  Fix sounds started with SndPlayEx looping.

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index c29c9591913..61b2c2fbb57 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1648,6 +1648,7 @@ bool Runtime::runWaitForAnimation() {
 			}
 		} else if (evt.type == kOSEventTypeKeymappedEvent && evt.keymappedEvent == kKeymappedEventSkipAnimation) {
 			_animFrameRateLock = Fraction(600, 1);
+			_animFramesDecoded = 0;	// Reset decoder count so the start time resyncs
 		}
 	}
 
@@ -3478,10 +3479,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 		}
 	}
 
-	if (_animFrameRateLock.numerator) {
-		_animFramesDecoded = 0;
-		_animStartTime = 0;
-	}
+	_animFramesDecoded = 0;
+	_animStartTime = 0;
 
 	debug(1, "Animation last frame set to %u", animDef.lastFrame);
 }
@@ -6779,8 +6778,12 @@ void Runtime::scriptOpSndPlayEx(ScriptArg_t arg) {
 	SoundInstance *cachedSound = nullptr;
 	resolveSoundByName(soundName, true, soundID, cachedSound);
 
+	// TODO: Need to figure out how looping is determined here.
+	// It looks like SndPlayEx is only used for non-looping.  For example,
+	// 3512_pour is called with SndPlayEx.  However, this doesn't explain what
+	// the SndHalt opcode is used for.
 	if (cachedSound)
-		triggerSound(true, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
+		triggerSound(false, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
 }
 
 void Runtime::scriptOpSndPlay3D(ScriptArg_t arg) {


Commit: 3b1246a4a45982a76d14e6a67ac79867ab241ee4
    https://github.com/scummvm/scummvm/commit/3b1246a4a45982a76d14e6a67ac79867ab241ee4
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:53:11-04:00

Commit Message:
VCRUISE: Correct changeL opcode behavior to fix the airship nav computer without also causing the infinite loop bug

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 61b2c2fbb57..4fca76e27fa 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -186,7 +186,7 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	return screenDirections[screen][direction].get();
 }
 
-ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false),
+ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false), isEntryScript(false),
 	panInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
 }
 
@@ -2326,7 +2326,7 @@ bool Runtime::checkCompletionConditions() {
 				if (interactionScriptIt != screenScriptSet.interactionScripts.end()) {
 					const Common::SharedPtr<Script> &script = interactionScriptIt->_value;
 					if (script) {
-						activateScript(script, ScriptEnvironmentVars());
+						activateScript(script, false, ScriptEnvironmentVars());
 						return true;
 					}
 				}
@@ -2907,7 +2907,7 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 				if (screenScriptIt != screenScriptsMap.end()) {
 					const Common::SharedPtr<Script> &script = screenScriptIt->_value->entryScript;
 					if (script)
-						activateScript(script, ScriptEnvironmentVars());
+						activateScript(script, true, ScriptEnvironmentVars());
 				}
 			}
 		}
@@ -3081,7 +3081,7 @@ bool Runtime::dischargeIdleMouseMove() {
 
 				ScriptEnvironmentVars vars;
 				vars.panInteractionID = interactionID;
-				activateScript(script, vars);
+				activateScript(script, false, vars);
 				return true;
 			}
 		}
@@ -3129,7 +3129,7 @@ bool Runtime::dischargeIdleMouseMove() {
 			Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
 
 			if (script) {
-				activateScript(script, ScriptEnvironmentVars());
+				activateScript(script, false, ScriptEnvironmentVars());
 				return true;
 			}
 		}
@@ -3162,7 +3162,7 @@ bool Runtime::dischargeIdleMouseDown() {
 			ScriptEnvironmentVars vars;
 			vars.lmbDrag = true;
 
-			activateScript(script, vars);
+			activateScript(script, false, vars);
 			return true;
 		}
 	}
@@ -3186,7 +3186,7 @@ bool Runtime::dischargeIdleClick() {
 				ScriptEnvironmentVars vars;
 				vars.lmb = true;
 
-				activateScript(script, vars);
+				activateScript(script, false, vars);
 				return true;
 			}
 		}
@@ -4060,13 +4060,14 @@ void Runtime::pushAnimDef(const AnimationDef &animDef) {
 	_scriptStack.push_back(StackValue(animNameIndex));
 }
 
-void Runtime::activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars) {
+void Runtime::activateScript(const Common::SharedPtr<Script> &script, bool isEntryScript, const ScriptEnvironmentVars &envVars) {
 	if (script->instrs.size() == 0)
 		return;
 
 	assert(_gameState != kGameStateScript);
 
 	_scriptEnv = envVars;
+	_scriptEnv.isEntryScript = isEntryScript;
 
 	CallStackFrame frame;
 	frame._script = script;
@@ -5410,13 +5411,20 @@ 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, doing 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
+	//
+	// 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 (_gameID == GID_REAH)
+	if (!_scriptEnv.isEntryScript)
 		_forceScreenChange = true;
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index e0661e5a1dd..d365c29dd21 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -163,6 +163,7 @@ struct ScriptEnvironmentVars {
 	bool esc;
 	bool exitToMenu;
 	bool animChangeSet;
+	bool isEntryScript;
 };
 
 struct SfxSound {
@@ -852,7 +853,7 @@ private:
 	void consumeAnimChangeAndAdjustAnim(AnimationDef &animDef);
 	void pushAnimDef(const AnimationDef &animDef);
 
-	void activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars);
+	void activateScript(const Common::SharedPtr<Script> &script, bool isEntryScript, const ScriptEnvironmentVars &envVars);
 	Common::SharedPtr<ScriptSet> compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) const;
 
 	bool parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value);


Commit: e980e80e537a000351dbbcd79b661738ed1e379e
    https://github.com/scummvm/scummvm/commit/e980e80e537a000351dbbcd79b661738ed1e379e
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-22T21:54:41-04:00

Commit Message:
VCRUISE: Add parsing and handling of "smpl" chunks in Schizm WAV files, which determine sound loop behavior

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


diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 95134a32ce3..abd6a6622d3 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -2,6 +2,7 @@ MODULE := engines/vcruise
 
 MODULE_OBJS = \
 	audio_player.o \
+	sampleloop.o \
 	metaengine.o \
 	menu.o \
 	runtime.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 4fca76e27fa..f4cce38b205 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -50,6 +50,7 @@
 #include "gui/message.h"
 
 #include "vcruise/audio_player.h"
+#include "vcruise/sampleloop.h"
 #include "vcruise/menu.h"
 #include "vcruise/runtime.h"
 #include "vcruise/script.h"
@@ -543,6 +544,9 @@ void SfxData::load(Common::SeekableReadStream &stream, Audio::Mixer *mixer) {
 	}
 }
 
+SoundCache::SoundCache() : isLoopActive(false) {
+}
+
 SoundCache::~SoundCache() {
 	// Dispose player first so playback stops
 	this->player.reset();
@@ -555,7 +559,8 @@ SoundCache::~SoundCache() {
 
 SoundInstance::SoundInstance()
 	: id(0), rampStartVolume(0), rampEndVolume(0), rampRatePerMSec(0), rampStartTime(0), rampTerminateOnCompletion(false),
-	  volume(0), balance(0), effectiveBalance(0), effectiveVolume(0), is3D(false), loopingType(kSoundLoopingTypeNotLooping), isSpeech(false), isSilencedLoop(false), x(0), y(0), startTime(0), endTime(0), duration(0) {
+	  volume(0), balance(0), effectiveBalance(0), effectiveVolume(0), is3D(false), isLooping(false), isSpeech(false), restartWhenAudible(false), tryToLoopWhenRestarted(false),
+	  x(0), y(0), startTime(0), endTime(0), duration(0) {
 }
 
 SoundInstance::~SoundInstance() {
@@ -677,7 +682,7 @@ void SaveGameSwappableState::InventoryItem::read(Common::ReadStream *stream) {
 	highlighted = (stream->readByte() != 0);
 }
 
-SaveGameSwappableState::Sound::Sound() : id(0), volume(0), balance(0), is3D(false), isLooping(false), isSpeech(false), x(0), y(0) {
+SaveGameSwappableState::Sound::Sound() : id(0), volume(0), balance(0), is3D(false), isLooping(false), tryToLoopWhenRestarted(false), isSpeech(false), x(0), y(0) {
 }
 
 void SaveGameSwappableState::Sound::write(Common::WriteStream *stream) const {
@@ -690,6 +695,7 @@ void SaveGameSwappableState::Sound::write(Common::WriteStream *stream) const {
 
 	stream->writeByte(is3D ? 1 : 0);
 	stream->writeByte(isLooping ? 1 : 0);
+	stream->writeByte(tryToLoopWhenRestarted ? 1 : 0);
 	stream->writeByte(isSpeech ? 1 : 0);
 
 	stream->writeSint32BE(x);
@@ -698,7 +704,7 @@ void SaveGameSwappableState::Sound::write(Common::WriteStream *stream) const {
 	params3D.write(stream);
 }
 
-void SaveGameSwappableState::Sound::read(Common::ReadStream *stream) {
+void SaveGameSwappableState::Sound::read(Common::ReadStream *stream, uint saveGameVersion) {
 	uint nameLen = stream->readUint32BE();
 
 	if (stream->eos() || stream->err() || nameLen > 256)
@@ -712,6 +718,12 @@ void SaveGameSwappableState::Sound::read(Common::ReadStream *stream) {
 
 	is3D = (stream->readByte() != 0);
 	isLooping = (stream->readByte() != 0);
+
+	if (saveGameVersion >= 8)
+		tryToLoopWhenRestarted = (stream->readByte() != 0);
+	else
+		tryToLoopWhenRestarted = false;
+
 	isSpeech = (stream->readByte() != 0);
 
 	x = stream->readSint32BE();
@@ -936,7 +948,7 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 			states[sti]->inventory[i].read(stream);
 
 		for (uint i = 0; i < numSounds[sti]; i++)
-			states[sti]->sounds[i].read(stream);
+			states[sti]->sounds[i].read(stream, saveVersion);
 	}
 
 	for (uint i = 0; i < numOneShots; i++)
@@ -2762,6 +2774,16 @@ SoundCache *Runtime::loadCache(SoundInstance &sound) {
 		return nullptr;
 	}
 
+	Common::SharedPtr<SoundLoopInfo> loopInfo;
+
+	if (_gameID == GID_SCHIZM) {
+		loopInfo = SoundLoopInfo::readFromWaveFile(*stream);
+		if (!stream->seek(0)) {
+			warning("Couldn't reset stream to 0 after reading sample table for sound '%s'", sound.name.c_str());
+			return nullptr;
+		}
+	}
+
 	Audio::SeekableAudioStream *audioStream = Audio::makeWAVStream(stream, DisposeAfterUse::YES);
 	if (!audioStream) {
 		warning("Couldn't open audio stream for sound '%s'", sound.name.c_str());
@@ -2771,6 +2793,7 @@ SoundCache *Runtime::loadCache(SoundInstance &sound) {
 	Common::SharedPtr<SoundCache> cachedSound(new SoundCache());
 
 	cachedSound->stream.reset(audioStream);
+	cachedSound->loopInfo = loopInfo;
 
 	_soundCache[_soundCacheIndex].first = sound.name;
 	_soundCache[_soundCacheIndex].second = cachedSound;
@@ -3497,18 +3520,15 @@ void Runtime::setSound3DParameters(SoundInstance &snd, int32 x, int32 y, const S
 	snd.params3D = soundParams3D;
 }
 
-void Runtime::triggerSound(bool looping, SoundInstance &snd, int32 volume, int32 balance, bool is3D, bool isSpeech) {
-	SoundLoopingType oldLoopingType = snd.loopingType;
-
+void Runtime::triggerSound(SoundLoopBehavior soundLoopBehavior, SoundInstance &snd, int32 volume, int32 balance, bool is3D, bool isSpeech) {
 	snd.volume = volume;
 	snd.balance = balance;
 	snd.is3D = is3D;
 	snd.isSpeech = isSpeech;
-	snd.loopingType = (looping ? kSoundLoopingTypeLooping : kSoundLoopingTypeNotLooping);
 
 	computeEffectiveVolumeAndBalance(snd);
 
-	if (volume == getSilentSoundVolume() && looping) {
+	if (volume == getSilentSoundVolume()) {
 		if (snd.cache) {
 			if (snd.cache->player)
 				snd.cache->player.reset();
@@ -3516,38 +3536,55 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, int32 volume, int32
 			snd.cache.reset();
 		}
 
-		snd.isSilencedLoop = true;
+		snd.isLooping = (soundLoopBehavior == kSoundLoopBehaviorYes);
+		snd.restartWhenAudible = true;
+		snd.tryToLoopWhenRestarted = (soundLoopBehavior == kSoundLoopBehaviorAuto);
 		snd.endTime = 0;
 		snd.duration = 0;
 		return;
 	}
 
-	snd.isSilencedLoop = false;
+	snd.restartWhenAudible = false;
 
 	SoundCache *cache = loadCache(snd);
 
+	switch (soundLoopBehavior) {
+	case kSoundLoopBehaviorYes:
+		snd.isLooping = true;
+		break;
+	case kSoundLoopBehaviorNo:
+		snd.isLooping = false;
+		break;
+	case kSoundLoopBehaviorAuto:
+		snd.isLooping = (cache->loopInfo.get() != nullptr);
+		break;
+	default:
+		error("Invalid sound loop behavior");
+	};
+
 	snd.duration = cache->stream->getLength().msecs();
 
-	// Reset if looping state changes
-	if (cache->loopingStream && !looping) {
+	// Reset if transitioning from loop to non-loop
+	if (cache->isLoopActive && !snd.isLooping) {
 		cache->player.reset();
 		cache->loopingStream.reset();
 		cache->stream->rewind();
+		cache->isLoopActive = false;
 	}
 
 	// Construct looping stream if needed and none exists
-	// FIXME: Bracket group in following if statement needs confirming as intended form...
-	if ((looping && !cache->loopingStream) || oldLoopingType == kSoundLoopingTypeTerminated) {
+	if (snd.isLooping && !cache->isLoopActive) {
 		cache->player.reset();
 		cache->loopingStream.reset();
-		cache->loopingStream.reset(new Audio::LoopingAudioStream(cache->stream.get(), 0, DisposeAfterUse::NO, true));
+		cache->loopingStream.reset(new SampleLoopAudioStream(cache->stream.get(), cache->loopInfo.get()));
+		cache->isLoopActive = true;
 	}
 
 	const Audio::Mixer::SoundType soundType = (isSpeech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType);
 
 	if (cache->player) {
 		// If there is already a player and this is non-looping, start over
-		if (!looping) {
+		if (!snd.isLooping) {
 			cache->player->stop();
 			cache->stream->rewind();
 			cache->player->play(snd.effectiveVolume, snd.effectiveBalance);
@@ -3556,12 +3593,12 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, int32 volume, int32
 			cache->player->setVolumeAndBalance(snd.effectiveVolume, snd.effectiveBalance);
 		}
 	} else {
-		cache->player.reset(new AudioPlayer(_mixer, looping ? cache->loopingStream.staticCast<Audio::AudioStream>() : cache->stream.staticCast<Audio::AudioStream>(), soundType));
+		cache->player.reset(new AudioPlayer(_mixer, snd.isLooping ? cache->loopingStream.staticCast<Audio::AudioStream>() : cache->stream.staticCast<Audio::AudioStream>(), soundType));
 		cache->player->play(snd.effectiveVolume, snd.effectiveBalance);
 	}
 
 	snd.startTime = g_system->getMillis();
-	if (looping)
+	if (snd.isLooping)
 		snd.endTime = 0;
 	else
 		snd.endTime = snd.startTime + snd.duration + 1000u;
@@ -3574,7 +3611,7 @@ void Runtime::triggerSoundRamp(SoundInstance &snd, uint durationMSec, int32 newV
 	snd.rampStartTime = g_system->getMillis();
 	snd.rampRatePerMSec = 65536;
 
-	if (snd.loopingType == kSoundLoopingTypeLooping && newVolume == getSilentSoundVolume())
+	if (snd.isLooping && newVolume == getSilentSoundVolume())
 		snd.rampTerminateOnCompletion = true;
 
 	if (durationMSec)
@@ -3650,7 +3687,8 @@ void Runtime::stopSound(SoundInstance &sound) {
 	sound.cache->player.reset();
 	sound.cache.reset();
 	sound.endTime = 0;
-	sound.isSilencedLoop = false;
+	sound.restartWhenAudible = false;
+	sound.tryToLoopWhenRestarted = false;
 }
 
 void Runtime::convertLoopingSoundToNonLooping(SoundInstance &sound) {
@@ -3658,8 +3696,9 @@ void Runtime::convertLoopingSoundToNonLooping(SoundInstance &sound) {
 		return;
 
 	if (sound.cache->loopingStream) {
-		sound.cache->loopingStream->setRemainingIterations(1);
-		sound.loopingType = kSoundLoopingTypeTerminated;
+		sound.cache->loopingStream->stopLooping();
+		sound.cache->isLoopActive = false;
+		sound.isLooping = false;
 
 		uint32 currentTime = g_system->getMillis();
 
@@ -3704,19 +3743,32 @@ void Runtime::updateSounds(uint32 timestamp) {
 			snd.endTime = 0;
 		}
 
-		if (snd.loopingType == kSoundLoopingTypeLooping) {
+		if (snd.isLooping) {
 			if (snd.volume <= getSilentSoundVolume()) {
-				if (!snd.isSilencedLoop) {
+				if (!snd.restartWhenAudible) {
 					if (snd.cache) {
 						snd.cache->player.reset();
 						snd.cache.reset();
 					}
-					snd.isSilencedLoop = true;
+					snd.restartWhenAudible = true;
 				}
 			} else {
-				if (snd.isSilencedLoop) {
-					triggerSound(true, snd, snd.volume, snd.balance, snd.is3D, snd.isSpeech);
-					assert(snd.isSilencedLoop == false);
+				if (snd.restartWhenAudible) {
+					triggerSound(kSoundLoopBehaviorYes, snd, snd.volume, snd.balance, snd.is3D, snd.isSpeech);
+					assert(snd.restartWhenAudible == false);
+				}
+			}
+		} else {
+			if (snd.volume > getSilentSoundVolume()) {
+				if (snd.restartWhenAudible) {
+					SoundLoopBehavior loopBehavior = kSoundLoopBehaviorNo;
+					if (snd.tryToLoopWhenRestarted) {
+						loopBehavior = kSoundLoopBehaviorAuto;
+						snd.tryToLoopWhenRestarted = false;
+					}
+
+					triggerSound(loopBehavior, snd, snd.volume, snd.balance, snd.is3D, snd.isSpeech);
+					assert(snd.restartWhenAudible == false);
 				}
 			}
 		}
@@ -3934,7 +3986,7 @@ void Runtime::triggerAmbientSounds() {
 			resolveSoundByName(sound.name, true, soundID, cachedSound);
 
 			if (cachedSound) {
-				triggerSound(false, *cachedSound, sound.volume, sound.balance, false, false);
+				triggerSound(kSoundLoopBehaviorNo, *cachedSound, sound.volume, sound.balance, false, false);
 				if (cachedSound->cache)
 					_ambientSoundFinishTime = timestamp + static_cast<uint>(cachedSound->cache->stream->getLength().msecs());
 			}
@@ -5103,7 +5155,8 @@ void Runtime::recordSounds(SaveGameSwappableState &state) {
 		}
 
 		saveSound.is3D = sound.is3D;
-		saveSound.isLooping = (sound.loopingType == kSoundLoopingTypeLooping);
+		saveSound.isLooping = sound.isLooping;
+		saveSound.tryToLoopWhenRestarted = sound.tryToLoopWhenRestarted;
 		saveSound.isSpeech = sound.isSpeech;
 		saveSound.x = sound.x;
 		saveSound.y = sound.y;
@@ -5199,7 +5252,8 @@ void Runtime::restoreSaveGameSnapshot() {
 		si->volume = sound.volume;
 		si->balance = sound.balance;
 		si->is3D = sound.is3D;
-		si->loopingType = (sound.isLooping ? kSoundLoopingTypeLooping : kSoundLoopingTypeNotLooping);
+		si->isLooping = sound.isLooping;
+		si->tryToLoopWhenRestarted = sound.tryToLoopWhenRestarted;
 		si->isSpeech = sound.isSpeech;
 		si->x = sound.x;
 		si->y = sound.y;
@@ -5208,7 +5262,9 @@ void Runtime::restoreSaveGameSnapshot() {
 		_activeSounds.push_back(si);
 
 		if (sound.isLooping)
-			triggerSound(true, *si, si->volume, si->balance, si->is3D, si->isSpeech);
+			triggerSound(kSoundLoopBehaviorYes, *si, si->volume, si->balance, si->is3D, si->isSpeech);
+		else if (sound.tryToLoopWhenRestarted)
+			triggerSound(kSoundLoopBehaviorAuto, *si, getSilentSoundVolume(), si->balance, si->is3D, si->isSpeech);
 	}
 
 	uint anim = mainState->loadedAnimation;
@@ -5832,7 +5888,7 @@ void Runtime::scriptOpSoundS1(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(false, *cachedSound, 100, 0, false, false);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, false);
 }
 
 void Runtime::scriptOpSoundS2(ScriptArg_t arg) {
@@ -5844,7 +5900,7 @@ void Runtime::scriptOpSoundS2(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(false, *cachedSound, sndParamArgs[0], 0, false, false);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], 0, false, false);
 }
 
 void Runtime::scriptOpSoundS3(ScriptArg_t arg) {
@@ -5856,7 +5912,7 @@ void Runtime::scriptOpSoundS3(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(false, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
 }
 
 void Runtime::scriptOpSoundL1(ScriptArg_t arg) {
@@ -5867,7 +5923,7 @@ void Runtime::scriptOpSoundL1(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(true, *cachedSound, getDefaultSoundVolume(), 0, false, false);
+		triggerSound(kSoundLoopBehaviorYes, *cachedSound, getDefaultSoundVolume(), 0, false, false);
 }
 
 void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
@@ -5879,7 +5935,7 @@ void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(true, *cachedSound, sndParamArgs[0], 0, false, false);
+		triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], 0, false, false);
 }
 
 void Runtime::scriptOpSoundL3(ScriptArg_t arg) {
@@ -5891,7 +5947,7 @@ void Runtime::scriptOpSoundL3(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(true, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
+		triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
 }
 
 void Runtime::scriptOp3DSoundL2(ScriptArg_t arg) {
@@ -5904,7 +5960,7 @@ void Runtime::scriptOp3DSoundL2(ScriptArg_t arg) {
 
 	if (cachedSound) {
 		setSound3DParameters(*cachedSound, sndParamArgs[1], sndParamArgs[2], _pendingSoundParams3D);
-		triggerSound(true, *cachedSound, sndParamArgs[0], 0, true, false);
+		triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], 0, true, false);
 	}
 }
 
@@ -5918,7 +5974,7 @@ void Runtime::scriptOp3DSoundL3(ScriptArg_t arg) {
 
 	if (cachedSound) {
 		setSound3DParameters(*cachedSound, sndParamArgs[2], sndParamArgs[3], _pendingSoundParams3D);
-		triggerSound(true, *cachedSound, sndParamArgs[0], sndParamArgs[1], true, false);
+		triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], sndParamArgs[1], true, false);
 	}
 }
 
@@ -5932,7 +5988,7 @@ void Runtime::scriptOp3DSoundS2(ScriptArg_t arg) {
 
 	if (cachedSound) {
 		setSound3DParameters(*cachedSound, sndParamArgs[1], sndParamArgs[2], _pendingSoundParams3D);
-		triggerSound(false, *cachedSound, sndParamArgs[0], 0, true, false);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], 0, true, false);
 	}
 }
 
@@ -6232,7 +6288,7 @@ void Runtime::scriptOpSay1(ScriptArg_t arg) {
 	resolveSoundByName(soundIDStr, true, soundID, cachedSound);
 
 	if (cachedSound) {
-		triggerSound(false, *cachedSound, 100, 0, false, true);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
 		triggerWaveSubtitles(*cachedSound, soundIDStr);
 	}
 }
@@ -6250,7 +6306,7 @@ void Runtime::scriptOpSay2(ScriptArg_t arg) {
 		if (sndParamArgs[1] != 1)
 			error("Invalid interrupt arg for say2, only 1 is supported.");
 
-		triggerSound(false, *cachedSound, 100, 0, false, true);
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
 		triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
 	}
 }
@@ -6273,7 +6329,7 @@ void Runtime::scriptOpSay3(ScriptArg_t arg) {
 			error("Invalid interrupt arg for say3, only 1 is supported.");
 
 		if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
-			triggerSound(false, *cachedSound, 100, 0, false, true);
+			triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
 			_triggeredOneShots.push_back(oneShot);
 
 			triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
@@ -6299,7 +6355,7 @@ void Runtime::scriptOpSay3Get(ScriptArg_t arg) {
 			error("Invalid interrupt arg for say3, only 1 is supported.");
 
 		if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
-			triggerSound(false, *cachedSound, 100, 0, false, true);
+			triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
 			_triggeredOneShots.push_back(oneShot);
 			_scriptStack.push_back(StackValue(soundID));
 		} else
@@ -6765,7 +6821,7 @@ void Runtime::scriptOpSndPlay(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(true, *cachedSound, getSilentSoundVolume(), 0, false, false);
+		triggerSound(kSoundLoopBehaviorAuto, *cachedSound, getSilentSoundVolume(), 0, false, false);
 }
 
 void Runtime::scriptOpSndPlayEx(ScriptArg_t arg) {
@@ -6786,12 +6842,8 @@ void Runtime::scriptOpSndPlayEx(ScriptArg_t arg) {
 	SoundInstance *cachedSound = nullptr;
 	resolveSoundByName(soundName, true, soundID, cachedSound);
 
-	// TODO: Need to figure out how looping is determined here.
-	// It looks like SndPlayEx is only used for non-looping.  For example,
-	// 3512_pour is called with SndPlayEx.  However, this doesn't explain what
-	// the SndHalt opcode is used for.
 	if (cachedSound)
-		triggerSound(false, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
+		triggerSound(kSoundLoopBehaviorAuto, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false);
 }
 
 void Runtime::scriptOpSndPlay3D(ScriptArg_t arg) {
@@ -6808,10 +6860,8 @@ void Runtime::scriptOpSndPlay3D(ScriptArg_t arg) {
 	sndParams.unknownRange = sndParamArgs[4]; // Doesn't appear to be the same thing as Reah.  Usually 1000, sometimes 2000 or 3000.
 
 	if (cachedSound) {
-		// FIXME: Should this be looping?  In the prayer bell puzzle, the prayer sounds (such as 6511_prayer)
-		// don't have a SndHalt afterwards, so 
 		setSound3DParameters(*cachedSound, sndParamArgs[0], sndParamArgs[1], sndParams);
-		triggerSound(false, *cachedSound, getSilentSoundVolume(), 0, true, false);
+		triggerSound(kSoundLoopBehaviorAuto, *cachedSound, getSilentSoundVolume(), 0, true, false);
 	}
 }
 
@@ -6960,7 +7010,7 @@ void Runtime::scriptOpSpeechEx(ScriptArg_t arg) {
 		oneShot.uniqueSlot = sndParamArgs[0];
 
 		if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
-			triggerSound(false, *cachedSound, sndParamArgs[1], 0, false, true);
+			triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[1], 0, false, true);
 			_triggeredOneShots.push_back(oneShot);
 
 			triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index d365c29dd21..95c908273d1 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -85,6 +85,8 @@ struct Script;
 struct IScriptCompilerGlobalState;
 struct Instruction;
 struct RoomScriptSet;
+struct SoundLoopInfo;
+class SampleLoopAudioStream;
 
 enum GameState {
 	kGameStateBoot,							// Booting the game
@@ -215,18 +217,18 @@ struct SoundParams3D {
 	void read(Common::ReadStream *stream);
 };
 
+
 struct SoundCache {
+	SoundCache();
 	~SoundCache();
 
+	Common::SharedPtr<SoundLoopInfo> loopInfo;
+
 	Common::SharedPtr<Audio::SeekableAudioStream> stream;
-	Common::SharedPtr<Audio::LoopingAudioStream> loopingStream;
+	Common::SharedPtr<SampleLoopAudioStream> loopingStream;
 	Common::SharedPtr<AudioPlayer> player;
-};
 
-enum SoundLoopingType {
-	kSoundLoopingTypeNotLooping,	// Was never looping
-	kSoundLoopingTypeTerminated,	// Was looping and then converted into non-looping
-	kSoundLoopingTypeLooping,		// Is looping
+	bool isLoopActive;
 };
 
 struct SoundInstance {
@@ -251,9 +253,10 @@ struct SoundInstance {
 	int32 effectiveBalance;
 
 	bool is3D;
-	SoundLoopingType loopingType;
+	bool isLooping;
 	bool isSpeech;
-	bool isSilencedLoop;	// Loop is still playing but reached 0 volume so the player was unloaded
+	bool restartWhenAudible;
+	bool tryToLoopWhenRestarted;
 	int32 x;
 	int32 y;
 
@@ -405,6 +408,7 @@ struct SaveGameSwappableState {
 
 		bool is3D;
 		bool isLooping;
+		bool tryToLoopWhenRestarted;
 		bool isSpeech;
 
 		int32 x;
@@ -413,7 +417,7 @@ struct SaveGameSwappableState {
 		SoundParams3D params3D;
 
 		void write(Common::WriteStream *stream) const;
-		void read(Common::ReadStream *stream);
+		void read(Common::ReadStream *stream, uint saveGameVersion);
 	};
 
 	SaveGameSwappableState();
@@ -447,7 +451,7 @@ struct SaveGameSnapshot {
 	LoadGameOutcome read(Common::ReadStream *stream);
 
 	static const uint kSaveGameIdentifier = 0x53566372;
-	static const uint kSaveGameCurrentVersion = 7;
+	static const uint kSaveGameCurrentVersion = 8;
 	static const uint kSaveGameEarliestSupportedVersion = 2;
 	static const uint kMaxStates = 2;
 
@@ -700,6 +704,12 @@ private:
 		kInGameMenuStateClickingInactive,
 	};
 
+	enum SoundLoopBehavior {
+		kSoundLoopBehaviorNo,
+		kSoundLoopBehaviorYes,
+		kSoundLoopBehaviorAuto,
+	};
+
 	static const uint kPanLeftInteraction = 1;
 	static const uint kPanDownInteraction = 2;
 	static const uint kPanRightInteraction = 3;
@@ -831,7 +841,7 @@ private:
 	void applyAnimationVolume();
 
 	void setSound3DParameters(SoundInstance &sound, int32 x, int32 y, const SoundParams3D &soundParams3D);
-	void triggerSound(bool looping, SoundInstance &sound, int32 volume, int32 balance, bool is3D, bool isSpeech);
+	void triggerSound(SoundLoopBehavior loopBehavior, SoundInstance &sound, int32 volume, int32 balance, bool is3D, bool isSpeech);
 	void triggerSoundRamp(SoundInstance &sound, uint durationMSec, int32 newVolume, bool terminateOnCompletion);
 	void stopSound(SoundInstance &sound);
 	void convertLoopingSoundToNonLooping(SoundInstance &sound);
diff --git a/engines/vcruise/sampleloop.cpp b/engines/vcruise/sampleloop.cpp
new file mode 100644
index 00000000000..803a24e3991
--- /dev/null
+++ b/engines/vcruise/sampleloop.cpp
@@ -0,0 +1,362 @@
+/* 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/stream.h"
+
+#include "vcruise/sampleloop.h"
+
+namespace VCruise {
+
+SampleLoop::SampleLoop() : identifier(0), type(0), start(0), end(0), fraction(0), playCount(0) {
+}
+
+bool SampleLoop::read(Common::ReadStream &stream, uint &availableBytes) {
+	if (availableBytes < 24)
+		return false;
+
+	byte bytes[24];
+
+	uint32 bytesRead = stream.read(bytes, 24);
+	availableBytes -= bytesRead;
+	if (bytesRead != 24)
+		return false;
+
+	this->identifier = READ_LE_UINT32(bytes + 0);
+	this->type = READ_LE_UINT32(bytes + 4);
+	this->start = READ_LE_UINT32(bytes + 8);
+	this->end = READ_LE_UINT32(bytes + 12);
+	this->fraction = READ_LE_UINT32(bytes + 16);
+	this->playCount = READ_LE_UINT32(bytes + 20);
+
+	return true;
+}
+
+SampleChunk::SampleChunk() : manufacturer(0), product(0), samplePeriod(0), midiUnityNote(0), midiPitchFraction(0), smpteFormat(0), smpteOffset(0) {
+}
+
+bool SampleChunk::read(Common::ReadStream &stream, uint &availableBytes) {
+	if (availableBytes < 36)
+		return false;
+
+	byte bytes[36];
+
+	uint32 bytesRead = stream.read(bytes, 36);
+	availableBytes -= bytesRead;
+	if (bytesRead != 36)
+		return false;
+	
+	this->manufacturer = READ_LE_UINT32(bytes + 0);
+	this->product = READ_LE_UINT32(bytes + 4);
+	this->samplePeriod = READ_LE_UINT32(bytes + 8);
+	this->midiUnityNote = READ_LE_UINT32(bytes + 12);
+	this->midiPitchFraction = READ_LE_UINT32(bytes + 16);
+	this->smpteFormat = READ_LE_UINT32(bytes + 20);
+	this->smpteOffset = READ_LE_UINT32(bytes + 24);
+
+	uint32 numSampleLoops = READ_LE_UINT32(bytes + 28);
+	uint32 sizeOfSamplerData = READ_LE_UINT32(bytes + 32);
+
+	loops.resize(numSampleLoops);
+	samplerData.resize(sizeOfSamplerData);
+
+	for (uint32 i = 0; i < numSampleLoops; i++)
+		if (!loops[i].read(stream, availableBytes))
+			return false;
+
+	if (sizeOfSamplerData > 0) {
+		if (availableBytes < sizeOfSamplerData)
+			return false;
+
+		bytesRead = stream.read(&samplerData[0], sizeOfSamplerData);
+		availableBytes -= bytesRead;
+		if (bytesRead != sizeOfSamplerData)
+			return false;
+	}
+
+	return true;
+}
+
+Common::SharedPtr<SoundLoopInfo> SoundLoopInfo::readFromWaveFile(Common::SeekableReadStream &stream) {
+	if (!stream.seek(0))
+		return nullptr;
+
+	int64 waveSize64 = stream.size();
+
+	if (waveSize64 > static_cast<int64>(0xffffffffu))
+		return nullptr;
+
+	uint availableBytes = waveSize64;
+
+	if (availableBytes < 8)
+		return nullptr;
+
+	byte riffHeader[8];
+	if (stream.read(riffHeader, 8) != 8)
+		return nullptr;
+
+	availableBytes -= 8;
+
+	if (READ_LE_UINT32(riffHeader + 0) != 0x46464952)
+		return nullptr;
+
+	uint riffDataSize = READ_LE_UINT32(riffHeader + 4);
+
+	if (riffDataSize > availableBytes)
+		return nullptr;
+
+	availableBytes = riffDataSize;
+
+	if (availableBytes < 4)
+		return nullptr;
+
+	byte waveHeader[4];
+	if (stream.read(waveHeader, 4) != 4)
+		return nullptr;
+
+	availableBytes -= 4;
+
+	if (READ_LE_UINT32(waveHeader + 0) != 0x45564157)
+		return nullptr;
+
+	while (availableBytes > 0) {
+		if (availableBytes < 8)
+			return nullptr;
+
+		byte chunkHeader[8];
+		if (stream.read(chunkHeader, 8) != 8)
+			return nullptr;
+
+		availableBytes -= 8;
+
+		uint32 chunkType = READ_LE_UINT32(chunkHeader + 0);
+		uint32 chunkSize = READ_LE_UINT32(chunkHeader + 4);
+
+		if (chunkSize > availableBytes)
+			return nullptr;
+
+		if (chunkType == 0x6c706d73) {
+			Common::SharedPtr<SoundLoopInfo> sndLoop(new SoundLoopInfo());
+
+			uint chunkAvailableBytes = chunkSize;
+			if (!sndLoop->_sampleChunk.read(stream, chunkAvailableBytes))
+				return nullptr;
+
+			if (sndLoop->_sampleChunk.loops.size() == 0)
+				return nullptr;
+
+			return sndLoop;
+		}
+
+		if (!stream.seek(chunkSize, SEEK_CUR))
+			return nullptr;
+
+		availableBytes -= chunkSize;
+	}
+
+	return nullptr;
+}
+
+SampleLoopAudioStream::LoopRange::LoopRange() : _startSampleInclusive(0), _endSampleExclusive(0), _playCount(0) {
+}
+
+SampleLoopAudioStream::SampleLoopAudioStream(Audio::SeekableAudioStream *baseStream, const SoundLoopInfo *loopInfo)
+	: _baseStream(baseStream), _terminated(false), _ignoreLoops(false), _currentSampleOffset(0), _currentLoop(-1), _currentLoopIteration(0), _streamFrames(0), _streamSamples(0) {
+
+	_streamFrames = baseStream->getLength().convertToFramerate(baseStream->getRate()).totalNumberOfFrames();
+	_streamSamples = _streamFrames;
+
+	if (_baseStream->isStereo())
+		_streamSamples *= 2;
+
+	if (loopInfo) {
+		_loopRanges.resize(loopInfo->_sampleChunk.loops.size());
+		for (uint i = 0; i < _loopRanges.size(); i++) {
+			const SampleLoop &inLoop = loopInfo->_sampleChunk.loops[i];
+			LoopRange &outLoop = _loopRanges[i];
+
+			outLoop._startSampleInclusive = inLoop.start;
+			outLoop._endSampleExclusive = inLoop.end;
+			outLoop._playCount = inLoop.playCount;
+		}
+	} else {
+		_loopRanges.resize(1);
+		_loopRanges[0]._startSampleInclusive = 0;
+		_loopRanges[0]._endSampleExclusive = _streamFrames;
+	}
+
+	for (LoopRange &range : _loopRanges) {
+		range._restartTimestamp = Audio::Timestamp(0, range._startSampleInclusive, baseStream->getRate());
+
+		if (range._startSampleInclusive > static_cast<uint>(_streamFrames))
+			range._startSampleInclusive = _streamFrames;
+		if (range._endSampleExclusive > static_cast<uint>(_streamFrames))
+			range._endSampleExclusive = _streamFrames;
+		if (range._endSampleExclusive < range._startSampleInclusive)
+			range._endSampleExclusive = range._startSampleInclusive;
+
+		if (_baseStream->isStereo()) {
+			range._startSampleInclusive *= 2;
+			range._endSampleExclusive *= 2;
+		}
+	}
+
+	for (uint i = 0; i < _loopRanges.size(); ) {
+		const LoopRange &thisRange = _loopRanges[i];
+
+		bool isValid = true;
+		if (thisRange._endSampleExclusive == thisRange._startSampleInclusive)
+			isValid = false;
+
+		if (i > 0) {
+			const LoopRange &prevRange = _loopRanges[i - 1];
+			if (thisRange._startSampleInclusive < prevRange._endSampleExclusive)
+				isValid = false;
+		}
+
+		if (isValid)
+			i++;
+		else
+			_loopRanges.remove_at(i);
+	}
+
+}
+
+SampleLoopAudioStream::~SampleLoopAudioStream() {
+}
+
+void SampleLoopAudioStream::stopLooping() {
+	_mutex.lock();
+	_ignoreLoops = true;
+	_mutex.unlock();
+}
+
+int SampleLoopAudioStream::readBuffer(int16 *buffer, int numSamples) {
+	bool ignoreLoops = false;
+
+	_mutex.lock();
+	ignoreLoops = _ignoreLoops;
+	_mutex.unlock();
+
+	int totalSamplesRead = 0;
+
+	for (;;) {
+		if (_terminated || numSamples == 0)
+			return totalSamplesRead;
+
+		int consecutiveSamplesAvailable = 0;
+		bool terminateIfReadCompletes = false;
+
+		if (_ignoreLoops) {
+			consecutiveSamplesAvailable = _streamSamples - _currentSampleOffset;
+			terminateIfReadCompletes = true;
+		} else if (_currentLoop < 0) {
+			// Not currently in a loop
+			int samplesUntilLoop = -1;
+			uint scanLoopIndex = 0;
+			for (const LoopRange &loopRange : _loopRanges) {
+				if (static_cast<int>(loopRange._startSampleInclusive) >= _currentSampleOffset) {
+					samplesUntilLoop = loopRange._startSampleInclusive - _currentSampleOffset;
+					break;
+				} else
+					scanLoopIndex++;
+			}
+
+			if (samplesUntilLoop < 0) {
+				// Past the end of the last loop
+				consecutiveSamplesAvailable = _streamSamples - _currentSampleOffset;
+				terminateIfReadCompletes = true;
+			} else if (samplesUntilLoop == 0) {
+				// At the start of a loop
+				_currentLoop = scanLoopIndex;
+				_currentLoopIteration = 0;
+				continue;
+			} else {
+				// Before a loop
+				consecutiveSamplesAvailable = samplesUntilLoop;
+			}
+		} else {
+			// In a loop
+			const LoopRange &loopRange = _loopRanges[_currentLoop];
+
+			int samplesAvailable = loopRange._endSampleExclusive - static_cast<int>(_currentSampleOffset);
+			if (samplesAvailable == 0) {
+				// At the end of the loop
+				if (loopRange._playCount > 0) {
+					if (_currentLoopIteration == loopRange._playCount) {
+						// Exit loop
+						_currentLoop = -1;
+						continue;
+					} else
+						_currentLoopIteration++;
+				}
+
+				if (!_baseStream->seek(loopRange._restartTimestamp)) {
+					_terminated = true;
+					return totalSamplesRead;
+				}
+
+				_currentSampleOffset = loopRange._startSampleInclusive;
+				continue;
+			} else {
+				// Inside of a loop
+				consecutiveSamplesAvailable = samplesAvailable;
+			}
+		}
+
+		if (consecutiveSamplesAvailable == 0)
+			_terminated = true;
+		else {
+			int samplesDesired = numSamples;
+			if (samplesDesired > consecutiveSamplesAvailable)
+				samplesDesired = consecutiveSamplesAvailable;
+
+			int samplesRead = _baseStream->readBuffer(buffer, samplesDesired);
+
+			if (samplesRead > 0)
+				totalSamplesRead += samplesRead;
+
+			if (samplesRead != samplesDesired)
+				_terminated = true;
+			else {
+				_currentSampleOffset += samplesRead;
+				buffer += samplesRead;
+				numSamples -= samplesRead;
+
+				if (samplesRead == consecutiveSamplesAvailable && terminateIfReadCompletes)
+					_terminated = true;
+			}
+		}
+	}
+}
+
+bool SampleLoopAudioStream::isStereo() const {
+	return _baseStream->isStereo();
+}
+
+int SampleLoopAudioStream::getRate() const {
+	return _baseStream->getRate();
+}
+
+bool SampleLoopAudioStream::endOfData() const {
+	return _terminated;
+}
+
+} // namespace VCruise
diff --git a/engines/vcruise/sampleloop.h b/engines/vcruise/sampleloop.h
new file mode 100644
index 00000000000..102867f6fb1
--- /dev/null
+++ b/engines/vcruise/sampleloop.h
@@ -0,0 +1,117 @@
+/* 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/>.
+ *
+ */
+
+#ifndef VCRUISE_SAMPLELOOP_H
+#define VCRUISE_SAMPLELOOP_H
+
+#include "common/ptr.h"
+#include "common/mutex.h"
+
+#include "audio/audiostream.h"
+
+namespace Common {
+
+class ReadStream;
+
+} // End of namespace Common
+
+namespace VCruise {
+
+struct SampleLoop {
+	SampleLoop();
+
+	bool read(Common::ReadStream &stream, uint &availableBytes);
+
+	uint32 identifier;
+	uint32 type;
+	uint32 start;
+	uint32 end;
+	uint32 fraction;
+	uint32 playCount;
+};
+
+struct SampleChunk {
+	SampleChunk();
+
+	bool read(Common::ReadStream &stream, uint &availableBytes);
+
+	uint32 manufacturer;
+	uint32 product;
+	uint32 samplePeriod;
+	uint32 midiUnityNote;
+	uint32 midiPitchFraction;
+	uint32 smpteFormat;
+	uint32 smpteOffset;
+
+	// uint32 numSampleLoops;
+	// uint32 sizeOfSamplerData;
+
+	Common::Array<SampleLoop> loops;
+	Common::Array<byte> samplerData;
+};
+
+struct SoundLoopInfo {
+	SampleChunk _sampleChunk;
+
+	static Common::SharedPtr<SoundLoopInfo> readFromWaveFile(Common::SeekableReadStream &stream);
+};
+
+class SampleLoopAudioStream : public Audio::AudioStream {
+public:
+	SampleLoopAudioStream(Audio::SeekableAudioStream *baseStream, const SoundLoopInfo *loopInfo);
+	virtual ~SampleLoopAudioStream();
+
+	void stopLooping();
+
+	int readBuffer(int16 *buffer, const int numSamples) override;
+	bool isStereo() const override;
+	int getRate() const override;
+	bool endOfData() const override;
+
+private:
+	struct LoopRange {
+		LoopRange();
+
+		Audio::Timestamp _restartTimestamp;
+
+		uint _startSampleInclusive;
+		uint _endSampleExclusive;
+		uint _playCount;
+	};
+
+	Common::Mutex _mutex;
+
+	int _currentSampleOffset;
+	int _currentLoop;
+	uint _currentLoopIteration;
+	int _streamFrames;
+	int _streamSamples;
+	bool _terminated;
+	bool _ignoreLoops;
+
+	Common::Array<LoopRange> _loopRanges;
+
+	Audio::SeekableAudioStream *_baseStream;
+};
+
+} // End of namespace VCruise
+
+#endif




More information about the Scummvm-git-logs mailing list