Files
scummvm-cursorfix/engines/ags/engine/game/game_init.cpp
2026-02-02 04:50:13 +01:00

524 lines
19 KiB
C++

/* 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