/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "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 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