Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

View File

@@ -0,0 +1,523 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ags/engine/ac/character.h"
#include "ags/engine/ac/dialog.h"
#include "ags/engine/ac/display.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/file.h"
#include "ags/engine/ac/game.h"
#include "ags/engine/ac/game_setup.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/lip_sync.h"
#include "ags/engine/ac/move_list.h"
#include "ags/engine/ac/dynobj/all_dynamic_classes.h"
#include "ags/engine/ac/dynobj/all_script_classes.h"
#include "ags/engine/ac/dynobj/dynobj_manager.h"
#include "ags/shared/ac/view.h"
#include "ags/shared/core/asset_manager.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/shared/debugging/out.h"
#include "ags/shared/font/ags_font_renderer.h"
#include "ags/shared/font/fonts.h"
#include "ags/engine/game/game_init.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/engine/gfx/ddb.h"
#include "ags/shared/gui/gui_label.h"
#include "ags/shared/gui/gui_inv.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/plugins/plugin_engine.h"
#include "ags/shared/script/cc_common.h"
#include "ags/engine/script/exports.h"
#include "ags/engine/script/script.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/shared/util/string_compat.h"
#include "ags/shared/util/string_utils.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace Shared;
using namespace Engine;
namespace AGS {
namespace Engine {
String GetGameInitErrorText(GameInitErrorType err) {
switch (err) {
case kGameInitErr_NoError:
return "No error.";
case kGameInitErr_NoFonts:
return "No fonts specified to be used in this game.";
case kGameInitErr_TooManyAudioTypes:
return "Too many audio types for this engine to handle.";
case kGameInitErr_EntityInitFail:
return "Failed to initialize game entities.";
case kGameInitErr_PluginNameInvalid:
return "Plugin name is invalid.";
case kGameInitErr_NoGlobalScript:
return "No global script in game.";
case kGameInitErr_ScriptLinkFailed:
return "Script link failed.";
}
return "Unknown error.";
}
// Initializes audio channels and clips and registers them in the script system
void InitAndRegisterAudioObjects(GameSetupStruct &game) {
for (int i = 0; i < game.numCompatGameChannels; ++i) {
_G(scrAudioChannel)[i].id = i;
ccRegisterManagedObject(&_G(scrAudioChannel)[i], &_GP(ccDynamicAudio));
}
for (size_t i = 0; i < game.audioClips.size(); ++i) {
// Note that as of 3.5.0 data format the clip IDs are still restricted
// to actual item index in array, so we don't make any difference
// between game versions, for now.
game.audioClips[i].id = i;
ccRegisterManagedObject(&game.audioClips[i], &_GP(ccDynamicAudioClip));
ccAddExternalScriptObject(game.audioClips[i].scriptName, &game.audioClips[i], &_GP(ccDynamicAudioClip));
}
}
// Initializes characters and registers them in the script system
void InitAndRegisterCharacters(GameSetupStruct &game) {
for (int i = 0; i < game.numcharacters; ++i) {
game.chars[i].walking = 0;
game.chars[i].animating = 0;
game.chars[i].pic_xoffs = 0;
game.chars[i].pic_yoffs = 0;
game.chars[i].blinkinterval = 140;
game.chars[i].blinktimer = game.chars[i].blinkinterval;
game.chars[i].index_id = i;
game.chars[i].blocking_width = 0;
game.chars[i].blocking_height = 0;
game.chars[i].prevroom = -1;
game.chars[i].loop = 0;
game.chars[i].frame = 0;
game.chars[i].walkwait = -1;
ccRegisterManagedObject(&game.chars[i], &_GP(ccDynamicCharacter));
// export the character's script object
ccAddExternalScriptObject(game.chars2[i].scrname_new, &game.chars[i], &_GP(ccDynamicCharacter));
}
}
// Initializes dialog and registers them in the script system
void InitAndRegisterDialogs(GameSetupStruct &game) {
_GP(scrDialog).resize(MAX(1, game.numdialog)); // ensure at least 1 element, we must register buffer
for (int i = 0; i < game.numdialog; ++i) {
_GP(scrDialog)[i].id = i;
_GP(scrDialog)[i].reserved = 0;
ccRegisterManagedObject(&_GP(scrDialog)[i], &_GP(ccDynamicDialog));
if (!game.dialogScriptNames[i].IsEmpty())
ccAddExternalScriptObject(game.dialogScriptNames[i], &_GP(scrDialog)[i], &_GP(ccDynamicDialog));
}
}
// Initializes dialog options rendering objects and registers them in the script system
void InitAndRegisterDialogOptions() {
ccRegisterManagedObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_G(dialogOptionsRenderingSurface) = new ScriptDrawingSurface();
_G(dialogOptionsRenderingSurface)->isLinkedBitmapOnly = true;
long dorsHandle = ccRegisterManagedObject(_G(dialogOptionsRenderingSurface), _G(dialogOptionsRenderingSurface));
ccAddObjectReference(dorsHandle);
}
// Initializes gui and registers them in the script system
HError InitAndRegisterGUI(GameSetupStruct &game) {
_GP(scrGui).resize(MAX(1, game.numgui)); // ensure at least 1 element, we must register buffer
for (int i = 0; i < game.numgui; ++i) {
_GP(scrGui)[i].id = -1;
}
for (int i = 0; i < game.numgui; ++i) {
// link controls to their parent guis
HError err = _GP(guis)[i].RebuildArray();
if (!err)
return err;
// export all the GUI's controls
export_gui_controls(i);
_GP(scrGui)[i].id = i;
ccAddExternalScriptObject(_GP(guis)[i].Name, &_GP(scrGui)[i], &_GP(ccDynamicGUI));
ccRegisterManagedObject(&_GP(scrGui)[i], &_GP(ccDynamicGUI));
}
return HError::None();
}
// Initializes inventory items and registers them in the script system
void InitAndRegisterInvItems(GameSetupStruct &game) {
for (int i = 0; i < MAX_INV; ++i) {
_G(scrInv)[i].id = i;
_G(scrInv)[i].reserved = 0;
ccRegisterManagedObject(&_G(scrInv)[i], &_GP(ccDynamicInv));
if (!game.invScriptNames[i].IsEmpty())
ccAddExternalScriptObject(game.invScriptNames[i], &_G(scrInv)[i], &_GP(ccDynamicInv));
}
}
// Initializes room hotspots and registers them in the script system
void InitAndRegisterHotspots() {
for (int i = 0; i < MAX_ROOM_HOTSPOTS; ++i) {
_G(scrHotspot)[i].id = i;
_G(scrHotspot)[i].reserved = 0;
ccRegisterManagedObject(&_G(scrHotspot)[i], &_GP(ccDynamicHotspot));
}
}
// Initializes room objects and registers them in the script system
void InitAndRegisterRoomObjects() {
for (int i = 0; i < MAX_ROOM_OBJECTS; ++i) {
ccRegisterManagedObject(&_G(scrObj)[i], &_GP(ccDynamicObject));
}
}
// Initializes room regions and registers them in the script system
void InitAndRegisterRegions() {
for (int i = 0; i < MAX_ROOM_REGIONS; ++i) {
_G(scrRegion)[i].id = i;
_G(scrRegion)[i].reserved = 0;
ccRegisterManagedObject(&_G(scrRegion)[i], &_GP(ccDynamicRegion));
}
}
// Registers static entity arrays in the script system
void RegisterStaticArrays(GameSetupStruct &game) {
_GP(StaticCharacterArray).Create(&_GP(ccDynamicCharacter), sizeof(CharacterInfo), sizeof(CharacterInfo));
_GP(StaticObjectArray).Create(&_GP(ccDynamicObject), sizeof(ScriptObject), sizeof(ScriptObject));
_GP(StaticGUIArray).Create(&_GP(ccDynamicGUI), sizeof(ScriptGUI), sizeof(ScriptGUI));
_GP(StaticHotspotArray).Create(&_GP(ccDynamicHotspot), sizeof(ScriptHotspot), sizeof(ScriptHotspot));
_GP(StaticRegionArray).Create(&_GP(ccDynamicRegion), sizeof(ScriptRegion), sizeof(ScriptRegion));
_GP(StaticInventoryArray).Create(&_GP(ccDynamicInv), sizeof(ScriptInvItem), sizeof(ScriptInvItem));
_GP(StaticDialogArray).Create(&_GP(ccDynamicDialog), sizeof(ScriptDialog), sizeof(ScriptDialog));
ccAddExternalStaticArray("character", &game.chars[0], &_GP(StaticCharacterArray));
ccAddExternalStaticArray("object", &_G(scrObj)[0], &_GP(StaticObjectArray));
ccAddExternalStaticArray("gui", &_GP(scrGui)[0], &_GP(StaticGUIArray));
ccAddExternalStaticArray("hotspot", &_G(scrHotspot)[0], &_GP(StaticHotspotArray));
ccAddExternalStaticArray("region", &_G(scrRegion)[0], &_GP(StaticRegionArray));
ccAddExternalStaticArray("inventory", &_G(scrInv)[0], &_GP(StaticInventoryArray));
ccAddExternalStaticArray("dialog", &_GP(scrDialog)[0], &_GP(StaticDialogArray));
}
// Initializes various game entities and registers them in the script system
HError InitAndRegisterGameEntities(GameSetupStruct &game) {
InitAndRegisterAudioObjects(game);
InitAndRegisterCharacters(game);
InitAndRegisterDialogs(game);
InitAndRegisterDialogOptions();
HError err = InitAndRegisterGUI(game);
if (!err)
return err;
InitAndRegisterInvItems(game);
InitAndRegisterHotspots();
InitAndRegisterRegions();
InitAndRegisterRoomObjects();
RegisterStaticArrays(game);
setup_player_character(game.playercharacter);
if (_G(loaded_game_file_version) >= kGameVersion_270)
ccAddExternalScriptObject("player", &_G(sc_PlayerCharPtr), &_GP(GlobalStaticManager));
return HError::None();
}
void LoadFonts(GameSetupStruct &game, GameDataVersion data_ver) {
for (int i = 0; i < _GP(game).numfonts; ++i) {
FontInfo &finfo = _GP(game).fonts[i];
if (!load_font_size(i, finfo))
quitprintf("Unable to load font %d, no renderer could load a matching file", i);
const bool is_wfn = is_bitmap_font(i);
// Outline thickness corresponds to 1 game pixel by default;
// but if it's a scaled up bitmap font, then it equals to scale
if (data_ver < kGameVersion_360) {
if (is_wfn && (finfo.Outline == FONT_OUTLINE_AUTO)) {
set_font_outline(i, FONT_OUTLINE_AUTO, FontInfo::kSquared, get_font_scaling_mul(i));
}
}
}
// Additional fixups - after all the fonts are registered
for (int i = 0; i < _GP(game).numfonts; ++i) {
if (!is_bitmap_font(i)) {
// Check for the LucasFan font since it comes with an outline font that
// is drawn incorrectly with Freetype versions > 2.1.3.
// A simple workaround is to disable outline fonts for it and use
// automatic outline drawing.
const int outline_font = get_font_outline(i);
if (outline_font < 0)
continue;
const char *name = get_font_name(i);
const char *outline_name = get_font_name(outline_font);
if ((ags_stricmp(name, "LucasFan-Font") == 0) &&
(ags_stricmp(outline_name, "Arcade") == 0))
set_font_outline(i, FONT_OUTLINE_AUTO);
}
}
}
void LoadLipsyncData() {
std::unique_ptr<Stream> speechsync(_GP(AssetMgr)->OpenAsset("syncdata.dat", "voice"));
if (!speechsync)
return;
// this game has voice lip sync
int lipsync_fmt = speechsync->ReadInt32();
if (lipsync_fmt != 4) {
Debug::Printf(kDbgMsg_Info, "Unknown speech lip sync format (%d).\nLip sync disabled.", lipsync_fmt);
} else {
_G(numLipLines) = speechsync->ReadInt32();
_GP(splipsync).resize(_G(numLipLines));
for (int ee = 0; ee < _G(numLipLines); ee++) {
_GP(splipsync)[ee].numPhonemes = speechsync->ReadInt16();
speechsync->Read(_GP(splipsync)[ee].filename, 14);
if (_GP(splipsync)[ee].numPhonemes == 0)
continue;
_GP(splipsync)[ee].endtimeoffs.resize(_GP(splipsync)[ee].numPhonemes);
speechsync->ReadArrayOfInt32(&_GP(splipsync)[ee].endtimeoffs.front(), _GP(splipsync)[ee].numPhonemes);
_GP(splipsync)[ee].frame.resize(_GP(splipsync)[ee].numPhonemes);
speechsync->ReadArrayOfInt16(&_GP(splipsync)[ee].frame.front(), _GP(splipsync)[ee].numPhonemes);
}
}
Debug::Printf(kDbgMsg_Info, "Lipsync data found and loaded");
}
// Convert guis position and size to proper game resolution.
// Necessary for pre 3.1.0 games only to sync with modern engine.
static void ConvertGuiToGameRes(GameSetupStruct &game, GameDataVersion data_ver) {
if (data_ver >= kGameVersion_310)
return;
const int mul = game.GetDataUpscaleMult();
for (int i = 0; i < game.numcursors; ++i) {
game.mcurs[i].hotx *= mul;
game.mcurs[i].hoty *= mul;
}
for (int i = 0; i < game.numinvitems; ++i) {
game.invinfo[i].hotx *= mul;
game.invinfo[i].hoty *= mul;
}
for (int i = 0; i < game.numgui; ++i) {
GUIMain *cgp = &_GP(guis)[i];
cgp->X *= mul;
cgp->Y *= mul;
if (cgp->Width < 1)
cgp->Width = 1;
if (cgp->Height < 1)
cgp->Height = 1;
// This is probably a way to fix GUIs meant to be covering whole screen
if (cgp->Width == game.GetDataRes().Width - 1)
cgp->Width = game.GetDataRes().Width;
cgp->Width *= mul;
cgp->Height *= mul;
cgp->PopupAtMouseY *= mul;
for (int j = 0; j < cgp->GetControlCount(); ++j) {
GUIObject *guio = cgp->GetControl(j);
guio->X *= mul;
guio->Y *= mul;
Size sz = guio->GetSize() * mul;
guio->SetSize(sz.Width, sz.Height);
guio->IsActivated = false;
guio->OnResized();
}
}
}
// Convert certain coordinates to data resolution (only if it's different from game resolution).
// Necessary for 3.1.0 and above games with legacy "low-res coordinates" setting.
static void ConvertObjectsToDataRes(GameSetupStruct &game, GameDataVersion data_ver) {
if (data_ver < kGameVersion_310 || game.GetDataUpscaleMult() == 1)
return;
const int mul = game.GetDataUpscaleMult();
for (int i = 0; i < game.numcharacters; ++i) {
game.chars[i].x /= mul;
game.chars[i].y /= mul;
}
for (auto &inv : _GP(guiinv)) {
inv.ItemWidth /= mul;
inv.ItemHeight /= mul;
inv.OnResized();
}
}
void InitGameResolution(GameSetupStruct &game, GameDataVersion data_ver) {
Debug::Printf("Initializing resolution settings");
const Size game_size = game.GetGameRes();
_GP(usetup).textheight = get_font_height_outlined(0) + 1;
Debug::Printf(kDbgMsg_Info, "Game native resolution: %d x %d (%d bit)%s", game_size.Width, game_size.Height, game.color_depth * 8,
game.IsLegacyLetterbox() ? " letterbox-by-design" : "");
// Backwards compatible resolution conversions
ConvertGuiToGameRes(game, data_ver);
ConvertObjectsToDataRes(game, data_ver);
// Assign general game viewports
Rect viewport = RectWH(game_size);
_GP(play).SetMainViewport(viewport);
_GP(play).SetUIViewport(viewport);
// Assign ScriptSystem's resolution variables
_GP(scsystem).width = game.GetGameRes().Width;
_GP(scsystem).height = game.GetGameRes().Height;
_GP(scsystem).coldepth = game.GetColorDepth();
_GP(scsystem).viewport_width = game_to_data_coord(_GP(play).GetMainViewport().GetWidth());
_GP(scsystem).viewport_height = game_to_data_coord(_GP(play).GetMainViewport().GetHeight());
}
HGameInitError InitGameState(const LoadedGameEntities &ents, GameDataVersion data_ver) {
GameSetupStruct &game = ents.Game;
const ScriptAPIVersion base_api = (ScriptAPIVersion)game.options[OPT_BASESCRIPTAPI];
const ScriptAPIVersion compat_api = (ScriptAPIVersion)game.options[OPT_SCRIPTCOMPATLEV];
if (data_ver >= kGameVersion_341) {
const char *base_api_name = GetScriptAPIName(base_api);
const char *compat_api_name = GetScriptAPIName(compat_api);
Debug::Printf(kDbgMsg_Info, "Requested script API: %s (%d), compat level: %s (%d)",
base_api >= 0 && base_api <= kScriptAPI_Current ? base_api_name : "unknown", base_api,
compat_api >= 0 && compat_api <= kScriptAPI_Current ? compat_api_name : "unknown", compat_api);
}
// If the game was compiled using unsupported version of the script API,
// we warn about potential incompatibilities but proceed further.
if (game.options[OPT_BASESCRIPTAPI] > kScriptAPI_Current)
_G(platform)->DisplayAlert("Warning: this game requests a higher version of AGS script API, it may not run correctly or run at all.");
//
// 1. Check that the loaded data is valid and compatible with the current
// engine capabilities.
//
if (game.numfonts == 0)
return new GameInitError(kGameInitErr_NoFonts);
if (game.audioClipTypes.size() > MAX_AUDIO_TYPES)
return new GameInitError(kGameInitErr_TooManyAudioTypes,
String::FromFormat("Required: %zu, max: %zu", game.audioClipTypes.size(), MAX_AUDIO_TYPES));
//
// 3. Allocate and init game objects
//
_GP(charextra).resize(game.numcharacters);
_GP(mls).resize(game.numcharacters + MAX_ROOM_OBJECTS + 1);
init_game_drawdata();
_GP(views) = std::move(ents.Views);
_GP(play).charProps.resize(game.numcharacters);
_G(dialog) = std::move(ents.Dialogs);
_G(old_dialog_scripts) = std::move(ents.OldDialogScripts);
_G(old_speech_lines) = std::move(ents.OldSpeechLines);
_G(old_dialog_scripts) = ents.OldDialogScripts;
_G(old_speech_lines) = ents.OldSpeechLines;
// Set number of game channels corresponding to the loaded game version
if (_G(loaded_game_file_version) < kGameVersion_360) {
_GP(game).numGameChannels = MAX_GAME_CHANNELS_v320;
_GP(game).numCompatGameChannels = TOTAL_AUDIO_CHANNELS_v320;
} else {
_GP(game).numGameChannels = MAX_GAME_CHANNELS;
_GP(game).numCompatGameChannels = MAX_GAME_CHANNELS;
}
HError err = InitAndRegisterGameEntities(game);
if (!err)
return new GameInitError(kGameInitErr_EntityInitFail, err);
LoadFonts(game, data_ver);
LoadLipsyncData();
//
// 4. Initialize certain runtime variables
//
_G(game_paused) = 0; // reset the game paused flag
_G(ifacepopped) = -1;
String svg_suffix;
if (game.saveGameFileExtension[0] != 0)
svg_suffix.Format(".%s", game.saveGameFileExtension);
set_save_game_suffix(svg_suffix);
_GP(play).score_sound = game.scoreClipID;
_GP(play).fade_effect = game.options[OPT_FADETYPE];
//
// 5. Initialize runtime state of certain game objects
//
InitGameResolution(game, data_ver);
for (auto &label : _GP(guilabels)) {
// labels are not clickable by default
label.SetClickable(false);
}
_GP(play).gui_draw_order.resize(game.numgui);
for (int i = 0; i < game.numgui; ++i)
_GP(play).gui_draw_order[i] = i;
update_gui_zorder();
calculate_reserved_channel_count();
// Default viewport and camera, draw data, etc, should be created when resolution is set
_GP(play).CreatePrimaryViewportAndCamera();
init_game_drawdata();
//
// 6. Register engine API exports
// NOTE: we must do this before plugin start, because some plugins may
// require access to script API at initialization time.
//
ccSetScriptAliveTimer(1000 / 60u, 1000u, 150000u);
setup_script_exports(base_api, compat_api);
//
// 7. Start up plugins
//
pl_register_plugins(ents.PluginInfos);
pl_startup_plugins();
//
// 8. Create script modules
// NOTE: we must do this after plugins, because some plugins may export
// script symbols too.
//
if (!ents.GlobalScript)
return new GameInitError(kGameInitErr_NoGlobalScript);
_GP(gamescript) = ents.GlobalScript;
_GP(dialogScriptsScript) = ents.DialogScript;
_G(numScriptModules) = ents.ScriptModules.size();
_GP(scriptModules) = ents.ScriptModules;
AllocScriptModules();
if (create_global_script())
return new GameInitError(kGameInitErr_ScriptLinkFailed, cc_get_error().ErrorString);
return HGameInitError::None();
}
} // namespace Engine
} // namespace AGS
} // namespace AGS3

