/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "common/std/map.h"
#include "ags/engine/game/savegame_components.h"
#include "ags/shared/ac/audio_clip_type.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/dialog.h"
#include "ags/engine/ac/button.h"
#include "ags/engine/ac/character.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/dynamic_sprite.h"
#include "ags/engine/ac/game.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/ac/gui.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/move_list.h"
#include "ags/engine/ac/overlay.h"
#include "ags/engine/ac/room_status.h"
#include "ags/engine/ac/screen_overlay.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/shared/ac/view.h"
#include "ags/engine/ac/system.h"
#include "ags/engine/ac/dynobj/cc_serializer.h"
#include "ags/engine/ac/dynobj/dynobj_manager.h"
#include "ags/shared/debugging/out.h"
#include "ags/engine/game/savegame_internal.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/engine/gui/animating_gui_button.h"
#include "ags/shared/gui/gui_button.h"
#include "ags/shared/gui/gui_inv.h"
#include "ags/shared/gui/gui_label.h"
#include "ags/shared/gui/gui_listbox.h"
#include "ags/shared/gui/gui_main.h"
#include "ags/shared/gui/gui_slider.h"
#include "ags/shared/gui/gui_textbox.h"
#include "ags/plugins/plugin_engine.h"
#include "ags/shared/script/cc_common.h"
#include "ags/engine/script/script.h"
#include "ags/shared/util/file_stream.h" // TODO: needed only because plugins expect file handle
#include "ags/engine/media/audio/audio_system.h"
namespace AGS3 {
using namespace Shared;
namespace AGS {
namespace Engine {
namespace SavegameComponents {
void WriteFormatTag(Stream *out, const String &tag, bool open = true) {
String full_tag = String::FromFormat(open ? "<%s>" : "%s>", tag.GetCStr());
out->Write(full_tag.GetCStr(), full_tag.GetLength());
}
bool ReadFormatTag(Stream *in, String &tag, bool open = true) {
if (in->ReadByte() != '<')
return false;
if (!open && in->ReadByte() != '/')
return false;
tag.Empty();
while (!in->EOS()) {
char c = in->ReadByte();
if (c == '>')
return true;
tag.AppendChar(c);
}
return false; // reached EOS before closing symbol
}
bool AssertFormatTag(Stream *in, const String &tag, bool open = true) {
String read_tag;
if (!ReadFormatTag(in, read_tag, open))
return false;
return read_tag.Compare(tag) == 0;
}
bool AssertFormatTagStrict(HSaveError &err, Stream *in, const String &tag, bool open = true) {
String read_tag;
if (!ReadFormatTag(in, read_tag, open) || read_tag.Compare(tag) != 0) {
err = new SavegameError(kSvgErr_InconsistentFormat,
String::FromFormat("Mismatching tag: %s.", tag.GetCStr()));
return false;
}
return true;
}
inline bool AssertCompatLimit(HSaveError &err, int count, int max_count, const char *content_name) {
if (count > max_count) {
err = new SavegameError(kSvgErr_IncompatibleEngine,
String::FromFormat("Incompatible number of %s (count: %d, max: %d).",
content_name, count, max_count));
return false;
}
return true;
}
inline bool AssertCompatRange(HSaveError &err, int value, int min_value, int max_value, const char *content_name) {
if (value < min_value || value > max_value) {
err = new SavegameError(kSvgErr_IncompatibleEngine,
String::FromFormat("Restore game error: incompatible %s (id: %d, range: %d - %d).",
content_name, value, min_value, max_value));
return false;
}
return true;
}
inline bool AssertGameContent(HSaveError &err, int new_val, int original_val, const char *content_name) {
if (new_val != original_val) {
err = new SavegameError(kSvgErr_GameContentAssertion,
String::FromFormat("Mismatching number of %s (game: %d, save: %d).",
content_name, original_val, new_val));
return false;
}
return true;
}
inline bool AssertGameObjectContent(HSaveError &err, int new_val, int original_val, const char *content_name,
const char *obj_type, int obj_id) {
if (new_val != original_val) {
err = new SavegameError(kSvgErr_GameContentAssertion,
String::FromFormat("Mismatching number of %s, %s #%d (game: %d, save: %d).",
content_name, obj_type, obj_id, original_val, new_val));
return false;
}
return true;
}
inline bool AssertGameObjectContent2(HSaveError &err, int new_val, int original_val, const char *content_name,
const char *obj1_type, int obj1_id, const char *obj2_type, int obj2_id) {
if (new_val != original_val) {
err = new SavegameError(kSvgErr_GameContentAssertion,
String::FromFormat("Mismatching number of %s, %s #%d, %s #%d (game: %d, save: %d).",
content_name, obj1_type, obj1_id, obj2_type, obj2_id, original_val, new_val));
return false;
}
return true;
}
void WriteCameraState(const Camera &cam, Stream *out) {
int flags = 0;
if (cam.IsLocked()) flags |= kSvgCamPosLocked;
out->WriteInt32(flags);
const Rect &rc = cam.GetRect();
out->WriteInt32(rc.Left);
out->WriteInt32(rc.Top);
out->WriteInt32(rc.GetWidth());
out->WriteInt32(rc.GetHeight());
}
void WriteViewportState(const Viewport &view, Stream *out) {
int flags = 0;
if (view.IsVisible()) flags |= kSvgViewportVisible;
out->WriteInt32(flags);
const Rect &rc = view.GetRect();
out->WriteInt32(rc.Left);
out->WriteInt32(rc.Top);
out->WriteInt32(rc.GetWidth());
out->WriteInt32(rc.GetHeight());
out->WriteInt32(view.GetZOrder());
auto cam = view.GetCamera();
if (cam)
out->WriteInt32(cam->GetID());
else
out->WriteInt32(-1);
}
HSaveError WriteGameState(Stream *out) {
// Game base
_GP(game).WriteForSavegame(out);
// Game palette
// TODO: probably no need to save this for hi/true-res game
out->WriteArray(_G(palette), sizeof(RGB), 256);
if (_G(loaded_game_file_version) <= kGameVersion_272) {
// Global variables
out->WriteInt32(_G(numGlobalVars));
for (int i = 0; i < _G(numGlobalVars); ++i)
_G(globalvars)[i].Write(out);
}
// Game state
_GP(play).WriteForSavegame(out);
// Other dynamic values
out->WriteInt32(_G(frames_per_second));
out->WriteInt32(_G(loopcounter));
out->WriteInt32(_G(ifacepopped));
out->WriteInt32(_G(game_paused));
// Mouse cursor
out->WriteInt32(_G(cur_mode));
out->WriteInt32(_G(cur_cursor));
out->WriteInt32(_G(mouse_on_iface));
// Viewports and cameras
int viewcam_flags = 0;
if (_GP(play).IsAutoRoomViewport())
viewcam_flags |= kSvgGameAutoRoomView;
out->WriteInt32(viewcam_flags);
out->WriteInt32(_GP(play).GetRoomCameraCount());
for (int i = 0; i < _GP(play).GetRoomCameraCount(); ++i)
WriteCameraState(*_GP(play).GetRoomCamera(i), out);
out->WriteInt32(_GP(play).GetRoomViewportCount());
for (int i = 0; i < _GP(play).GetRoomViewportCount(); ++i)
WriteViewportState(*_GP(play).GetRoomViewport(i), out);
return HSaveError::None();
}
void ReadLegacyCameraState(Stream *in, RestoredData & r_data) {
// Precreate viewport and camera and save data in temp structs
int camx = in->ReadInt32();
int camy = in->ReadInt32();
_GP(play).CreateRoomCamera();
_GP(play).CreateRoomViewport();
RestoredData::CameraData cam_dat;
cam_dat.ID = 0;
cam_dat.Left = camx;
cam_dat.Top = camy;
r_data.Cameras.push_back(cam_dat);
RestoredData::ViewportData view_dat;
view_dat.ID = 0;
view_dat.Flags = kSvgViewportVisible;
view_dat.CamID = 0;
r_data.Viewports.push_back(view_dat);
r_data.LegacyViewCamera = true;
}
void ReadCameraState(RestoredData &r_data, Stream *in) {
RestoredData::CameraData cam;
cam.ID = r_data.Cameras.size();
cam.Flags = in->ReadInt32();
cam.Left = in->ReadInt32();
cam.Top = in->ReadInt32();
cam.Width = in->ReadInt32();
cam.Height = in->ReadInt32();
r_data.Cameras.push_back(cam);
}
void ReadViewportState(RestoredData &r_data, Stream *in) {
RestoredData::ViewportData view;
view.ID = r_data.Viewports.size();
view.Flags = in->ReadInt32();
view.Left = in->ReadInt32();
view.Top = in->ReadInt32();
view.Width = in->ReadInt32();
view.Height = in->ReadInt32();
view.ZOrder = in->ReadInt32();
view.CamID = in->ReadInt32();
r_data.Viewports.push_back(view);
}
HSaveError ReadGameState(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData &r_data) {
HSaveError err;
GameStateSvgVersion svg_ver = (GameStateSvgVersion)cmp_ver;
// Game base
_GP(game).ReadFromSavegame(in);
// Game palette
in->ReadArray(_G(palette), sizeof(RGB), 256);
if (_G(loaded_game_file_version) <= kGameVersion_272) {
// Legacy interaction global variables
if (!AssertGameContent(err, in->ReadInt32(), _G(numGlobalVars), "Global Variables"))
return err;
for (int i = 0; i < _G(numGlobalVars); ++i)
_G(globalvars)[i].Read(in);
}
// Game state
_GP(play).ReadFromSavegame(in, _G(loaded_game_file_version), svg_ver, r_data);
// Other dynamic values
r_data.FPS = in->ReadInt32();
set_loop_counter(in->ReadInt32());
_G(ifacepopped) = in->ReadInt32();
_G(game_paused) = in->ReadInt32();
// Mouse cursor state
r_data.CursorMode = in->ReadInt32();
r_data.CursorID = in->ReadInt32();
_G(mouse_on_iface) = in->ReadInt32();
// Viewports and cameras
if (svg_ver < kGSSvgVersion_350_10) {
ReadLegacyCameraState(in, r_data);
r_data.Cameras[0].Flags = r_data.Camera0_Flags;
} else {
int viewcam_flags = in->ReadInt32();
_GP(play).SetAutoRoomViewport((viewcam_flags & kSvgGameAutoRoomView) != 0);
// TODO: we create viewport and camera objects here because they are
// required for the managed pool deserialization, but read actual
// data into temp structs because we need to apply it after active
// room is loaded.
// See comments to RestoredData struct for further details.
int cam_count = in->ReadInt32();
for (int i = 0; i < cam_count; ++i) {
_GP(play).CreateRoomCamera();
ReadCameraState(r_data, in);
}
int view_count = in->ReadInt32();
for (int i = 0; i < view_count; ++i) {
_GP(play).CreateRoomViewport();
ReadViewportState(r_data, in);
}
}
return err;
}
// Savegame data format for RoomStatus
enum AudioSvgVersion {
kAudioSvgVersion_Initial = 0,
kAudioSvgVersion_35026 = 1, // source position settings
kAudioSvgVersion_36009 = 2, // up number of channels
kAudioSvgVersion_36130 = 3060130, // playback state
};
HSaveError WriteAudio(Stream *out) {
// Game content assertion
out->WriteInt32(_GP(game).audioClipTypes.size());
out->WriteInt8(TOTAL_AUDIO_CHANNELS);
out->WriteInt8(_GP(game).numGameChannels);
out->WriteInt16(0); // reserved 2 bytes (remains of int32)
// Audio types
for (size_t i = 0; i < _GP(game).audioClipTypes.size(); ++i) {
_GP(game).audioClipTypes[i].WriteToSavegame(out);
out->WriteInt32(_GP(play).default_audio_type_volumes[i]);
}
// Audio clips and crossfade
for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
auto *ch = AudioChans::GetChannelIfPlaying(i);
if ((ch != nullptr) && (ch->_sourceClipID >= 0)) {
out->WriteInt32(ch->_sourceClipID);
out->WriteInt32(ch->get_pos());
out->WriteInt32(ch->_priority);
out->WriteInt32(ch->_repeat ? 1 : 0);
out->WriteInt32(ch->get_volume255());
out->WriteInt32(0); // was redundant data
out->WriteInt32(ch->get_volume100());
out->WriteInt32(ch->get_panning());
out->WriteInt32(ch->get_speed());
// since version kAudioSvgVersion_35026
out->WriteInt32(ch->_xSource);
out->WriteInt32(ch->_ySource);
out->WriteInt32(ch->_maximumPossibleDistanceAway);
// since version kAudioSvgVersion_36130
int playback_flags = 0;
if (ch->is_paused())
playback_flags |= kSvgAudioPaused;
out->WriteInt32(playback_flags);
out->WriteInt32(0); // reserved 3 ints
out->WriteInt32(0);
out->WriteInt32(0);
} else {
out->WriteInt32(-1);
}
}
out->WriteInt32(_G(crossFading));
out->WriteInt32(_G(crossFadeVolumePerStep));
out->WriteInt32(_G(crossFadeStep));
out->WriteInt32(_G(crossFadeVolumeAtStart));
// CHECKME: why this needs to be saved?
out->WriteInt32(_G(current_music_type));
// Ambient sound
for (int i = 0; i < _GP(game).numGameChannels; ++i)
_GP(ambient)[i].WriteToFile(out);
return HSaveError::None();
}
HSaveError ReadAudio(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData &r_data) {
HSaveError err;
// Game content assertion
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).audioClipTypes.size(), "Audio Clip Types"))
return err;
int total_channels, max_game_channels;
if (cmp_ver >= kAudioSvgVersion_36009) {
total_channels = in->ReadInt8();
max_game_channels = in->ReadInt8();
in->ReadInt16(); // reserved 2 bytes
if (!AssertCompatLimit(err, total_channels, TOTAL_AUDIO_CHANNELS, "System Audio Channels") ||
!AssertCompatLimit(err, max_game_channels, MAX_GAME_CHANNELS, "Game Audio Channels"))
return err;
} else {
total_channels = TOTAL_AUDIO_CHANNELS_v320;
max_game_channels = MAX_GAME_CHANNELS_v320;
in->ReadInt32(); // unused in prev format ver
}
// Audio types
for (size_t i = 0; i < _GP(game).audioClipTypes.size(); ++i) {
_GP(game).audioClipTypes[i].ReadFromSavegame(in);
_GP(play).default_audio_type_volumes[i] = in->ReadInt32();
}
// Audio clips and crossfade
for (int i = 0; i < total_channels; ++i) {
RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
chan_info.Pos = 0;
chan_info.ClipID = in->ReadInt32();
if (chan_info.ClipID >= 0) {
chan_info.Pos = in->ReadInt32();
if (chan_info.Pos < 0)
chan_info.Pos = 0;
chan_info.Priority = in->ReadInt32();
chan_info.Repeat = in->ReadInt32();
chan_info.Vol = in->ReadInt32();
in->ReadInt32(); // was redundant data
chan_info.VolAsPercent = in->ReadInt32();
chan_info.Pan = in->ReadInt32();
chan_info.Speed = 1000;
chan_info.Speed = in->ReadInt32();
if (cmp_ver >= kAudioSvgVersion_35026) {
chan_info.XSource = in->ReadInt32();
chan_info.YSource = in->ReadInt32();
chan_info.MaxDist = in->ReadInt32();
}
if (cmp_ver >= kAudioSvgVersion_36130) {
chan_info.Flags = in->ReadInt32();
in->ReadInt32(); // reserved 3 ints
in->ReadInt32();
in->ReadInt32();
}
}
}
_G(crossFading) = in->ReadInt32();
_G(crossFadeVolumePerStep) = in->ReadInt32();
_G(crossFadeStep) = in->ReadInt32();
_G(crossFadeVolumeAtStart) = in->ReadInt32();
// preserve legacy music type setting
_G(current_music_type) = in->ReadInt32();
// Ambient sound
for (int i = 0; i < max_game_channels; ++i)
_GP(ambient)[i].ReadFromFile(in);
for (int i = NUM_SPEECH_CHANS; i < max_game_channels; ++i) {
if (_GP(ambient)[i].channel == 0) {
r_data.DoAmbient[i] = 0;
} else {
r_data.DoAmbient[i] = _GP(ambient)[i].num;
_GP(ambient)[i].channel = 0;
}
}
return err;
}
void WriteTimesRun272(const Interaction &intr, Stream *out) {
for (size_t i = 0; i < intr.Events.size(); ++i)
out->WriteInt32(intr.Events[i].TimesRun);
}
void WriteInteraction272(const Interaction &intr, Stream *out) {
const size_t evt_count = intr.Events.size();
out->WriteInt32(evt_count);
for (size_t i = 0; i < evt_count; ++i)
out->WriteInt32(intr.Events[i].Type);
WriteTimesRun272(intr, out);
}
void ReadTimesRun272(Interaction &intr, Stream *in) {
for (size_t i = 0; i < intr.Events.size(); ++i)
intr.Events[i].TimesRun = in->ReadInt32();
}
HSaveError ReadInteraction272(Interaction &intr, Stream *in) {
HSaveError err;
const size_t evt_count = in->ReadInt32();
if (!AssertCompatLimit(err, evt_count, MAX_NEWINTERACTION_EVENTS, "interactions"))
return err;
intr.Events.resize(evt_count);
for (size_t i = 0; i < evt_count; ++i)
intr.Events[i].Type = in->ReadInt32();
ReadTimesRun272(intr, in);
return err;
}
HSaveError WriteCharacters(Stream *out) {
out->WriteInt32(_GP(game).numcharacters);
for (int i = 0; i < _GP(game).numcharacters; ++i) {
_GP(game).chars[i].WriteToSavegame(out, _GP(game).chars2[i]);
_GP(charextra)[i].WriteToSavegame(out);
Properties::WriteValues(_GP(play).charProps[i], out);
if (_G(loaded_game_file_version) <= kGameVersion_272)
WriteTimesRun272(*_GP(game).intrChar[i], out);
}
return HSaveError::None();
}
HSaveError ReadCharacters(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).numcharacters, "Characters"))
return err;
for (int i = 0; i < _GP(game).numcharacters; ++i) {
_GP(game).chars[i].ReadFromSavegame(in, _GP(game).chars2[i], static_cast(cmp_ver));
_GP(charextra)[i].ReadFromSavegame(in, static_cast(cmp_ver));
Properties::ReadValues(_GP(play).charProps[i], in);
if (_G(loaded_game_file_version) <= kGameVersion_272)
ReadTimesRun272(*_GP(game).intrChar[i], in);
// character movement path (for old saves)
if (cmp_ver < kCharSvgVersion_36109) {
err = _GP(mls)[CHMLSOFFS + i].ReadFromSavegame(in, kMoveSvgVersion_350);
if (!err)
return err;
}
}
return err;
}
HSaveError WriteDialogs(Stream *out) {
out->WriteInt32(_GP(game).numdialog);
for (int i = 0; i < _GP(game).numdialog; ++i) {
_G(dialog)[i].WriteToSavegame(out);
}
return HSaveError::None();
}
HSaveError ReadDialogs(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).numdialog, "Dialogs"))
return err;
for (int i = 0; i < _GP(game).numdialog; ++i) {
_G(dialog)[i].ReadFromSavegame(in);
}
return err;
}
HSaveError WriteGUI(Stream *out) {
// GUI state
WriteFormatTag(out, "GUIs");
out->WriteInt32(_GP(game).numgui);
for (const auto &gui : _GP(guis))
gui.WriteToSavegame(out);
WriteFormatTag(out, "GUIButtons");
out->WriteInt32(static_cast(_GP(guibuts).size()));
for (const auto &but : _GP(guibuts))
but.WriteToSavegame(out);
WriteFormatTag(out, "GUILabels");
out->WriteInt32(static_cast(_GP(guilabels).size()));
for (const auto &label : _GP(guilabels))
label.WriteToSavegame(out);
WriteFormatTag(out, "GUIInvWindows");
out->WriteInt32(static_cast(_GP(guiinv).size()));
for (const auto &inv : _GP(guiinv))
inv.WriteToSavegame(out);
WriteFormatTag(out, "GUISliders");
out->WriteInt32(static_cast(_GP(guislider).size()));
for (const auto &slider : _GP(guislider))
slider.WriteToSavegame(out);
WriteFormatTag(out, "GUITextBoxes");
out->WriteInt32(static_cast(_GP(guitext).size()));
for (const auto &tb : _GP(guitext))
tb.WriteToSavegame(out);
WriteFormatTag(out, "GUIListBoxes");
out->WriteInt32(static_cast(_GP(guilist).size()));
for (const auto &list : _GP(guilist))
list.WriteToSavegame(out);
// Animated buttons
WriteFormatTag(out, "AnimatedButtons");
size_t num_abuts = GetAnimatingButtonCount();
out->WriteInt32(num_abuts);
for (size_t i = 0; i < num_abuts; ++i)
GetAnimatingButtonByIndex(i)->WriteToSavegame(out);
return HSaveError::None();
}
HSaveError ReadGUI(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
const GuiSvgVersion svg_ver = (GuiSvgVersion)cmp_ver;
// GUI state
if (!AssertFormatTagStrict(err, in, "GUIs"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(game).numgui, "GUIs"))
return err;
for (int i = 0; i < _GP(game).numgui; ++i)
_GP(guis)[i].ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUIButtons"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guibuts).size(), "GUI Buttons"))
return err;
for (auto &but : _GP(guibuts))
but.ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUILabels"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guilabels).size(), "GUI Labels"))
return err;
for (auto &label : _GP(guilabels))
label.ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUIInvWindows"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guiinv).size(), "GUI InvWindows"))
return err;
for (auto &inv : _GP(guiinv))
inv.ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUISliders"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guislider).size(), "GUI Sliders"))
return err;
for (auto &slider : _GP(guislider))
slider.ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUITextBoxes"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guitext).size(), "GUI TextBoxes"))
return err;
for (auto &tb : _GP(guitext))
tb.ReadFromSavegame(in, svg_ver);
if (!AssertFormatTagStrict(err, in, "GUIListBoxes"))
return err;
if (!AssertGameContent(err, static_cast(in->ReadInt32()), _GP(guilist).size(), "GUI ListBoxes"))
return err;
for (auto &list : _GP(guilist))
list.ReadFromSavegame(in, svg_ver);
// Animated buttons
if (!AssertFormatTagStrict(err, in, "AnimatedButtons"))
return err;
int anim_count = in->ReadInt32();
for (int i = 0; i < anim_count; ++i) {
AnimatingGUIButton abut;
abut.ReadFromSavegame(in, cmp_ver);
AddButtonAnimation(abut);
}
return err;
}
HSaveError WriteInventory(Stream *out) {
out->WriteInt32(_GP(game).numinvitems);
for (int i = 0; i < _GP(game).numinvitems; ++i) {
_GP(game).invinfo[i].WriteToSavegame(out);
Properties::WriteValues(_GP(play).invProps[i], out);
if (_G(loaded_game_file_version) <= kGameVersion_272)
WriteTimesRun272(*_GP(game).intrInv[i], out);
}
return HSaveError::None();
}
HSaveError ReadInventory(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).numinvitems, "Inventory Items"))
return err;
for (int i = 0; i < _GP(game).numinvitems; ++i) {
_GP(game).invinfo[i].ReadFromSavegame(in);
Properties::ReadValues(_GP(play).invProps[i], in);
if (_G(loaded_game_file_version) <= kGameVersion_272)
ReadTimesRun272(*_GP(game).intrInv[i], in);
}
return err;
}
HSaveError WriteMouseCursors(Stream *out) {
out->WriteInt32(_GP(game).numcursors);
for (int i = 0; i < _GP(game).numcursors; ++i) {
_GP(game).mcurs[i].WriteToSavegame(out);
}
return HSaveError::None();
}
HSaveError ReadMouseCursors(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).numcursors, "Mouse Cursors"))
return err;
for (int i = 0; i < _GP(game).numcursors; ++i) {
_GP(game).mcurs[i].ReadFromSavegame(in, cmp_ver);
}
return err;
}
HSaveError WriteViews(Stream *out) {
out->WriteInt32(_GP(game).numviews);
for (int view = 0; view < _GP(game).numviews; ++view) {
out->WriteInt32(_GP(views)[view].numLoops);
for (int loop = 0; loop < _GP(views)[view].numLoops; ++loop) {
out->WriteInt32(_GP(views)[view].loops[loop].numFrames);
for (int frame = 0; frame < _GP(views)[view].loops[loop].numFrames; ++frame) {
out->WriteInt32(_GP(views)[view].loops[loop].frames[frame].sound);
out->WriteInt32(_GP(views)[view].loops[loop].frames[frame].pic);
}
}
}
return HSaveError::None();
}
HSaveError ReadViews(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
if (!AssertGameContent(err, in->ReadInt32(), _GP(game).numviews, "Views"))
return err;
for (int view = 0; view < _GP(game).numviews; ++view) {
if (!AssertGameObjectContent(err, in->ReadInt32(), _GP(views)[view].numLoops,
"Loops", "View", view))
return err;
for (int loop = 0; loop < _GP(views)[view].numLoops; ++loop) {
if (!AssertGameObjectContent2(err, in->ReadInt32(), _GP(views)[view].loops[loop].numFrames,
"Frame", "View", view, "Loop", loop))
return err;
for (int frame = 0; frame < _GP(views)[view].loops[loop].numFrames; ++frame) {
_GP(views)[view].loops[loop].frames[frame].sound = in->ReadInt32();
_GP(views)[view].loops[loop].frames[frame].pic = in->ReadInt32();
}
}
}
return err;
}
HSaveError WriteDynamicSprites(Stream *out) {
const soff_t ref_pos = out->GetPosition();
out->WriteInt32(0); // number of dynamic sprites
out->WriteInt32(0); // top index
int count = 0;
int top_index = 1;
for (size_t i = 1; i < _GP(spriteset).GetSpriteSlotCount(); ++i) {
if (_GP(game).SpriteInfos[i].Flags & SPF_DYNAMICALLOC) {
count++;
top_index = i;
out->WriteInt32(i);
out->WriteInt32(_GP(game).SpriteInfos[i].Flags);
serialize_bitmap(_GP(spriteset)[i], out);
}
}
const soff_t end_pos = out->GetPosition();
out->Seek(ref_pos, kSeekBegin);
out->WriteInt32(count);
out->WriteInt32(top_index);
out->Seek(end_pos, kSeekBegin);
return HSaveError::None();
}
HSaveError ReadDynamicSprites(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
const int spr_count = in->ReadInt32();
// ensure the sprite set is at least large enough
// to accommodate top dynamic sprite index
const int top_index = in->ReadInt32();
_GP(spriteset).EnlargeTo(top_index);
for (int i = 0; i < spr_count; ++i) {
int id = in->ReadInt32();
int flags = in->ReadInt32();
std::unique_ptr image(read_serialized_bitmap(in));
add_dynamic_sprite(id, std::move(image), (flags & SPF_ALPHACHANNEL) != 0, flags);
}
return err;
}
HSaveError WriteOverlays(Stream *out) {
const auto &overs = get_overlays();
// Calculate and save valid overlays only
uint32_t valid_count = 0;
soff_t count_off = out->GetPosition();
out->WriteInt32(0);
for (const auto &over : overs) {
if (over.type < 0)
continue;
valid_count++;
over.WriteToSavegame(out);
}
out->Seek(count_off, kSeekBegin);
out->WriteInt32(valid_count);
out->Seek(0, kSeekEnd);
return HSaveError::None();
}
HSaveError ReadOverlays(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData &r_data) {
// Remember that overlay indexes may be non-sequential
// the vector may be resized during read
size_t over_count = in->ReadInt32();
auto &overs = get_overlays();
overs.resize(over_count); // reserve minimal size
for (size_t i = 0; i < over_count; ++i) {
ScreenOverlay over;
bool has_bitmap;
over.ReadFromSavegame(in, has_bitmap, cmp_ver);
if (over.type < 0)
continue; // safety abort
if (has_bitmap)
r_data.OverlayImages[over.type].reset(read_serialized_bitmap(in));
if (overs.size() <= static_cast(over.type))
overs.resize(over.type + 1);
overs[over.type] = std::move(over);
}
return HSaveError::None();
}
HSaveError WriteDynamicSurfaces(Stream *out) {
out->WriteInt32(MAX_DYNAMIC_SURFACES);
for (int i = 0; i < MAX_DYNAMIC_SURFACES; ++i) {
if (_G(dynamicallyCreatedSurfaces)[i] == nullptr) {
out->WriteInt8(0);
} else {
out->WriteInt8(1);
serialize_bitmap(_G(dynamicallyCreatedSurfaces)[i].get(), out);
}
}
return HSaveError::None();
}
HSaveError ReadDynamicSurfaces(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData &r_data) {
HSaveError err;
if (!AssertCompatLimit(err, in->ReadInt32(), MAX_DYNAMIC_SURFACES, "Dynamic Surfaces"))
return err;
// Load the surfaces into a temporary array since ccUnserialiseObjects will destroy them otherwise
r_data.DynamicSurfaces.resize(MAX_DYNAMIC_SURFACES);
for (int i = 0; i < MAX_DYNAMIC_SURFACES; ++i) {
if (in->ReadInt8() == 0)
r_data.DynamicSurfaces[i].reset();
else
r_data.DynamicSurfaces[i].reset(read_serialized_bitmap(in));
}
return err;
}
HSaveError WriteScriptModules(Stream *out) {
// write the data segment of the global script
int data_len = _G(gameinst)->globaldatasize;
out->WriteInt32(data_len);
if (data_len > 0)
out->Write(_G(gameinst)->globaldata, data_len);
// write the script modules data segments
out->WriteInt32(_G(numScriptModules));
for (size_t i = 0; i < _G(numScriptModules); ++i) {
data_len = _GP(moduleInst)[i]->globaldatasize;
out->WriteInt32(data_len);
if (data_len > 0)
out->Write(_GP(moduleInst)[i]->globaldata, data_len);
}
return HSaveError::None();
}
HSaveError ReadScriptModules(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams &pp, RestoredData &r_data) {
HSaveError err;
// read the global script data segment
int data_len = in->ReadInt32();
if (!AssertGameContent(err, data_len, pp.GlScDataSize, "global script data"))
return err;
r_data.GlobalScript.Len = data_len;
r_data.GlobalScript.Data.resize(data_len);
if (data_len > 0)
in->Read(&r_data.GlobalScript.Data.front(), data_len);
if (!AssertGameContent(err, in->ReadInt32(), _G(numScriptModules), "Script Modules"))
return err;
r_data.ScriptModules.resize(_G(numScriptModules));
for (size_t i = 0; i < _G(numScriptModules); ++i) {
data_len = in->ReadInt32();
if (!AssertGameObjectContent(err, data_len, pp.ScMdDataSize[i], "script module data", "module", i))
return err;
r_data.ScriptModules[i].Len = data_len;
r_data.ScriptModules[i].Data.resize(data_len);
if (data_len > 0)
in->Read(&r_data.ScriptModules[i].Data.front(), data_len);
}
return err;
}
HSaveError WriteRoomStates(Stream *out) {
// write the room state for all the rooms the player has been in
out->WriteInt32(MAX_ROOMS);
for (int i = 0; i < MAX_ROOMS; ++i) {
if (isRoomStatusValid(i)) {
RoomStatus *roomstat = getRoomStatus(i);
if (roomstat->beenhere) {
out->WriteInt32(i);
WriteFormatTag(out, "RoomState", true);
roomstat->WriteToSavegame(out, _G(loaded_game_file_version));
WriteFormatTag(out, "RoomState", false);
} else
out->WriteInt32(-1);
} else
out->WriteInt32(-1);
}
return HSaveError::None();
}
HSaveError ReadRoomStates(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
int roomstat_count = in->ReadInt32();
for (; roomstat_count > 0; --roomstat_count) {
int id = in->ReadInt32();
// If id == -1, then the player has not been there yet (or room state was reset)
if (id != -1) {
if (!AssertCompatRange(err, id, 0, MAX_ROOMS - 1, "room index"))
return err;
if (!AssertFormatTagStrict(err, in, "RoomState", true))
return err;
RoomStatus *roomstat = getRoomStatus(id);
roomstat->ReadFromSavegame(in, _G(loaded_game_file_version), (RoomStatSvgVersion)cmp_ver);
if (!AssertFormatTagStrict(err, in, "RoomState", false))
return err;
}
}
return HSaveError::None();
}
HSaveError WriteThisRoom(Stream *out) {
out->WriteInt32(_G(displayed_room));
if (_G(displayed_room) < 0)
return HSaveError::None();
// modified room backgrounds
for (int i = 0; i < MAX_ROOM_BGFRAMES; ++i) {
out->WriteBool(_GP(play).raw_modified[i] != 0);
if (_GP(play).raw_modified[i])
serialize_bitmap(_GP(thisroom).BgFrames[i].Graphic.get(), out);
}
out->WriteBool(_G(raw_saved_screen) != nullptr);
if (_G(raw_saved_screen))
serialize_bitmap(_G(raw_saved_screen).get(), out);
// room region state
for (int i = 0; i < MAX_ROOM_REGIONS; ++i) {
out->WriteInt32(_GP(thisroom).Regions[i].Light);
out->WriteInt32(_GP(thisroom).Regions[i].Tint);
}
for (int i = 0; i < MAX_WALK_AREAS; ++i) {
out->WriteInt32(_GP(thisroom).WalkAreas[i].ScalingFar);
out->WriteInt32(_GP(thisroom).WalkAreas[i].ScalingNear);
}
// room music volume
out->WriteInt32(_GP(thisroom).Options.MusicVolume);
// persistent room's indicator
const bool persist = _G(displayed_room) < MAX_ROOMS;
out->WriteBool(persist);
// write the current troom state, in case they save in temporary room
if (!persist)
_GP(troom).WriteToSavegame(out, _G(loaded_game_file_version));
return HSaveError::None();
}
HSaveError ReadThisRoom(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData &r_data) {
HSaveError err;
_G(displayed_room) = in->ReadInt32();
if (_G(displayed_room) < 0)
return err;
// modified room backgrounds
for (int i = 0; i < MAX_ROOM_BGFRAMES; ++i) {
_GP(play).raw_modified[i] = in->ReadBool();
if (_GP(play).raw_modified[i])
r_data.RoomBkgScene[i].reset(read_serialized_bitmap(in));
else
r_data.RoomBkgScene[i] = nullptr;
}
if (in->ReadBool())
_G(raw_saved_screen).reset(read_serialized_bitmap(in));
// room region state
for (int i = 0; i < MAX_ROOM_REGIONS; ++i) {
r_data.RoomLightLevels[i] = in->ReadInt32();
r_data.RoomTintLevels[i] = in->ReadInt32();
}
for (int i = 0; i < MAX_WALK_AREAS; ++i) {
r_data.RoomZoomLevels1[i] = in->ReadInt32();
r_data.RoomZoomLevels2[i] = in->ReadInt32();
}
// room object movement paths, for old saves
if (cmp_ver < kRoomStatSvgVersion_36109) {
int objmls_count = in->ReadInt32();
if (!AssertCompatLimit(err, objmls_count, CHMLSOFFS, "room object move lists"))
return err;
for (int i = 0; i < objmls_count; ++i) {
err = _GP(mls)[i].ReadFromSavegame(in, kMoveSvgVersion_350);
if (!err)
return err;
}
}
// save the new room music vol for later use
r_data.RoomVolume = (RoomVolumeMod)in->ReadInt32();
// read the current troom state, in case they saved in temporary room
if (!in->ReadBool())
_GP(troom).ReadFromSavegame(in, _G(loaded_game_file_version), (RoomStatSvgVersion)cmp_ver);
return HSaveError::None();
}
HSaveError WriteMoveLists(Stream *out) {
out->WriteInt32(static_cast(_GP(mls).size()));
for (const auto &movelist : _GP(mls)) {
movelist.WriteToSavegame(out);
}
return HSaveError::None();
}
HSaveError ReadMoveLists(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
HSaveError err;
size_t movelist_count = in->ReadInt32();
// TODO: this assertion is needed only because mls size is fixed to the
// number of characters + max number of objects, where each game object
// has a fixed movelist index. It may be removed if movelists will be
// allocated on demand with an arbitrary index instead.
if (!AssertGameContent(err, movelist_count, _GP(mls).size(), "Move Lists"))
return err;
for (size_t i = 0; i < movelist_count; ++i) {
err = _GP(mls)[i].ReadFromSavegame(in, cmp_ver);
if (!err)
return err;
}
return err;
}
HSaveError WriteManagedPool(Stream *out) {
ccSerializeAllObjects(out);
return HSaveError::None();
}
HSaveError ReadManagedPool(Stream *in, int32_t /*cmp_ver*/, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
if (ccUnserializeAllObjects(in, &_GP(ccUnserializer))) {
return new SavegameError(kSvgErr_GameObjectInitFailed,
String::FromFormat("Managed pool deserialization failed: %s", cc_get_error().ErrorString.GetCStr()));
}
return HSaveError::None();
}
HSaveError WritePluginData(Stream *out) {
WritePluginSaveData(out);
return HSaveError::None();
}
HSaveError ReadPluginData(Stream *in, int32_t cmp_ver, soff_t cmp_size, const PreservedParams & /*pp*/, RestoredData & /*r_data*/) {
ReadPluginSaveData(in, static_cast(cmp_ver), cmp_size);
return HSaveError::None();
}
// Description of a supported game state serialization component
struct ComponentHandler {
String Name; // internal component's ID
int32_t Version; // current version to write and the highest supported version
int32_t LowestVersion; // lowest supported version that the engine can read
HSaveError(*Serialize)(Stream *);
HSaveError(*Unserialize)(Stream *, int32_t cmp_ver, soff_t cmp_size, const PreservedParams &, RestoredData &);
};
// Array of supported components
struct ComponentHandlers {
// NOTE: the new format values should now be defined as AGS version
// at which a change was introduced, represented as NN,NN,NN,NN.
const ComponentHandler _items[18] = {
{
"Game State",
kGSSvgVersion_361_14,
kGSSvgVersion_Initial,
WriteGameState,
ReadGameState
},
{
"Audio",
kAudioSvgVersion_36130,
kAudioSvgVersion_Initial,
WriteAudio,
ReadAudio
},
{
"Characters",
kCharSvgVersion_36115,
kCharSvgVersion_350, // skip pre-alpha 3.5.0 ver
WriteCharacters,
ReadCharacters
},
{
"Dialogs",
0,
0,
WriteDialogs,
ReadDialogs
},
{
"GUI",
kGuiSvgVersion_36025,
kGuiSvgVersion_Initial,
WriteGUI,
ReadGUI
},
{
"Inventory Items",
0,
0,
WriteInventory,
ReadInventory
},
{
"Mouse Cursors",
kCursorSvgVersion_36016,
kCursorSvgVersion_Initial,
WriteMouseCursors,
ReadMouseCursors
},
{
"Views",
0,
0,
WriteViews,
ReadViews
},
{
"Dynamic Sprites",
0,
0,
WriteDynamicSprites,
ReadDynamicSprites
},
{
"Overlays",
kOverSvgVersion_36108,
kOverSvgVersion_Initial,
WriteOverlays,
ReadOverlays
},
{
"Dynamic Surfaces",
0,
0,
WriteDynamicSurfaces,
ReadDynamicSurfaces
},
{
"Script Modules",
0,
0,
WriteScriptModules,
ReadScriptModules
},
{
"Room States",
kRoomStatSvgVersion_36109,
kRoomStatSvgVersion_350_Mismatch, // support mismatching 3.5.0 ver here
WriteRoomStates,
ReadRoomStates
},
{
"Loaded Room State",
kRoomStatSvgVersion_36109, // must correspond to "Room States"
kRoomStatSvgVersion_350, // skip pre-alpha 3.5.0 ver
WriteThisRoom,
ReadThisRoom
},
{
"Move Lists",
kMoveSvgVersion_36109,
kMoveSvgVersion_350, // skip pre-alpha 3.5.0 ver
WriteMoveLists,
ReadMoveLists
},
{
"Managed Pool",
0,
0,
WriteManagedPool,
ReadManagedPool
},
{
"Plugin Data",
kPluginSvgVersion_36115,
kPluginSvgVersion_Initial,
WritePluginData,
ReadPluginData
},
{ nullptr, 0, 0, nullptr, nullptr } // end of array
};
const ComponentHandler &operator[](uint idx) {
return _items[idx];
}
};
ComponentHandlers *g_componentHandlers;
void component_handlers_init() {
g_componentHandlers = new ComponentHandlers();
}
void component_handlers_free() {
delete g_componentHandlers;
}
typedef std::map HandlersMap;
void GenerateHandlersMap(HandlersMap &map) {
map.clear();
for (int i = 0; !(*g_componentHandlers)[i].Name.IsEmpty(); ++i)
map[(*g_componentHandlers)[i].Name] = (*g_componentHandlers)[i];
}
// A helper struct to pass to (de)serialization handlers
struct SvgCmpReadHelper {
SavegameVersion Version; // general savegame version
const PreservedParams &PP; // previous game state kept for reference
RestoredData &RData; // temporary storage for loaded data, that
// will be applied after loading is done
// The map of serialization handlers, one per supported component type ID
HandlersMap Handlers;
SvgCmpReadHelper(SavegameVersion svg_version, const PreservedParams &pp, RestoredData &r_data)
: Version(svg_version)
, PP(pp)
, RData(r_data) {
}
};
// The basic information about deserialized component, used for debugging purposes
struct ComponentInfo {
String Name; // internal component's ID
int32_t Version; // data format version
soff_t Offset; // offset at which an opening tag is located
soff_t DataOffset; // offset at which component data begins
soff_t DataSize; // expected size of component data
ComponentInfo() : Version(-1), Offset(0), DataOffset(0), DataSize(0) {}
};
HSaveError ReadComponent(Stream *in, SvgCmpReadHelper &hlp, ComponentInfo &info) {
info = ComponentInfo(); // reset in case of early error
info.Offset = in->GetPosition();
if (!ReadFormatTag(in, info.Name, true))
return new SavegameError(kSvgErr_ComponentOpeningTagFormat);
info.Version = in->ReadInt32();
info.DataSize = hlp.Version >= kSvgVersion_Cmp_64bit ? in->ReadInt64() : in->ReadInt32();
info.DataOffset = in->GetPosition();
// WORKAROUND: For some period, the component "Dynamic Surfaces"
// was mis-named as "Drawing Surfaces"
String componentName = info.Name;
if (componentName == "Drawing Surfaces")
componentName = "Dynamic Surfaces";
const ComponentHandler *handler = nullptr;
std::map::const_iterator it_hdr = hlp.Handlers.find(componentName);
if (it_hdr != hlp.Handlers.end())
handler = &it_hdr->_value;
// WORKAROUND: Managed Pool was incorrectly set as version 1
// in the codebase originally imported to ScummVM
if (componentName == "Managed Pool" && info.Version == 1
&& handler && handler->Version == 0)
info.Version = 0;
if (!handler || !handler->Unserialize)
return new SavegameError(kSvgErr_UnsupportedComponent);
if (info.Version > handler->Version || info.Version < handler->LowestVersion)
return new SavegameError(kSvgErr_UnsupportedComponentVersion, String::FromFormat("Saved version: %d, supported: %d - %d", info.Version, handler->LowestVersion, handler->Version));
HSaveError err = handler->Unserialize(in, info.Version, info.DataSize, hlp.PP, hlp.RData);
if (!err)
return err;
if (in->GetPosition() - info.DataOffset != info.DataSize)
return new SavegameError(kSvgErr_ComponentSizeMismatch, String::FromFormat("Expected: %llu, actual: %llu",
static_cast(info.DataSize), static_cast(in->GetPosition() - info.DataOffset)));
if (!AssertFormatTag(in, info.Name, false))
return new SavegameError(kSvgErr_ComponentClosingTagFormat);
return HSaveError::None();
}
HSaveError ReadAll(Stream *in, SavegameVersion svg_version, const PreservedParams &pp, RestoredData &r_data) {
// Prepare a helper struct we will be passing to the block reading proc
SvgCmpReadHelper hlp(svg_version, pp, r_data);
GenerateHandlersMap(hlp.Handlers);
size_t idx = 0;
const String ComponentListTag = "Components";
if (!AssertFormatTag(in, ComponentListTag, true))
return new SavegameError(kSvgErr_ComponentListOpeningTagFormat);
do {
// Look out for the end of the component list:
// this is the only way how this function ends with success
soff_t off = in->GetPosition();
if (AssertFormatTag(in, ComponentListTag, false))
return HSaveError::None();
// If the list's end was not detected, then seek back and continue reading
in->Seek(off, kSeekBegin);
ComponentInfo info;
HSaveError err = ReadComponent(in, hlp, info);
if (!err) {
return new SavegameError(kSvgErr_ComponentUnserialization,
String::FromFormat("(#%d) %s, version %i, at offset %lld.",
idx, info.Name.IsEmpty() ? "unknown" : info.Name.GetCStr(), info.Version, info.Offset),
err);
}
idx++;
} while (!in->EOS());
return new SavegameError(kSvgErr_ComponentListClosingTagMissing);
}
HSaveError WriteComponent(Stream *out, const ComponentHandler &hdlr) {
WriteFormatTag(out, hdlr.Name, true);
out->WriteInt32(hdlr.Version);
soff_t ref_pos = out->GetPosition();
out->WriteInt64(0); // placeholder for the component size
HSaveError err = hdlr.Serialize(out);
soff_t end_pos = out->GetPosition();
out->Seek(ref_pos, kSeekBegin);
out->WriteInt64(end_pos - ref_pos - sizeof(int64_t)); // size of serialized component data
out->Seek(end_pos, kSeekBegin);
if (err)
WriteFormatTag(out, hdlr.Name, false);
return err;
}
HSaveError WriteAllCommon(Stream *out) {
const String ComponentListTag = "Components";
WriteFormatTag(out, ComponentListTag, true);
for (int type = 0; !(*g_componentHandlers)[type].Name.IsEmpty(); ++type) {
HSaveError err = WriteComponent(out, (*g_componentHandlers)[type]);
if (!err) {
return new SavegameError(kSvgErr_ComponentSerialization,
String::FromFormat("Component: (#%d) %s", type, (*g_componentHandlers)[type].Name.GetCStr()),
err);
}
}
WriteFormatTag(out, ComponentListTag, false);
return HSaveError::None();
}
} // namespace SavegameBlocks
} // namespace Engine
} // namespace AGS
} // namespace AGS3