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