View File

@@ -0,0 +1,65 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
//=============================================================================
//
// This unit provides game initialization routine, which takes place after
// main game file was successfully loaded.
//
//=============================================================================
#ifndef AGS_ENGINE_GAME_GAME_INIT_H
#define AGS_ENGINE_GAME_GAME_INIT_H
#include "ags/shared/game/main_game_file.h"
#include "ags/shared/util/string.h"
namespace AGS3 {
namespace AGS {
namespace Engine {
using namespace Shared;
// Error codes for initializing the game
enum GameInitErrorType {
kGameInitErr_NoError,
// currently AGS requires at least one font to be present in game
kGameInitErr_NoFonts,
kGameInitErr_TooManyAudioTypes,
kGameInitErr_EntityInitFail,
kGameInitErr_PluginNameInvalid,
kGameInitErr_NoGlobalScript,
kGameInitErr_ScriptLinkFailed
};
String GetGameInitErrorText(GameInitErrorType err);
typedef TypedCodeError<GameInitErrorType, GetGameInitErrorText> GameInitError;
typedef ErrorHandle<GameInitError> HGameInitError;
// Sets up game state for play using preloaded data
HGameInitError InitGameState(const LoadedGameEntities &ents, GameDataVersion data_ver);
} // namespace Engine
} // namespace AGS
} // namespace AGS3
#endif

View File

