1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "audio/mixer.h"
24 #include "common/config-manager.h"
25 #include "common/gui_options.h"
26 #include "common/savefile.h"
27 #include "sci/engine/features.h"
28 #include "sci/engine/file.h"
29 #include "sci/engine/guest_additions.h"
30 #include "sci/engine/kernel.h"
31 #include "sci/engine/savegame.h"
32 #include "sci/engine/state.h"
33 #include "sci/engine/vm.h"
34 #ifdef ENABLE_SCI32
35 #include "common/translation.h"
36 #include "gui/saveload.h"
37 #include "sci/graphics/frameout.h"
38 #endif
39 #include "sci/sound/music.h"
40 #include "sci/sci.h"
41 
42 namespace Sci {
43 
44 enum {
45 	kSoundsMusicType = 0,
46 	kSoundsSoundType = 1
47 };
48 
49 enum {
50 	kMessageTypeSubtitles = 1,
51 	kMessageTypeSpeech    = 2
52 };
53 
GuestAdditions(EngineState * state,GameFeatures * features,Kernel * kernel)54 GuestAdditions::GuestAdditions(EngineState *state, GameFeatures *features, Kernel *kernel) :
55 	_state(state),
56 	_features(features),
57 	_kernel(kernel),
58 	_segMan(state->_segMan),
59 #ifdef ENABLE_SCI32
60 	_restoring(false),
61 #endif
62 	_messageTypeSynced(false) {}
63 
64 #pragma mark -
65 
syncSoundSettingsFromScummVM() const66 void GuestAdditions::syncSoundSettingsFromScummVM() const {
67 	if (_features->audioVolumeSyncUsesGlobals())
68 		syncAudioVolumeGlobalsFromScummVM();
69 	else
70 		syncMasterVolumeFromScummVM();
71 }
72 
syncAudioOptionsFromScummVM() const73 void GuestAdditions::syncAudioOptionsFromScummVM() const {
74 #ifdef ENABLE_SCI32
75 	if (_features->supportsTextSpeed()) {
76 		syncTextSpeedFromScummVM();
77 	}
78 #endif
79 	syncMessageTypeFromScummVM();
80 }
81 
reset()82 void GuestAdditions::reset() {
83 	_messageTypeSynced = false;
84 }
85 
invokeSelector(const reg_t objId,const Selector selector,const int argc,const StackPtr argv) const86 void GuestAdditions::invokeSelector(const reg_t objId, const Selector selector, const int argc, const StackPtr argv) const {
87 	::Sci::invokeSelector(_state, objId, selector, 0, _state->_executionStack.back().sp, argc, argv);
88 }
89 
shouldSyncAudioToScummVM() const90 bool GuestAdditions::shouldSyncAudioToScummVM() const {
91 	const SciGameId gameId = g_sci->getGameId();
92 	Common::List<ExecStack>::const_iterator it;
93 	for (it = _state->_executionStack.begin(); it != _state->_executionStack.end(); ++it) {
94 		const ExecStack &call = *it;
95 		const Common::String objName = _segMan->getObjectName(call.sendp);
96 
97 		if (getSciVersion() < SCI_VERSION_2 && (objName == "TheMenuBar" ||
98 												objName == "MenuBar")) {
99 			// SCI16 with menu bar
100 			return true;
101 		} else if (objName == "volumeSlider") {
102 			// SCI16 with icon bar, QFG4, Hoyle5, RAMA
103 			return true;
104 		} else if (gameId == GID_MOTHERGOOSE256 && objName == "soundBut") {
105 			return true;
106 		} else if (gameId == GID_SLATER && objName == "volButton") {
107 			return true;
108 		} else if (gameId == GID_LSL6 && (objName == "menuBar" ||
109 										  objName == "volumeDial")) {
110 			return true;
111 #ifdef ENABLE_SCI32
112 		} else if ((gameId == GID_GK1 || gameId == GID_SQ6) && (objName == "musicBar" ||
113 																objName == "soundBar")) {
114 			return true;
115 		} else if (gameId == GID_GK2 && objName == "soundSlider") {
116 			return true;
117 		} else if (gameId == GID_HOYLE5 && objName == "volumeSliderF") {
118 			// Hoyle5 has a second control panel with a different slider name
119 			return true;
120 		} else if (gameId == GID_KQ7 && (objName == "volumeUp" ||
121 										 objName == "volumeDown")) {
122 			return true;
123 		} else if (gameId == GID_LSL6HIRES && (objName == "hiResMenu" ||
124 											   objName == "volumeDial")) {
125 			return true;
126 		} else if ((gameId == GID_LSL7 || gameId == GID_TORIN) && (objName == "oMusicScroll" ||
127 																   objName == "oSFXScroll" ||
128 																   objName == "oAudioScroll")) {
129 			return true;
130 		} else if (gameId == GID_MOTHERGOOSEHIRES && objName == "MgButtonBar") {
131 			return true;
132 		} else if (gameId == GID_PHANTASMAGORIA && (objName == "midiVolDown" ||
133 													objName == "midiVolUp" ||
134 													objName == "dacVolDown" ||
135 													objName == "dacVolUp")) {
136 			return true;
137 		} else if (gameId == GID_PHANTASMAGORIA2 && objName == "foo2") {
138 			return true;
139 		} else if (gameId == GID_PQ4 && (objName == "increaseVolume" ||
140 										 objName == "decreaseVolume")) {
141 			return true;
142 		} else if (gameId == GID_PQSWAT && (objName == "volumeDownButn" ||
143 											objName == "volumeUpButn")) {
144 			return true;
145 		} else if (gameId == GID_SHIVERS && objName == "spVolume") {
146 			return true;
147 #endif
148 		}
149 	}
150 
151 	return false;
152 }
153 
154 #pragma mark -
155 #pragma mark Hooks
156 
sciEngineRunGameHook()157 void GuestAdditions::sciEngineRunGameHook() {
158 	_messageTypeSynced = true;
159 }
160 
writeVarHook(const int type,const int index,const reg_t value)161 void GuestAdditions::writeVarHook(const int type, const int index, const reg_t value) {
162 	if (type == VAR_GLOBAL) {
163 		if (_features->audioVolumeSyncUsesGlobals() && shouldSyncAudioToScummVM()) {
164 			syncAudioVolumeGlobalsToScummVM(index, value);
165 #ifdef ENABLE_SCI32
166 		} else if (g_sci->getGameId() == GID_GK1) {
167 			syncGK1StartupVolumeFromScummVM(index, value);
168 		} else if (g_sci->getGameId() == GID_HOYLE5 && index == kGlobalVarHoyle5MusicVolume) {
169 			syncHoyle5VolumeFromScummVM((ConfMan.getInt("music_volume") + 1) * kHoyle5VolumeMax / Audio::Mixer::kMaxMixerVolume);
170 		} else if (g_sci->getGameId() == GID_RAMA && !g_sci->isDemo() && index == kGlobalVarRamaMusicVolume) {
171 			syncRamaVolumeFromScummVM((ConfMan.getInt("music_volume") + 1) * kRamaVolumeMax / Audio::Mixer::kMaxMixerVolume);
172 		}
173 
174 		if (_features->supportsTextSpeed()) {
175 			syncTextSpeedToScummVM(index, value);
176 #endif
177 		}
178 
179 		syncMessageTypeToScummVM(index, value);
180 	}
181 }
182 
kDoSoundMasterVolumeHook(const int volume) const183 bool GuestAdditions::kDoSoundMasterVolumeHook(const int volume) const {
184 	if (!_features->audioVolumeSyncUsesGlobals() && shouldSyncAudioToScummVM()) {
185 		syncMasterVolumeToScummVM(volume);
186 		return true;
187 	}
188 	return false;
189 }
190 
191 #ifdef ENABLE_SCI32
sciEngineInitGameHook()192 void GuestAdditions::sciEngineInitGameHook() {
193 	if (g_sci->getGameId() == GID_PHANTASMAGORIA2 && Common::checkGameGUIOption(GAMEOPTION_ENABLE_CENSORING, ConfMan.get("guioptions"))) {
194 		_state->variables[VAR_GLOBAL][kGlobalVarPhant2CensorshipFlag] = make_reg(0, ConfMan.getBool("enable_censoring"));
195 	}
196 
197 	if (g_sci->getGameId() == GID_KQ7 && Common::checkGameGUIOption(GAMEOPTION_UPSCALE_VIDEOS, ConfMan.get("guioptions"))) {
198 		uint16 value = ConfMan.getBool("enable_video_upscale") ? 32 : 0;
199 		_state->variables[VAR_GLOBAL][kGlobalVarKQ7UpscaleVideos] = make_reg(0, value);
200 	}
201 }
202 
sendSelectorHook(const reg_t sendObj,Selector & selector,reg_t * argp)203 void GuestAdditions::sendSelectorHook(const reg_t sendObj, Selector &selector, reg_t *argp) {
204 	if (_features->getMessageTypeSyncStrategy() == kMessageTypeSyncStrategyLSL6Hires) {
205 		syncMessageTypeToScummVMUsingLSL6HiresStrategy(sendObj, selector, argp);
206 	}
207 }
208 
audio32SetVolumeHook(const int16 channelIndex,int16 volume) const209 bool GuestAdditions::audio32SetVolumeHook(const int16 channelIndex, int16 volume) const {
210 	if (!_features->audioVolumeSyncUsesGlobals() && shouldSyncAudioToScummVM()) {
211 		volume = volume * Audio::Mixer::kMaxMixerVolume / Audio32::kMaxVolume;
212 		if (Common::checkGameGUIOption(GUIO_LINKMUSICTOSFX, ConfMan.get("guioptions"))) {
213 			ConfMan.setInt("music_volume", volume);
214 		}
215 		ConfMan.setInt("sfx_volume", volume);
216 		ConfMan.setInt("speech_volume", volume);
217 		g_sci->updateSoundMixerVolumes();
218 		return true;
219 	}
220 
221 	return false;
222 }
223 
kDoSoundSetVolumeHook(const reg_t soundObj,const int16 volume) const224 void GuestAdditions::kDoSoundSetVolumeHook(const reg_t soundObj, const int16 volume) const {
225 	if (g_sci->getGameId() == GID_GK1 && shouldSyncAudioToScummVM()) {
226 		syncGK1AudioVolumeToScummVM(soundObj, volume);
227 	}
228 }
229 
instantiateScriptHook(Script & script,const bool ignoreDelayedRestore) const230 void GuestAdditions::instantiateScriptHook(Script &script, const bool ignoreDelayedRestore) const {
231 	if (getSciVersion() < SCI_VERSION_2) {
232 		return;
233 	}
234 
235 	// If there is a delayed restore, we still want to patch the script so
236 	// that the automatic return of the game ID works, but we do not want to
237 	// patch the scripts that get restored
238 	if (ConfMan.getBool("originalsaveload") &&
239 		(ignoreDelayedRestore || _state->_delayedRestoreGameId == -1)) {
240 		return;
241 	}
242 
243 	if ((g_sci->getGameId() == GID_LSL7 || g_sci->getGameId() == GID_TORIN) &&
244 		script.getScriptNumber() == 64866) {
245 
246 		patchGameSaveRestoreTorin(script);
247 	} else if (g_sci->getGameId() == GID_PHANTASMAGORIA2 && script.getScriptNumber() == 64978) {
248 		patchGameSaveRestorePhant2(script);
249 	} else if (script.getScriptNumber() == 64990) {
250 		// 64990 is the system script containing SRDialog. This script is used
251 		// by the main Game object, but it is not loaded immediately, so we wait
252 		// for it to be loaded before patching it. Attempting to preload this
253 		// script early for patching will cause the order of entries in the
254 		// segment table to change (versus save games that are not patched),
255 		// breaking persistent objects (like the control panel in SQ6) which
256 		// require reg_ts created during game startup to always be the same
257 
258 		if (g_sci->getGameId() == GID_RAMA) {
259 			patchGameSaveRestoreRama(script);
260 		} else {
261 			patchGameSaveRestoreSCI32(script);
262 		}
263 	}
264 }
265 
segManSaveLoadScriptHook(Script & script) const266 void GuestAdditions::segManSaveLoadScriptHook(Script &script) const {
267 	instantiateScriptHook(script, true);
268 }
269 
270 #endif
271 
kGetEventHook() const272 bool GuestAdditions::kGetEventHook() const {
273 	if (_state->_delayedRestoreGameId == -1) {
274 		return false;
275 	}
276 
277 #ifdef ENABLE_SCI32
278 	// Loading a save game while Lighthouse is still initializing itself will
279 	// cause loading to fail if the save game contains a saved Robot state,
280 	// because the Robot will try to restore itself into a game plane which does
281 	// not exist yet
282 	if (g_sci->getGameId() == GID_LIGHTHOUSE && _state->callInStack(g_sci->getGameObject(), SELECTOR(init))) {
283 		return false;
284 	}
285 #endif
286 
287 	return g_sci->_guestAdditions->restoreFromLauncher();
288 }
289 
kWaitHook() const290 bool GuestAdditions::kWaitHook() const {
291 	if (_state->_delayedRestoreGameId == -1) {
292 		return false;
293 	}
294 
295 	return g_sci->_guestAdditions->restoreFromLauncher();
296 }
297 
298 #ifdef ENABLE_SCI32
kPlayDuckPlayVMDHook() const299 bool GuestAdditions::kPlayDuckPlayVMDHook() const {
300 	return _state->_delayedRestoreGameId != -1;
301 }
302 #endif
303 
304 #pragma mark -
305 #pragma mark Integrated save & restore
306 
patchGameSaveRestore() const307 void GuestAdditions::patchGameSaveRestore() const {
308 	if (ConfMan.getBool("originalsaveload") || getSciVersion() >= SCI_VERSION_2)
309 		return;
310 
311 	patchGameSaveRestoreSCI16();
312 }
313 
314 static const byte kSaveRestorePatch[] = {
315 	0x39, 0x03,        // pushi 03
316 	0x76,              // push0
317 	0x38, 0xff, 0xff,  // pushi -1
318 	0x76,              // push0
319 	0x43, 0xff, 0x06,  // callk kRestoreGame/kSaveGame (will get changed afterwards)
320 	0x48               // ret
321 };
322 
patchKSaveRestore(SegManager * segMan,reg_t methodAddress,byte id)323 static void patchKSaveRestore(SegManager *segMan, reg_t methodAddress, byte id) {
324 	Script *script = segMan->getScript(methodAddress.getSegment());
325 	byte *patchPtr = const_cast<byte *>(script->getBuf(methodAddress.getOffset()));
326 	memcpy(patchPtr, kSaveRestorePatch, sizeof(kSaveRestorePatch));
327 	patchPtr[8] = id;
328 }
329 
patchGameSaveRestoreSCI16() const330 void GuestAdditions::patchGameSaveRestoreSCI16() const {
331 	const Object *gameObject = _segMan->getObject(g_sci->getGameObject());
332 	const Object *gameSuperObject = _segMan->getObject(gameObject->getSuperClassSelector());
333 	if (!gameSuperObject)
334 		gameSuperObject = gameObject;	// happens in KQ5CD, when loading saved games before r54510
335 	byte kernelIdRestore = 0;
336 	byte kernelIdSave = 0;
337 
338 	switch (g_sci->getGameId()) {
339 	case GID_HOYLE1: // gets confused, although the game doesn't support saving/restoring at all
340 	case GID_HOYLE2: // gets confused, see hoyle1
341 	case GID_JONES: // gets confused, when we patch us in, the game is only able to save to 1 slot, so hooking is not required
342 	case GID_MOTHERGOOSE: // mother goose EGA saves/restores directly and has no save/restore dialogs
343 	case GID_MOTHERGOOSE256: // mother goose saves/restores directly and has no save/restore dialogs
344 		return;
345 	default:
346 		break;
347 	}
348 
349 	uint16 kernelNamesSize = _kernel->getKernelNamesSize();
350 	for (uint16 kernelNr = 0; kernelNr < kernelNamesSize; kernelNr++) {
351 		Common::String kernelName = _kernel->getKernelName(kernelNr);
352 		if (kernelName == "RestoreGame")
353 			kernelIdRestore = kernelNr;
354 		if (kernelName == "SaveGame")
355 			kernelIdSave = kernelNr;
356 		if (kernelName == "Save")
357 			kernelIdSave = kernelIdRestore = kernelNr;
358 	}
359 
360 	// Search for gameobject superclass ::restore
361 	uint16 gameSuperObjectMethodCount = gameSuperObject->getMethodCount();
362 	for (uint16 methodNr = 0; methodNr < gameSuperObjectMethodCount; methodNr++) {
363 		uint16 selectorId = gameSuperObject->getFuncSelector(methodNr);
364 		Common::String methodName = _kernel->getSelectorName(selectorId);
365 		if (methodName == "restore") {
366 				patchKSaveRestore(_segMan, gameSuperObject->getFunction(methodNr), kernelIdRestore);
367 		} else if (methodName == "save") {
368 			if (g_sci->getGameId() != GID_FAIRYTALES) {	// Fairy Tales saves automatically without a dialog
369 					patchKSaveRestore(_segMan, gameSuperObject->getFunction(methodNr), kernelIdSave);
370 			}
371 		}
372 	}
373 
374 	// Patch gameobject ::save for now for SCI0 - SCI1.1
375 	// TODO: It seems this was never adjusted to superclass, but adjusting it now may cause
376 	// issues with some game. Needs to get checked and then possibly changed.
377 	const Object *patchObjectSave = gameObject;
378 
379 	// Search for gameobject ::save, if there is one patch that one too
380 	uint16 patchObjectMethodCount = patchObjectSave->getMethodCount();
381 	for (uint16 methodNr = 0; methodNr < patchObjectMethodCount; methodNr++) {
382 		uint16 selectorId = patchObjectSave->getFuncSelector(methodNr);
383 		Common::String methodName = _kernel->getSelectorName(selectorId);
384 		if (methodName == "save") {
385 			if (g_sci->getGameId() != GID_FAIRYTALES &&  // Fairy Tales saves automatically without a dialog
386 				g_sci->getGameId() != GID_QFG3) { // QFG3 does automatic saving in Glory:save
387 					patchKSaveRestore(_segMan, patchObjectSave->getFunction(methodNr), kernelIdSave);
388 			}
389 			break;
390 		}
391 	}
392 }
393 
394 #ifdef ENABLE_SCI32
395 static const byte SRDialogPatch[] = {
396 	0x76,                                 // push0
397 	0x59, 0x01,                           // &rest 1
398 	0x43, kScummVMSaveLoadId, 0x00, 0x00, // callk kScummVMSaveLoad, 0
399 	0x48                                  // ret
400 };
401 
patchGameSaveRestoreSCI32(Script & script) const402 void GuestAdditions::patchGameSaveRestoreSCI32(Script &script) const {
403 	patchSRDialogDoit(script, "SRDialog", SRDialogPatch, sizeof(SRDialogPatch));
404 }
405 
406 static const byte SRTorinPatch[] = {
407 	0x38, 0xFF, 0xFF,                     // pushi new
408 	0x76,                                 // push0
409 	0x51, 0x0f,                           // class Str
410 	0x4a, 0x04, 0x00,                     // send 4
411 	0xa3, 0x01,                           // sal 1
412 	0x76,                                 // push0
413 	0x59, 0x01,                           // &rest 1
414 	0x43, kScummVMSaveLoadId, 0x00, 0x00, // callk kScummVMSaveLoad, 0
415 	0x48                                  // ret
416 };
417 
patchGameSaveRestoreTorin(Script & script) const418 void GuestAdditions::patchGameSaveRestoreTorin(Script &script) const {
419 	const uint32 address = script.validateExportFunc(2, true);
420 	byte *patchPtr = const_cast<byte *>(script.getBuf(address));
421 	memcpy(patchPtr, SRTorinPatch, sizeof(SRTorinPatch));
422 
423 	const Selector newSelector = SELECTOR(new_);
424 	assert(newSelector != -1);
425 	patchPtr[1] = newSelector & 0xFF;
426 	patchPtr[2] = (newSelector >> 8) & 0xFF;
427 
428 	if (g_sci->isBE()) {
429 		SWAP(patchPtr[1], patchPtr[2]);
430 		SWAP(patchPtr[7], patchPtr[8]);
431 	}
432 }
433 
patchGameSaveRestorePhant2(Script & script) const434 void GuestAdditions::patchGameSaveRestorePhant2(Script &script) const {
435 	const ObjMap &objects = script.getObjectMap();
436 	for (ObjMap::const_iterator it = objects.begin(); it != objects.end(); ++it) {
437 		const Object &obj = it->_value;
438 
439 		if (strcmp(_segMan->derefString(obj.getNameSelector()), "srGetGame") != 0) {
440 			continue;
441 		}
442 
443 		int methodIndex = obj.funcSelectorPosition(SELECTOR(init));
444 		if (methodIndex == -1) {
445 			continue;
446 		}
447 
448 		byte *scriptData = const_cast<byte *>(script.getBuf(obj.getFunction(methodIndex).getOffset()));
449 		memcpy(scriptData, SRDialogPatch, sizeof(SRDialogPatch));
450 		break;
451 	}
452 }
453 
454 static const byte RamaSRDialogPatch[] = {
455 	0x78,                                 // push1
456 	0x7c,                                 // pushSelf
457 	0x43, kScummVMSaveLoadId, 0x02, 0x00, // callk kScummVMSaveLoad, 0
458 	0x48                                  // ret
459 };
460 
461 static const int RamaSRDialogUint16Offsets[] = { 4 };
462 
patchGameSaveRestoreRama(Script & script) const463 void GuestAdditions::patchGameSaveRestoreRama(Script &script) const {
464 	patchSRDialogDoit(script, "Save", RamaSRDialogPatch, sizeof(RamaSRDialogPatch), RamaSRDialogUint16Offsets, ARRAYSIZE(RamaSRDialogUint16Offsets));
465 	patchSRDialogDoit(script, "Restore", RamaSRDialogPatch, sizeof(RamaSRDialogPatch), RamaSRDialogUint16Offsets, ARRAYSIZE(RamaSRDialogUint16Offsets));
466 }
467 
patchSRDialogDoit(Script & script,const char * const objectName,const byte * patchData,const int patchSize,const int * uint16Offsets,const uint numOffsets) const468 void GuestAdditions::patchSRDialogDoit(Script &script, const char *const objectName, const byte *patchData, const int patchSize, const int *uint16Offsets, const uint numOffsets) const {
469 	const ObjMap &objMap = script.getObjectMap();
470 	for (ObjMap::const_iterator it = objMap.begin(); it != objMap.end(); ++it) {
471 		const Object &obj = it->_value;
472 		if (strcmp(_segMan->getObjectName(obj.getPos()), objectName) != 0) {
473 			continue;
474 		}
475 
476 		const uint16 methodCount = obj.getMethodCount();
477 		for (uint16 methodNr = 0; methodNr < methodCount; ++methodNr) {
478 			const uint16 selectorId = obj.getFuncSelector(methodNr);
479 			const Common::String methodName = _kernel->getSelectorName(selectorId);
480 			if (methodName == "doit") {
481 				const reg_t methodAddress = obj.getFunction(methodNr);
482 				byte *patchPtr = const_cast<byte *>(script.getBuf(methodAddress.getOffset()));
483 				memcpy(patchPtr, patchData, patchSize);
484 
485 				if (g_sci->isBE()) {
486 					for (uint i = 0; i < numOffsets; ++i) {
487 						const int offset = uint16Offsets[i];
488 						SWAP(patchPtr[offset], patchPtr[offset + 1]);
489 					}
490 				}
491 
492 				return;
493 			}
494 		}
495 	}
496 }
497 
kScummVMSaveLoad(EngineState * s,int argc,reg_t * argv) const498 reg_t GuestAdditions::kScummVMSaveLoad(EngineState *s, int argc, reg_t *argv) const {
499 	if (g_sci->getGameId() == GID_PHANTASMAGORIA2) {
500 		return promptSaveRestorePhant2(s, argc, argv);
501 	}
502 
503 	if (g_sci->getGameId() == GID_LSL7 || g_sci->getGameId() == GID_TORIN) {
504 		return promptSaveRestoreTorin(s, argc, argv);
505 	}
506 
507 	if (g_sci->getGameId() == GID_RAMA) {
508 		return promptSaveRestoreRama(s, argc, argv);
509 	}
510 
511 	if (g_sci->getGameId() == GID_HOYLE5) {
512 		return promptSaveRestoreHoyle5(s, argc, argv);
513 	}
514 
515 	return promptSaveRestoreDefault(s, argc, argv);
516 }
517 
promptSaveRestoreDefault(EngineState * s,int argc,reg_t * argv) const518 reg_t GuestAdditions::promptSaveRestoreDefault(EngineState *s, int argc, reg_t *argv) const {
519 	return make_reg(0, runSaveRestore(argc > 0, argc > 0 ? argv[0] : NULL_REG, s->_delayedRestoreGameId));
520 }
521 
promptSaveRestoreTorin(EngineState * s,int argc,reg_t * argv) const522 reg_t GuestAdditions::promptSaveRestoreTorin(EngineState *s, int argc, reg_t *argv) const {
523 	const bool isSave = (argc > 0 && (bool)argv[0].toSint16());
524 
525 	reg_t descriptionId = NULL_REG;
526 	if (isSave) {
527 		_segMan->allocateArray(kArrayTypeString, 0, &descriptionId);
528 	}
529 
530 	const int saveNo = runSaveRestore(isSave, descriptionId, s->_delayedRestoreGameId);
531 
532 	if (saveNo != -1) {
533 		assert(s->variablesMax[VAR_LOCAL] > 2);
534 		writeSelector(_segMan, s->variables[VAR_LOCAL][1], SELECTOR(data), descriptionId);
535 		s->variables[VAR_LOCAL][2] = make_reg(0, saveNo);
536 		s->variables[VAR_LOCAL][3] = make_reg(0, isSave ? 1 : 0);
537 	} else if (isSave) {
538 		_segMan->freeArray(descriptionId);
539 	}
540 
541 	return make_reg(0, saveNo != -1);
542 }
543 
promptSaveRestorePhant2(EngineState * s,int argc,reg_t * argv) const544 reg_t GuestAdditions::promptSaveRestorePhant2(EngineState *s, int argc, reg_t *argv) const {
545 	assert(argc == 2);
546 	const bool isSave = argv[1].toSint16() == 0;
547 	const int saveNo = runSaveRestore(isSave, argv[0], s->_delayedRestoreGameId);
548 
549 	// Clear the highlighted state of the button so if the same control panel is
550 	// opened again it does not appear to be opened to the save/load panels
551 	reg_t button;
552 	if (isSave) {
553 		button = _segMan->findObjectByName("saveButton");
554 	} else {
555 		button = _segMan->findObjectByName("loadButton");
556 	}
557 	writeSelectorValue(_segMan, button, SELECTOR(cel), 0);
558 
559 	// This causes the control panel to quit its internal event loop and hide
560 	// itself
561 	const reg_t controlPanel = s->variables[VAR_GLOBAL][kGlobalVarPhant2ControlPanel];
562 	writeSelector(_segMan, controlPanel, SELECTOR(scratch), TRUE_REG);
563 
564 	return make_reg(0, saveNo);
565 }
566 
promptSaveRestoreRama(EngineState * s,int argc,reg_t * argv) const567 reg_t GuestAdditions::promptSaveRestoreRama(EngineState *s, int argc, reg_t *argv) const {
568 	assert(argc == 1);
569 	const bool isSave = (strcmp(_segMan->getObjectName(argv[0]), "Save") == 0);
570 
571 	const reg_t editor = _segMan->findObjectByName("editI");
572 	reg_t outDescription = readSelector(_segMan, editor, SELECTOR(text));
573 	if (!_segMan->isValidAddr(outDescription, SEG_TYPE_ARRAY)) {
574 		_segMan->allocateArray(kArrayTypeString, 0, &outDescription);
575 		writeSelector(_segMan, editor, SELECTOR(text), outDescription);
576 	}
577 
578 	int saveNo = runSaveRestore(isSave, outDescription, s->_delayedRestoreGameId);
579 	int saveIndex = -1;
580 	if (saveNo != -1) {
581 		// The save number returned by runSaveRestore is a SCI save number
582 		// because normally SRDialogs return the save ID, but RAMA returns the
583 		// save game's index in the save game list instead, so we need to
584 		// convert back to the ScummVM save number here to find the correct
585 		// index
586 		saveNo += kSaveIdShift;
587 
588 		Common::Array<SavegameDesc> saves;
589 		listSavegames(saves);
590 		saveIndex = findSavegame(saves, saveNo);
591 
592 		if (isSave) {
593 			bool resetCatalogFile = false;
594 			const Common::String saveGameName = _segMan->getString(outDescription);
595 
596 			// The original game save/restore code returns index 0 when a game
597 			// is created that does not already exist and then the scripts find
598 			// the next hole and insert there, but the ScummVM GUI works
599 			// differently and allows users to insert a game wherever they want,
600 			// so we need to force the save game to exist in advance so RAMA's
601 			// save code will successfully put it where we want it
602 			if (saveIndex == -1) {
603 				// We need to touch the save file just so it exists here, since
604 				// otherwise the game will not let us save to the new save slot
605 				// (it will try to come up with a brand new slot instead)
606 				Common::OutSaveFile *out = g_sci->getSaveFileManager()->openForSaving(g_sci->getSavegameName(saveNo));
607 				set_savegame_metadata(out, saveGameName, "");
608 
609 				// Make sure the save file is fully written before we try to
610 				// re-retrieve the list of saves, since otherwise it may not
611 				// show up in the list
612 				delete out;
613 
614 				// We have to re-retrieve saves and find the index instead of
615 				// assuming the newest save will be in index 0 because save game
616 				// times are not guaranteed to be steady
617 				saves.clear();
618 				listSavegames(saves);
619 				saveIndex = findSavegame(saves, saveNo);
620 				if (saveIndex == -1) {
621 					warning("Stub save not found when trying to save a new game to slot %d", saveNo);
622 				} else {
623 					// Kick the CatalogFile into believing that this new save
624 					// game exists already, otherwise it the game will not
625 					// actually save into the new save
626 					resetCatalogFile = true;
627 				}
628 			} else if (strncmp(saveGameName.c_str(), saves[saveIndex].name, kMaxSaveNameLength) != 0) {
629 				// The game doesn't let the save game name change for the same
630 				// slot, but ScummVM's GUI does, so force the new name into the
631 				// save file metadata if it has changed so it actually makes it
632 				// into the save game
633 				Common::ScopedPtr<Common::OutSaveFile> out(g_sci->getSaveFileManager()->openForSaving(g_sci->getSavegameName(saveNo)));
634 				set_savegame_metadata(out.get(), saveGameName, "");
635 				resetCatalogFile = true;
636 			}
637 
638 			if (resetCatalogFile) {
639 				const reg_t catalogFileId = _state->variables[VAR_GLOBAL][kGlobalVarRamaCatalogFile];
640 				if (catalogFileId.isNull()) {
641 					warning("Could not find CatalogFile when saving from launcher");
642 				}
643 				reg_t args[] = { NULL_REG };
644 				invokeSelector(catalogFileId, SELECTOR(dispose));
645 				invokeSelector(catalogFileId, SELECTOR(init), ARRAYSIZE(args), args);
646 			}
647 		}
648 	}
649 
650 	return make_reg(0, saveIndex);
651 }
652 
runSaveRestore(const bool isSave,reg_t outDescription,const int forcedSaveId) const653 int GuestAdditions::runSaveRestore(const bool isSave, reg_t outDescription, const int forcedSaveId) const {
654 	assert(!(isSave && outDescription.isNull()));
655 
656 	Common::String descriptionString;
657 	int saveId = runSaveRestore(isSave, descriptionString, forcedSaveId);
658 
659 	if (!outDescription.isNull()) {
660 		if (_segMan->isObject(outDescription)) {
661 			outDescription = readSelector(_segMan, outDescription, SELECTOR(data));
662 		}
663 		SciArray &description = *_segMan->lookupArray(outDescription);
664 		description.fromString(descriptionString);
665 	}
666 
667 	return saveId;
668 }
669 
runSaveRestore(const bool isSave,Common::String & outDescription,const int forcedSaveId) const670 int GuestAdditions::runSaveRestore(const bool isSave, Common::String &outDescription, const int forcedSaveId) const {
671 	int saveId;
672 
673 	if (!isSave && forcedSaveId != -1) {
674 		saveId = forcedSaveId;
675 	} else {
676 		Common::U32String title;
677 		Common::U32String action;
678 		if (isSave) {
679 			title = _("Save game:");
680 			action = _("Save");
681 		} else {
682 			title = _("Restore game:");
683 			action = _("Restore");
684 		}
685 
686 		GUI::SaveLoadChooser dialog(title, action, isSave);
687 		saveId = dialog.runModalWithCurrentTarget();
688 		if (saveId != -1) {
689 			outDescription = dialog.getResultString();
690 			if (outDescription.empty()) {
691 				outDescription = dialog.createDefaultSaveDescription(saveId - 1);
692 			}
693 		}
694 	}
695 
696 	// The autosave slot in ScummVM takes up slot 0, but in SCI the first
697 	// non-autosave save game number needs to be 0, so reduce the save
698 	// number here to match what would come from the normal SCI save/restore
699 	// dialog. Wrap slot 0 around to kMaxShiftedSaveId so that it remains
700 	// a legal SCI value.
701 	saveId = shiftScummVMToSciSaveId(saveId);
702 
703 	return saveId;
704 }
705 
promptSaveRestoreHoyle5(EngineState * s,int argc,reg_t * argv) const706 reg_t GuestAdditions::promptSaveRestoreHoyle5(EngineState *s, int argc, reg_t *argv) const {
707 	assert(argc == 2);
708 	Common::String callerName = s->_segMan->getObjectName(s->r_acc);
709 	const bool isSave = (callerName == "Save");
710 	return make_reg(0, runSaveRestore(isSave, argc > 0 ? argv[0] : NULL_REG, s->_delayedRestoreGameId));
711 }
712 
713 #endif
714 
715 #pragma mark -
716 #pragma mark Restore from launcher
717 
restoreFromLauncher() const718 bool GuestAdditions::restoreFromLauncher() const {
719 	assert(_state->_delayedRestoreGameId != -1);
720 
721 #ifdef ENABLE_SCI32
722 	if (getSciVersion() >= SCI_VERSION_2) {
723 		if (_restoring) {
724 			// Recursion will occur if a restore fails, as
725 			// _delayedRestoreGameId will not be reset so the kernel will try
726 			// to keep restoring forever
727 			_state->_delayedRestoreGameId = -1;
728 			_restoring = false;
729 			return false;
730 		}
731 
732 		// Delayed restore should not happen until after the benchmarking room.
733 		// In particular, in SQ6, delayed restore must not happen until room 100
734 		// (the Sierra logo & main menu room), otherwise the game scripts will
735 		// try to make calls to the subtitles ScrollWindow, which does not
736 		// exist. In other games, restoring early either breaks benchmarking,
737 		// or, when trying to load an invalid save game, makes the dialog
738 		// telling the user that the game is invalid impossible to read
739 		if (strcmp(_segMan->getObjectName(_state->variables[VAR_GLOBAL][kGlobalVarCurrentRoom]), "speedRoom") == 0) {
740 			return false;
741 		}
742 
743 		// Delayed restore should not happen in LSL6 hires or PQ4 until the room number is set.
744 		//  LSL6:restore and pq4:restore assume the room number has already been set, but the
745 		//  Mac versions of these game add a call to kGetEvent in the games' init method before
746 		//  the initial call to newRoom. If the room number isn't set yet then LSL6 doesn't
747 		//  allow the restore and PQ4 sends a message to an invalid object.
748 		if ((g_sci->getGameId() == GID_LSL6HIRES || g_sci->getGameId() == GID_PQ4) &&
749 			_state->variables[VAR_GLOBAL][kGlobalVarCurrentRoomNo] == NULL_REG) {
750 			return false;
751 		}
752 
753 		_restoring = true;
754 
755 		// Any events queued up before the game restore can cause accidental
756 		// input into the game if they are not flushed (this is particularly
757 		// noticeable in Phant2, where the game will display "Click to continue"
758 		// for one frame if the user clicked during startup)
759 		g_sci->getEventManager()->flushEvents();
760 
761 		if (g_sci->getGameId() == GID_PHANTASMAGORIA2) {
762 			// Phantasmagoria 2 moves the function that actually restores
763 			// a game, and uses a property of the main game object when picking
764 			// the save game to restore. Before calling this function, we
765 			// must ensure that input is enabled, as the "Click to continue"
766 			// prompt expects this. Otherwise, restoring from our overlay during
767 			// a handsOff sequence breaks the prompt and crashes the next room.
768 			// We enable input by calling p2User:canInput(1).
769 			reg_t canInputParams[] = { TRUE_REG };
770 			invokeSelector(_state->variables[VAR_GLOBAL][kGlobalVarPhant2User], SELECTOR(canInput), 1, canInputParams);
771 
772 			writeSelectorValue(_segMan, g_sci->getGameObject(), SELECTOR(num), _state->_delayedRestoreGameId - kSaveIdShift);
773 			invokeSelector(g_sci->getGameObject(), SELECTOR(reallyRestore));
774 		} else if (g_sci->getGameId() == GID_SHIVERS) {
775 			// Shivers accepts the save game number as a parameter to
776 			// `SHIVERS::restore`
777 			reg_t args[] = { make_reg(0, _state->_delayedRestoreGameId - kSaveIdShift) };
778 			invokeSelector(g_sci->getGameObject(), SELECTOR(restore), 1, args);
779 		} else {
780 			int saveId = _state->_delayedRestoreGameId;
781 
782 			// When `Game::restore` is invoked, it will call to `Restore::doit`
783 			// which will automatically return the `_delayedRestoreGameId` instead
784 			// of prompting the user for a save game
785 			invokeSelector(g_sci->getGameObject(), SELECTOR(restore));
786 
787 			// initialize KQ7 Mac's global save state by recording the save id
788 			//  and description. this is necessary for subsequent saves to work
789 			//  after restoring from launcher.
790 			if (g_sci->getGameId() == GID_KQ7 && g_sci->getPlatform() == Common::kPlatformMacintosh) {
791 				_state->_kq7MacSaveGameId = saveId;
792 
793 				SavegameDesc savegameDesc;
794 				if (fillSavegameDesc(g_sci->getSavegameName(saveId), savegameDesc)) {
795 					_state->_kq7MacSaveGameDescription = savegameDesc.name;
796 				}
797 			}
798 
799 			// The normal save game system resets _delayedRestoreGameId with a
800 			// call to `EngineState::reset`, but RAMA uses a custom save game
801 			// system which does not reset the engine, so we need to clear the
802 			// ID here or the engine will just try to restore the game forever
803 			if (g_sci->getGameId() == GID_RAMA) {
804 				_state->_delayedRestoreGameId = -1;
805 			}
806 		}
807 
808 		_restoring = false;
809 
810 		return true;
811 	} else {
812 #else
813 	{
814 #endif
815 		int savegameId = _state->_delayedRestoreGameId; // delayedRestoreGameId gets destroyed within gamestate_restore()!
816 		Common::String fileName = g_sci->getSavegameName(savegameId);
817 		Common::SeekableReadStream *in = g_sci->getSaveFileManager()->openForLoading(fileName);
818 
819 		if (in) {
820 			// found a savegame file
821 			gamestate_restore(_state, in);
822 			delete in;
823 			if (_state->r_acc != make_reg(0, 1)) {
824 				gamestate_afterRestoreFixUp(_state, savegameId);
825 				return true;
826 			}
827 		}
828 
829 		error("Restoring gamestate '%s' failed", fileName.c_str());
830 	}
831 }
832 
833 #pragma mark -
834 #pragma mark Message type sync
835 
836 void GuestAdditions::syncMessageTypeFromScummVM() const {
837 	switch (_features->getMessageTypeSyncStrategy()) {
838 	case kMessageTypeSyncStrategyDefault:
839 		syncMessageTypeFromScummVMUsingDefaultStrategy();
840 		break;
841 
842 #ifdef ENABLE_SCI32
843 	case kMessageTypeSyncStrategyShivers:
844 		syncMessageTypeFromScummVMUsingShiversStrategy();
845 		break;
846 
847 	case kMessageTypeSyncStrategyLSL6Hires:
848 		syncMessageTypeFromScummVMUsingLSL6HiresStrategy();
849 		break;
850 #endif
851 	case kMessageTypeSyncStrategyNone:
852 	default:
853 		break;
854 	}
855 }
856 
857 void GuestAdditions::syncMessageTypeFromScummVMUsingDefaultStrategy() const {
858 	uint8 value = 0;
859 	if (ConfMan.getBool("subtitles")) {
860 		value |= kMessageTypeSubtitles;
861 	}
862 	if (!ConfMan.getBool(("speech_mute"))) {
863 		value |= kMessageTypeSpeech;
864 	}
865 
866 	if (value == kMessageTypeSubtitles + kMessageTypeSpeech && !_features->supportsSpeechWithSubtitles()) {
867 		value &= ~kMessageTypeSubtitles;
868 	}
869 
870 	if (value) {
871 		_state->variables[VAR_GLOBAL][kGlobalVarMessageType] = make_reg(0, value);
872 	}
873 
874 #ifdef ENABLE_SCI32
875 	if (g_sci->getGameId() == GID_GK1 && value == kMessageTypeSubtitles) {
876 		// The narrator speech needs to be forced off if speech has been
877 		// disabled in ScummVM, but otherwise the narrator toggle should just
878 		// be allowed to persist to whatever the user chose previously, since
879 		// it is controlled independently of other speech in the game and there
880 		// is no equivalent option in the ScummVM GUI
881 		_state->variables[VAR_GLOBAL][kGlobalVarGK1NarratorMode] = NULL_REG;
882 	}
883 
884 	if (g_sci->getGameId() == GID_QFG4) {
885 		// QFG4 uses a game flag to control the Audio button's state in the control panel.
886 		//  This flag must be kept in sync with the standard global 90 speech bit.
887 		uint flagNumber = 400;
888 		uint globalNumber = kGlobalVarQFG4Flags + (flagNumber / 16);
889 		if (value & kMessageTypeSpeech) {
890 			_state->variables[VAR_GLOBAL][globalNumber] |= (int16)0x8000;
891 		} else {
892 			_state->variables[VAR_GLOBAL][globalNumber] &= (int16)~0x8000;
893 		}
894 	}
895 
896 	if (g_sci->getGameId() == GID_SQ6) {
897 		// The SQ6 control panel buttons for speech and text only update
898 		//  their states when clicked so they need synchronization.
899 		const reg_t iconSpeech = _segMan->findObjectByName("iconSpeech");
900 		if (!iconSpeech.isNull()) {
901 			const reg_t iconCel = make_reg(0, (value & kMessageTypeSpeech) ? 2 : 0);
902 			writeSelector(_segMan, iconSpeech, SELECTOR(mainCel), iconCel);
903 		}
904 		const reg_t iconText = _segMan->findObjectByName("iconText");
905 		if (!iconText.isNull()) {
906 			const reg_t iconCel = make_reg(0, (value & kMessageTypeSubtitles) ? 2 : 0);
907 			writeSelector(_segMan, iconText, SELECTOR(mainCel), iconCel);
908 		}
909 	}
910 #endif
911 }
912 
913 #ifdef ENABLE_SCI32
914 void GuestAdditions::syncMessageTypeFromScummVMUsingShiversStrategy() const {
915 	if (ConfMan.getBool("subtitles")) {
916 		_state->variables[VAR_GLOBAL][kGlobalVarShiversFlags] |= 256;
917 	} else {
918 		_state->variables[VAR_GLOBAL][kGlobalVarShiversFlags] &= ~256;
919 	}
920 }
921 
922 void GuestAdditions::syncMessageTypeFromScummVMUsingLSL6HiresStrategy() const {
923 	// LSL6hires synchronisation happens in send_selector, except when
924 	// restoring a game, where it happens here
925 	if (_state->variables[VAR_GLOBAL][kGlobalVarLSL6HiresGameFlags].isNull()) {
926 		return;
927 	}
928 
929 	reg_t params[] = { make_reg(0, kLSL6HiresSubtitleFlag) };
930 	Selector selector;
931 	reg_t restore;
932 
933 	if (ConfMan.getBool("subtitles")) {
934 		restore = TRUE_REG;
935 		selector = SELECTOR(clear);
936 	} else {
937 		restore = NULL_REG;
938 		selector = SELECTOR(set);
939 	}
940 
941 	// Attempting to show or hide the ScrollWindow used for subtitles
942 	// directly (by invoking `show` or `hide`) causes the game to crash with
943 	// an error about passing an invalid ScrollWindow ID. Fortunately, the
944 	// game scripts store a flag that restores the window when a game is
945 	// restored
946 	_state->variables[VAR_GLOBAL][kGlobalVarLSL6HiresRestoreTextWindow] = restore;
947 	invokeSelector(_state->variables[VAR_GLOBAL][kGlobalVarLSL6HiresGameFlags], selector, 1, params);
948 }
949 #endif
950 
951 void GuestAdditions::syncMessageTypeToScummVM(const int index, const reg_t value) {
952 	switch (_features->getMessageTypeSyncStrategy()) {
953 	case kMessageTypeSyncStrategyDefault:
954 		syncMessageTypeToScummVMUsingDefaultStrategy(index, value);
955 		break;
956 
957 #ifdef ENABLE_SCI32
958 	case kMessageTypeSyncStrategyShivers:
959 		syncMessageTypeToScummVMUsingShiversStrategy(index, value);
960 		break;
961 
962 	case kMessageTypeSyncStrategyLSL6Hires:
963 		// LSL6hires synchronisation happens via send_selector
964 #endif
965 	case kMessageTypeSyncStrategyNone:
966 	default:
967 		break;
968 	}
969 }
970 
971 void GuestAdditions::syncMessageTypeToScummVMUsingDefaultStrategy(const int index, const reg_t value) {
972 	if (index == kGlobalVarMessageType) {
973 		// ScummVM audio options haven't been applied yet. Use this set call
974 		// as a trigger to apply defaults from ScummVM, ignoring the default
975 		// value that was just received from the game scripts
976 		if (!_messageTypeSynced || _state->variables[VAR_GLOBAL][kGlobalVarQuit] == TRUE_REG) {
977 			_messageTypeSynced = true;
978 			syncAudioOptionsFromScummVM();
979 			return;
980 		}
981 
982 		ConfMan.setBool("subtitles", value.toSint16() & kMessageTypeSubtitles);
983 		ConfMan.setBool("speech_mute", !(value.toSint16() & kMessageTypeSpeech));
984 
985 		// need to update sound mixer volumes so that speech_mute will take effect
986 		g_sci->updateSoundMixerVolumes();
987 	}
988 }
989 
990 #ifdef ENABLE_SCI32
991 void GuestAdditions::syncMessageTypeToScummVMUsingShiversStrategy(const int index, const reg_t value) {
992 	if (index == kGlobalVarShiversFlags) {
993 		// ScummVM audio options haven't been applied yet, so apply them
994 		// and ignore the default value that was just received from the
995 		// game scripts
996 		if (!_messageTypeSynced || _state->variables[VAR_GLOBAL][kGlobalVarQuit] == TRUE_REG) {
997 			_messageTypeSynced = true;
998 			syncAudioOptionsFromScummVM();
999 			return;
1000 		}
1001 
1002 		ConfMan.setBool("subtitles", value.toUint16() & 256);
1003 	}
1004 }
1005 
1006 void GuestAdditions::syncMessageTypeToScummVMUsingLSL6HiresStrategy(const reg_t sendObj, Selector &selector, reg_t *argp) {
1007 	if (_state->variables[VAR_GLOBAL][kGlobalVarLSL6HiresGameFlags] == sendObj &&
1008 		(selector == SELECTOR(clear) || selector == SELECTOR(set))) {
1009 
1010 		if (argp[1].toUint16() == kLSL6HiresSubtitleFlag) {
1011 			if (_messageTypeSynced) {
1012 				ConfMan.setBool("subtitles", selector == SELECTOR(clear));
1013 			} else if (ConfMan.getBool("subtitles")) {
1014 				selector = SELECTOR(clear);
1015 				argp[-1].setOffset(selector);
1016 				_messageTypeSynced = true;
1017 			} else {
1018 				selector = SELECTOR(set);
1019 				argp[-1].setOffset(selector);
1020 				_messageTypeSynced = true;
1021 			}
1022 		}
1023 	}
1024 }
1025 #endif
1026 
1027 #pragma mark -
1028 #pragma mark Master volume sync
1029 
1030 void GuestAdditions::syncMasterVolumeFromScummVM() const {
1031 #ifdef ENABLE_SCI32
1032 	const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * MUSIC_MASTERVOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
1033 	const int16 sfxVolume = (ConfMan.getInt("sfx_volume") + 1) * Audio32::kMaxVolume / Audio::Mixer::kMaxMixerVolume;
1034 
1035 	// Volume was changed from ScummVM during the game, so resync the
1036 	// in-game UI
1037 	syncInGameUI(musicVolume, sfxVolume);
1038 #endif
1039 }
1040 
1041 void GuestAdditions::syncMasterVolumeToScummVM(const int16 masterVolume) const {
1042 	const int scummVMVolume = masterVolume * Audio::Mixer::kMaxMixerVolume / MUSIC_MASTERVOLUME_MAX;
1043 	ConfMan.setInt("music_volume", scummVMVolume);
1044 
1045 	if (Common::checkGameGUIOption(GUIO_LINKMUSICTOSFX, ConfMan.get("guioptions"))) {
1046 		ConfMan.setInt("sfx_volume", scummVMVolume);
1047 		if (Common::checkGameGUIOption(GUIO_LINKSPEECHTOSFX, ConfMan.get("guioptions"))) {
1048 			ConfMan.setInt("speech_volume", scummVMVolume);
1049 		}
1050 	}
1051 
1052 	// In SCI32, digital audio volume is controlled separately by
1053 	// kDoAudioVolume
1054 	// TODO: In SCI16, the volume slider only changed the music volume.
1055 	// Is this non-standard behavior better, or just wrong?
1056 	if (getSciVersion() < SCI_VERSION_2) {
1057 		ConfMan.setInt("sfx_volume", scummVMVolume);
1058 		ConfMan.setInt("speech_volume", scummVMVolume);
1059 	}
1060 	g_sci->updateSoundMixerVolumes();
1061 }
1062 
1063 #pragma mark -
1064 #pragma mark Globals volume sync
1065 
1066 void GuestAdditions::syncAudioVolumeGlobalsFromScummVM() const {
1067 	// On muting: Setting the music volume to zero when mute is enabled is done
1068 	// only for the games that use MIDI for music playback, since MIDI playback
1069 	// does not always run through the ScummVM mixer. Games that use digital
1070 	// audio for music do not need any extra code since that always runs
1071 	// straight through the audio mixer, which gets muted directly
1072 	switch (g_sci->getGameId()) {
1073 #ifdef ENABLE_SCI32
1074 	case GID_GK1: {
1075 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * MUSIC_VOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
1076 		const int16 dacVolume = (ConfMan.getInt("sfx_volume") + 1) * Audio32::kMaxVolume / Audio::Mixer::kMaxMixerVolume;
1077 		syncGK1VolumeFromScummVM(musicVolume, dacVolume);
1078 		syncGK1UI();
1079 		break;
1080 	}
1081 
1082 	case GID_GK2: {
1083 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * Audio32::kMaxVolume / Audio::Mixer::kMaxMixerVolume;
1084 		syncGK2VolumeFromScummVM(musicVolume);
1085 		syncGK2UI();
1086 		break;
1087 	}
1088 
1089 	case GID_HOYLE5: {
1090 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * kHoyle5VolumeMax / Audio::Mixer::kMaxMixerVolume;
1091 		syncHoyle5VolumeFromScummVM(musicVolume);
1092 		syncHoyle5UI(musicVolume);
1093 		break;
1094 	}
1095 
1096 	case GID_PHANTASMAGORIA: {
1097 		reg_t &musicGlobal = _state->variables[VAR_GLOBAL][kGlobalVarPhant1MusicVolume];
1098 		reg_t &dacGlobal   = _state->variables[VAR_GLOBAL][kGlobalVarPhant1DACVolume];
1099 
1100 		const int16 oldMusicVolume = musicGlobal.toSint16();
1101 		const int16 oldDacVolume   = dacGlobal.toSint16();
1102 
1103 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * MUSIC_MASTERVOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
1104 		const int16 dacVolume   = (ConfMan.getInt("sfx_volume") + 1)   * Audio32::kMaxVolume / Audio::Mixer::kMaxMixerVolume;
1105 
1106 		g_sci->_soundCmd->setMasterVolume(ConfMan.getBool("mute") ? 0 : musicVolume);
1107 
1108 		// Phant1 has a fragile volume UI. Global volumes need to be set during
1109 		// UI updates to move the volume bars to the correct position
1110 		syncPhant1UI(oldMusicVolume, musicVolume, musicGlobal, oldDacVolume, dacVolume, dacGlobal);
1111 		break;
1112 	}
1113 
1114 	case GID_PHANTASMAGORIA2: {
1115 		const int16 masterVolume = (ConfMan.getInt("sfx_volume") + 1) * kPhant2VolumeMax / Audio::Mixer::kMaxMixerVolume;
1116 		syncPhant2VolumeFromScummVM(masterVolume);
1117 		syncPhant2UI(masterVolume);
1118 		break;
1119 	}
1120 
1121 	case GID_RAMA: {
1122 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * kRamaVolumeMax / Audio::Mixer::kMaxMixerVolume;
1123 		syncRamaVolumeFromScummVM(musicVolume);
1124 		syncRamaUI(musicVolume);
1125 		break;
1126 	}
1127 
1128 	case GID_LSL7:
1129 	case GID_TORIN: {
1130 		const int16 musicVolume  = (ConfMan.getInt("music_volume") + 1)  * 100 / Audio::Mixer::kMaxMixerVolume;
1131 		const int16 sfxVolume    = (ConfMan.getInt("sfx_volume") + 1)    * 100 / Audio::Mixer::kMaxMixerVolume;
1132 		const int16 speechVolume = (ConfMan.getInt("speech_volume") + 1) * 100 / Audio::Mixer::kMaxMixerVolume;
1133 		syncTorinVolumeFromScummVM(musicVolume, sfxVolume, speechVolume);
1134 		syncTorinUI(musicVolume, sfxVolume, speechVolume);
1135 		break;
1136 	}
1137 #endif
1138 
1139 	case GID_LSL6:
1140 	case GID_LSL6HIRES:	{
1141 		const int16 musicVolume = (ConfMan.getInt("music_volume") + 1) * kLSL6UIVolumeMax / Audio::Mixer::kMaxMixerVolume;
1142 		syncLSL6VolumeFromScummVM(musicVolume);
1143 		syncLSL6UI(musicVolume);
1144 		break;
1145 	}
1146 
1147 	default:
1148 		error("Trying to sync audio volume globals in a game with no implementation");
1149 	}
1150 }
1151 
1152 void GuestAdditions::syncLSL6VolumeFromScummVM(const int16 musicVolume) const {
1153 	_state->variables[VAR_GLOBAL][kGlobalVarLSL6MusicVolume] = make_reg(0, musicVolume);
1154 	g_sci->_soundCmd->setMasterVolume(ConfMan.getBool("mute") ? 0 : (musicVolume * MUSIC_MASTERVOLUME_MAX / kLSL6UIVolumeMax));
1155 }
1156 
1157 #ifdef ENABLE_SCI32
1158 void GuestAdditions::syncGK1StartupVolumeFromScummVM(const int index, const reg_t value) const {
1159 	if (index == kGlobalVarGK1Music1 || index == kGlobalVarGK1Music2 ||
1160 		index == kGlobalVarGK1DAC1 || index == kGlobalVarGK1DAC2 ||
1161 		index == kGlobalVarGK1DAC3) {
1162 
1163 		int16 volume;
1164 		Selector selector;
1165 
1166 		switch (readSelectorValue(_segMan, value, SELECTOR(type))) {
1167 		case kSoundsMusicType: {
1168 			volume = (ConfMan.getInt("music_volume") + 1) * MUSIC_VOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
1169 			selector = SELECTOR(musicVolume);
1170 			break;
1171 		}
1172 
1173 		case kSoundsSoundType: {
1174 			volume = (ConfMan.getInt("sfx_volume") + 1) * MUSIC_VOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
1175 			selector = SELECTOR(soundVolume);
1176 			break;
1177 		}
1178 
1179 		default:
1180 			error("Unknown sound type");
1181 		}
1182 
1183 		writeSelectorValue(_segMan, value, selector, volume);
1184 	}
1185 }
1186 
1187 void GuestAdditions::syncGK1VolumeFromScummVM(const int16 musicVolume, const int16 dacVolume) const {
1188 	const reg_t soundsId = _state->variables[VAR_GLOBAL][kGlobalVarSounds];
1189 	if (!soundsId.isNull()) {
1190 		List *sounds = _segMan->lookupList(readSelector(_segMan, soundsId, SELECTOR(elements)));
1191 		reg_t soundId = sounds->first;
1192 		while (!soundId.isNull()) {
1193 			Node *sound = _segMan->lookupNode(soundId);
1194 			const int16 type = readSelectorValue(_segMan, sound->value, SELECTOR(type));
1195 			int16 volume;
1196 
1197 			if (type == kSoundsMusicType) {
1198 				volume = ConfMan.getBool("mute") ? 0 : musicVolume;
1199 				writeSelectorValue(_segMan, sound->value, SELECTOR(musicVolume), musicVolume);
1200 			} else if (type == kSoundsSoundType) {
1201 				volume = dacVolume;
1202 				writeSelectorValue(_segMan, sound->value, SELECTOR(soundVolume), dacVolume);
1203 			} else {
1204 				error("Unknown sound type %d", type);
1205 			}
1206 
1207 			// `setVolume` will set the `vol` property on the sound object;
1208 			// if it did not do this, an invocation of the `setVol` selector
1209 			// would need to be here (though doing so would result in
1210 			// recursion, so don't)
1211 			g_sci->_soundCmd->setVolume(sound->value, volume);
1212 			soundId = sound->succ;
1213 		}
1214 	}
1215 }
1216 
1217 void GuestAdditions::syncGK2VolumeFromScummVM(const int16 musicVolume) const {
1218 	_state->variables[VAR_GLOBAL][kGlobalVarGK2MusicVolume] = make_reg(0, musicVolume);
1219 
1220 	// Calling `setVol` on all sounds is necessary to propagate the volume
1221 	// change to existing sounds, and matches how game scripts propagate
1222 	// volume changes when the in-game music slider is moved
1223 	const reg_t soundsId = _state->variables[VAR_GLOBAL][kGlobalVarSounds];
1224 	if (!soundsId.isNull()) {
1225 		List *sounds = _segMan->lookupList(readSelector(_segMan, soundsId, SELECTOR(elements)));
1226 		reg_t soundId = sounds->first;
1227 		while (!soundId.isNull()) {
1228 			Node *sound = _segMan->lookupNode(soundId);
1229 			reg_t params[] = { make_reg(0, musicVolume) };
1230 			invokeSelector(sound->value, SELECTOR(setVol), 1, params);
1231 			soundId = sound->succ;
1232 		}
1233 	}
1234 }
1235 
1236 void GuestAdditions::syncHoyle5VolumeFromScummVM(const int16 musicVolume) const {
1237 	_state->variables[VAR_GLOBAL][kGlobalVarHoyle5MusicVolume] = make_reg(0, musicVolume);
1238 	g_sci->_soundCmd->setMasterVolume(ConfMan.getBool("mute") ? 0 : (musicVolume * MUSIC_MASTERVOLUME_MAX / kHoyle5VolumeMax));
1239 }
1240 
1241 void GuestAdditions::syncPhant2VolumeFromScummVM(const int16 masterVolume) const {
1242 	_state->variables[VAR_GLOBAL][kGlobalVarPhant2MasterVolume] = make_reg(0, masterVolume);
1243 	_state->variables[VAR_GLOBAL][kGlobalVarPhant2SecondaryVolume] = make_reg(0, masterVolume);
1244 
1245 	const reg_t soundsId = _state->variables[VAR_GLOBAL][kGlobalVarSounds];
1246 	if (!soundsId.isNull()) {
1247 		reg_t params[] = { make_reg(0, SELECTOR(setVol)), make_reg(0, masterVolume) };
1248 		invokeSelector(soundsId, SELECTOR(eachElementDo), 2, params);
1249 	}
1250 }
1251 
1252 void GuestAdditions::syncRamaVolumeFromScummVM(const int16 musicVolume) const {
1253 	_state->variables[VAR_GLOBAL][kGlobalVarRamaMusicVolume] = make_reg(0, musicVolume);
1254 	const reg_t gameId = _state->variables[VAR_GLOBAL][kGlobalVarGame];
1255 	if (!gameId.isNull()) {
1256 		reg_t args[] = { make_reg(0, musicVolume) };
1257 		invokeSelector(gameId, SELECTOR(masterVolume), 1, args);
1258 	}
1259 }
1260 
1261 void GuestAdditions::syncTorinVolumeFromScummVM(const int16 musicVolume, const int16 sfxVolume, const int16 speechVolume) const {
1262 	_state->variables[VAR_GLOBAL][kGlobalVarTorinMusicVolume]  = make_reg(0, musicVolume);
1263 	_state->variables[VAR_GLOBAL][kGlobalVarTorinSFXVolume]    = make_reg(0, sfxVolume);
1264 	_state->variables[VAR_GLOBAL][kGlobalVarTorinSpeechVolume] = make_reg(0, speechVolume);
1265 
1266 	// Calling `reSyncVol` on all sounds is necessary to propagate the
1267 	// volume change to existing sounds, and matches how game scripts
1268 	// propagate volume changes when the in-game volume sliders are moved
1269 	const reg_t soundsId = _state->variables[VAR_GLOBAL][kGlobalVarSounds];
1270 	if (!soundsId.isNull()) {
1271 		const Selector selector = SELECTOR(reSyncVol);
1272 		List *sounds = _segMan->lookupList(readSelector(_segMan, soundsId, SELECTOR(elements)));
1273 		reg_t soundId = sounds->first;
1274 		while (!soundId.isNull()) {
1275 			Node *sound = _segMan->lookupNode(soundId);
1276 			const reg_t &soundObj = sound->value;
1277 
1278 			if (_segMan->isHeapObject(soundObj) && lookupSelector(_segMan, soundObj, selector, nullptr, nullptr) != kSelectorNone) {
1279 				invokeSelector(sound->value, selector);
1280 			}
1281 			soundId = sound->succ;
1282 		}
1283 	}
1284 }
1285 #endif
1286 
1287 void GuestAdditions::syncAudioVolumeGlobalsToScummVM(const int index, const reg_t value) const {
1288 	switch (g_sci->getGameId()) {
1289 #ifdef ENABLE_SCI32
1290 	case GID_GK2:
1291 		if (index == kGlobalVarGK2MusicVolume) {
1292 			const int16 musicVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / Audio32::kMaxVolume;
1293 			ConfMan.setInt("music_volume", musicVolume);
1294 		}
1295 		break;
1296 
1297 	case GID_HOYLE5:
1298 		if (index == kGlobalVarHoyle5MusicVolume) {
1299 			const int16 masterVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / kHoyle5VolumeMax;
1300 			ConfMan.setInt("music_volume", masterVolume);
1301 			ConfMan.setInt("sfx_volume", masterVolume);
1302 			ConfMan.setInt("speech_volume", masterVolume);
1303 		}
1304 		break;
1305 
1306 	case GID_PHANTASMAGORIA:
1307 		if (index == kGlobalVarPhant1MusicVolume) {
1308 			const int16 musicVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / MUSIC_MASTERVOLUME_MAX;
1309 			ConfMan.setInt("music_volume", musicVolume);
1310 		} else if (index == kGlobalVarPhant1DACVolume) {
1311 			const int16 dacVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / Audio32::kMaxVolume;
1312 			ConfMan.setInt("sfx_volume", dacVolume);
1313 			ConfMan.setInt("speech_volume", dacVolume);
1314 		}
1315 		break;
1316 
1317 	case GID_PHANTASMAGORIA2:
1318 		if (index == kGlobalVarPhant2MasterVolume) {
1319 			const int16 masterVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / kPhant2VolumeMax;
1320 			ConfMan.setInt("music_volume", masterVolume);
1321 			ConfMan.setInt("sfx_volume", masterVolume);
1322 			ConfMan.setInt("speech_volume", masterVolume);
1323 		}
1324 		break;
1325 
1326 	case GID_RAMA:
1327 		if (index == kGlobalVarRamaMusicVolume) {
1328 			const int16 musicVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / kRamaVolumeMax;
1329 			ConfMan.setInt("music_volume", musicVolume);
1330 		}
1331 		break;
1332 
1333 	case GID_LSL7:
1334 	case GID_TORIN:
1335 		if (index == kGlobalVarTorinMusicVolume ||
1336 			index == kGlobalVarTorinSFXVolume ||
1337 			index == kGlobalVarTorinSpeechVolume) {
1338 
1339 			const int16 volume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / 100;
1340 
1341 			switch (index) {
1342 			case kGlobalVarTorinMusicVolume:
1343 				ConfMan.setInt("music_volume", volume);
1344 				break;
1345 			case kGlobalVarTorinSFXVolume:
1346 				ConfMan.setInt("sfx_volume", volume);
1347 				break;
1348 			case kGlobalVarTorinSpeechVolume:
1349 				ConfMan.setInt("speech_volume", volume);
1350 				break;
1351 			default:
1352 				break;
1353 			}
1354 		}
1355 		break;
1356 #endif
1357 
1358 	case GID_LSL6:
1359 	case GID_LSL6HIRES:
1360 		if (index == kGlobalVarLSL6MusicVolume) {
1361 			const int16 musicVolume = value.toSint16() * Audio::Mixer::kMaxMixerVolume / kLSL6UIVolumeMax;
1362 			ConfMan.setInt("music_volume", musicVolume);
1363 		}
1364 		break;
1365 
1366 	default:
1367 		break;
1368 	}
1369 }
1370 
1371 #ifdef ENABLE_SCI32
1372 void GuestAdditions::syncGK1AudioVolumeToScummVM(const reg_t soundObj, int16 volume) const {
1373 	const Common::String objName = _segMan->getObjectName(soundObj);
1374 	volume = volume * Audio::Mixer::kMaxMixerVolume / MUSIC_VOLUME_MAX;
1375 
1376 	// Using highest-numbered sound objects to sync only after all slots
1377 	// have been set by the volume slider
1378 	if (objName == "gkMusic2") {
1379 		ConfMan.setInt("music_volume", volume);
1380 		g_sci->updateSoundMixerVolumes();
1381 	} else if (objName == "gkSound3") {
1382 		ConfMan.setInt("sfx_volume", volume);
1383 		ConfMan.setInt("speech_volume", volume);
1384 		g_sci->updateSoundMixerVolumes();
1385 	}
1386 }
1387 
1388 #pragma mark -
1389 #pragma mark Audio UI sync
1390 
1391 void GuestAdditions::syncInGameUI(const int16 musicVolume, const int16 sfxVolume) const {
1392 	if (_state->abortScriptProcessing != kAbortNone) {
1393 		// Attempting to update a UI that is in the process of being destroyed
1394 		// will result in a crash
1395 		return;
1396 	}
1397 
1398 	switch (g_sci->getGameId()) {
1399 	case GID_MOTHERGOOSEHIRES:
1400 		syncMGDXUI(musicVolume);
1401 		break;
1402 
1403 	case GID_PQ4:
1404 		syncPQ4UI(musicVolume);
1405 		break;
1406 
1407 	case GID_PQSWAT:
1408 		syncPQSWATUI();
1409 		break;
1410 
1411 	case GID_QFG4:
1412 		syncQFG4UI(musicVolume);
1413 		break;
1414 
1415 	case GID_HOYLE5:
1416 		syncHoyle5UI(musicVolume);
1417 		break;
1418 
1419 	case GID_SHIVERS:
1420 		syncShivers1UI(sfxVolume);
1421 		break;
1422 
1423 	case GID_SQ6:
1424 		syncSQ6UI();
1425 		break;
1426 
1427 	default:
1428 		break;
1429 	}
1430 }
1431 
1432 void GuestAdditions::syncGK1UI() const {
1433 	const reg_t bars[] = { _segMan->findObjectByName("musicBar"),
1434 						   _segMan->findObjectByName("soundBar") };
1435 
1436 	for (int i = 0; i < ARRAYSIZE(bars); ++i) {
1437 		const reg_t barId = bars[i];
1438 		if (!barId.isNull()) {
1439 			// Resetting the position to 0 causes the bar to refresh its
1440 			// position when it next draws
1441 			writeSelectorValue(_segMan, barId, SELECTOR(position), 0);
1442 
1443 			// The `signal` property indicates bar visibility (for some
1444 			// reason, the normal `-info-` flag is not used)
1445 			if (readSelectorValue(_segMan, barId, SELECTOR(signal)) & 0x20) {
1446 				// `show` pulls a new value from the underlying sound object
1447 				// and refreshes the bar rendering
1448 				invokeSelector(barId, SELECTOR(show));
1449 			}
1450 		}
1451 	}
1452 }
1453 
1454 void GuestAdditions::syncGK2UI() const {
1455 	const reg_t sliderId = _segMan->findObjectByName("soundSlider");
1456 	if (!sliderId.isNull() && _segMan->getObject(sliderId)->isInserted()) {
1457 		const reg_t oldAcc = _state->r_acc;
1458 		invokeSelector(sliderId, SELECTOR(initialOff));
1459 		writeSelector(_segMan, sliderId, SELECTOR(x), _state->r_acc);
1460 		_state->r_acc = oldAcc;
1461 	}
1462 }
1463 
1464 void GuestAdditions::syncHoyle5UI(const int16 musicVolume) const {
1465 	// Hoyle5 has two control panels with different slider names
1466 	const reg_t sliders[] = { _segMan->findObjectByName("volumeSlider"),
1467 							  _segMan->findObjectByName("volumeSliderF") };
1468 	for (int i = 0; i < ARRAYSIZE(sliders); ++i) {
1469 		const reg_t sliderId = sliders[i];
1470 		if (!sliderId.isNull()) {
1471 			const int16 yPosition = 167 - musicVolume * 145 / 10;
1472 			writeSelectorValue(_segMan, sliderId, SELECTOR(y), yPosition);
1473 
1474 			// There does not seem to be any good way to learn whether the
1475 			// volume slider is visible (and thus eligible for
1476 			// kUpdateScreenItem)
1477 			const reg_t planeId = readSelector(_segMan, sliderId, SELECTOR(plane));
1478 			if (g_sci->_gfxFrameout->getPlanes().findByObject(planeId) != nullptr) {
1479 				g_sci->_gfxFrameout->kernelUpdateScreenItem(sliderId);
1480 			}
1481 		}
1482 	}
1483 }
1484 #endif
1485 
1486 void GuestAdditions::syncLSL6UI(const int16 musicVolume) const {
1487 	const reg_t musicDialId = _segMan->findObjectByName("volumeDial");
1488 	if (!musicDialId.isNull()) {
1489 		writeSelectorValue(_segMan, musicDialId, SELECTOR(curPos), musicVolume);
1490 		writeSelectorValue(_segMan, musicDialId, SELECTOR(cel), musicVolume);
1491 		reg_t params[] = { make_reg(0, musicVolume) };
1492 		// volumeDial's method is doit in the lo-res version and update in hi-res
1493 		if (g_sci->getGameId() == GID_LSL6) {
1494 			invokeSelector(musicDialId, SELECTOR(doit), 1, params);
1495 		} else {
1496 			invokeSelector(musicDialId, SELECTOR(update), 1, params);
1497 		}
1498 #ifdef ENABLE_SCI32
1499 		if (g_sci->getGameId() == GID_LSL6HIRES) {
1500 			if (_segMan->getObject(musicDialId)->isInserted()) {
1501 				g_sci->_gfxFrameout->kernelUpdateScreenItem(musicDialId);
1502 			}
1503 		}
1504 #endif
1505 	}
1506 }
1507 
1508 #ifdef ENABLE_SCI32
1509 void GuestAdditions::syncPhant1UI(const int16 oldMusicVolume, const int16 musicVolume, reg_t &musicGlobal, const int16 oldDacVolume, const int16 dacVolume, reg_t &dacGlobal) const {
1510 	const reg_t buttonId = _segMan->findObjectByName("dacVolUp");
1511 	if (buttonId.isNull() || !_segMan->getObject(buttonId)->isInserted()) {
1512 		// No inserted dacVolUp button means the control panel with the
1513 		// volume controls is not visible and we can just update the values
1514 		// and leave
1515 		musicGlobal.setOffset(musicVolume);
1516 		dacGlobal.setOffset(dacVolume);
1517 		return;
1518 	}
1519 
1520 	reg_t thermo = _segMan->findObjectByName("midiVolThermo");
1521 	if (!thermo.isNull()) {
1522 		int count = ABS(musicVolume - oldMusicVolume);
1523 		const int stepSize = (musicVolume > oldMusicVolume ? 1 : -1);
1524 		while (count--) {
1525 			musicGlobal.incOffset(stepSize);
1526 			invokeSelector(thermo, SELECTOR(doit));
1527 		}
1528 	}
1529 
1530 	thermo = _segMan->findObjectByName("dacVolThermo");
1531 	if (!thermo.isNull()) {
1532 		int count = ABS(dacVolume - oldDacVolume) / 8;
1533 		const int stepSize = (dacVolume > oldDacVolume ? 8 : -8);
1534 		while (count--) {
1535 			dacGlobal.incOffset(stepSize);
1536 			invokeSelector(thermo, SELECTOR(doit));
1537 		}
1538 	}
1539 }
1540 
1541 void GuestAdditions::syncPhant2UI(const int16 masterVolume) const {
1542 	const reg_t masterVolumeScript = _segMan->findObjectByName("foo2");
1543 	Common::Array<reg_t> scrollBars = _segMan->findObjectsByName("P2ScrollBar");
1544 	for (uint i = 0; i < scrollBars.size(); ++i) {
1545 		if (readSelector(_segMan, scrollBars[i], SELECTOR(client)) == masterVolumeScript) {
1546 			// P2ScrollBar objects may exist without actually being on-screen;
1547 			// the easiest way to tell seems to be to look to see if it has
1548 			// non-null pointers to subviews. (The game will correctly set the
1549 			// position of the scrollbar when it first becomes visible, so this
1550 			// is fine.)
1551 			if (!readSelector(_segMan, scrollBars[i], SELECTOR(physicalBar)).isNull()) {
1552 				reg_t params[] = { make_reg(0, masterVolume), make_reg(0, 1) };
1553 				invokeSelector(scrollBars[i], SELECTOR(move), 2, params);
1554 				break;
1555 			}
1556 		}
1557 	}
1558 }
1559 
1560 void GuestAdditions::syncMGDXUI(const int16 musicVolume) const {
1561 	const reg_t sliderId = _segMan->findObjectByName("icon1");
1562 	if (!sliderId.isNull()) {
1563 		const int16 celNo = 7 - (musicVolume * 8 / (MUSIC_MASTERVOLUME_MAX + 1));
1564 		writeSelectorValue(_segMan, sliderId, SELECTOR(mainCel), celNo);
1565 		writeSelectorValue(_segMan, sliderId, SELECTOR(cel), celNo);
1566 
1567 		// There does not seem to be any good way to learn whether the
1568 		// volume slider is visible (and thus eligible for
1569 		// kUpdateScreenItem)
1570 		const reg_t planeId = readSelector(_segMan, sliderId, SELECTOR(plane));
1571 		if (g_sci->_gfxFrameout->getPlanes().findByObject(planeId) != nullptr) {
1572 			g_sci->_gfxFrameout->kernelUpdateScreenItem(sliderId);
1573 		}
1574 	}
1575 }
1576 
1577 void GuestAdditions::syncPQ4UI(const int16 musicVolume) const {
1578 	const SegmentId segment = _segMan->getScriptSegment(9, SCRIPT_GET_DONT_LOAD);
1579 	if (segment != 0 && _segMan->getScript(segment)->getLocalsCount() > 2) {
1580 		const reg_t barId = _segMan->getScript(segment)->getLocalsBegin()[2];
1581 		if (!barId.isNull()) {
1582 			reg_t params[] = { make_reg(0, musicVolume) };
1583 			invokeSelector(barId, SELECTOR(setSize), 1, params);
1584 		}
1585 	}
1586 }
1587 
1588 void GuestAdditions::syncPQSWATUI() const {
1589 	const reg_t barId = _segMan->findObjectByName("volumeLed");
1590 	if (!barId.isNull() && _segMan->getObject(barId)->isInserted()) {
1591 		invokeSelector(barId, SELECTOR(displayValue));
1592 	}
1593 }
1594 
1595 void GuestAdditions::syncQFG4UI(const int16 musicVolume) const {
1596 	const reg_t sliderId = _segMan->findObjectByName("volumeSlider");
1597 	if (!sliderId.isNull()) {
1598 		const int16 yPosition = 84 - musicVolume * 34 / 10;
1599 		writeSelectorValue(_segMan, sliderId, SELECTOR(y), yPosition);
1600 
1601 		// There does not seem to be any good way to learn whether the
1602 		// volume slider is visible (and thus eligible for
1603 		// kUpdateScreenItem)
1604 		const reg_t planeId = readSelector(_segMan, sliderId, SELECTOR(plane));
1605 		if (g_sci->_gfxFrameout->getPlanes().findByObject(planeId) != nullptr) {
1606 			g_sci->_gfxFrameout->kernelUpdateScreenItem(sliderId);
1607 		}
1608 	}
1609 }
1610 
1611 void GuestAdditions::syncRamaUI(const int16 musicVolume) const {
1612 	const reg_t sliderId = _segMan->findObjectByName("volumeSlider");
1613 	if (!sliderId.isNull() && !readSelector(_segMan, sliderId, SELECTOR(plane)).isNull()) {
1614 		reg_t args[] = { make_reg(0, musicVolume) };
1615 		invokeSelector(sliderId, SELECTOR(setCel), 1, args);
1616 	}
1617 }
1618 
1619 void GuestAdditions::syncShivers1UI(const int16 dacVolume) const {
1620 	const reg_t sliderId = _segMan->findObjectByName("spVolume");
1621 	if (!sliderId.isNull()) {
1622 		const int16 xPosition = dacVolume * 78 / Audio32::kMaxVolume + 32;
1623 		writeSelectorValue(_segMan, sliderId, SELECTOR(x), xPosition);
1624 		if (_segMan->getObject(sliderId)->isInserted()) {
1625 			g_sci->_gfxFrameout->kernelUpdateScreenItem(sliderId);
1626 		}
1627 	}
1628 }
1629 
1630 void GuestAdditions::syncSQ6UI() const {
1631 	const reg_t bars[] = { _segMan->findObjectByName("musicBar"),
1632 						   _segMan->findObjectByName("soundBar") };
1633 	for (int i = 0; i < ARRAYSIZE(bars); ++i) {
1634 		const reg_t barId = bars[i];
1635 		if (!barId.isNull()) {
1636 			invokeSelector(barId, SELECTOR(show));
1637 		}
1638 	}
1639 }
1640 
1641 void GuestAdditions::syncTorinUI(const int16 musicVolume, const int16 sfxVolume, const int16 speechVolume) const {
1642 	const reg_t sliders[] = { _segMan->findObjectByName("oMusicScroll"),
1643 							  _segMan->findObjectByName("oSFXScroll"),
1644 							  _segMan->findObjectByName("oAudioScroll") };
1645 	const int16 values[] = { musicVolume, sfxVolume, speechVolume };
1646 	for (int i = 0; i < ARRAYSIZE(sliders); ++i) {
1647 		const reg_t sliderId = sliders[i];
1648 		if (!sliderId.isNull()) {
1649 			reg_t params[] = { make_reg(0, values[i]) };
1650 			invokeSelector(sliderId, SELECTOR(setPos), 1, params);
1651 		}
1652 	}
1653 }
1654 
1655 #pragma mark -
1656 #pragma mark Talk speed sync
1657 
1658 void GuestAdditions::syncTextSpeedFromScummVM() const {
1659 	const int16 textSpeed = 8 - (ConfMan.getInt("talkspeed") + 1) * 8 / 255;
1660 
1661 	_state->variables[VAR_GLOBAL][kGlobalVarTextSpeed] = make_reg(0, textSpeed);
1662 
1663 	if (g_sci->getGameId() == GID_GK1) {
1664 		const reg_t textBarId = _segMan->findObjectByName("textBar");
1665 		if (!textBarId.isNull()) {
1666 			// Resetting the bar position to 0 causes the game to retrieve the
1667 			// new text speed value and re-render
1668 			writeSelectorValue(_segMan, textBarId, SELECTOR(position), 0);
1669 		}
1670 	}
1671 }
1672 
1673 void GuestAdditions::syncTextSpeedToScummVM(const int index, const reg_t value) const {
1674 	if (index == kGlobalVarTextSpeed) {
1675 		ConfMan.setInt("talkspeed", (8 - value.toSint16()) * 255 / 8);
1676 	}
1677 }
1678 
1679 #endif
1680 
1681 } // End of namespace Sci
1682