1 //=============================================================================
2 //
3 // Adventure Game Studio (AGS)
4 //
5 // Copyright (C) 1999-2011 Chris Jones and 2011-20xx others
6 // The full list of copyright holders can be found in the Copyright.txt
7 // file, which is part of this source code distribution.
8 //
9 // The AGS source code is provided under the Artistic License 2.0.
10 // A copy of this license can be found in the file License.txt and at
11 // http://www.opensource.org/licenses/artistic-license-2.0.php
12 //
13 //=============================================================================
14
15 #include "ac/character.h"
16 #include "ac/common.h"
17 #include "ac/draw.h"
18 #include "ac/dynamicsprite.h"
19 #include "ac/event.h"
20 #include "ac/game.h"
21 #include "ac/gamesetupstruct.h"
22 #include "ac/gamestate.h"
23 #include "ac/gamesetup.h"
24 #include "ac/global_audio.h"
25 #include "ac/global_character.h"
26 #include "ac/gui.h"
27 #include "ac/mouse.h"
28 #include "ac/overlay.h"
29 #include "ac/region.h"
30 #include "ac/richgamemedia.h"
31 #include "ac/room.h"
32 #include "ac/roomstatus.h"
33 #include "ac/spritecache.h"
34 #include "ac/system.h"
35 #include "debug/out.h"
36 #include "device/mousew32.h"
37 #include "gfx/bitmap.h"
38 #include "gfx/ddb.h"
39 #include "gfx/graphicsdriver.h"
40 #include "game/savegame.h"
41 #include "game/savegame_internal.h"
42 #include "main/main.h"
43 #include "media/audio/audio.h"
44 #include "media/audio/soundclip.h"
45 #include "platform/base/agsplatformdriver.h"
46 #include "plugin/agsplugin.h"
47 #include "plugin/plugin_engine.h"
48 #include "script/script.h"
49 #include "script/cc_error.h"
50 #include "util/alignedstream.h"
51 #include "util/file.h"
52 #include "util/stream.h"
53 #include "util/string_utils.h"
54 #include "util/version.h"
55
56 using namespace Common;
57 using namespace Engine;
58
59 // function is currently implemented in game.cpp
60 SavegameError restore_game_data(Stream *in, SavegameVersion svg_version, const PreservedParams &pp, RestoredData &r_data);
61 void save_game_data(Stream *out);
62
63 extern GameSetupStruct game;
64 extern Bitmap **guibg;
65 extern AGS::Engine::IDriverDependantBitmap **guibgbmp;
66 extern AGS::Engine::IGraphicsDriver *gfxDriver;
67 extern Bitmap *dynamicallyCreatedSurfaces[MAX_DYNAMIC_SURFACES];
68 extern Bitmap *raw_saved_screen;
69 extern RoomStatus troom;
70 extern RoomStatus *croom;
71
72
73 namespace AGS
74 {
75 namespace Engine
76 {
77
78 const String SavegameSource::Signature = "Adventure Game Studio saved game";
79
80
SavegameSource()81 SavegameSource::SavegameSource()
82 : Version(kSvgVersion_Undefined)
83 {
84 }
85
SavegameDescription()86 SavegameDescription::SavegameDescription()
87 : ColorDepth(0)
88 {
89 }
90
PreservedParams()91 PreservedParams::PreservedParams()
92 : SpeechVOX(0)
93 , MusicVOX(0)
94 {
95 }
96
ScriptData()97 RestoredData::ScriptData::ScriptData()
98 : Len(0)
99 {
100 }
101
RestoredData()102 RestoredData::RestoredData()
103 : FPS(0)
104 , RoomVolume(0)
105 , CursorID(0)
106 , CursorMode(0)
107 {
108 memset(RoomBkgScene, 0, sizeof(RoomBkgScene));
109 memset(RoomLightLevels, 0, sizeof(RoomLightLevels));
110 memset(RoomTintLevels, 0, sizeof(RoomTintLevels));
111 memset(RoomZoomLevels1, 0, sizeof(RoomZoomLevels1));
112 memset(RoomZoomLevels2, 0, sizeof(RoomZoomLevels2));
113 memset(DoAmbient, 0, sizeof(DoAmbient));
114 }
115
GetSavegameErrorText(SavegameError err)116 String GetSavegameErrorText(SavegameError err)
117 {
118 switch (err)
119 {
120 case kSvgErr_NoError:
121 return "No error";
122 case kSvgErr_FileNotFound:
123 return "File not found";
124 case kSvgErr_NoStream:
125 return "Failed to open input stream";
126 case kSvgErr_SignatureFailed:
127 return "Not an AGS saved game or unsupported format";
128 case kSvgErr_FormatVersionNotSupported:
129 return "Save format version not supported";
130 case kSvgErr_IncompatibleEngine:
131 return "Save was written by incompatible engine, or file is corrupted";
132 case kSvgErr_InconsistentFormat:
133 return "Inconsistent format, or file is corrupted";
134 case kSvgErr_GameContentAssertion:
135 return "Saved content does not match current game";
136 case kSvgErr_InconsistentPlugin:
137 return "One of the game plugins did not restore its game data correctly";
138 case kSvgErr_DifferentColorDepth:
139 return "Saved with the engine running at a different colour depth";
140 case kSvgErr_GameObjectInitFailed:
141 return "Game object initialization failed after save restoration";
142 }
143 return "Unknown error";
144 }
145
RestoreSaveImage(Stream * in)146 Bitmap *RestoreSaveImage(Stream *in)
147 {
148 if (in->ReadInt32())
149 return read_serialized_bitmap(in);
150 return NULL;
151 }
152
SkipSaveImage(Stream * in)153 void SkipSaveImage(Stream *in)
154 {
155 if (in->ReadInt32())
156 skip_serialized_bitmap(in);
157 }
158
OpenSavegameBase(const String & filename,SavegameSource * src,SavegameDescription * desc,SavegameDescElem elems)159 SavegameError OpenSavegameBase(const String &filename, SavegameSource *src, SavegameDescription *desc, SavegameDescElem elems)
160 {
161 AStream in(File::OpenFileRead(filename));
162 if (!in.get())
163 return kSvgErr_FileNotFound;
164
165 // Skip MS Windows Vista rich media header
166 RICH_GAME_MEDIA_HEADER rich_media_header;
167 rich_media_header.ReadFromFile(in.get());
168
169 // Check saved game signature
170 String svg_sig = String::FromStreamCount(in.get(), SavegameSource::Signature.GetLength());
171 if (svg_sig.Compare(SavegameSource::Signature))
172 return kSvgErr_SignatureFailed;
173
174 String desc_text;
175 if (desc && elems == kSvgDesc_UserText)
176 desc_text.Read(in.get());
177 else
178 for (; in->ReadByte(); ); // skip until null terminator
179 SavegameVersion svg_ver = (SavegameVersion)in->ReadInt32();
180
181 // Check saved game format version
182 if (svg_ver < kSvgVersion_LowestSupported ||
183 svg_ver > kSvgVersion_Current)
184 {
185 return kSvgErr_FormatVersionNotSupported;
186 }
187
188 ABitmap image;
189 if (desc && elems == kSvgDesc_UserImage)
190 image.reset(RestoreSaveImage(in.get()));
191 else
192 SkipSaveImage(in.get());
193
194 String version_str = String::FromStream(in.get());
195 Version eng_version(version_str);
196 if (eng_version > EngineVersion ||
197 eng_version < SavedgameLowestBackwardCompatVersion)
198 {
199 // Engine version is either non-forward or non-backward compatible
200 return kSvgErr_IncompatibleEngine;
201 }
202 String main_file;
203 int color_depth;
204 if (desc && elems == kSvgDesc_EnvInfo)
205 {
206 main_file.Read(in.get());
207 in->ReadInt32(); // unscaled game height with borders, now obsolete
208 color_depth = in->ReadInt32();
209 }
210 else
211 {
212 for (; in->ReadByte(); ); // skip until null terminator
213 in->ReadInt32(); // unscaled game height with borders, now obsolete
214 in->ReadInt32(); // color depth
215 }
216
217 if (src)
218 {
219 src->Filename = filename;
220 src->Version = svg_ver;
221 src->InputStream.reset(in.release());
222 }
223 if (desc)
224 {
225 if (elems == kSvgDesc_EnvInfo)
226 {
227 desc->EngineVersion = eng_version;
228 desc->MainDataFilename = main_file;
229 desc->ColorDepth = color_depth;
230 }
231 if (elems == kSvgDesc_UserText)
232 desc->UserText = desc_text;
233 if (elems == kSvgDesc_UserImage)
234 desc->UserImage.reset(image.release());
235 }
236 return kSvgErr_NoError;
237 }
238
OpenSavegame(const String & filename,SavegameSource & src,SavegameDescription & desc,SavegameDescElem elems)239 SavegameError OpenSavegame(const String &filename, SavegameSource &src, SavegameDescription &desc, SavegameDescElem elems)
240 {
241 return OpenSavegameBase(filename, &src, &desc, elems);
242 }
243
OpenSavegame(const String & filename,SavegameDescription & desc,SavegameDescElem elems)244 SavegameError OpenSavegame(const String &filename, SavegameDescription &desc, SavegameDescElem elems)
245 {
246 return OpenSavegameBase(filename, NULL, &desc, elems);
247 }
248
249 // Prepares engine for actual save restore (stops processes, cleans up memory)
DoBeforeRestore(PreservedParams & pp)250 void DoBeforeRestore(PreservedParams &pp)
251 {
252 pp.SpeechVOX = play.want_speech;
253 pp.MusicVOX = play.separate_music_lib;
254
255 unload_old_room();
256 delete raw_saved_screen;
257 raw_saved_screen = NULL;
258 remove_screen_overlay(-1);
259 is_complete_overlay = 0;
260 is_text_overlay = 0;
261
262 // cleanup dynamic sprites
263 // NOTE: sprite 0 is a special constant sprite that cannot be dynamic
264 for (int i = 1; i < spriteset.elements; ++i)
265 {
266 if (game.spriteflags[i] & SPF_DYNAMICALLOC)
267 {
268 // do this early, so that it changing guibuts doesn't
269 // affect the restored data
270 free_dynamic_sprite(i);
271 }
272 }
273
274 // cleanup GUI backgrounds
275 for (int i = 0; i < game.numgui; ++i)
276 {
277 delete guibg[i];
278 guibg[i] = NULL;
279
280 if (guibgbmp[i])
281 gfxDriver->DestroyDDB(guibgbmp[i]);
282 guibgbmp[i] = NULL;
283 }
284
285 // preserve script data sizes and cleanup scripts
286 pp.GlScDataSize = gameinst->globaldatasize;
287 delete gameinstFork;
288 delete gameinst;
289 gameinstFork = NULL;
290 gameinst = NULL;
291 pp.ScMdDataSize.resize(numScriptModules);
292 for (int i = 0; i < numScriptModules; ++i)
293 {
294 pp.ScMdDataSize[i] = moduleInst[i]->globaldatasize;
295 delete moduleInstFork[i];
296 delete moduleInst[i];
297 moduleInst[i] = NULL;
298 }
299
300 play.FreeProperties();
301
302 delete roominstFork;
303 delete roominst;
304 roominstFork = NULL;
305 roominst = NULL;
306
307 delete dialogScriptsInst;
308 dialogScriptsInst = NULL;
309
310 resetRoomStatuses();
311 troom.FreeScriptData();
312 troom.FreeProperties();
313 free_do_once_tokens();
314
315 // unregister gui controls from API exports
316 // TODO: find out why are we doing this here? perhaps remove if we do full managed pool reset in DoBeforeRestore
317 for (int i = 0; i < game.numgui; ++i)
318 {
319 unexport_gui_controls(i);
320 }
321
322 // NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
323 for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i)
324 {
325 stop_and_destroy_channel_ex(i, false);
326 }
327
328 clear_music_cache();
329 }
330
331 // Final processing after successfully restoring from save
DoAfterRestore(const PreservedParams & pp,const RestoredData & r_data)332 SavegameError DoAfterRestore(const PreservedParams &pp, const RestoredData &r_data)
333 {
334 // Use a yellow dialog highlight for older game versions
335 // CHECKME: it is dubious that this should be right here
336 if(loaded_game_file_version < kGameVersion_331)
337 play.dialog_options_highlight_color = DIALOG_OPTIONS_HIGHLIGHT_COLOR_DEFAULT;
338
339 // Preserve whether the music vox is available
340 play.separate_music_lib = pp.MusicVOX;
341 // If they had the vox when they saved it, but they don't now
342 if ((pp.SpeechVOX < 0) && (play.want_speech >= 0))
343 play.want_speech = (-play.want_speech) - 1;
344 // If they didn't have the vox before, but now they do
345 else if ((pp.SpeechVOX >= 0) && (play.want_speech < 0))
346 play.want_speech = (-play.want_speech) - 1;
347
348 // recache queued clips
349 for (int i = 0; i < play.new_music_queue_size; ++i)
350 {
351 play.new_music_queue[i].cachedClip = NULL;
352 }
353
354 // restore these to the ones retrieved from the save game
355 const size_t dynsurf_num = Math::Min((size_t)MAX_DYNAMIC_SURFACES, r_data.DynamicSurfaces.size());
356 for (size_t i = 0; i < dynsurf_num; ++i)
357 {
358 dynamicallyCreatedSurfaces[i] = r_data.DynamicSurfaces[i];
359 }
360
361 for (int i = 0; i < game.numgui; ++i)
362 export_gui_controls(i);
363 update_gui_zorder();
364
365 if (create_global_script())
366 {
367 Debug::Printf(kDbgMsg_Error, "Restore game error: unable to recreate global script: %s", ccErrorString);
368 return kSvgErr_GameObjectInitFailed;
369 }
370
371 // read the global data into the newly created script
372 if (r_data.GlobalScript.Data.get())
373 memcpy(gameinst->globaldata, r_data.GlobalScript.Data.get(),
374 Math::Min((size_t)gameinst->globaldatasize, r_data.GlobalScript.Len));
375
376 // restore the script module data
377 for (int i = 0; i < numScriptModules; ++i)
378 {
379 if (r_data.ScriptModules[i].Data.get())
380 memcpy(moduleInst[i]->globaldata, r_data.ScriptModules[i].Data.get(),
381 Math::Min((size_t)moduleInst[i]->globaldatasize, r_data.ScriptModules[i].Len));
382 }
383
384 setup_player_character(game.playercharacter);
385
386 // Save some parameters to restore them after room load
387 int gstimer=play.gscript_timer;
388 int oldx1 = play.mboundx1, oldx2 = play.mboundx2;
389 int oldy1 = play.mboundy1, oldy2 = play.mboundy2;
390
391 // disable the queue momentarily
392 int queuedMusicSize = play.music_queue_size;
393 play.music_queue_size = 0;
394
395 update_polled_stuff_if_runtime();
396
397 // load the room the game was saved in
398 if (displayed_room >= 0)
399 load_new_room(displayed_room, NULL);
400
401 update_polled_stuff_if_runtime();
402
403 play.gscript_timer=gstimer;
404 // restore the correct room volume (they might have modified
405 // it with SetMusicVolume)
406 thisroom.options[ST_VOLUME] = r_data.RoomVolume;
407
408 Mouse::SetMoveLimit(Rect(oldx1, oldy1, oldx2, oldy2));
409
410 set_cursor_mode(r_data.CursorMode);
411 set_mouse_cursor(r_data.CursorID);
412 if (r_data.CursorMode == MODE_USE)
413 SetActiveInventory(playerchar->activeinv);
414 // ensure that the current cursor is locked
415 spriteset.precache(game.mcurs[r_data.CursorID].pic);
416
417 #if (ALLEGRO_DATE > 19990103)
418 set_window_title(play.game_name);
419 #endif
420
421 update_polled_stuff_if_runtime();
422
423 if (displayed_room >= 0)
424 {
425 for (int i = 0; i < MAX_BSCENE; ++i)
426 {
427 if (r_data.RoomBkgScene[i])
428 {
429 delete thisroom.ebscene[i];
430 thisroom.ebscene[i] = r_data.RoomBkgScene[i];
431 }
432 }
433
434 in_new_room=3; // don't run "enters screen" events
435 // now that room has loaded, copy saved light levels in
436 memcpy(thisroom.regionLightLevel, r_data.RoomLightLevels, sizeof(short) * MAX_REGIONS);
437 memcpy(thisroom.regionTintLevel, r_data.RoomTintLevels, sizeof(int) * MAX_REGIONS);
438 generate_light_table();
439
440 memcpy(thisroom.walk_area_zoom, r_data.RoomZoomLevels1, sizeof(short) * (MAX_WALK_AREAS + 1));
441 memcpy(thisroom.walk_area_zoom2, r_data.RoomZoomLevels2, sizeof(short) * (MAX_WALK_AREAS + 1));
442
443 on_background_frame_change();
444 }
445
446 gui_disabled_style = convert_gui_disabled_style(game.options[OPT_DISABLEOFF]);
447
448 // restore the queue now that the music is playing
449 play.music_queue_size = queuedMusicSize;
450
451 if (play.digital_master_volume >= 0)
452 System_SetVolume(play.digital_master_volume);
453
454 // Run audio clips on channels
455 // these two crossfading parameters have to be temporarily reset
456 const int cf_in_chan = play.crossfading_in_channel;
457 const int cf_out_chan = play.crossfading_out_channel;
458 play.crossfading_in_channel = 0;
459 play.crossfading_out_channel = 0;
460 // NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
461 for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i)
462 {
463 const RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
464 if (chan_info.ClipID < 0)
465 continue;
466 if (chan_info.ClipID >= game.audioClipCount)
467 {
468 Debug::Printf(kDbgMsg_Error, "Restore game error: invalid audio clip index: %d (clip count: %d)", chan_info.ClipID, game.audioClipCount);
469 return kSvgErr_GameObjectInitFailed;
470 }
471 play_audio_clip_on_channel(i, &game.audioClips[chan_info.ClipID],
472 chan_info.Priority, chan_info.Repeat, chan_info.Pos);
473 if (channels[i] != NULL)
474 {
475 channels[i]->set_volume_direct(chan_info.VolAsPercent, chan_info.Vol);
476 channels[i]->set_speed(chan_info.Speed);
477 channels[i]->set_panning(chan_info.Pan);
478 channels[i]->panningAsPercentage = chan_info.PanAsPercent;
479 }
480 }
481 if ((cf_in_chan > 0) && (channels[cf_in_chan] != NULL))
482 play.crossfading_in_channel = cf_in_chan;
483 if ((cf_out_chan > 0) && (channels[cf_out_chan] != NULL))
484 play.crossfading_out_channel = cf_out_chan;
485
486 // If there were synced audio tracks, the time taken to load in the
487 // different channels will have thrown them out of sync, so re-time it
488 // NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
489 for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i)
490 {
491 int pos = r_data.AudioChans[i].Pos;
492 if ((pos > 0) && (channels[i] != NULL) && (channels[i]->done == 0))
493 {
494 channels[i]->seek(pos);
495 }
496 }
497
498 // TODO: investigate loop range
499 for (int i = 1; i < MAX_SOUND_CHANNELS; ++i)
500 {
501 if (r_data.DoAmbient[i])
502 PlayAmbientSound(i, r_data.DoAmbient[i], ambient[i].vol, ambient[i].x, ambient[i].y);
503 }
504
505 for (int i = 0; i < game.numgui; ++i)
506 {
507 guibg[i] = BitmapHelper::CreateBitmap(guis[i].Width, guis[i].Height, game.GetColorDepth());
508 guibg[i] = ReplaceBitmapWithSupportedFormat(guibg[i]);
509 }
510
511 recreate_overlay_ddbs();
512
513 guis_need_update = 1;
514
515 play.ignore_user_input_until_time = 0;
516 update_polled_stuff_if_runtime();
517
518 pl_run_plugin_hooks(AGSE_POSTRESTOREGAME, 0);
519
520 if (displayed_room < 0)
521 {
522 // the restart point, no room was loaded
523 load_new_room(playerchar->room, playerchar);
524 playerchar->prevroom = -1;
525
526 first_room_initialization();
527 }
528
529 if ((play.music_queue_size > 0) && (cachedQueuedMusic == NULL))
530 {
531 cachedQueuedMusic = load_music_from_disk(play.music_queue[0], 0);
532 }
533
534 // test if the playing music was properly loaded
535 if (current_music_type > 0)
536 {
537 if (crossFading > 0 && !channels[crossFading] ||
538 crossFading <= 0 && !channels[SCHAN_MUSIC])
539 {
540 current_music_type = 0;
541 }
542 }
543
544 set_game_speed(r_data.FPS);
545
546 return kSvgErr_NoError;
547 }
548
RestoreGameState(Stream * in,SavegameVersion svg_version)549 SavegameError RestoreGameState(Stream *in, SavegameVersion svg_version)
550 {
551 PreservedParams pp;
552 RestoredData r_data;
553 DoBeforeRestore(pp);
554 SavegameError err = restore_game_data(in, svg_version, pp, r_data);
555 if (err != kSvgErr_NoError)
556 return err;
557 return DoAfterRestore(pp, r_data);
558 }
559
WriteSaveImage(Stream * out,const Bitmap * screenshot)560 void WriteSaveImage(Stream *out, const Bitmap *screenshot)
561 {
562 // store the screenshot at the start to make it easily accesible
563 out->WriteInt32((screenshot == NULL) ? 0 : 1);
564
565 if (screenshot)
566 serialize_bitmap(screenshot, out);
567 }
568
StartSavegame(const String & filename,const String & desc,const Bitmap * image)569 Stream *StartSavegame(const String &filename, const String &desc, const Bitmap *image)
570 {
571 Stream *out = Common::File::CreateFile(filename);
572 if (!out)
573 return NULL;
574
575 // Initialize and write Vista header
576 RICH_GAME_MEDIA_HEADER vistaHeader;
577 memset(&vistaHeader, 0, sizeof(RICH_GAME_MEDIA_HEADER));
578 memcpy(&vistaHeader.dwMagicNumber, RM_MAGICNUMBER, sizeof(int));
579 vistaHeader.dwHeaderVersion = 1;
580 vistaHeader.dwHeaderSize = sizeof(RICH_GAME_MEDIA_HEADER);
581 vistaHeader.dwThumbnailOffsetHigherDword = 0;
582 vistaHeader.dwThumbnailOffsetLowerDword = 0;
583 vistaHeader.dwThumbnailSize = 0;
584 convert_guid_from_text_to_binary(game.guid, &vistaHeader.guidGameId[0]);
585 uconvert(game.gamename, U_ASCII, (char*)&vistaHeader.szGameName[0], U_UNICODE, RM_MAXLENGTH);
586 uconvert(desc, U_ASCII, (char*)&vistaHeader.szSaveName[0], U_UNICODE, RM_MAXLENGTH);
587 vistaHeader.szLevelName[0] = 0;
588 vistaHeader.szComments[0] = 0;
589
590 // MS Windows Vista rich media header
591 vistaHeader.WriteToFile(out);
592
593 // Savegame signature
594 out->Write(SavegameSource::Signature, SavegameSource::Signature.GetLength());
595 // Description
596 StrUtil::WriteCStr(desc, out);
597
598 pl_run_plugin_hooks(AGSE_PRESAVEGAME, 0);
599 out->WriteInt32(kSvgVersion_Current);
600 WriteSaveImage(out, image);
601
602 // Write lowest forward-compatible version string, so that
603 // earlier versions could load savedgames made by current engine
604 String compat_version;
605 if (SavedgameLowestForwardCompatVersion <= Version::LastOldFormatVersion)
606 compat_version = SavedgameLowestForwardCompatVersion.BackwardCompatibleString;
607 else
608 compat_version = SavedgameLowestForwardCompatVersion.LongString;
609 StrUtil::WriteCStr(compat_version, out);
610 StrUtil::WriteCStr(usetup.main_data_filename, out);
611
612 // Write current display mode parameters
613 out->WriteInt32(play.viewport.GetHeight()); // for compatibility with old engines
614 out->WriteInt32(game.GetColorDepth());
615 return out;
616 }
617
DoBeforeSave()618 void DoBeforeSave()
619 {
620 if (play.cur_music_number >= 0)
621 {
622 if (IsMusicPlaying() == 0)
623 play.cur_music_number = -1;
624 }
625
626 if (displayed_room >= 0)
627 {
628 // update the current room script's data segment copy
629 if (roominst)
630 save_room_data_segment();
631
632 // Update the saved interaction variable values
633 for (int i = 0; i < thisroom.numLocalVars; ++i)
634 croom->interactionVariableValues[i] = thisroom.localvars[i].Value;
635 }
636 }
637
SaveGameState(Stream * out)638 void SaveGameState(Stream *out)
639 {
640 DoBeforeSave();
641 save_game_data(out);
642 }
643
644 } // namespace Engine
645 } // namespace AGS
646