@@ -0,0 +1,843 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ags/engine/ac/button.h"
#include "ags/engine/ac/character.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/dynamic_sprite.h"
#include "ags/engine/ac/event.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/game_setup.h"
#include "ags/engine/ac/global_audio.h"
#include "ags/engine/ac/global_character.h"
#include "ags/engine/ac/gui.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/overlay.h"
#include "ags/engine/ac/region.h"
#include "ags/engine/ac/rich_game_media.h"
#include "ags/engine/ac/room.h"
#include "ags/engine/ac/room_status.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/ac/system.h"
#include "ags/engine/ac/timer.h"
#include "ags/engine/ac/dynobj/dynobj_manager.h"
#include "ags/engine/debugging/debugger.h"
#include "ags/shared/debugging/out.h"
#include "ags/engine/device/mouse_w32.h"
#include "ags/shared/font/fonts.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/engine/gfx/ddb.h"
#include "ags/engine/gfx/graphics_driver.h"
#include "ags/engine/game/savegame.h"
#include "ags/engine/game/savegame_components.h"
#include "ags/engine/game/savegame_internal.h"
#include "ags/engine/main/game_run.h"
#include "ags/engine/main/engine.h"
#include "ags/engine/main/main.h"
#include "ags/engine/main/update.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/engine/platform/base/sys_main.h"
#include "ags/plugins/ags_plugin_evts.h"
#include "ags/plugins/plugin_engine.h"
#include "ags/engine/script/script.h"
#include "ags/shared/script/cc_common.h"
#include "ags/shared/util/data_stream.h"
#include "ags/shared/util/file.h"
#include "ags/shared/util/stream.h"
#include "ags/shared/util/string_utils.h"
#include "ags/shared/util/math.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace Shared;
using namespace Engine;
// function is currently implemented in savegame_v321.cpp
HSaveError restore_save_data_v321(Stream *in, GameDataVersion data_ver, const PreservedParams &pp, RestoredData &r_data);
namespace AGS {
namespace Engine {
const char *SavegameSource::LegacySignature = "Adventure Game Studio saved game";
const char *SavegameSource::Signature = "Adventure Game Studio saved game v2";
SavegameSource::SavegameSource()
: Version(kSvgVersion_Undefined) {
}
SavegameDescription::SavegameDescription()
: MainDataVersion(kGameVersion_Undefined)
, ColorDepth(0)
, LegacyID(0) {
}
PreservedParams::PreservedParams()
: SpeechVOX(0)
, MusicVOX(0)
, GlScDataSize(0) {
}
RestoredData::ScriptData::ScriptData()
: Len(0) {
}
RestoredData::RestoredData()
: FPS(0)
, DoOnceCount(0u)
, RoomVolume(kRoomVolumeNormal)
, CursorID(0)
, CursorMode(0) {
memset(RoomLightLevels, 0, sizeof(RoomLightLevels));
memset(RoomTintLevels, 0, sizeof(RoomTintLevels));
memset(RoomZoomLevels1, 0, sizeof(RoomZoomLevels1));
memset(RoomZoomLevels2, 0, sizeof(RoomZoomLevels2));
memset(DoAmbient, 0, sizeof(DoAmbient));
}
String GetSavegameErrorText(SavegameErrorType err) {
switch (err) {
case kSvgErr_NoError:
return "No error.";
case kSvgErr_FileOpenFailed:
return "File not found or could not be opened.";
case kSvgErr_SignatureFailed:
return "Not an AGS saved game or unsupported format.";
case kSvgErr_FormatVersionNotSupported:
return "Save format version not supported.";
case kSvgErr_IncompatibleEngine:
return "Save was written by incompatible engine, or file is corrupted.";
case kSvgErr_GameGuidMismatch:
return "Game GUID does not match, saved by a different game.";
case kSvgErr_ComponentListOpeningTagFormat:
return "Failed to parse opening tag of the components list.";
case kSvgErr_ComponentListClosingTagMissing:
return "Closing tag of the components list was not met.";
case kSvgErr_ComponentOpeningTagFormat:
return "Failed to parse opening component tag.";
case kSvgErr_ComponentClosingTagFormat:
return "Failed to parse closing component tag.";
case kSvgErr_ComponentSizeMismatch:
return "Component data size mismatch.";
case kSvgErr_UnsupportedComponent:
return "Unknown and/or unsupported component.";
case kSvgErr_ComponentSerialization:
return "Failed to write the savegame component.";
case kSvgErr_ComponentUnserialization:
return "Failed to restore the savegame component.";
case kSvgErr_InconsistentFormat:
return "Inconsistent format, or file is corrupted.";
case kSvgErr_UnsupportedComponentVersion:
return "Component data version not supported.";
case kSvgErr_GameContentAssertion:
return "Saved content does not match current game.";
case kSvgErr_InconsistentData:
return "Inconsistent save data, or file is corrupted.";
case kSvgErr_InconsistentPlugin:
return "One of the game plugins did not restore its game data correctly.";
case kSvgErr_DifferentColorDepth:
return "Saved with the engine running at a different colour depth.";
case kSvgErr_GameObjectInitFailed:
return "Game object initialization failed after save restoration.";
default:
return "Unknown error.";
}
}
Bitmap *RestoreSaveImage(Stream *in) {
if (in->ReadInt32())
return read_serialized_bitmap(in);
return nullptr;
}
void SkipSaveImage(Stream *in) {
if (in->ReadInt32())
skip_serialized_bitmap(in);
}
HSaveError ReadDescription(Stream *in, SavegameVersion &svg_ver, SavegameDescription &desc, SavegameDescElem elems) {
svg_ver = (SavegameVersion)in->ReadInt32();
if (svg_ver < kSvgVersion_LowestSupported || svg_ver > kSvgVersion_Current)
return new SavegameError(kSvgErr_FormatVersionNotSupported,
String::FromFormat("Required: %d, supported: %d - %d.", svg_ver, kSvgVersion_LowestSupported, kSvgVersion_Current));
// Environment information
if (svg_ver >= kSvgVersion_351)
in->ReadInt32(); // environment info size
if (elems & kSvgDesc_EnvInfo) {
desc.EngineName = StrUtil::ReadString(in);
desc.EngineVersion.SetFromString(StrUtil::ReadString(in));
desc.GameGuid = StrUtil::ReadString(in);
desc.GameTitle = StrUtil::ReadString(in);
desc.MainDataFilename = StrUtil::ReadString(in);
if (svg_ver >= kSvgVersion_Cmp_64bit)
desc.MainDataVersion = (GameDataVersion)in->ReadInt32();
desc.ColorDepth = in->ReadInt32();
if (svg_ver >= kSvgVersion_351)
desc.LegacyID = in->ReadInt32();
} else {
StrUtil::SkipString(in); // engine name
StrUtil::SkipString(in); // engine version
StrUtil::SkipString(in); // game guid
StrUtil::SkipString(in); // game title
StrUtil::SkipString(in); // main data filename
if (svg_ver >= kSvgVersion_Cmp_64bit)
in->ReadInt32(); // game data version
in->ReadInt32(); // color depth
if (svg_ver >= kSvgVersion_351)
in->ReadInt32(); // game legacy id
}
// User description
if (elems & kSvgDesc_UserText)
desc.UserText = StrUtil::ReadString(in);
else
StrUtil::SkipString(in);
if (elems & kSvgDesc_UserImage)
desc.UserImage.reset(RestoreSaveImage(in));
else
SkipSaveImage(in);
return HSaveError::None();
}
HSaveError ReadDescription_v321(Stream *in, SavegameVersion &svg_ver, SavegameDescription &desc, SavegameDescElem elems) {
// Legacy savegame header
if (elems & kSvgDesc_UserText)
desc.UserText.Read(in);
else
StrUtil::SkipCStr(in);
svg_ver = (SavegameVersion)in->ReadInt32();
// Check saved game format version
if (svg_ver < kSvgVersion_LowestSupported ||
svg_ver > kSvgVersion_Current) {
return new SavegameError(kSvgErr_FormatVersionNotSupported,
String::FromFormat("Required: %d, supported: %d - %d.", svg_ver, kSvgVersion_LowestSupported, kSvgVersion_Current));
}
if (elems & kSvgDesc_UserImage)
desc.UserImage.reset(RestoreSaveImage(in));
else
SkipSaveImage(in);
// This is the lowest legacy save format we support,
// judging by the engine code received from CJ.
const Version low_compat_version(3, 2, 0, 1103);
String version_str = String::FromStream(in);
Version eng_version(version_str);
if (eng_version > _G(EngineVersion) || eng_version < low_compat_version) {
// Engine version is either non-forward or non-backward compatible
return new SavegameError(kSvgErr_IncompatibleEngine,
String::FromFormat("Required: %s, supported: %s - %s.", eng_version.LongString.GetCStr(), low_compat_version.LongString.GetCStr(), _G(EngineVersion).LongString.GetCStr()));
}
if (elems & kSvgDesc_EnvInfo) {
desc.MainDataFilename.Read(in);
in->ReadInt32(); // unscaled game height with borders, now obsolete
desc.ColorDepth = in->ReadInt32();
} else {
StrUtil::SkipCStr(in);
in->ReadInt32(); // unscaled game height with borders, now obsolete
in->ReadInt32(); // color depth
}
return HSaveError::None();
}
HSaveError OpenSavegameBase(const String &filename, SavegameSource *src, SavegameDescription *desc, SavegameDescElem elems) {
UStream in(File::OpenFileRead(filename));
if (!in.get())
return new SavegameError(kSvgErr_FileOpenFailed, String::FromFormat("Requested filename: %s.", filename.GetCStr()));
// Skip MS Windows Vista rich media header
RICH_GAME_MEDIA_HEADER rich_media_header;
rich_media_header.ReadFromFile(in.get());
// Check saved game signature
bool is_new_save = false;
size_t pre_sig_pos = in->GetPosition();
String svg_sig = String::FromStreamCount(in.get(), strlen(SavegameSource::Signature));
if (svg_sig.Compare(SavegameSource::Signature) == 0) {
is_new_save = true;
} else {
in->Seek(pre_sig_pos, kSeekBegin);
svg_sig = String::FromStreamCount(in.get(), strlen(SavegameSource::LegacySignature));
if (svg_sig.Compare(SavegameSource::LegacySignature) != 0)
return new SavegameError(kSvgErr_SignatureFailed);
}
SavegameVersion svg_ver;
SavegameDescription temp_desc;
HSaveError err;
if (is_new_save)
err = ReadDescription(in.get(), svg_ver, temp_desc, desc ? elems : kSvgDesc_None);
else
err = ReadDescription_v321(in.get(), svg_ver, temp_desc, desc ? elems : kSvgDesc_None);
if (!err)
return err;
if (src) {
src->Filename = filename;
src->Version = svg_ver;
src->InputStream.reset(in.release()); // give the stream away to the caller
}
if (desc) {
if (elems & kSvgDesc_EnvInfo) {
desc->EngineName = temp_desc.EngineName;
desc->EngineVersion = temp_desc.EngineVersion;
desc->GameGuid = temp_desc.GameGuid;
desc->LegacyID = temp_desc.LegacyID;
desc->GameTitle = temp_desc.GameTitle;
desc->MainDataFilename = temp_desc.MainDataFilename;
desc->MainDataVersion = temp_desc.MainDataVersion;
desc->ColorDepth = temp_desc.ColorDepth;
}
if (elems & kSvgDesc_UserText)
desc->UserText = temp_desc.UserText;
if (elems & kSvgDesc_UserImage)
desc->UserImage.reset(temp_desc.UserImage.release());
}
return err;
}
HSaveError OpenSavegame(const String &filename, SavegameSource &src, SavegameDescription &desc, SavegameDescElem elems) {
return OpenSavegameBase(filename, &src, &desc, elems);
}
HSaveError OpenSavegame(const String &filename, SavegameDescription &desc, SavegameDescElem elems) {
return OpenSavegameBase(filename, nullptr, &desc, elems);
}
// Prepares engine for actual save restore (stops processes, cleans up memory)
void DoBeforeRestore(PreservedParams &pp) {
pp.SpeechVOX = _GP(play).voice_avail;
pp.MusicVOX = _GP(play).separate_music_lib;
memcpy(pp.GameOptions, _GP(game).options, GameSetupStruct::MAX_OPTIONS * sizeof(int));
unload_old_room();
_G(raw_saved_screen).reset();
remove_all_overlays();
_GP(play).complete_overlay_on = 0;
_GP(play).text_overlay_on = 0;
// cleanup dynamic sprites
// NOTE: sprite 0 is a special constant sprite that cannot be dynamic
for (int i = 1; i < (int)_GP(spriteset).GetSpriteSlotCount(); ++i) {
if (_GP(game).SpriteInfos[i].Flags & SPF_DYNAMICALLOC) {
free_dynamic_sprite(i);
}
}
// Cleanup drawn caches
clear_drawobj_cache();
// preserve script data sizes and cleanup scripts
pp.GlScDataSize = _G(gameinst)->globaldatasize;
pp.ScMdDataSize.resize(_G(numScriptModules));
for (size_t i = 0; i < _G(numScriptModules); ++i) {
pp.ScMdDataSize[i] = _GP(moduleInst)[i]->globaldatasize;
}
FreeAllScriptInstances();
// reset saved room states
resetRoomStatuses();
// reset temp room state
_GP(troom) = RoomStatus();
// reset (some of the?) GameState data
// FIXME: investigate and refactor to be able to just reset whole object
_GP(play).FreeProperties();
_GP(play).FreeViewportsAndCameras();
free_do_once_tokens();
RemoveAllButtonAnimations();
// unregister gui controls from API exports
// CHECKME: find out why are we doing this here? why only to gui controls?
for (int i = 0; i < _GP(game).numgui; ++i) {
unexport_gui_controls(i);
}
// Clear the managed object pool
ccUnregisterAllObjects();
// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
stop_and_destroy_channel_ex(i, false);
}
clear_music_cache();
}
void RestoreViewportsAndCameras(const RestoredData &r_data) {
// If restored from older saves, we have to adjust
// cam and view sizes to a main viewport, which is init later
const auto &main_view = _GP(play).GetMainViewport();
for (size_t i = 0; i < r_data.Cameras.size(); ++i) {
const auto &cam_dat = r_data.Cameras[i];
auto cam = _GP(play).GetRoomCamera(i);
cam->SetID(cam_dat.ID);
if ((cam_dat.Flags & kSvgCamPosLocked) != 0)
cam->Lock();
else
cam->Release();
// Set size first, or offset position may clamp to the room
if (r_data.LegacyViewCamera)
cam->SetSize(main_view.GetSize());
else
cam->SetSize(Size(cam_dat.Width, cam_dat.Height));
cam->SetAt(cam_dat.Left, cam_dat.Top);
}
for (size_t i = 0; i < r_data.Viewports.size(); ++i) {
const auto &view_dat = r_data.Viewports[i];
auto view = _GP(play).GetRoomViewport(i);
view->SetID(view_dat.ID);
view->SetVisible((view_dat.Flags & kSvgViewportVisible) != 0);
if (r_data.LegacyViewCamera)
view->SetRect(RectWH(view_dat.Left, view_dat.Top, main_view.GetWidth(), main_view.GetHeight()));
else
view->SetRect(RectWH(view_dat.Left, view_dat.Top, view_dat.Width, view_dat.Height));
view->SetZOrder(view_dat.ZOrder);
// Restore camera link
int cam_index = view_dat.CamID;
if (cam_index < 0) continue;
auto cam = _GP(play).GetRoomCamera(cam_index);
view->LinkCamera(cam);
cam->LinkToViewport(view);
}
_GP(play).InvalidateViewportZOrder();
}
// Resets a number of options that are not supposed to be changed at runtime
static void CopyPreservedGameOptions(GameSetupStructBase &gs, const PreservedParams &pp) {
const auto restricted_opts = GameSetupStructBase::GetRestrictedOptions();
for (auto opt : restricted_opts)
gs.options[opt] = pp.GameOptions[opt];
}
// Final processing after successfully restoring from save
HSaveError DoAfterRestore(const PreservedParams &pp, RestoredData &r_data) {
// Use a yellow dialog highlight for older game versions
// CHECKME: it is dubious that this should be right here
if (_G(loaded_game_file_version) < kGameVersion_331)
_GP(play).dialog_options_highlight_color = DIALOG_OPTIONS_HIGHLIGHT_COLOR_DEFAULT;
// Preserve whether the music vox is available
_GP(play).voice_avail = pp.SpeechVOX;
_GP(play).separate_music_lib = pp.MusicVOX;
// Restore particular game options that must not change at runtime
CopyPreservedGameOptions(_GP(game), pp);
// Restore debug flags
if (_G(debug_flags) & DBG_DEBUGMODE)
_GP(play).debug_mode = 1;
// recache queued clips
for (int i = 0; i < _GP(play).new_music_queue_size; ++i) {
_GP(play).new_music_queue[i].cachedClip = nullptr;
}
// Remap old sound nums in case we restored a save having a different list of audio clips
RemapLegacySoundNums(_GP(game), _GP(views), _G(loaded_game_file_version));
// Restore Overlay bitmaps (older save format, which stored them along with overlays)
auto &overs = get_overlays();
for (auto &over_im : r_data.OverlayImages) {
auto &over = overs[over_im._key];
over.SetImage(std::move(over_im._value), over.HasAlphaChannel(), over.offsetX, over.offsetY);
}
// Restore dynamic surfaces
const size_t dynsurf_num = MIN((uint)MAX_DYNAMIC_SURFACES, r_data.DynamicSurfaces.size());
for (size_t i = 0; i < dynsurf_num; ++i) {
_G(dynamicallyCreatedSurfaces)[i] = std::move(r_data.DynamicSurfaces[i]);
}
// Re-export any missing audio channel script objects, e.g. if restoring old save
export_missing_audiochans();
// CHECKME: find out why are we doing this here? why only to gui controls?
for (int i = 0; i < _GP(game).numgui; ++i)
export_gui_controls(i);
update_gui_zorder();
AllocScriptModules();
if (create_global_script()) {
return new SavegameError(kSvgErr_GameObjectInitFailed,
String::FromFormat("Unable to recreate global script: %s", cc_get_error().ErrorString.GetCStr()));
}
// read the global data into the newly created script
if (!r_data.GlobalScript.Data.empty())
memcpy(_G(gameinst)->globaldata, &r_data.GlobalScript.Data.front(),
MIN((size_t)_G(gameinst)->globaldatasize, r_data.GlobalScript.Len));
// restore the script module data
for (size_t i = 0; i < _G(numScriptModules); ++i) {
if (!r_data.ScriptModules[i].Data.empty())
memcpy(_GP(moduleInst)[i]->globaldata, &r_data.ScriptModules[i].Data.front(),
MIN((size_t)_GP(moduleInst)[i]->globaldatasize, r_data.ScriptModules[i].Len));
}
setup_player_character(_GP(game).playercharacter);
// Save some parameters to restore them after room load
int gstimer = _GP(play).gscript_timer;
int oldx1 = _GP(play).mboundx1, oldx2 = _GP(play).mboundx2;
int oldy1 = _GP(play).mboundy1, oldy2 = _GP(play).mboundy2;
// disable the queue momentarily
int queuedMusicSize = _GP(play).music_queue_size;
_GP(play).music_queue_size = 0;
// load the room the game was saved in
if (_G(displayed_room) >= 0)
load_new_room(_G(displayed_room), nullptr);
else
set_room_placeholder();
_GP(play).gscript_timer = gstimer;
// restore the correct room volume (they might have modified
// it with SetMusicVolume)
_GP(thisroom).Options.MusicVolume = r_data.RoomVolume;
_GP(mouse).SetMoveLimit(Rect(oldx1, oldy1, oldx2, oldy2));
set_cursor_mode(r_data.CursorMode);
set_mouse_cursor(r_data.CursorID, true);
if (r_data.CursorMode == MODE_USE)
SetActiveInventory(_G(playerchar)->activeinv);
// ensure that the current cursor is locked
_GP(spriteset).PrecacheSprite(_GP(game).mcurs[r_data.CursorID].pic);
sys_window_set_title(_GP(play).game_name.GetCStr());
if (_G(displayed_room) >= 0) {
// Fixup the frame index, in case the restored room does not have enough background frames
if (_GP(play).bg_frame < 0 || static_cast<size_t>(_GP(play).bg_frame) >= _GP(thisroom).BgFrameCount)
_GP(play).bg_frame = 0;
for (int i = 0; i < MAX_ROOM_BGFRAMES; ++i) {
if (r_data.RoomBkgScene[i]) {
_GP(thisroom).BgFrames[i].Graphic = r_data.RoomBkgScene[i];
}
}
_G(in_new_room) = 3; // don't run "enters screen" events
// now that room has loaded, copy saved light levels in
for (size_t i = 0; i < MAX_ROOM_REGIONS; ++i) {
_GP(thisroom).Regions[i].Light = r_data.RoomLightLevels[i];
_GP(thisroom).Regions[i].Tint = r_data.RoomTintLevels[i];
}
generate_light_table();
for (size_t i = 0; i < MAX_WALK_AREAS; ++i) {
_GP(thisroom).WalkAreas[i].ScalingFar = r_data.RoomZoomLevels1[i];
_GP(thisroom).WalkAreas[i].ScalingNear = r_data.RoomZoomLevels2[i];
}
on_background_frame_change();
}
GUI::Options.DisabledStyle = static_cast<GuiDisableStyle>(_GP(game).options[OPT_DISABLEOFF]);
// restore the queue now that the music is playing
_GP(play).music_queue_size = queuedMusicSize;
if (_GP(play).digital_master_volume >= 0) {
int temp_vol = _GP(play).digital_master_volume;
_GP(play).digital_master_volume = -1; // reset to invalid state before re-applying
System_SetVolume(temp_vol);
}
// Run audio clips on channels
// these two crossfading parameters have to be temporarily reset
const int cf_in_chan = _GP(play).crossfading_in_channel;
const int cf_out_chan = _GP(play).crossfading_out_channel;
_GP(play).crossfading_in_channel = 0;
_GP(play).crossfading_out_channel = 0;
// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
const RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
if (chan_info.ClipID < 0)
continue;
if ((size_t)chan_info.ClipID >= _GP(game).audioClips.size()) {
return new SavegameError(kSvgErr_GameObjectInitFailed,
String::FromFormat("Invalid audio clip index: %d (clip count: %zu).", chan_info.ClipID, _GP(game).audioClips.size()));
}
play_audio_clip_on_channel(i, &_GP(game).audioClips[chan_info.ClipID],
chan_info.Priority, chan_info.Repeat, chan_info.Pos);
auto *ch = AudioChans::GetChannel(i);
if (ch != nullptr) {
ch->set_volume_direct(chan_info.VolAsPercent, chan_info.Vol);
ch->set_speed(chan_info.Speed);
ch->set_panning(chan_info.Pan);
ch->_xSource = chan_info.XSource;
ch->_ySource = chan_info.YSource;
ch->_maximumPossibleDistanceAway = chan_info.MaxDist;
if ((chan_info.Flags & kSvgAudioPaused) != 0)
ch->pause();
}
}
if ((cf_in_chan > 0) && (AudioChans::GetChannel(cf_in_chan) != nullptr))
_GP(play).crossfading_in_channel = cf_in_chan;
if ((cf_out_chan > 0) && (AudioChans::GetChannel(cf_out_chan) != nullptr))
_GP(play).crossfading_out_channel = cf_out_chan;
// If there were synced audio tracks, the time taken to load in the
// different channels will have thrown them out of sync, so re-time it
// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
auto *ch = AudioChans::GetChannelIfPlaying(i);
int pos = r_data.AudioChans[i].Pos;
if ((pos > 0) && (ch != nullptr)) {
ch->seek(pos);
}
}
for (int i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; ++i) {
if (r_data.DoAmbient[i])
PlayAmbientSound(i, r_data.DoAmbient[i], _GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
}
update_directional_sound_vol();
adjust_fonts_for_render_mode(_GP(game).options[OPT_ANTIALIASFONTS] != 0);
restore_characters();
restore_overlays();
restore_movelists();
GUI::MarkAllGUIForUpdate(true, true);
RestoreViewportsAndCameras(r_data);
_GP(play).ClearIgnoreInput(); // don't keep ignored input after save restore
update_polled_stuff();
pl_run_plugin_hooks(AGSE_POSTRESTOREGAME, 0);
if (_G(displayed_room) < 0) {
// the restart point, no room was loaded
load_new_room(_G(playerchar)->room, _G(playerchar));
first_room_initialization();
}
if ((_GP(play).music_queue_size > 0) && (_G(cachedQueuedMusic) == nullptr)) {
_G(cachedQueuedMusic) = load_music_from_disk(_GP(play).music_queue[0], 0);
}
// Test if the old-style audio had playing music and it was properly loaded
if (_G(current_music_type) > 0) {
if ((_G(crossFading) > 0 && !AudioChans::GetChannelIfPlaying(_G(crossFading))) ||
(_G(crossFading) <= 0 && !AudioChans::GetChannelIfPlaying(SCHAN_MUSIC))) {
_G(current_music_type) = 0; // playback failed, reset flag
}
}
set_game_speed(r_data.FPS);
return HSaveError::None();
}
HSaveError RestoreGameState(Stream *in, SavegameVersion svg_version) {
PreservedParams pp;
RestoredData r_data;
DoBeforeRestore(pp);
HSaveError err;
if (svg_version >= kSvgVersion_Components) {
err = SavegameComponents::ReadAll(in, svg_version, pp, r_data);
} else {
GameDataVersion use_dataver = _GP(usetup).legacysave_assume_dataver != kGameVersion_Undefined ? _GP(usetup).legacysave_assume_dataver
: _G(loaded_game_file_version);
err = restore_save_data_v321(in, use_dataver, pp, r_data);
}
if (!err)
return err;
return DoAfterRestore(pp, r_data);
}
void WriteSaveImage(Stream *out, const Bitmap *screenshot) {
// store the screenshot at the start to make it easily accessible
out->WriteInt32((screenshot == nullptr) ? 0 : 1);
if (screenshot)
serialize_bitmap(screenshot, out);
}
void WriteDescription(Stream *out, const String &user_text, const Bitmap *user_image) {
// Data format version
out->WriteInt32(kSvgVersion_Current);
soff_t env_pos = out->GetPosition();
out->WriteInt32(0);
// Environment information
StrUtil::WriteString(get_engine_name(), out);
StrUtil::WriteString(_G(EngineVersion).LongString, out);
StrUtil::WriteString(_GP(game).guid, out);
StrUtil::WriteString(_GP(game).gamename, out);
StrUtil::WriteString(_GP(ResPaths).GamePak.Name, out);
out->WriteInt32(_G(loaded_game_file_version));
out->WriteInt32(_GP(game).GetColorDepth());
out->WriteInt32(_GP(game).uniqueid);
soff_t env_end_pos = out->GetPosition();
out->Seek(env_pos, kSeekBegin);
out->WriteInt32(env_end_pos - env_pos);
out->Seek(env_end_pos, kSeekBegin);
// User description
StrUtil::WriteString(user_text, out);
WriteSaveImage(out, user_image);
}
Stream *StartSavegame(const String &filename, const String &user_text, const Bitmap *user_image) {
Stream *out = Shared::File::CreateFile(filename);
if (!out)
return nullptr;
// Initialize and write Vista header
RICH_GAME_MEDIA_HEADER vistaHeader;
memset(&vistaHeader, 0, sizeof(RICH_GAME_MEDIA_HEADER));
vistaHeader.dwMagicNumber = RM_MAGICNUMBER;
vistaHeader.dwHeaderVersion = 1;
vistaHeader.dwHeaderSize = sizeof(RICH_GAME_MEDIA_HEADER);
vistaHeader.dwThumbnailOffsetHigherDword = 0;
vistaHeader.dwThumbnailOffsetLowerDword = 0;
vistaHeader.dwThumbnailSize = 0;
convert_guid_from_text_to_binary(_GP(game).guid, &vistaHeader.guidGameId[0]);
vistaHeader.setSaveName(user_text);
vistaHeader.szLevelName[0] = 0;
vistaHeader.szComments[0] = 0;
// MS Windows Vista rich media header
vistaHeader.WriteToFile(out);
// Savegame signature
out->Write(SavegameSource::Signature, strlen(SavegameSource::Signature));
// CHECKME: what is this plugin hook suppose to mean, and if it is called here correctly
pl_run_plugin_hooks(AGSE_PRESAVEGAME, 0);
// Write descrition block
WriteDescription(out, user_text, user_image);
return out;
}
void DoBeforeSave() {
if (_GP(play).cur_music_number >= 0) {
if (IsMusicPlaying() == 0)
_GP(play).cur_music_number = -1;
}
if (_G(displayed_room) >= 0) {
// update the current room script's data segment copy
if (_G(roominst))
save_room_data_segment();
// Update the saved interaction variable values
for (size_t i = 0; i < _GP(thisroom).LocalVariables.size() && i < (size_t)MAX_GLOBAL_VARIABLES; ++i)
_G(croom)->interactionVariableValues[i] = _GP(thisroom).LocalVariables[i].Value;
}
}
void SaveGameState(Stream *out) {
DoBeforeSave();
SavegameComponents::WriteAllCommon(out);
}
void ReadPluginSaveData(Stream *in, PluginSvgVersion svg_ver, soff_t max_size) {
const soff_t start_pos = in->GetPosition();
const soff_t end_pos = start_pos + max_size;
if (svg_ver >= kPluginSvgVersion_36115) {
uint32_t num_plugins_read = in->ReadInt32();
soff_t cur_pos = start_pos;
while ((num_plugins_read--) > 0 && (cur_pos < end_pos)) {
String pl_name = StrUtil::ReadString(in);
size_t data_size = in->ReadInt32();
soff_t data_start = in->GetPosition();
auto pl_handle = AGSE_RESTOREGAME;
pl_set_file_handle(pl_handle, in);
pl_run_plugin_hook_by_name(pl_name, AGSE_RESTOREGAME, pl_handle);
pl_clear_file_handle();
// Seek to the end of plugin data, in case it ended up reading not in the end
cur_pos = data_start + data_size;
in->Seek(cur_pos, kSeekBegin);
}
} else {
String pl_name;
for (uint32_t pl_index = 0; pl_query_next_plugin_for_event(AGSE_RESTOREGAME, pl_index, pl_name); ++pl_index) {
auto pl_handle = AGSE_RESTOREGAME;
pl_set_file_handle(pl_handle, in);
pl_run_plugin_hook_by_index(pl_index, AGSE_RESTOREGAME, pl_handle);
pl_clear_file_handle();
}
}
}
void WritePluginSaveData(Stream *out) {
soff_t pluginnum_pos = out->GetPosition();
out->WriteInt32(0); // number of plugins which wrote data
uint32_t num_plugins_wrote = 0;
String pl_name;
for (uint32_t pl_index = 0; pl_query_next_plugin_for_event(AGSE_SAVEGAME, pl_index, pl_name); ++pl_index) {
// NOTE: we don't care if they really write anything,
// but count them so long as they subscribed to AGSE_SAVEGAME
num_plugins_wrote++;
// Write a header for plugin data
StrUtil::WriteString(pl_name, out);
soff_t data_size_pos = out->GetPosition();
out->WriteInt32(0); // data size
// Create a stream section and write plugin data
soff_t data_start_pos = out->GetPosition();
auto pl_handle = AGSE_SAVEGAME;
pl_set_file_handle(pl_handle, out);
pl_run_plugin_hook_by_index(pl_index, AGSE_SAVEGAME, pl_handle);
pl_clear_file_handle();
// Finalize header
soff_t data_end_pos = out->GetPosition();
out->Seek(data_size_pos, kSeekBegin);
out->WriteInt32(data_end_pos - data_start_pos);
out->Seek(0, kSeekEnd);
}
// Write number of plugins
out->Seek(pluginnum_pos, kSeekBegin);
out->WriteInt32(num_plugins_wrote);
out->Seek(0, kSeekEnd);
}
} // namespace Engine
} // namespace AGS
} // namespace AGS3

