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