/* 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 . * */ // // Engine initialization // #include "ags/shared/core/platform.h" #include "ags/lib/allegro.h" // allegro_install and _exit #include "ags/engine/ac/asset_helper.h" #include "ags/shared/ac/common.h" #include "ags/engine/ac/character.h" #include "ags/engine/ac/character_extras.h" #include "ags/shared/ac/character_info.h" #include "ags/engine/ac/draw.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/global_character.h" #include "ags/engine/ac/global_game.h" #include "ags/engine/ac/gui.h" #include "ags/engine/ac/lip_sync.h" #include "ags/engine/ac/path_helper.h" #include "ags/engine/ac/route_finder.h" #include "ags/engine/ac/sys_events.h" #include "ags/engine/ac/room_status.h" #include "ags/engine/ac/speech.h" #include "ags/shared/ac/sprite_cache.h" #include "ags/engine/ac/translation.h" #include "ags/engine/ac/view_frame.h" #include "ags/engine/ac/dynobj/script_object.h" #include "ags/engine/ac/dynobj/script_system.h" #include "ags/shared/core/asset_manager.h" #include "ags/engine/debugging/debug_log.h" #include "ags/engine/debugging/debugger.h" #include "ags/shared/debugging/out.h" #include "ags/engine/device/mouse_w32.h" #include "ags/shared/font/ags_font_renderer.h" #include "ags/shared/font/fonts.h" #include "ags/shared/gfx/image.h" #include "ags/engine/gfx/graphics_driver.h" #include "ags/engine/gfx/gfx_driver_factory.h" #include "ags/engine/gfx/ddb.h" #include "ags/engine/main/config.h" #include "ags/engine/main/game_file.h" #include "ags/engine/main/game_start.h" #include "ags/engine/main/engine.h" #include "ags/engine/main/engine_setup.h" #include "ags/engine/main/graphics_mode.h" #include "ags/engine/main/main.h" #include "ags/engine/platform/base/sys_main.h" #include "ags/engine/platform/base/ags_platform_driver.h" #include "ags/shared/util/directory.h" #include "ags/shared/util/error.h" #include "ags/shared/util/path.h" #include "ags/shared/util/string_utils.h" #include "ags/ags.h" #include "ags/globals.h" namespace AGS3 { using namespace AGS::Shared; using namespace AGS::Engine; bool engine_init_backend() { set_our_eip(-199); _G(platform)->PreBackendInit(); // Initialize SDL Debug::Printf(kDbgMsg_Info, "Initializing backend libs"); if (sys_main_init()) { const char *user_hint = _G(platform)->GetBackendFailUserHint(); _G(platform)->DisplayAlert("Unable to initialize SDL library.\n\n%s", user_hint); return false; } // Initialize stripped allegro library if (install_allegro()) { _G(platform)->DisplayAlert("Internal error: unable to initialize stripped Allegro 4 library."); return false; } _G(platform)->PostBackendInit(); return true; } void winclosehook() { _G(want_exit) = true; _G(abort_engine) = true; _G(check_dynamic_sprites_at_exit) = 0; AbortGame(); } void engine_setup_window() { Debug::Printf(kDbgMsg_Info, "Setting up window"); set_our_eip(-198); sys_window_set_title(_GP(game).gamename.GetCStr()); sys_window_set_icon(); sys_evt_set_quit_callback(winclosehook); set_our_eip(-197); } // Fills map with game settings, to e.g. let setup application(s) // display correct properties to the user static void fill_game_properties(StringOrderMap &map) { map["title"] = _GP(game).gamename; map["guid"] = _GP(game).guid; map["legacy_uniqueid"] = StrUtil::IntToString(_GP(game).uniqueid); map["legacy_resolution"] = StrUtil::IntToString(_GP(game).GetResolutionType()); map["legacy_letterbox"] = StrUtil::IntToString(_GP(game).options[OPT_LETTERBOX]); map["resolution_width"] = StrUtil::IntToString(_GP(game).GetDefaultRes().Width); map["resolution_height"] = StrUtil::IntToString(_GP(game).GetDefaultRes().Height); map["resolution_bpp"] = StrUtil::IntToString(_GP(game).GetColorDepth()); map["render_at_screenres"] = StrUtil::IntToString( _GP(game).options[OPT_RENDERATSCREENRES] == kRenderAtScreenRes_UserDefined ? -1 : (_GP(game).options[OPT_RENDERATSCREENRES] == kRenderAtScreenRes_Enabled ? 1 : 0)); } // Starts up setup application, if capable. // Returns TRUE if should continue running the game, otherwise FALSE. bool engine_run_setup(const ConfigTree &cfg) { #if AGS_PLATFORM_OS_WINDOWS { Debug::Printf(kDbgMsg_Info, "Running Setup"); ConfigTree cfg_with_meta = cfg; fill_game_properties(cfg_with_meta["gameproperties"]); ConfigTree cfg_out; SetupReturnValue res = _G(platform)->RunSetup(cfg_with_meta, cfg_out); if (res != kSetup_Cancel) { String cfg_file = PreparePathForWriting(GetGameUserConfigDir(), DefaultConfigFileName); if (cfg_file.IsEmpty()) { _G(platform)->DisplayAlert("Unable to write into directory '%s'.\n%s", GetGameUserConfigDir().FullDir.GetCStr(), _G(platform)->GetDiskWriteAccessTroubleshootingText()); } else if (!IniUtil::Merge(cfg_file, cfg_out)) { _G(platform)->DisplayAlert("Unable to write to the configuration file (error code 0x%08X).\n%s", _G(platform)->GetLastSystemError(), _G(platform)->GetDiskWriteAccessTroubleshootingText()); } } if (res != kSetup_RunGame) return false; // Start the game in the new process, and close the current one afterwards String args = String::FromFormat("\"%s\"", appPath.GetCStr()); _spawnl(_P_NOWAIT, appPath.GetCStr(), args.GetCStr(), NULL); return false; } #endif return true; } // Scans given directory for the AGS game config. If such config exists // and it contains data file name, then returns one. // Otherwise returns empty string. static String find_game_data_in_config(const String &path) { // First look for config ConfigTree cfg; String def_cfg_file = Path::ConcatPaths(path, DefaultConfigFileName); if (IniUtil::Read(def_cfg_file, cfg)) { String data_file = CfgReadString(cfg, "misc", "datafile"); Debug::Printf("Found game config: %s", def_cfg_file.GetCStr()); Debug::Printf(" Cfg: data file: %s", data_file.GetCStr()); // Only accept if it's a relative path if (!data_file.IsEmpty() && Path::IsRelativePath(data_file)) return Path::ConcatPaths(path, data_file); } return ""; // not found in config } // Scans for game data in several common locations. // When it does so, it first looks for game config file, which contains // explicit directions to game data in its settings. // If such config is not found, it scans same location for *any* game data instead. String search_for_game_data_file(String &was_searching_in) { Debug::Printf("Looking for the game data.\n Cwd: %s\n Path arg: %s", Directory::GetCurrentDirectory().GetCStr(), _G(cmdGameDataPath).GetCStr()); // 1. From command line argument, which may be a directory or actual file if (!_G(cmdGameDataPath).IsEmpty()) { if (File::IsFile(_G(cmdGameDataPath))) return _G(cmdGameDataPath); // this path is a file if (!File::IsDirectory(_G(cmdGameDataPath))) return ""; // path is neither file nor directory was_searching_in = _G(cmdGameDataPath); Debug::Printf("Searching in (cmd arg): %s", was_searching_in.GetCStr()); // first scan for config String data_path = find_game_data_in_config(_G(cmdGameDataPath)); if (!data_path.IsEmpty()) return data_path; // if not found in config, lookup for data in same dir return FindGameData(_G(cmdGameDataPath)); } // 2. Look in other known locations // 2.1. Look for attachment in the running executable if (!_G(appPath).IsEmpty() && Shared::AssetManager::IsDataFile(_G(appPath))) { Debug::Printf("Found game data embedded in executable"); was_searching_in = Path::GetDirectoryPath(_G(appPath)); return _G(appPath); } // 2.2 Look in current working directory String cur_dir = Directory::GetCurrentDirectory(); was_searching_in = cur_dir; Debug::Printf("Searching in (cwd): %s", was_searching_in.GetCStr()); // first scan for config String data_path = find_game_data_in_config(cur_dir); if (!data_path.IsEmpty()) return data_path; // if not found in config, lookup for data in same dir data_path = FindGameData(cur_dir); if (!data_path.IsEmpty()) return data_path; // 2.3 Look in executable's directory (if it's different from current dir) if (Path::ComparePaths(_G(appDirectory), cur_dir) == 0) return ""; // no luck was_searching_in = _G(appDirectory); Debug::Printf("Searching in (exe dir): %s", was_searching_in.GetCStr()); // first scan for config data_path = find_game_data_in_config(_G(appDirectory)); if (!data_path.IsEmpty()) return data_path; // if not found in config, lookup for data in same dir return FindGameData(_G(appDirectory)); } void engine_init_fonts() { Debug::Printf(kDbgMsg_Info, "Initializing TTF renderer"); init_font_renderer(); } void engine_init_mouse() { int res = _GP(mouse).GetButtonCount(); if (res < 0) Debug::Printf(kDbgMsg_Info, "Initializing mouse: failed"); else Debug::Printf(kDbgMsg_Info, "Initializing mouse: number of buttons reported is %d", res); _GP(mouse).SetSpeed(_GP(usetup).mouse_speed); } void engine_locate_speech_pak() { init_voicepak(""); } void engine_locate_audio_pak() { String music_file = _GP(game).GetAudioVOXName(); String music_filepath = find_assetlib(music_file); if (!music_filepath.IsEmpty()) { if (_GP(AssetMgr)->AddLibrary(music_filepath) == kAssetNoError) { Debug::Printf(kDbgMsg_Info, "%s found and initialized.", music_file.GetCStr()); _GP(ResPaths).AudioPak.Name = music_file; _GP(ResPaths).AudioPak.Path = music_filepath; } else { _G(platform)->DisplayAlert("Unable to initialize digital audio pack '%s', file could be corrupt or of unsupported format.", music_file.GetCStr()); } } else if (!_GP(ResPaths).AudioDir2.IsEmpty() && Path::ComparePaths(_GP(ResPaths).DataDir, _GP(ResPaths).AudioDir2) != 0) { Debug::Printf(kDbgMsg_Info, "Audio pack was not found, but explicit audio directory is defined."); } } // Assign asset locations to the AssetManager void engine_assign_assetpaths() { _GP(AssetMgr)->AddLibrary(_GP(ResPaths).GamePak.Path, ",audio"); // main pack may have audio bundled too // The asset filters are currently a workaround for limiting search to certain locations; // this is both an optimization and to prevent unexpected behavior. // - empty filter is for regular files // audio - audio clips // voice - voice-over clips // NOTE: we add extra optional directories first because they should have higher priority // TODO: maybe change AssetManager library order to stack-like later (last added = top priority)? if (!_GP(ResPaths).DataDir2.IsEmpty() && Path::ComparePaths(_GP(ResPaths).DataDir2, _GP(ResPaths).DataDir) != 0) _GP(AssetMgr)->AddLibrary(_GP(ResPaths).DataDir2, ",audio,voice"); // dir may have anything if (!_GP(ResPaths).AudioDir2.IsEmpty() && Path::ComparePaths(_GP(ResPaths).AudioDir2, _GP(ResPaths).DataDir) != 0) _GP(AssetMgr)->AddLibrary(_GP(ResPaths).AudioDir2, "audio"); if (!_GP(ResPaths).VoiceDir2.IsEmpty() && Path::ComparePaths(_GP(ResPaths).VoiceDir2, _GP(ResPaths).DataDir) != 0) _GP(AssetMgr)->AddLibrary(_GP(ResPaths).VoiceDir2, "voice"); _GP(AssetMgr)->AddLibrary(_GP(ResPaths).DataDir, ",audio,voice"); // dir may have anything if (!_GP(ResPaths).AudioPak.Path.IsEmpty()) _GP(AssetMgr)->AddLibrary(_GP(ResPaths).AudioPak.Path, "audio"); if (!_GP(ResPaths).SpeechPak.Path.IsEmpty()) _GP(AssetMgr)->AddLibrary(_GP(ResPaths).SpeechPak.Path, "voice"); } void engine_init_keyboard() { /* do nothing */ } void engine_init_audio() { #if !AGS_PLATFORM_SCUMMVM if (usetup.audio_backend != 0) { Debug::Printf("Initializing audio"); try { audio_core_init(); // audio core system } catch (std::runtime_error ex) { Debug::Printf(kDbgMsg_Error, "Failed to initialize audio: %s", ex.what()); usetup.audio_backend = 0; } } #endif if (!_GP(usetup).audio_enabled) { // all audio is disabled Debug::Printf(kDbgMsg_Info, "Audio is disabled"); } } void engine_init_debug() { if (_GP(usetup).show_fps) _G(display_fps) = kFPS_Forced; if ((_G(debug_flags) & (~DBG_DEBUGMODE)) > 0) { _G(platform)->DisplayAlert("Engine debugging enabled.\n" "\nNOTE: You have selected to enable one or more engine debugging options.\n" "These options cause many parts of the game to behave abnormally, and you\n" "may not see the game as you are used to it. The point is to test whether\n" "the engine passes a point where it is crashing on you normally.\n" "[Debug flags enabled: 0x%02X]", _G(debug_flags)); } } void engine_init_pathfinder() { init_pathfinder(_G(loaded_game_file_version)); } void engine_pre_init_gfx() { //Debug::Printf("Initialize gfx"); //_G(platform)->InitialiseAbufAtStartup(); } int engine_load_game_data() { Debug::Printf("Load game data"); set_our_eip(-17); HError err = load_game_file(); if (!err) { _G(proper_exit) = 1; display_game_file_error(err); return EXIT_ERROR; } return 0; } // Replace special tokens inside a user path option static void resolve_configured_path(String &option) { option.Replace(Shared::String("$GAMENAME$"), _GP(game).gamename); } // Setup paths and directories that may be affected by user configuration void engine_init_user_directories() { resolve_configured_path(_GP(usetup).user_data_dir); resolve_configured_path(_GP(usetup).shared_data_dir); if (!_GP(usetup).user_conf_dir.IsEmpty()) Debug::Printf(kDbgMsg_Info, "User config directory: %s", _GP(usetup).user_conf_dir.GetCStr()); if (!_GP(usetup).user_data_dir.IsEmpty()) Debug::Printf(kDbgMsg_Info, "User data directory: %s", _GP(usetup).user_data_dir.GetCStr()); if (!_GP(usetup).shared_data_dir.IsEmpty()) Debug::Printf(kDbgMsg_Info, "Shared data directory: %s", _GP(usetup).shared_data_dir.GetCStr()); // Initialize default save directory early, for we'll need it to set restart point SetDefaultSaveDirectory(); } #if AGS_PLATFORM_OS_ANDROID extern char android_base_directory[256]; #endif // AGS_PLATFORM_OS_ANDROID // TODO: remake/remove this nonsense int check_write_access() { #if AGS_PLATFORM_SCUMMVM return true; #else set_our_eip(-1895); // The Save Game Dir is the only place that we should write to String svg_dir = get_save_game_directory(); if (platform->GetDiskFreeSpaceMB(svg_dir) < 2) return 0; String tempPath = String::FromFormat("%s""tmptest.tmp", svg_dir.GetCStr()); Stream *temp_s = Shared::File::CreateFile(tempPath); if (!temp_s) // TODO: The fallback should be done on all platforms, and there's // already similar procedure found in SetSaveGameDirectoryPath. // If Android has extra dirs to fallback to, they should be provided // by platform driver's method, not right here! #if AGS_PLATFORM_OS_ANDROID { put_backslash(android_base_directory); tempPath.Format("%s""tmptest.tmp", android_base_directory); temp_s = Shared::File::CreateFile(tempPath); if (temp_s == NULL) return 0; else SetCustomSaveParent(android_base_directory); } #else return 0; #endif // AGS_PLATFORM_OS_ANDROID set_our_eip(-1896); temp_s->Write("just to test the drive free space", 30); delete temp_s; set_our_eip(-1897); if (File::DeleteFile(tempPath)) return 0; return 1; #endif } int engine_check_disk_space() { Debug::Printf(kDbgMsg_Info, "Checking for disk space"); if (check_write_access() == 0) { _G(platform)->DisplayAlert("Unable to write in the savegame directory.\n%s", _G(platform)->GetDiskWriteAccessTroubleshootingText()); _G(proper_exit) = 1; return EXIT_ERROR; } return 0; } int engine_check_font_was_loaded() { if (!font_first_renderer_loaded()) { _G(platform)->DisplayAlert("No game fonts found. At least one font is required to run the _GP(game)."); _G(proper_exit) = 1; return EXIT_ERROR; } return 0; } // Do the preload graphic if available void show_preload() { RGB temppal[256]; Bitmap *splashsc = BitmapHelper::CreateRawBitmapOwner(load_pcx("preload.pcx", temppal)); if (splashsc != nullptr) { Debug::Printf("Displaying preload image"); if (splashsc->GetColorDepth() == 8) set_palette_range(temppal, 0, 255, 0); if (_G(gfxDriver)->UsesMemoryBackBuffer()) _G(gfxDriver)->GetMemoryBackBuffer()->Clear(); const Rect &view = _GP(play).GetMainViewport(); Bitmap *tsc = BitmapHelper::CreateBitmapCopy(splashsc, _GP(game).GetColorDepth()); if (!_G(gfxDriver)->HasAcceleratedTransform() && view.GetSize() != tsc->GetSize()) { Bitmap *stretched = new Bitmap(view.GetWidth(), view.GetHeight(), tsc->GetColorDepth()); stretched->StretchBlt(tsc, RectWH(0, 0, view.GetWidth(), view.GetHeight())); delete tsc; tsc = stretched; } IDriverDependantBitmap *ddb = _G(gfxDriver)->CreateDDBFromBitmap(tsc, false, true); ddb->SetStretch(view.GetWidth(), view.GetHeight()); _G(gfxDriver)->ClearDrawLists(); _G(gfxDriver)->BeginSpriteBatch(view); _G(gfxDriver)->DrawSprite(0, 0, ddb); _G(gfxDriver)->EndSpriteBatch(); render_to_screen(); _G(gfxDriver)->DestroyDDB(ddb); delete splashsc; delete tsc; _G(platform)->Delay(500); } } int engine_init_sprites() { Debug::Printf(kDbgMsg_Info, "Initialize sprites"); HError err = _GP(spriteset).InitFile(SpriteFile::DefaultSpriteFileName, SpriteFile::DefaultSpriteIndexName); if (!err) { sys_main_shutdown(); allegro_exit(); _G(proper_exit) = 1; _G(platform)->DisplayAlert("Could not load sprite set file %s\n%s", SpriteFile::DefaultSpriteFileName, err->FullMessage().GetCStr()); return EXIT_ERROR; } if (_GP(usetup).SpriteCacheSize > 0) _GP(spriteset).SetMaxCacheSize(_GP(usetup).SpriteCacheSize * 1024); Debug::Printf("Sprite cache set: %zu KB", _GP(spriteset).GetMaxCacheSize() / 1024); return 0; } // TODO: this should not be a part of "engine_" function group, // move this elsewhere (InitGameState?). void engine_init_game_settings() { set_our_eip(-7); Debug::Printf("Initialize game settings"); // Initialize randomizer _GP(play).randseed = g_system->getMillis(); ::AGS::g_vm->setRandomNumberSeed(_GP(play).randseed); if (_GP(usetup).audio_enabled) { _GP(play).separate_music_lib = !_GP(ResPaths).AudioPak.Name.IsEmpty(); _GP(play).voice_avail = _GP(ResPaths).VoiceAvail; } else { _GP(play).voice_avail = false; _GP(play).separate_music_lib = false; } // Setup a text encoding mode depending on the game data hint if (_GP(game).options[OPT_GAMETEXTENCODING] == 65001) // utf-8 codepage number set_uformat(U_UTF8); else set_uformat(U_ASCII); int ee; for (ee = 0; ee < 256; ee++) { if (_GP(game).paluses[ee] != PAL_BACKGROUND) _G(palette)[ee] = _GP(game).defpal[ee]; } for (ee = 0; ee < _GP(game).numcursors; ee++) { // The cursor graphics are assigned to mousecurs[] and so cannot // be removed from memory if (_GP(game).mcurs[ee].pic >= 0) _GP(spriteset).PrecacheSprite(_GP(game).mcurs[ee].pic); // just in case they typed an invalid view number in the editor if (_GP(game).mcurs[ee].view >= _GP(game).numviews) _GP(game).mcurs[ee].view = -1; if (_GP(game).mcurs[ee].view >= 0) precache_view(_GP(game).mcurs[ee].view); } // may as well preload the character gfx if (_G(playerchar)->view >= 0) precache_view(_G(playerchar)->view, 0, Character_GetDiagonalWalking(_G(playerchar)) ? 8 : 4); set_our_eip(-6); for (ee = 0; ee < MAX_ROOM_OBJECTS; ee++) { _G(scrObj)[ee].id = ee; } for (ee = 0; ee < _GP(game).numcharacters; ee++) { memset(&_GP(game).chars[ee].inv[0], 0, MAX_INV * sizeof(short)); _GP(game).chars[ee].activeinv = -1; _GP(game).chars[ee].following = -1; _GP(game).chars[ee].followinfo = 97 | (10 << 8); if (_G(loaded_game_file_version) < kGameVersion_360) _GP(game).chars[ee].idletime = 20; // default to 20 seconds _GP(game).chars[ee].idleleft = _GP(game).chars[ee].idletime; _GP(game).chars[ee].transparency = 0; _GP(game).chars[ee].baseline = -1; _GP(game).chars[ee].walkwaitcounter = 0; _GP(game).chars[ee].z = 0; _GP(charextra)[ee].xwas = INVALID_X; _GP(charextra)[ee].zoom = 100; if (_GP(game).chars[ee].view >= 0) { // set initial loop to 0 _GP(game).chars[ee].loop = 0; // or to 1 if they don't have up/down frames if (_GP(views)[_GP(game).chars[ee].view].loops[0].numFrames < 1) _GP(game).chars[ee].loop = 1; } _GP(charextra)[ee].process_idle_this_time = 0; _GP(charextra)[ee].invorder_count = 0; _GP(charextra)[ee].slow_move_counter = 0; _GP(charextra)[ee].animwait = 0; } set_our_eip(-5); for (ee = 0; ee < _GP(game).numinvitems; ee++) { if (_GP(game).invinfo[ee].flags & IFLG_STARTWITH) _G(playerchar)->inv[ee] = 1; else _G(playerchar)->inv[ee] = 0; } // // TODO: following big initialization sequence could be in GameState ctor _GP(play).score = 0; _GP(play).sierra_inv_color = 7; // copy the value set by the editor if (_GP(game).options[OPT_GLOBALTALKANIMSPD] >= 0) { _GP(play).talkanim_speed = _GP(game).options[OPT_GLOBALTALKANIMSPD]; _GP(game).options[OPT_GLOBALTALKANIMSPD] = 1; } else { _GP(play).talkanim_speed = -_GP(game).options[OPT_GLOBALTALKANIMSPD] - 1; _GP(game).options[OPT_GLOBALTALKANIMSPD] = 0; } _GP(play).inv_item_wid = 40; _GP(play).inv_item_hit = 22; _GP(play).messagetime = -1; _GP(play).disabled_user_interface = 0; _GP(play).gscript_timer = -1; _GP(play).debug_mode = _GP(game).options[OPT_DEBUGMODE]; _GP(play).inv_top = 0; _GP(play).inv_numdisp = 0; _GP(play).inv_numorder = 0; _GP(play).text_speed = 15; _GP(play).text_min_display_time_ms = 1000; _GP(play).ignore_user_input_after_text_timeout_ms = 500; _GP(play).ClearIgnoreInput(); _GP(play).lipsync_speed = 15; _GP(play).close_mouth_speech_time = 10; _GP(play).disable_antialiasing = 0; _GP(play).rtint_enabled = false; _GP(play).rtint_level = 0; _GP(play).rtint_light = 0; _GP(play).text_speed_modifier = 0; _GP(play).text_align = kHAlignLeft; // Make the default alignment to the right with right-to-left text if (_GP(game).options[OPT_RIGHTLEFTWRITE]) _GP(play).text_align = kHAlignRight; _GP(play).speech_bubble_width = get_fixed_pixel_size(100); _GP(play).bg_frame = 0; _GP(play).bg_frame_locked = 0; _GP(play).bg_anim_delay = 0; _GP(play).anim_background_speed = 0; _GP(play).mouse_cursor_hidden = 0; _GP(play).silent_midi = 0; _GP(play).current_music_repeating = 0; _GP(play).skip_until_char_stops = -1; _GP(play).get_loc_name_last_time = -1; _GP(play).get_loc_name_save_cursor = -1; _GP(play).restore_cursor_mode_to = -1; _GP(play).restore_cursor_image_to = -1; _GP(play).ground_level_areas_disabled = 0; _GP(play).next_screen_transition = -1; _GP(play).temporarily_turned_off_character = -1; _GP(play).inv_backwards_compatibility = 0; _GP(play).gamma_adjustment = 100; _GP(play).music_queue_size = 0; _GP(play).shakesc_length = 0; _GP(play).wait_counter = 0; _GP(play).SetWaitSkipResult(SKIP_NONE); _GP(play).key_skip_wait = SKIP_NONE; _GP(play).cur_music_number = -1; _GP(play).music_repeat = 1; _GP(play).music_master_volume = 100 + LegacyMusicMasterVolumeAdjustment; _GP(play).digital_master_volume = 100; _GP(play).screen_flipped = 0; _GP(play).speech_mode = kSpeech_VoiceText; _GP(play).speech_skip_style = user_to_internal_skip_speech((SkipSpeechStyle)_GP(game).options[OPT_NOSKIPTEXT]); _GP(play).sound_volume = 255; _GP(play).speech_volume = 255; _GP(play).normal_font = 0; _GP(play).speech_font = 1; _GP(play).speech_text_shadow = 16; _GP(play).screen_tint = -1; _GP(play).bad_parsed_word[0] = 0; _GP(play).swap_portrait_side = 0; _GP(play).swap_portrait_lastchar = -1; _GP(play).swap_portrait_lastlastchar = -1; _GP(play).in_conversation = 0; _GP(play).skip_display = 3; _GP(play).no_multiloop_repeat = 0; _GP(play).in_cutscene = 0; _GP(play).fast_forward = 0; _GP(play).totalscore = _GP(game).totalscore; _GP(play).roomscript_finished = 0; _GP(play).no_textbg_when_voice = 0; _GP(play).max_dialogoption_width = get_fixed_pixel_size(180); _GP(play).no_hicolor_fadein = 0; _GP(play).bgspeech_game_speed = 0; _GP(play).bgspeech_stay_on_display = 0; _GP(play).unfactor_speech_from_textlength = 0; _GP(play).mp3_loop_before_end = 70; _GP(play).speech_music_drop = 60; _GP(play).room_changes = 0; _GP(play).check_interaction_only = 0; _GP(play).replay_hotkey_unused = -1; // StartRecording: not supported. _GP(play).dialog_options_x = 0; _GP(play).dialog_options_y = 0; _GP(play).min_dialogoption_width = 0; _GP(play).disable_dialog_parser = 0; _GP(play).ambient_sounds_persist = 0; _GP(play).screen_is_faded_out = 0; _GP(play).player_on_region = 0; _GP(play).top_bar_backcolor = 8; _GP(play).top_bar_textcolor = 16; _GP(play).top_bar_bordercolor = 8; _GP(play).top_bar_borderwidth = 1; _GP(play).top_bar_ypos = 25; _GP(play).top_bar_font = -1; _GP(play).screenshot_width = 160; _GP(play).screenshot_height = 100; _GP(play).speech_text_align = kHAlignCenter; _GP(play).auto_use_walkto_points = 1; _GP(play).inventory_greys_out = 0; _GP(play).skip_speech_specific_key = 0; _GP(play).abort_key = 324; // Alt+X _GP(play).fade_to_red = 0; _GP(play).fade_to_green = 0; _GP(play).fade_to_blue = 0; _GP(play).show_single_dialog_option = 0; _GP(play).keep_screen_during_instant_transition = 0; _GP(play).read_dialog_option_colour = -1; _GP(play).stop_dialog_at_end = DIALOG_NONE; _GP(play).speech_portrait_placement = 0; _GP(play).speech_portrait_x = 0; _GP(play).speech_portrait_y = 0; _GP(play).speech_display_post_time_ms = 0; _GP(play).dialog_options_highlight_color = DIALOG_OPTIONS_HIGHLIGHT_COLOR_DEFAULT; _GP(play).speech_has_voice = false; _GP(play).speech_voice_blocking = false; _GP(play).speech_in_post_state = false; _GP(play).complete_overlay_on = 0; _GP(play).text_overlay_on = 0; _GP(play).narrator_speech = _GP(game).playercharacter; _GP(play).crossfading_out_channel = 0; _GP(play).speech_textwindow_gui = _GP(game).options[OPT_TWCUSTOM]; if (_GP(play).speech_textwindow_gui == 0) _GP(play).speech_textwindow_gui = -1; _GP(play).game_name = _GP(game).gamename; _GP(play).lastParserEntry[0] = 0; _GP(play).follow_change_room_timer = 150; for (ee = 0; ee < MAX_ROOM_BGFRAMES; ee++) _GP(play).raw_modified[ee] = 0; _GP(play).game_speed_modifier = 0; if (_G(debug_flags) & DBG_DEBUGMODE) _GP(play).debug_mode = 1; _GP(play).shake_screen_yoff = 0; GUI::Options.DisabledStyle = static_cast(_GP(game).options[OPT_DISABLEOFF]); GUI::Options.ClipControls = _GP(game).options[OPT_CLIPGUICONTROLS] != 0; // Force GUI metrics recalculation, accommodating for loaded fonts GUI::MarkForFontUpdate(-1); memset(&_GP(play).walkable_areas_on[0], 1, MAX_WALK_AREAS); memset(&_GP(play).script_timers[0], 0, MAX_TIMERS * sizeof(int)); memset(&_GP(play).default_audio_type_volumes[0], -1, MAX_AUDIO_TYPES * sizeof(int)); if (!_GP(usetup).translation.IsEmpty()) Game_ChangeTranslation(_GP(usetup).translation.GetCStr()); update_invorder(); _G(displayed_room) = -10; set_our_eip(-4); _G(mousey) = 100; // stop icon bar popping up // We use same variable to read config and be used at runtime for now, // so update it here with regards to game design option _GP(usetup).RenderAtScreenRes = (_GP(game).options[OPT_RENDERATSCREENRES] == kRenderAtScreenRes_UserDefined && _GP(usetup).RenderAtScreenRes) || _GP(game).options[OPT_RENDERATSCREENRES] == kRenderAtScreenRes_Enabled; } void engine_setup_scsystem_auxiliary() { // ScriptSystem::aci_version is only 10 chars long snprintf(_GP(scsystem).aci_version, sizeof(_GP(scsystem).aci_version), "%s", _G(EngineVersion).LongString.GetCStr()); if (_GP(usetup).override_script_os >= 0) { _GP(scsystem).os = _GP(usetup).override_script_os; } else { _GP(scsystem).os = _G(platform)->GetSystemOSID(); } } void engine_prepare_to_start_game() { Debug::Printf("Prepare to start game"); engine_setup_scsystem_auxiliary(); if (_GP(usetup).load_latest_save) { #ifndef AGS_PLATFORM_SCUMMVM int slot = GetLastSaveSlot(); if (slot >= 0) loadSaveGameOnStartup = get_save_game_path(slot); #endif } } // Define location of the game data either using direct settings or searching // for the available resource packs in common locations. // Returns two paths: // - startup_dir: this is where engine found game config and/or data; // - data_path: full path of the main data pack; // data_path's directory (may or not be eq to startup_dir) should be considered data directory, // and this is where engine look for all game data. HError define_gamedata_location_checkall(String &data_path, String &startup_dir) { // First try if they provided a startup option if (!_G(cmdGameDataPath).IsEmpty()) { // If not a valid path - bail out if (!File::IsFileOrDir(_G(cmdGameDataPath))) return new Error(String::FromFormat("Provided game location is not a valid path.\n Cwd: %s\n Path: %s", Directory::GetCurrentDirectory().GetCStr(), _G(cmdGameDataPath).GetCStr())); // If it's a file, then keep it and proceed if (File::IsFile(_G(cmdGameDataPath))) { Debug::Printf("Using provided game data path: %s", _G(cmdGameDataPath).GetCStr()); startup_dir = Path::GetDirectoryPath(_G(cmdGameDataPath)); data_path = _G(cmdGameDataPath); return HError::None(); } } #if AGS_SEARCH_FOR_GAME_ON_LAUNCH // No direct filepath provided, search in common locations. data_path = search_for_game_data_file(startup_dir); if (data_path.IsEmpty()) { return new Error("Engine was not able to find any compatible game data.", startup_dir.IsEmpty() ? String() : String::FromFormat("Searched in: %s", startup_dir.GetCStr())); } data_path = Path::MakeAbsolutePath(data_path); Debug::Printf(kDbgMsg_Info, "Located game data pak: %s", data_path.GetCStr()); return HError::None(); #else // No direct filepath provided, bail out. return new Error("The game location was not defined by startup settings."); #endif } // Define location of the game data bool define_gamedata_location() { String data_path, startup_dir; HError err = define_gamedata_location_checkall(data_path, startup_dir); if (!err) { _G(platform)->DisplayAlert("ERROR: Unable to determine game data.\n%s", err->FullMessage().GetCStr()); main_print_help(); return false; } // On success: set all the necessary path and filename settings _GP(usetup).startup_dir = startup_dir; _GP(usetup).main_data_file = data_path; _GP(usetup).main_data_dir = Path::GetDirectoryPath(data_path); return true; } // Find and preload main game data bool engine_init_gamedata() { Debug::Printf(kDbgMsg_Info, "Initializing game data"); // First, find data location if (!define_gamedata_location()) return false; // Try init game lib AssetError asset_err = _GP(AssetMgr)->AddLibrary(_GP(usetup).main_data_file); if (asset_err != kAssetNoError) { _G(platform)->DisplayAlert("ERROR: The game data is missing, is of unsupported format or corrupt.\nFile: '%s'", _GP(usetup).main_data_file.GetCStr()); return false; } // Pre-load game name and savegame folder names from data file // TODO: research if that is possible to avoid this step and just // read the full head game data at this point. This might require // further changes of the order of initialization. HError err = preload_game_data(); if (!err) { display_game_file_error(err); return false; } // Setup _GP(ResPaths), so that we know out main locations further _GP(ResPaths).GamePak.Path = _GP(usetup).main_data_file; _GP(ResPaths).GamePak.Name = Path::GetFilename(_GP(usetup).main_data_file); _GP(ResPaths).DataDir = _GP(usetup).install_dir.IsEmpty() ? _GP(usetup).main_data_dir : Path::MakeAbsolutePath(_GP(usetup).install_dir); _GP(ResPaths).DataDir2 = Path::MakeAbsolutePath(_GP(usetup).opt_data_dir); _GP(ResPaths).AudioDir2 = Path::MakeAbsolutePath(_GP(usetup).opt_audio_dir); _GP(ResPaths).VoiceDir2 = Path::MakeAbsolutePath(_GP(usetup).opt_voice_dir); Debug::Printf(kDbgMsg_Info, "Startup directory: %s", _GP(usetup).startup_dir.GetCStr()); Debug::Printf(kDbgMsg_Info, "Data directory: %s", _GP(ResPaths).DataDir.GetCStr()); if (!_GP(ResPaths).DataDir2.IsEmpty()) Debug::Printf(kDbgMsg_Info, "Opt data directory: %s", _GP(ResPaths).DataDir2.GetCStr()); if (!_GP(ResPaths).AudioDir2.IsEmpty()) Debug::Printf(kDbgMsg_Info, "Opt audio directory: %s", _GP(ResPaths).AudioDir2.GetCStr()); if (!_GP(ResPaths).VoiceDir2.IsEmpty()) Debug::Printf(kDbgMsg_Info, "Opt voice-over directory: %s", _GP(ResPaths).VoiceDir2.GetCStr()); return true; } void engine_read_config(ConfigTree &cfg) { if (!_GP(usetup).conf_path.IsEmpty()) { IniUtil::Read(_GP(usetup).conf_path, cfg); return; } // Read default configuration file String def_cfg_file = find_default_cfg_file(); IniUtil::Read(def_cfg_file, cfg); // Disabled on Windows because people were afraid that this config could be mistakenly // created by some installer and screw up their games. Until any kind of solution is found. String user_global_cfg_file; // Read user global configuration file user_global_cfg_file = find_user_global_cfg_file(); if (Path::ComparePaths(user_global_cfg_file, def_cfg_file) != 0) IniUtil::Read(user_global_cfg_file, cfg); // Handle directive to search for the user config inside the custom directory; // this option may come either from command line or default/global config. if (_GP(usetup).user_conf_dir.IsEmpty()) _GP(usetup).user_conf_dir = CfgReadString(cfg, "misc", "user_conf_dir"); if (_GP(usetup).user_conf_dir.IsEmpty()) // also try deprecated option _GP(usetup).user_conf_dir = CfgReadBoolInt(cfg, "misc", "localuserconf") ? "." : ""; // Test if the file is writeable, if it is then both engine and setup // applications may actually use it fully as a user config, otherwise // fallback to default behavior. if (!_GP(usetup).user_conf_dir.IsEmpty()) { resolve_configured_path(_GP(usetup).user_conf_dir); if (Path::IsRelativePath(_GP(usetup).user_conf_dir)) _GP(usetup).user_conf_dir = Path::ConcatPaths(_GP(usetup).startup_dir, _GP(usetup).user_conf_dir); if (!Directory::CreateDirectory(_GP(usetup).user_conf_dir) || !File::TestWriteFile(Path::ConcatPaths(_GP(usetup).user_conf_dir, DefaultConfigFileName))) { Debug::Printf(kDbgMsg_Warn, "Write test failed at user config dir '%s', using default path.", _GP(usetup).user_conf_dir.GetCStr()); _GP(usetup).user_conf_dir = ""; } } // Handle directive to search for the user config inside the game directory; // this option may come either from command line or default/global config. _GP(usetup).local_user_conf |= CfgReadInt(cfg, "misc", "localuserconf", 0) != 0; if (_GP(usetup).local_user_conf) { // Test if the file is writeable, if it is then both engine and setup // applications may actually use it fully as a user config, otherwise // fallback to default behavior. _GP(usetup).local_user_conf = File::TestWriteFile(def_cfg_file); } // Read user configuration file String user_cfg_file = find_user_cfg_file(); if (Path::ComparePaths(user_cfg_file, def_cfg_file) != 0 && Path::ComparePaths(user_cfg_file, user_global_cfg_file) != 0) IniUtil::Read(user_cfg_file, cfg); // Apply overriding options from platform settings // TODO: normally, those should be instead stored in the same config file in a uniform way override_config_ext(cfg); } // Gathers settings from all available sources into single ConfigTree void engine_prepare_config(ConfigTree &cfg, const ConfigTree &startup_opts) { Debug::Printf(kDbgMsg_Info, "Setting up game configuration"); // Read configuration files engine_read_config(cfg); // Merge startup options in for (const auto §n : startup_opts) for (const auto &opt : sectn._value) cfg[sectn._key][opt._key] = opt._value; } // Applies configuration to the running game void engine_set_config(const ConfigTree cfg) { config_defaults(); apply_config(cfg); post_config(); } static bool print_info_needs_game(const std::set &keys) { return keys.count("all") > 0 || keys.count("config") > 0 || keys.count("configpath") > 0 || keys.count("data") > 0 || keys.count("filepath") > 0 || keys.count("gameproperties") > 0; } static void engine_print_info(const std::set &keys, ConfigTree *user_cfg) { const bool all = keys.count("all") > 0; ConfigTree data; if (all || keys.count("engine") > 0) { data["engine"]["name"] = get_engine_name(); data["engine"]["version"] = get_engine_version(); } if (all || keys.count("graphicdriver") > 0) { StringV drv; AGS::Engine::GetGfxDriverFactoryNames(drv); for (size_t i = 0; i < drv.size(); ++i) { data["graphicdriver"][String::FromFormat("%zu", i)] = drv[i]; } } if (all || keys.count("configpath") > 0) { String def_cfg_file = find_default_cfg_file(); String gl_cfg_file = find_user_global_cfg_file(); String user_cfg_file = find_user_cfg_file(); data["configpath"]["default"] = def_cfg_file; data["configpath"]["global"] = gl_cfg_file; data["configpath"]["user"] = user_cfg_file; } if ((all || keys.count("config") > 0) && user_cfg) { for (const auto §n : *user_cfg) { String cfg_sectn = String::FromFormat("config@%s", sectn._key.GetCStr()); for (const auto &opt : sectn._value) data[cfg_sectn][opt._key] = opt._value; } } if (all || keys.count("data") > 0) { data["data"]["gamename"] = _GP(game).gamename; data["data"]["version"] = StrUtil::IntToString(_G(loaded_game_file_version)); data["data"]["compiledwith"] = _GP(game).compiled_with; data["data"]["basepack"] = _GP(ResPaths).GamePak.Path; } if (all || keys.count("gameproperties") > 0) { fill_game_properties(data["gameproperties"]); } if (all || keys.count("filepath") > 0) { data["filepath"]["exe"] = _G(appPath); data["filepath"]["cwd"] = Directory::GetCurrentDirectory(); data["filepath"]["datadir"] = Path::MakePathNoSlash(_GP(ResPaths).DataDir); if (!_GP(ResPaths).DataDir2.IsEmpty()) { data["filepath"]["datadir2"] = Path::MakePathNoSlash(_GP(ResPaths).DataDir2); data["filepath"]["audiodir2"] = Path::MakePathNoSlash(_GP(ResPaths).AudioDir2); data["filepath"]["voicedir2"] = Path::MakePathNoSlash(_GP(ResPaths).VoiceDir2); } data["filepath"]["savegamedir"] = Path::MakePathNoSlash(GetGameUserDataDir().FullDir); data["filepath"]["appdatadir"] = Path::MakePathNoSlash(GetGameAppDataDir().FullDir); } String full; IniUtil::WriteToString(full, data); _G(platform)->WriteStdOut("%s", full.GetCStr()); } // TODO: this function is still a big mess, engine/system-related initialization // is mixed with game-related data adjustments. Divide it in parts, move game // data init into either InitGameState() or other game method as appropriate. int initialize_engine(const ConfigTree &startup_opts) { _G(proper_exit) = false; if (_G(engine_pre_init_callback)) { _G(engine_pre_init_callback)(); } //----------------------------------------------------- // Install backend if (!engine_init_backend()) return EXIT_ERROR; //----------------------------------------------------- // Locate game data and assemble game config if (_G(justTellInfo) && !print_info_needs_game(_G(tellInfoKeys))) { engine_print_info(_G(tellInfoKeys), nullptr); return EXIT_NORMAL; } if (!engine_init_gamedata()) return EXIT_ERROR; ConfigTree cfg; engine_prepare_config(cfg, startup_opts); // Test if need to run built-in setup program (where available) if (!_G(justTellInfo) && _G(justRunSetup)) { if (!engine_run_setup(cfg)) return EXIT_NORMAL; } // Set up game options from user config engine_set_config(cfg); if (_G(justTellInfo)) { engine_print_info(_G(tellInfoKeys), &cfg); return EXIT_NORMAL; } set_our_eip(-190); //----------------------------------------------------- // Init auxiliary data files and other directories, initialize asset manager engine_init_user_directories(); set_our_eip(-191); engine_locate_speech_pak(); set_our_eip(-192); engine_locate_audio_pak(); set_our_eip(-193); engine_assign_assetpaths(); //----------------------------------------------------- // Begin setting up systems set_our_eip(-194); engine_init_fonts(); set_our_eip(-195); engine_init_keyboard(); set_our_eip(-196); engine_init_mouse(); set_our_eip(-198); engine_init_audio(); set_our_eip(-199); engine_init_debug(); set_our_eip(-10); engine_init_pathfinder(); set_game_speed(40); set_our_eip(-20); set_our_eip(-19); int res = engine_load_game_data(); if (res != 0) return res; set_our_eip(-189); res = engine_check_disk_space(); if (res != 0) return res; // Make sure that at least one font was loaded in the process of loading // the game data. // TODO: Fold this check into engine_load_game_data() res = engine_check_font_was_loaded(); if (res != 0) return res; set_our_eip(-179); engine_adjust_for_rotation_settings(); // Attempt to initialize graphics mode if (!engine_try_set_gfxmode_any(_GP(usetup).Screen)) return EXIT_ERROR; // Configure game window after renderer was initialized engine_setup_window(); SetMultitasking(_GP(usetup).multitasking); sys_window_show_cursor(false); // hide the system cursor show_preload(); res = engine_init_sprites(); if (res != 0) return res; engine_init_game_settings(); engine_prepare_to_start_game(); initialize_start_and_play_game(_G(override_start_room), _G(loadSaveGameOnStartup)); return EXIT_NORMAL; } bool engine_try_set_gfxmode_any(const DisplayModeSetup &setup) { const DisplayMode old_dm = _G(gfxDriver) ? _G(gfxDriver)->GetDisplayMode() : DisplayMode(); engine_shutdown_gfxmode(); sys_renderer_set_output(_GP(usetup).software_render_driver); const Size init_desktop = get_desktop_size(); bool res = graphics_mode_init_any(GraphicResolution(_GP(game).GetGameRes(), _GP(game).color_depth * 8), setup, ColorDepthOption(_GP(game).GetColorDepth())); if (res) engine_post_gfxmode_setup(init_desktop, old_dm); // Make sure that we don't receive window events queued during init sys_flush_events(); return res; } bool engine_try_switch_windowed_gfxmode() { if (!_G(gfxDriver) || !_G(gfxDriver)->IsModeSet()) return false; // Keep previous mode in case we need to revert back DisplayMode old_dm = _G(gfxDriver)->GetDisplayMode(); FrameScaleDef old_frame = graphics_mode_get_render_frame(); // Release engine resources that depend on display mode engine_pre_gfxmode_release(); Size init_desktop = get_desktop_size(); bool windowed = !old_dm.IsWindowed(); ActiveDisplaySetting setting = graphics_mode_get_last_setting(windowed); DisplayMode last_opposite_mode = setting.Dm; FrameScaleDef frame = setting.Frame; // Apply vsync in case it has been toggled at runtime last_opposite_mode.Vsync = _GP(usetup).Screen.Params.VSync; // If there are saved parameters for given mode (fullscreen/windowed), // *and* if the window is on the same display where it's been last time, // then use old params, otherwise - get default setup for the new mode. bool res; if (last_opposite_mode.IsValid() && (setting.DisplayIndex == sys_get_window_display_index())) { res = graphics_mode_set_dm(last_opposite_mode); } else { WindowSetup ws = windowed ? _GP(usetup).Screen.WinSetup : _GP(usetup).Screen.FsSetup; frame = windowed ? _GP(usetup).Screen.WinGameFrame : _GP(usetup).Screen.FsGameFrame; res = graphics_mode_set_dm_any(_GP(game).GetGameRes(), ws, old_dm.ColorDepth, frame, _GP(usetup).Screen.Params); } // Apply corresponding frame render method if (res) res = graphics_mode_set_render_frame(frame); if (!res) { // If failed, try switching back to previous gfx mode res = graphics_mode_set_dm(old_dm) && graphics_mode_set_render_frame(old_frame); if (!res) quitprintf("Failed to restore graphics mode."); } // If succeeded (with any case), update engine objects that rely on // active display mode. if (!_G(gfxDriver)->GetDisplayMode().IsRealFullscreen()) init_desktop = get_desktop_size(); engine_post_gfxmode_setup(init_desktop, old_dm); // Make sure that we don't receive window events queued during init sys_flush_events(); return res; } void engine_on_window_changed(const Size &sz) { graphics_mode_on_window_changed(sz); on_coordinates_scaling_changed(); invalidate_screen(); } void engine_shutdown_gfxmode() { if (!_G(gfxDriver)) return; engine_pre_gfxsystem_shutdown(); graphics_mode_shutdown(); } const char *get_engine_name() { return "Adventure Game Studio run-time engine"; } const char *get_engine_version() { return _G(EngineVersion).LongString.GetCStr(); } String get_engine_version_and_build() { const char *bit = (AGS_PLATFORM_64BIT) ? "64-bit" : "32-bit"; const char *end = (AGS_PLATFORM_ENDIAN_LITTLE) ? "LE" : "BE"; #ifdef BUILD_STR return String::FromFormat("%s (Build: %s), %s %s", _G(EngineVersion).LongString.GetCStr(), EngineVersion.BuildInfo.GetCStr(), bit, end); #else return String::FromFormat("%s, %s %s", _G(EngineVersion).LongString.GetCStr(), bit, end); #endif } void engine_set_pre_init_callback(t_engine_pre_init_callback callback) { _G(engine_pre_init_callback) = callback; } } // namespace AGS3