View File

@@ -0,0 +1,180 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef AGS_ENGINE_GAME_SAVEGAME_H
#define AGS_ENGINE_GAME_SAVEGAME_H
#include "common/std/memory.h"
#include "ags/shared/core/platform.h"
#include "ags/shared/ac/game_version.h"
#include "ags/shared/util/error.h"
#include "ags/shared/util/version.h"
namespace AGS3 {
namespace AGS {
namespace Shared {
class Bitmap;
class Stream;
} // namespace Shared
namespace Engine {
using Shared::Bitmap;
using Shared::ErrorHandle;
using Shared::TypedCodeError;
using Shared::Stream;
using Shared::String;
using Shared::Version;
typedef std::shared_ptr<Stream> PStream;
//-----------------------------------------------------------------------------
// Savegame version history
//
// 8 last old style saved game format (of AGS 3.2.1)
// 9 first new style (self-descriptive block-based) format version
// Since 3.6.0: value is defined as AGS version represented as NN,NN,NN,NN.
//-----------------------------------------------------------------------------
enum SavegameVersion {
kSvgVersion_Undefined = 0,
kSvgVersion_321 = 8,
kSvgVersion_Components = 9,
kSvgVersion_Cmp_64bit = 10,
kSvgVersion_350_final = 11,
kSvgVersion_350_final2 = 12,
kSvgVersion_351 = 13,
kSvgVersion_360_beta = 3060023,
kSvgVersion_360_final = 3060041,
kSvgVersion_361 = 3060115,
kSvgVersion_361_p8 = 3060130,
kSvgVersion_Current = kSvgVersion_361_p8,
kSvgVersion_LowestSupported = kSvgVersion_321 // change if support dropped
};
// Error codes for save restoration routine
enum SavegameErrorType {
kSvgErr_NoError,
kSvgErr_FileOpenFailed,
kSvgErr_SignatureFailed,
kSvgErr_FormatVersionNotSupported,
kSvgErr_IncompatibleEngine,
kSvgErr_GameGuidMismatch,
kSvgErr_ComponentListOpeningTagFormat,
kSvgErr_ComponentListClosingTagMissing,
kSvgErr_ComponentOpeningTagFormat,
kSvgErr_ComponentClosingTagFormat,
kSvgErr_ComponentSizeMismatch,
kSvgErr_UnsupportedComponent,
kSvgErr_ComponentSerialization,
kSvgErr_ComponentUnserialization,
kSvgErr_InconsistentFormat,
kSvgErr_UnsupportedComponentVersion,
kSvgErr_GameContentAssertion,
kSvgErr_InconsistentData,
kSvgErr_InconsistentPlugin,
kSvgErr_DifferentColorDepth,
kSvgErr_GameObjectInitFailed,
kNumSavegameError
};
String GetSavegameErrorText(SavegameErrorType err);
typedef TypedCodeError<SavegameErrorType, GetSavegameErrorText> SavegameError;
typedef ErrorHandle<SavegameError> HSaveError;
// SavegameSource defines a successfully opened savegame stream
struct SavegameSource {
// Signature of the current savegame format
static const char *Signature;
// Signature of the legacy savegame format
static const char *LegacySignature;
// Name of the savefile
String Filename;
// Savegame format version
SavegameVersion Version;
// A ponter to the opened stream
std::unique_ptr<Stream> InputStream;
SavegameSource();
};
// Supported elements of savegame description;
// these may be used as flags to define valid fields
enum SavegameDescElem {
kSvgDesc_None = 0,
kSvgDesc_EnvInfo = 0x0001,
kSvgDesc_UserText = 0x0002,
kSvgDesc_UserImage = 0x0004,
kSvgDesc_All = kSvgDesc_EnvInfo | kSvgDesc_UserText | kSvgDesc_UserImage
};
// SavegameDescription describes savegame with information about the environment
// it was created in, and custom data provided by user
struct SavegameDescription {
// Name of the engine that saved the game
String EngineName;
// Version of the engine that saved the game
Version EngineVersion;
// Guid of the game which made this save
String GameGuid;
// Legacy uniqueid of the game, for use in older games with no GUID
int LegacyID;
// Title of the game which made this save
String GameTitle;
// Name of the main data file used; this is needed to properly
// load saves made by "minigames"
String MainDataFilename;
// Game's main data version; should be checked early to know
// if the save was made for the supported game format
GameDataVersion MainDataVersion;
// Native color depth of the game; this is required to
// properly restore dynamic graphics from the save
int ColorDepth;
String UserText;
std::unique_ptr<Bitmap> UserImage;
SavegameDescription();
};
// Opens savegame for reading; optionally reads description, if any is provided
HSaveError OpenSavegame(const String &filename, SavegameSource &src,
SavegameDescription &desc, SavegameDescElem elems = kSvgDesc_All);
// Opens savegame and reads the savegame description
HSaveError OpenSavegame(const String &filename, SavegameDescription &desc, SavegameDescElem elems = kSvgDesc_All);
// Reads the game data from the save stream and reinitializes game state
HSaveError RestoreGameState(Stream *in, SavegameVersion svg_version);
// Opens savegame for writing and puts in savegame description
Stream *StartSavegame(const String &filename, const String &user_text, const Bitmap *user_image);
// Prepares game for saving state and writes game data into the save stream
void SaveGameState(Stream *out);
} // namespace Engine
} // namespace AGS
} // namespace AGS3
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef AGS_ENGINE_GAME_SAVEGAME_COMPONENTS_H
#define AGS_ENGINE_GAME_SAVEGAME_COMPONENTS_H
#include "ags/engine/game/savegame.h"
#include "ags/shared/util/stream.h"
namespace AGS3 {
namespace AGS {
namespace Shared {
struct Interaction;
} // namespace Shared
namespace Engine {
using Shared::Stream;
using Shared::Interaction;
struct PreservedParams;
struct RestoredData;
namespace SavegameComponents {
extern void component_handlers_init();
extern void component_handlers_free();
// Reads all available components from the stream
HSaveError ReadAll(Stream *in, SavegameVersion svg_version, const PreservedParams &pp, RestoredData &r_data);
// Writes a full list of common components to the stream
HSaveError WriteAllCommon(Stream *out);
// Utility functions for reading and writing legacy interactions,
// or their "times run" counters separately.
void ReadTimesRun272(Interaction &intr, Stream *in);
HSaveError ReadInteraction272(Interaction &intr, Stream *in);
void WriteTimesRun272(const Interaction &intr, Stream *out);
void WriteInteraction272(const Interaction &intr, Stream *out);
// Precreates primary camera and viewport and reads legacy camera data
void ReadLegacyCameraState(Stream *in, RestoredData &r_data);
} // namespace SavegameComponents
} // namespace Engine
} // namespace AGS
} // namespace AGS3
#endif

View File

@@ -0,0 +1,168 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef AGS_ENGINE_GAME_SAVEGAME_INTERNAL_H
#define AGS_ENGINE_GAME_SAVEGAME_INTERNAL_H
#include "common/std/memory.h"
#include "common/std/vector.h"
#include "common/std/map.h"
#include "ags/shared/ac/common_defines.h"
#include "ags/shared/game/room_struct.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/engine/media/audio/audio_defines.h"
namespace AGS3 {
namespace AGS {
namespace Engine {
using AGS::Shared::Bitmap;
using AGS::Shared::Stream;
typedef std::shared_ptr<Bitmap> PBitmap;
// PreservedParams keeps old values of particular gameplay
// parameters that are saved before the save restoration
// and either applied or compared to new values after
// loading save data
struct PreservedParams {
// Whether speech and audio packages available
bool SpeechVOX = false;
bool MusicVOX = false;
// Game options, to preserve ones that must not change at runtime
int GameOptions[GameSetupStructBase::MAX_OPTIONS]{};
// Script global data sizes
size_t GlScDataSize = 0u;
std::vector<size_t> ScMdDataSize;
PreservedParams();
};
// Audio playback state flags, used only in serialization
enum AudioSvgPlaybackFlags {
kSvgAudioPaused = 0x01
};
enum GameViewCamFlags {
kSvgGameAutoRoomView = 0x01
};
enum CameraSaveFlags {
kSvgCamPosLocked = 0x01
};
enum ViewportSaveFlags {
kSvgViewportVisible = 0x01
};
// RestoredData keeps certain temporary data to help with
// the restoration process
struct RestoredData {
int FPS;
// Unserialized bitmaps for dynamic surfaces
std::vector<std::unique_ptr<Bitmap>> DynamicSurfaces;
// Unserialized bitmaps for overlays (old-style saves)
std::unordered_map<int, std::unique_ptr<Bitmap> > OverlayImages;
// Scripts global data
struct ScriptData {
std::vector<char> Data;
size_t Len;
ScriptData();
};
ScriptData GlobalScript;
std::vector<ScriptData> ScriptModules;
// Game state data (loaded ahead)
uint32_t DoOnceCount;
// Room data (has to be be preserved until room is loaded)
PBitmap RoomBkgScene[MAX_ROOM_BGFRAMES];
int16_t RoomLightLevels[MAX_ROOM_REGIONS];
int32_t RoomTintLevels[MAX_ROOM_REGIONS];
int16_t RoomZoomLevels1[MAX_WALK_AREAS];
int16_t RoomZoomLevels2[MAX_WALK_AREAS];
RoomVolumeMod RoomVolume;
// Mouse cursor parameters
int CursorID;
int CursorMode;
// General audio
struct ChannelInfo {
int ClipID = -1;
int Flags = 0;
int Pos = 0;
int Priority = 0;
int Repeat = 0;
int Vol = 0;
int VolAsPercent = 0;
int Pan = 0;
int Speed = 0;
// since version 1
int XSource = -1;
int YSource = -1;
int MaxDist = 0;
};
ChannelInfo AudioChans[TOTAL_AUDIO_CHANNELS];
// Ambient sounds
int DoAmbient[MAX_GAME_CHANNELS];
// Viewport and camera data, has to be preserved and applied only after
// room gets loaded, because we must clamp these to room parameters.
struct ViewportData {
int ID = -1;
int Flags = 0;
int Left = 0;
int Top = 0;
int Width = 0;
int Height = 0;
int ZOrder = 0;
int CamID = -1;
};
struct CameraData {
int ID = -1;
int Flags = 0;
int Left = 0;
int Top = 0;
int Width = 0;
int Height = 0;
};
std::vector<ViewportData> Viewports;
std::vector<CameraData> Cameras;
bool LegacyViewCamera = false;
int32_t Camera0_Flags = 0; // flags for primary camera, when data is read in legacy order
RestoredData();
};
enum PluginSvgVersion {
kPluginSvgVersion_Initial = 0,
kPluginSvgVersion_36115 = 1,
};
// Runs plugin events, requesting to read save data from the given stream.
// NOTE: there's no error check in this function, because plugin API currently
// does not let plugins report any errors when restoring their saved data.
void ReadPluginSaveData(Stream *in, PluginSvgVersion svg_ver, soff_t max_size);
// Runs plugin events, requesting to write save data into the given stream.
void WritePluginSaveData(Stream *out);
} // namespace Engine
} // namespace AGS
} // namespace AGS3
#endif

View File

@@ -0,0 +1,504 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
//=============================================================================
//
// These functions are restoring old savegames used by engines 3.2.1 - 3.5.0.
// The main point of keeping these today are to be able to compare game
// behavior when running with original/older binaries and latest engine.
// Perhaps the optimal solution would be to have a savegame converter instead.
//
//=============================================================================
#include "common/std/vector.h"
#include "ags/shared/core/types.h"
#include "ags/engine/ac/character_extras.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/dialog.h"
#include "ags/engine/ac/button.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/game_setup.h"
#include "ags/engine/ac/move_list.h"
#include "ags/engine/ac/overlay.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/ac/room_status.h"
#include "ags/shared/ac/view.h"
#include "ags/engine/ac/dynobj/cc_serializer.h"
#include "ags/engine/ac/dynobj/dynobj_manager.h"
#include "ags/engine/game/savegame.h"
#include "ags/engine/game/savegame_components.h"
#include "ags/engine/game/savegame_internal.h"
#include "ags/engine/gui/animating_gui_button.h"
#include "ags/shared/gui/gui_main.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_slider.h"
#include "ags/shared/gui/gui_textbox.h"
#include "ags/engine/media/audio/audio.h"
#include "ags/plugins/plugin_engine.h"
#include "ags/engine/script/script.h"
#include "ags/shared/script/cc_common.h"
#include "ags/shared/util/string_utils.h"
namespace AGS3 {
using namespace AGS::Shared;
using namespace AGS::Engine;
static const uint32_t MAGICNUMBER = 0xbeefcafe;
inline bool AssertGameContent(HSaveError &err, int game_val, int sav_val, const char *content_name, bool warn_only = false) {
if (game_val != sav_val) {
String msg = String::FromFormat("Mismatching number of %s (game: %d, save: %d).", content_name, game_val, sav_val);
if (warn_only)
Debug::Printf(kDbgMsg_Warn, "WARNING: restored save may be incompatible: %s", msg.GetCStr());
else
err = new SavegameError(kSvgErr_GameContentAssertion, msg);
}
return warn_only || (game_val == sav_val);
}
template<typename TObject>
inline bool AssertAndCopyGameContent(const std::vector<TObject> &old_list, std::vector<TObject> &new_list,
HSaveError &err, const char *content_name, bool warn_only = false) {
if (!AssertGameContent(err, old_list.size(), new_list.size(), content_name, warn_only))
return false;
if (new_list.size() < old_list.size()) {
size_t copy_at = new_list.size();
new_list.resize(old_list.size());
Common::copy(old_list.begin() + copy_at, old_list.end(), new_list.begin() + copy_at);
}
return true;
}
static HSaveError restore_game_head_dynamic_values(Stream *in, RestoredData &r_data) {
r_data.FPS = in->ReadInt32();
r_data.CursorMode = in->ReadInt32();
r_data.CursorID = in->ReadInt32();
SavegameComponents::ReadLegacyCameraState(in, r_data);
set_loop_counter(in->ReadInt32());
return HSaveError::None();
}
static void restore_game_spriteset(Stream *in) {
// ensure the sprite set is at least as large as it was
// when the game was saved
_GP(spriteset).EnlargeTo(in->ReadInt32() - 1); // they saved top_index + 1
// get serialized dynamic sprites
int sprnum = in->ReadInt32();
while (sprnum) {
unsigned char spriteflag = in->ReadInt8();
std::unique_ptr<Bitmap> image(read_serialized_bitmap(in));
add_dynamic_sprite(sprnum, std::move(image));
_GP(game).SpriteInfos[sprnum].Flags = spriteflag;
sprnum = in->ReadInt32();
}
}
static HSaveError restore_game_scripts(Stream *in, const PreservedParams &pp, RestoredData &r_data) {
HSaveError err;
// read the global script data segment
size_t gdatasize = (uint32_t)in->ReadInt32();
if (!AssertGameContent(err, pp.GlScDataSize, gdatasize, "global script data"))
return err;
r_data.GlobalScript.Len = gdatasize;
r_data.GlobalScript.Data.resize(gdatasize);
if (gdatasize > 0)
in->Read(&r_data.GlobalScript.Data.front(), gdatasize);
uint32_t num_modules = (uint32_t)in->ReadInt32();
if (!AssertGameContent(err, _G(numScriptModules), num_modules, "Script Modules"))
return err;
r_data.ScriptModules.resize(_G(numScriptModules));
for (size_t i = 0; i < _G(numScriptModules); ++i) {
size_t module_size = (uint32_t)in->ReadInt32();
if (pp.ScMdDataSize[i] != module_size) {
return new SavegameError(kSvgErr_GameContentAssertion, String::FromFormat("Mismatching size of script module data, module %zu.", i));
}
r_data.ScriptModules[i].Len = module_size;
r_data.ScriptModules[i].Data.resize(module_size);
if (module_size > 0)
in->Read(&r_data.ScriptModules[i].Data.front(), module_size);
}
return HSaveError::None();
}
static void restore_game_room_state(Stream *in, GameDataVersion data_ver) {
_G(displayed_room) = in->ReadInt32();
// read the room state for all the rooms the player has been in
for (int vv = 0; vv < MAX_ROOMS; vv++) {
int beenhere = in->ReadInt8();
if (beenhere) {
RoomStatus *roomstat = getRoomStatus(vv);
roomstat->beenhere = beenhere;
if (roomstat->beenhere) {
roomstat->ReadFromSavegame_v321(in, data_ver);
if (roomstat->tsdatasize > 0) {
roomstat->tsdata.resize(roomstat->tsdatasize);
in->Read(roomstat->tsdata.data(), roomstat->tsdatasize);
}
}
}
}
}
static void restore_game_play(Stream *in, GameDataVersion data_ver, RestoredData &r_data) {
int screenfadedout_was = _GP(play).screen_is_faded_out;
int roomchanges_was = _GP(play).room_changes;
_GP(play).ReadFromSavegame(in, data_ver, kGSSvgVersion_OldFormat, r_data);
r_data.Cameras[0].Flags = r_data.Camera0_Flags;
_GP(play).screen_is_faded_out = screenfadedout_was;
_GP(play).room_changes = roomchanges_was;
char rbuffer[200]; // old doonceonly token length
for (size_t i = 0; i < r_data.DoOnceCount; ++i) {
StrUtil::ReadCStr(rbuffer, in, sizeof(rbuffer));
_GP(play).do_once_tokens.insert(rbuffer);
}
// Skip gui_draw_order (no longer applied from saves)
in->Seek(_GP(game).numgui * sizeof(int32_t));
}
static void restore_game_palette(Stream *in) {
in->ReadArray(&_G(palette)[0], sizeof(RGB), 256);
}
static void restore_game_dialogs(Stream *in) {
for (int vv = 0; vv < _GP(game).numdialog; vv++)
in->ReadArrayOfInt32(&_G(dialog)[vv].optionflags[0], MAXTOPICOPTIONS);
}
static void restore_game_more_dynamic_values(Stream *in) {
_G(mouse_on_iface) = in->ReadInt32();
in->ReadInt32(); // mouse_on_iface_button
in->ReadInt32(); // mouse_pushed_iface
_G(ifacepopped) = in->ReadInt32();
_G(game_paused) = in->ReadInt32();
}
static HSaveError restore_game_gui(Stream *in) {
// Legacy saves allowed to resize gui lists, and stored full gui data
// (could be unintentional side effect). Here we emulate this for
// upgraded games by letting read **less** data from saves, and copying
// missing elements from reserved game data.
const std::vector<GUIMain> res_guis = std::move(_GP(guis));
const std::vector<GUIButton> res_guibuts = std::move(_GP(guibuts));
const std::vector<GUIInvWindow> res_guiinv = std::move(_GP(guiinv));
const std::vector<GUILabel> res_guilabels = std::move(_GP(guilabels));
const std::vector<GUIListBox> res_guilist = std::move(_GP(guilist));
const std::vector<GUISlider> res_guislider = std::move(_GP(guislider));
const std::vector<GUITextBox> res_guitext = std::move(_GP(guitext));
HError guierr = GUI::ReadGUI(in, true);
if (!guierr)
return new SavegameError(kSvgErr_GameObjectInitFailed, guierr);
HSaveError err;
const bool warn_only = _GP(usetup).legacysave_let_gui_diff;
if (!AssertAndCopyGameContent(res_guis, _GP(guis), err, "GUIs", warn_only) ||
!AssertAndCopyGameContent(res_guibuts, _GP(guibuts), err, "GUI Buttons", warn_only) ||
!AssertAndCopyGameContent(res_guiinv, _GP(guiinv), err, "GUI InvWindows", warn_only) ||
!AssertAndCopyGameContent(res_guilabels, _GP(guilabels), err, "GUI Labels", warn_only) ||
!AssertAndCopyGameContent(res_guilist, _GP(guilist), err, "GUI ListBoxes", warn_only) ||
!AssertAndCopyGameContent(res_guislider, _GP(guislider), err, "GUI Sliders", warn_only) ||
!AssertAndCopyGameContent(res_guitext, _GP(guitext), err, "GUI TextBoxes", warn_only))
return err;
GUI::RebuildGUI(); // rebuild guis in case they were copied from reserved game data
_GP(game).numgui = _GP(guis).size();
int anim_count = in->ReadInt32();
for (int i = 0; i < anim_count; ++i) {
AnimatingGUIButton abtn;
abtn.ReadFromSavegame(in, 0);
AddButtonAnimation(abtn);
}
return HSaveError::None();
}
static HSaveError restore_game_audiocliptypes(Stream *in) {
HSaveError err;
if (!AssertGameContent(err, _GP(game).audioClipTypes.size(), (uint32_t)in->ReadInt32(), "Audio Clip Types"))
return err;
for (size_t i = 0; i < _GP(game).audioClipTypes.size(); ++i) {
_GP(game).audioClipTypes[i].ReadFromFile(in);
}
return HSaveError::None();
}
static void restore_game_thisroom(Stream *in, RestoredData &r_data) {
in->ReadArrayOfInt16(r_data.RoomLightLevels, MAX_ROOM_REGIONS);
in->ReadArrayOfInt32(r_data.RoomTintLevels, MAX_ROOM_REGIONS);
in->ReadArrayOfInt16(r_data.RoomZoomLevels1, MAX_WALK_AREAS);
in->ReadArrayOfInt16(r_data.RoomZoomLevels2, MAX_WALK_AREAS);
}
static void restore_game_ambientsounds(Stream *in, RestoredData &r_data) {
for (int i = 0; i < MAX_GAME_CHANNELS_v320; ++i) {
_GP(ambient)[i].ReadFromFile(in);
}
for (int bb = NUM_SPEECH_CHANS; bb < MAX_GAME_CHANNELS_v320; bb++) {
if (_GP(ambient)[bb].channel == 0)
r_data.DoAmbient[bb] = 0;
else {
r_data.DoAmbient[bb] = _GP(ambient)[bb].num;
_GP(ambient)[bb].channel = 0;
}
}
}
static void ReadOverlays_Aligned(Stream *in, std::vector<int> &has_bitmap, size_t num_overs) {
// Remember that overlay indexes may be non-sequential
auto &overs = get_overlays();
for (size_t i = 0; i < num_overs; ++i) {
bool has_bm;
ScreenOverlay over;
over.ReadFromSavegame(in, has_bm, -1);
if (over.type < 0)
continue; // safety abort
if (overs.size() <= static_cast<uint32_t>(over.type))
overs.resize(over.type + 1);
overs[over.type] = std::move(over);
if (has_bm)
has_bitmap.push_back(over.type);
}
}
static void restore_game_overlays(Stream *in, RestoredData &r_data) {
size_t num_overs = in->ReadInt32();
// Remember that overlay indexes may be not sequential,
// the vector may be resized during read
auto &overs = get_overlays();
overs.resize(num_overs);
std::vector<int> has_bitmap;
ReadOverlays_Aligned(in, has_bitmap, num_overs);
for (auto over_id : has_bitmap) {
r_data.OverlayImages[over_id].reset(read_serialized_bitmap(in));
}
}
static void restore_game_dynamic_surfaces(Stream *in, RestoredData &r_data) {
// load into a temp array since ccUnserialiseObjects will destroy
// it 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));
}
}
}
static void restore_game_displayed_room_status(Stream *in, GameDataVersion data_ver, RestoredData &r_data) {
int bb;
for (bb = 0; bb < MAX_ROOM_BGFRAMES; bb++)
r_data.RoomBkgScene[bb].reset();
if (_G(displayed_room) >= 0) {
for (bb = 0; bb < MAX_ROOM_BGFRAMES; bb++) {
r_data.RoomBkgScene[bb] = nullptr;
if (_GP(play).raw_modified[bb]) {
r_data.RoomBkgScene[bb].reset(read_serialized_bitmap(in));
}
}
bb = in->ReadInt32();
if (bb)
_G(raw_saved_screen).reset(read_serialized_bitmap(in));
// get the current troom, in case they save in room 600 or whatever
_GP(troom).ReadFromSavegame_v321(in, data_ver);
if (_GP(troom).tsdatasize > 0) {
_GP(troom).tsdata.resize(_GP(troom).tsdatasize);
in->Read(_GP(troom).tsdata.data(), _GP(troom).tsdatasize);
} else
_GP(troom).tsdata.clear();
}
}
static HSaveError restore_game_globalvars(Stream *in) {
HSaveError err;
if (!AssertGameContent(err, _G(numGlobalVars), in->ReadInt32(), "Global Variables"))
return err;
for (int i = 0; i < _G(numGlobalVars); ++i) {
_G(globalvars)[i].Read(in);
}
return HSaveError::None();
}
static HSaveError restore_game_views(Stream *in) {
HSaveError err;
if (!AssertGameContent(err, _GP(game).numviews, in->ReadInt32(), "Views"))
return err;
for (int bb = 0; bb < _GP(game).numviews; bb++) {
for (int cc = 0; cc < _GP(views)[bb].numLoops; cc++) {
for (int dd = 0; dd < _GP(views)[bb].loops[cc].numFrames; dd++) {
_GP(views)[bb].loops[cc].frames[dd].sound = in->ReadInt32();
_GP(views)[bb].loops[cc].frames[dd].pic = in->ReadInt32();
}
}
}
return HSaveError::None();
}
static HSaveError restore_game_audio_and_crossfade(Stream *in, GameDataVersion data_ver, RestoredData &r_data) {
in->ReadInt32(); // audio clips count, ignore
for (int i = 0; i < TOTAL_AUDIO_CHANNELS_v320; ++i) {
RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
chan_info.Pos = 0;
chan_info.ClipID = in->ReadInt32();
if (chan_info.ClipID >= 0) {
if ((size_t)chan_info.ClipID >= _GP(game).audioClips.size()) {
return new SavegameError(kSvgErr_GameObjectInitFailed,
String::FromFormat("Invalid audio clip index %zu (valid range is 0..%zu)",
(size_t)chan_info.ClipID, _GP(game).audioClips.size() - 1));
}
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(); // unused
chan_info.VolAsPercent = in->ReadInt32();
chan_info.Pan = in->ReadInt32();
chan_info.Speed = 1000;
if (data_ver >= kGameVersion_340_2)
chan_info.Speed = in->ReadInt32();
}
}
_G(crossFading) = in->ReadInt32();
_G(crossFadeVolumePerStep) = in->ReadInt32();
_G(crossFadeStep) = in->ReadInt32();
_G(crossFadeVolumeAtStart) = in->ReadInt32();
return HSaveError::None();
}
HSaveError restore_save_data_v321(Stream *in, GameDataVersion data_ver, const PreservedParams &pp, RestoredData &r_data) {
HSaveError err = restore_game_head_dynamic_values(in, r_data);
if (!err)
return err;
restore_game_spriteset(in);
err = restore_game_scripts(in, pp, r_data);
if (!err)
return err;
restore_game_room_state(in, data_ver);
restore_game_play(in, data_ver, r_data);
// Global character movelists
for (int i = 0; i < _GP(game).numcharacters + MAX_ROOM_OBJECTS_v300 + 1; ++i) {
_GP(mls)[i].ReadFromSavegame_Legacy(in);
}
// List of game objects, used to compare with the save contents
struct ObjectCounts {
int CharacterCount = _GP(game).numcharacters;
int DialogCount = _GP(game).numdialog;
int InvItemCount = _GP(game).numinvitems;
int ViewCount = _GP(game).numviews;
} objwas;
GameSetupStruct::SerializeInfo info;
_GP(game).GameSetupStructBase::ReadFromFile(in, data_ver, info);
if (!AssertGameContent(err, objwas.CharacterCount, _GP(game).numcharacters, "Characters") ||
!AssertGameContent(err, objwas.DialogCount, _GP(game).numdialog, "Dialogs") ||
!AssertGameContent(err, objwas.InvItemCount, _GP(game).numinvitems, "Inventory Items") ||
!AssertGameContent(err, objwas.ViewCount, _GP(game).numviews, "Views"))
return err;
_GP(game).ReadFromSaveGame_v321(in);
// Modified custom properties are read separately to keep existing save format
_GP(play).ReadCustomProperties_v340(in, data_ver);
// Character extras (runtime only data)
for (int i = 0; i < _GP(game).numcharacters; ++i) {
_GP(charextra)[i].ReadFromSavegame(in, kCharSvgVersion_Initial);
}
restore_game_palette(in);
restore_game_dialogs(in);
restore_game_more_dynamic_values(in);
err = restore_game_gui(in);
if (!err)
return err;
err = restore_game_audiocliptypes(in);
if (!err)
return err;
restore_game_thisroom(in, r_data);
restore_game_ambientsounds(in, r_data);
restore_game_overlays(in, r_data);
restore_game_dynamic_surfaces(in, r_data);
restore_game_displayed_room_status(in, data_ver, r_data);
err = restore_game_globalvars(in);
if (!err)
return err;
err = restore_game_views(in);
if (!err)
return err;
if (static_cast<uint32_t>(in->ReadInt32()) != (MAGICNUMBER + 1)) {
return new SavegameError(kSvgErr_InconsistentFormat, "Audio section header expected but not found.");
}
err = restore_game_audio_and_crossfade(in, data_ver, r_data);
if (!err)
return err;
ReadPluginSaveData(in, kPluginSvgVersion_Initial, SIZE_MAX);
if (static_cast<uint32_t>(in->ReadInt32()) != MAGICNUMBER)
return new SavegameError(kSvgErr_InconsistentPlugin);
// save the new room music vol for later use
r_data.RoomVolume = (RoomVolumeMod)in->ReadInt32();
if (ccUnserializeAllObjects(in, &_GP(ccUnserializer))) {
return new SavegameError(kSvgErr_GameObjectInitFailed,
String::FromFormat("Managed pool deserialization failed: %s.", cc_get_error().ErrorString.GetCStr()));
}
// preserve legacy music type setting
_G(current_music_type) = in->ReadInt32();
return HSaveError::None();
}
} // namespace AGS3

View File

@@ -0,0 +1,221 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/shared/game/room_struct.h"
#include "ags/engine/game/viewport.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace AGS::Shared;
void Camera::SetID(int id) {
_id = id;
}
// Returns Room camera position and size inside the room (in room coordinates)
const Rect &Camera::GetRect() const {
return _position;
}
// Sets explicit room camera's orthographic size
void Camera::SetSize(const Size cam_size) {
// TODO: currently we don't support having camera larger than room background
// (or rather - looking outside of the room background); look into this later
const Size real_room_sz = (_G(displayed_room) >= 0 && (_GP(thisroom).Width > 0 && _GP(thisroom).Height > 0)) ?
Size(data_to_game_coord(_GP(thisroom).Width), data_to_game_coord(_GP(thisroom).Height)) :
Size(INT32_MAX, INT32_MAX);
const Size real_size = Size::Clamp(cam_size, Size(1, 1), real_room_sz);
if (_position.GetWidth() == real_size.Width && _position.GetHeight() == real_size.Height)
return;
_position.SetWidth(real_size.Width);
_position.SetHeight(real_size.Height);
SetAt(_position.Left, _position.Top); // readjust in case went off-room after size changed
for (auto vp = _viewportRefs.begin(); vp != _viewportRefs.end(); ++vp) {
auto locked_vp = vp->lock();
if (locked_vp)
locked_vp->AdjustTransformation();
}
_hasChangedSize = true;
}
// Puts room camera to the new location in the room
void Camera::SetAt(int x, int y) {
int cw = _position.GetWidth();
int ch = _position.GetHeight();
int room_width = data_to_game_coord(_GP(thisroom).Width);
int room_height = data_to_game_coord(_GP(thisroom).Height);
x = Math::Clamp(x, 0, room_width - cw);
y = Math::Clamp(y, 0, room_height - ch);
if (_position.Left == x && _position.Top == y)
return;
_position.MoveTo(Point(x, y));
_hasChangedPosition = true;
}
// Tells if camera is currently locked at custom position
bool Camera::IsLocked() const {
return _locked;
}
// Locks room camera at its current position
void Camera::Lock() {
debug_script_log("Room camera locked");
_locked = true;
}
// Similar to SetAt, but also locks camera preventing it from following player character
void Camera::LockAt(int x, int y) {
debug_script_log("Room camera locked to %d,%d", x, y);
SetAt(x, y);
_locked = true;
}
// Releases camera lock, letting it follow player character
void Camera::Release() {
_locked = false;
debug_script_log("Room camera released back to engine control");
}
// Link this camera to a new viewport; this does not unlink any linked ones
void Camera::LinkToViewport(ViewportRef viewport) {
auto new_locked = viewport.lock();
if (!new_locked)
return;
for (auto vp = _viewportRefs.begin(); vp != _viewportRefs.end(); ++vp) {
auto old_locked = vp->lock();
if (old_locked->GetID() == new_locked->GetID())
return;
}
_viewportRefs.push_back(viewport);
}
// Unlinks this camera from a given viewport; does nothing if link did not exist
void Camera::UnlinkFromViewport(int id) {
for (auto vp = _viewportRefs.begin(); vp != _viewportRefs.end(); ++vp) {
auto locked = vp->lock();
if (locked && locked->GetID() == id) {
_viewportRefs.erase(vp);
return;
}
}
}
const std::vector<ViewportRef> &Camera::GetLinkedViewports() const {
return _viewportRefs;
}
void Viewport::SetID(int id) {
_id = id;
}
void Viewport::SetRect(const Rect &rc) {
// TODO: consider allowing size 0,0, in which case viewport is considered not visible
Size fix_size = rc.GetSize().IsNull() ? Size(1, 1) : rc.GetSize();
Rect new_pos = RectWH(rc.Left, rc.Top, fix_size.Width, fix_size.Height);
if (new_pos == _position)
return;
_position = new_pos;
AdjustTransformation();
_hasChangedPosition = true;
_hasChangedSize = true;
}
void Viewport::SetSize(const Size sz) {
// TODO: consider allowing size 0,0, in which case viewport is considered not visible
Size fix_size = sz.IsNull() ? Size(1, 1) : sz;
if (_position.GetWidth() == fix_size.Width && _position.GetHeight() == fix_size.Height)
return;
_position = RectWH(_position.Left, _position.Top, fix_size.Width, fix_size.Height);
AdjustTransformation();
_hasChangedSize = true;
}
void Viewport::SetAt(int x, int y) {
if (_position.Left == x && _position.Top == y)
return;
_position.MoveTo(Point(x, y));
AdjustTransformation();
_hasChangedPosition = true;
}
void Viewport::SetVisible(bool on) {
_visible = on;
_hasChangedVisible = true;
}
void Viewport::SetZOrder(int zorder) {
_zorder = zorder;
_hasChangedVisible = true;
}
void Viewport::AdjustTransformation() {
auto locked_cam = _camera.lock();
if (locked_cam)
_transform.Init(locked_cam->GetRect().GetSize(), _position);
}
PCamera Viewport::GetCamera() const {
return _camera.lock();
}
void Viewport::LinkCamera(PCamera cam) {
_camera = cam;
AdjustTransformation();
}
VpPoint Viewport::RoomToScreen(int roomx, int roomy, bool clip) const {
auto cam = _camera.lock();
if (!cam)
return std::make_pair(Point(), -1);
const Rect &camr = cam->GetRect();
Point screen_pt = _transform.Scale(Point(roomx - camr.Left, roomy - camr.Top));
if (clip && !_position.IsInside(screen_pt))
return std::make_pair(Point(), -1);
return std::make_pair(screen_pt, _id);
}
VpPoint Viewport::ScreenToRoom(int scrx, int scry, bool clip, bool convert_cam_to_data) const {
Point screen_pt(scrx, scry);
if (clip && !_position.IsInside(screen_pt))
return std::make_pair(Point(), -1);
auto cam = _camera.lock();
if (!cam)
return std::make_pair(Point(), -1);
const Rect &camr = cam->GetRect();
Point p = _transform.UnScale(screen_pt);
if (convert_cam_to_data) {
p.X += game_to_data_coord(camr.Left);
p.Y += game_to_data_coord(camr.Top);
} else {
p.X += camr.Left;
p.Y += camr.Top;
}
return std::make_pair(p, _id);
}
} // namespace AGS3

View File

@@ -0,0 +1,226 @@
/* 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 <http://www.gnu.org/licenses/>.
*
*/
//=============================================================================
//
// Definition for the game viewports and cameras.
//
//=============================================================================
#ifndef AGS_ENGINE_GAME_VIEWPORT_H
#define AGS_ENGINE_GAME_VIEWPORT_H
#include "common/std/memory.h"
#include "common/std/vector.h"
#include "ags/shared/util/geometry.h"
#include "ags/shared/util/scaling.h"
namespace AGS3 {
class Camera;
class Viewport;
typedef std::shared_ptr<Camera> PCamera;
typedef std::shared_ptr<Viewport> PViewport;
typedef std::weak_ptr<Camera> CameraRef;
typedef std::weak_ptr<Viewport> ViewportRef;
// TODO: move to utility header
// From https://stackoverflow.com/questions/45507041/how-to-check-if-weak-ptr-is-empty-non-assigned
// Tests that weak_ptr is empty (was not initialized with valid reference)
template <typename T>
bool is_uninitialized(std::weak_ptr<T> const &weak) {
using wt = std::weak_ptr<T>;
return !weak.owner_before(wt{}) &&!wt{} .owner_before(weak);
}
// Camera defines a "looking eye" inside the room, its position and size.
// It does not render anywhere on its own, instead it is linked to a viewport
// and latter draws what that camera sees.
// One camera may be linked to multiple viewports.
// Camera does not move on its own, this is performed by separate behavior
// algorithm. But it provides "lock" property that tells if its position is
// currently owned by user script.
class Camera {
public:
// Gets camera ID (serves as an index)
inline int GetID() const {
return _id;
}
// Sets new camera ID
void SetID(int id);
// Returns Room camera position and size inside the room (in room coordinates)
const Rect &GetRect() const;
// Sets explicit room camera's orthographic size
void SetSize(const Size sz);
// Puts room camera to the new location in the room
void SetAt(int x, int y);
// Tells if camera is currently locked at custom position
bool IsLocked() const;
// Locks room camera at its current position
void Lock();
// Similar to SetAt, but also locks camera preventing it from following player character
void LockAt(int x, int y);
// Releases camera lock, letting it follow player character
void Release();
// Link this camera to a new viewport; this does not unlink any linked ones
void LinkToViewport(ViewportRef viewport);
// Unlinks this camera from a given viewport; does nothing if link did not exist
void UnlinkFromViewport(int id);
// Get the array of linked viewport references
const std::vector<ViewportRef> &GetLinkedViewports() const;
// Tell if this camera has changed recently
inline bool HasChangedPosition() const {
return _hasChangedPosition;
}
inline bool HasChangedSize() const {
return _hasChangedSize;
}
// Clears the changed flags
void ClearChangedFlags() {
_hasChangedPosition = false;
_hasChangedSize = false;
}
private:
int _id = -1;
// Actual position and orthographic size
Rect _position;
// Locked or following player automatically
bool _locked = false;
// Linked viewport refs, used to notify viewports of camera changes
std::vector<ViewportRef> _viewportRefs;
// Flags that tell whether this camera's position on screen has changed recently
bool _hasChangedPosition = false;
bool _hasChangedSize = false;
};
// A result of coordinate conversion between screen and the room,
// tells which viewport was used to pass the "touch" through.
typedef std::pair<Point, int> VpPoint;
// Viewport class defines a rectangular area on game screen where the contents
// of a Camera are rendered.
// Viewport may have one linked camera at a time.
class Viewport {
public:
// Gets viewport ID (serves as an index)
inline int GetID() const {
return _id;
}
// Sets new viewport ID
void SetID(int id);
// Returns viewport's position on screen
inline const Rect &GetRect() const {
return _position;
}
// Returns viewport's room-to-screen transformation
inline const AGS::Shared::PlaneScaling &GetTransform() const {
return _transform;
}
// Set viewport's rectangle on screen
void SetRect(const Rect &rc);
// Sets viewport size
void SetSize(const Size sz);
// Sets viewport's position on screen
void SetAt(int x, int y);
// Tells whether viewport content is rendered on screen
bool IsVisible() const {
return _visible;
}
// Changes viewport visibility
void SetVisible(bool on);
// Gets the order viewport is displayed on screen
int GetZOrder() const {
return _zorder;
}
// Sets the viewport's z-order on screen
void SetZOrder(int zorder);
// Calculates room-to-viewport coordinate conversion.
void AdjustTransformation();
// Returns linked camera
PCamera GetCamera() const;
// Links new camera to this viewport, overriding existing link;
// pass nullptr to leave viewport without a camera link
void LinkCamera(PCamera cam);
// TODO: provide a Transform object here that does these conversions instead
// Converts room coordinates to the game screen coordinates through this viewport;
// if clipping is on, the function will fail for room coordinates outside of camera
VpPoint RoomToScreen(int roomx, int roomy, bool clip = false) const;
// Converts game screen coordinates to the room coordinates through this viewport;
// if clipping is on, the function will fail for screen coordinates outside of viewport;
// convert_cam_to_data parameter converts camera "game" coordinates to "data" units (legacy mode)
VpPoint ScreenToRoom(int scrx, int scry, bool clip = false, bool convert_cam_to_data = false) const;
// Following functions tell if this viewport has changed recently
inline bool HasChangedPosition() const {
return _hasChangedPosition;
}
inline bool HasChangedSize() const {
return _hasChangedSize;
}
inline bool HasChangedVisible() const {
return _hasChangedVisible;
}
inline void SetChangedVisible() {
_hasChangedVisible = true;
}
// Clears the changed flags
inline void ClearChangedFlags() {
_hasChangedPosition = false;
_hasChangedSize = false;
_hasChangedVisible = false;
}
private:
// Parameterized implementation of screen-to-room coordinate conversion
VpPoint ScreenToRoomImpl(int scrx, int scry, bool clip, bool convert_cam_to_data);
int _id = -1;
// Position of the viewport on screen
Rect _position;
// TODO: Camera reference (when supporting multiple cameras)
// Coordinate transform between camera and viewport
// TODO: need to add rotate conversion to let script API support that;
// (maybe use full 3D matrix for that)
AGS::Shared::PlaneScaling _transform;
// Linked camera reference
CameraRef _camera;
bool _visible = true;
int _zorder = 0;
// Flags that tell whether this viewport's position on screen has changed recently
bool _hasChangedPosition = false;
bool _hasChangedOffscreen = false;
bool _hasChangedSize = false;
bool _hasChangedVisible = false;
};
} // namespace AGS3
#endif // AGS_ENGINE_AC_VIEWPORT_H