/***************************************************************************** * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #ifdef __EMSCRIPTEN__ # include #endif // __EMSCRIPTEN__ #include "Context.h" #include "Editor.h" #include "FileClassifier.h" #include "Game.h" #include "GameState.h" #include "GameStateSnapshots.h" #include "Input.h" #include "Intro.h" #include "OpenRCT2.h" #include "ParkImporter.h" #include "PlatformEnvironment.h" #include "ReplayManager.h" #include "Version.h" #include "actions/GameAction.h" #include "audio/AudioContext.h" #include "audio/audio.h" #include "config/Config.h" #include "core/Console.hpp" #include "core/File.h" #include "core/FileScanner.h" #include "core/FileStream.h" #include "core/Guard.hpp" #include "core/Http.h" #include "core/MemoryStream.h" #include "core/Path.hpp" #include "core/String.hpp" #include "drawing/IDrawingEngine.h" #include "drawing/LightFX.h" #include "interface/Chat.h" #include "interface/InteractiveConsole.h" #include "interface/Viewport.h" #include "localisation/Date.h" #include "localisation/Localisation.h" #include "localisation/LocalisationService.h" #include "network/DiscordService.h" #include "network/network.h" #include "object/ObjectManager.h" #include "object/ObjectRepository.h" #include "paint/Painter.h" #include "platform/Crash.h" #include "platform/Platform2.h" #include "platform/platform.h" #include "ride/TrackDesignRepository.h" #include "scenario/Scenario.h" #include "scenario/ScenarioRepository.h" #include "scripting/HookEngine.h" #include "scripting/ScriptEngine.h" #include "title/TitleScreen.h" #include "title/TitleSequenceManager.h" #include "ui/UiContext.h" #include "ui/WindowManager.h" #include "util/Util.h" #include "world/Park.h" #include #include #include #include #include #include #include using namespace OpenRCT2; using namespace OpenRCT2::Audio; using namespace OpenRCT2::Drawing; using namespace OpenRCT2::Localisation; using namespace OpenRCT2::Paint; using namespace OpenRCT2::Scripting; using namespace OpenRCT2::Ui; namespace OpenRCT2 { class Context final : public IContext { private: // Dependencies std::shared_ptr const _env; std::shared_ptr const _audioContext; std::shared_ptr const _uiContext; // Services std::unique_ptr _localisationService; std::unique_ptr _objectRepository; std::unique_ptr _objectManager; std::unique_ptr _trackDesignRepository; std::unique_ptr _scenarioRepository; std::unique_ptr _replayManager; std::unique_ptr _gameStateSnapshots; #ifdef __ENABLE_DISCORD__ std::unique_ptr _discordService; #endif StdInOutConsole _stdInOutConsole; #ifdef ENABLE_SCRIPTING ScriptEngine _scriptEngine; #endif // Game states std::unique_ptr _titleScreen; std::unique_ptr _gameState; DrawingEngine _drawingEngineType = DrawingEngine::Software; std::unique_ptr _drawingEngine; std::unique_ptr _painter; bool _initialised = false; bool _isWindowMinimised = false; uint32_t _lastTick = 0; uint32_t _accumulator = 0; uint32_t _lastUpdateTime = 0; bool _variableFrame = false; // If set, will end the OpenRCT2 game loop. Intentially private to this module so that the flag can not be set back to // false. bool _finished = false; std::future _versionCheckFuture; NewVersionInfo _newVersionInfo; bool _hasNewVersionInfo = false; public: // Singleton of Context. // Remove this when GetContext() is no longer called so that // multiple instances can be created in parallel static Context* Instance; public: Context( const std::shared_ptr& env, const std::shared_ptr& audioContext, const std::shared_ptr& uiContext) : _env(env) , _audioContext(audioContext) , _uiContext(uiContext) , _localisationService(std::make_unique(env)) #ifdef ENABLE_SCRIPTING , _scriptEngine(_stdInOutConsole, *env) #endif , _painter(std::make_unique(uiContext)) { // Can't have more than one context currently. Guard::Assert(Instance == nullptr); Instance = this; } ~Context() override { // NOTE: We must shutdown all systems here before Instance is set back to null. // If objects use GetContext() in their destructor things won't go well. GameActions::ClearQueue(); network_close(); window_close_all(); // Unload objects after closing all windows, this is to overcome windows like // the object selection window which loads objects when closed. if (_objectManager != nullptr) { _objectManager->UnloadAll(); } gfx_object_check_all_images_freed(); gfx_unload_g2(); gfx_unload_g1(); config_release(); Instance = nullptr; } std::shared_ptr GetAudioContext() override { return _audioContext; } std::shared_ptr GetUiContext() override { return _uiContext; } #ifdef ENABLE_SCRIPTING Scripting::ScriptEngine& GetScriptEngine() override { return _scriptEngine; } #endif GameState* GetGameState() override { return _gameState.get(); } std::shared_ptr GetPlatformEnvironment() override { return _env; } Localisation::LocalisationService& GetLocalisationService() override { return *_localisationService; } IObjectManager& GetObjectManager() override { return *_objectManager; } IObjectRepository& GetObjectRepository() override { return *_objectRepository; } ITrackDesignRepository* GetTrackDesignRepository() override { return _trackDesignRepository.get(); } IScenarioRepository* GetScenarioRepository() override { return _scenarioRepository.get(); } IReplayManager* GetReplayManager() override { return _replayManager.get(); } IGameStateSnapshots* GetGameStateSnapshots() override { return _gameStateSnapshots.get(); } DrawingEngine GetDrawingEngineType() override { return _drawingEngineType; } IDrawingEngine* GetDrawingEngine() override { return _drawingEngine.get(); } virtual Paint::Painter* GetPainter() override { return _painter.get(); } int32_t RunOpenRCT2(int argc, const char** argv) override { if (Initialise()) { Launch(); return EXIT_SUCCESS; } return EXIT_FAILURE; } void WriteLine(const std::string& s) override { _stdInOutConsole.WriteLine(s); } /** * Causes the OpenRCT2 game loop to finish. */ void Finish() override { _finished = true; } void Quit() override { gSavePromptMode = PromptMode::Quit; context_open_window(WC_SAVE_PROMPT); } std::string GetPathLegacy(int32_t pathId) override { static constexpr const char* const LegacyFileNames[PATH_ID_END] = { nullptr, nullptr, "css1.dat", "css2.dat", "css4.dat", "css5.dat", "css6.dat", "css7.dat", "css8.dat", "css9.dat", "css11.dat", "css12.dat", "css13.dat", "css14.dat", "css15.dat", "css3.dat", "css17.dat", "css18.dat", "css19.dat", "css20.dat", "css21.dat", "css22.dat", nullptr, "css23.dat", "css24.dat", "css25.dat", "css26.dat", "css27.dat", "css28.dat", "css29.dat", "css30.dat", "css31.dat", "css32.dat", "css33.dat", "css34.dat", "css35.dat", "css36.dat", "css37.dat", "css38.dat", "CUSTOM1.WAV", "CUSTOM2.WAV", "css39.dat", "css40.dat", "css41.dat", nullptr, "css42.dat", "css43.dat", "css44.dat", "css45.dat", "css46.dat", "css50.dat", }; std::string result; if (pathId == PATH_ID_CSS50) { if (!(_env->GetDirectoryPath(DIRBASE::RCT1).empty())) { auto dataPath = _env->GetDirectoryPath(DIRBASE::RCT1, DIRID::DATA); result = Path::ResolveCasing(Path::Combine(dataPath, "css17.dat")); if (!File::Exists(result)) { auto rct1Path = _env->GetDirectoryPath(DIRBASE::RCT1); result = Path::ResolveCasing(Path::Combine(rct1Path, "RCTdeluxe_install", "Data", "css17.dat")); } } else { auto dataPath = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::DATA); result = Path::ResolveCasing(Path::Combine(dataPath, "css50.dat")); } } else if (pathId >= 0 && pathId < PATH_ID_END) { auto fileName = LegacyFileNames[pathId]; if (fileName != nullptr) { auto dataPath = _env->GetDirectoryPath(DIRBASE::RCT2, DIRID::DATA); result = Path::Combine(dataPath, fileName); } } return result; } bool Initialise() final override { if (_initialised) { throw std::runtime_error("Context already initialised."); } _initialised = true; crash_init(); if (gConfigGeneral.last_run_version != nullptr && String::Equals(gConfigGeneral.last_run_version, OPENRCT2_VERSION)) { gOpenRCT2ShowChangelog = false; } else { gOpenRCT2ShowChangelog = true; gConfigGeneral.last_run_version = String::Duplicate(OPENRCT2_VERSION); config_save_default(); } try { _localisationService->OpenLanguage(gConfigGeneral.language); } catch (const std::exception& e) { log_error("Failed to open configured language: %s", e.what()); try { _localisationService->OpenLanguage(LANGUAGE_ENGLISH_UK); } catch (const std::exception& eFallback) { log_fatal("Failed to open fallback language: %s", eFallback.what()); auto uiContext = GetContext()->GetUiContext(); uiContext->ShowMessageBox("Failed to load language file!\nYour installation may be damaged."); return false; } } // TODO add configuration option to allow multiple instances // if (!gOpenRCT2Headless && !platform_lock_single_instance()) { // log_fatal("OpenRCT2 is already running."); // return false; // } //This comment was relocated so it would stay where it was in relation to the following lines of code. if (!gOpenRCT2Headless) { auto rct2InstallPath = GetOrPromptRCT2Path(); if (rct2InstallPath.empty()) { return false; } _env->SetBasePath(DIRBASE::RCT2, rct2InstallPath); } _objectRepository = CreateObjectRepository(_env); _objectManager = CreateObjectManager(*_objectRepository); _trackDesignRepository = CreateTrackDesignRepository(_env); _scenarioRepository = CreateScenarioRepository(_env); _replayManager = CreateReplayManager(); _gameStateSnapshots = CreateGameStateSnapshots(); #ifdef __ENABLE_DISCORD__ if (!gOpenRCT2Headless) { _discordService = std::make_unique(); } #endif if (platform_process_is_elevated()) { std::string elevationWarning = _localisationService->GetString(STR_ADMIN_NOT_RECOMMENDED); if (gOpenRCT2Headless) { Console::Error::WriteLine(elevationWarning.c_str()); } else { _uiContext->ShowMessageBox(elevationWarning); } } if (Platform::IsRunningInWine()) { std::string wineWarning = _localisationService->GetString(STR_WINE_NOT_RECOMMENDED); if (gOpenRCT2Headless) { Console::Error::WriteLine(wineWarning.c_str()); } else { _uiContext->ShowMessageBox(wineWarning); } } if (!gOpenRCT2Headless) { _uiContext->CreateWindow(); } EnsureUserContentDirectoriesExist(); // TODO Ideally we want to delay this until we show the title so that we can // still open the game window and draw a progress screen for the creation // of the object cache. _objectRepository->LoadOrConstruct(_localisationService->GetCurrentLanguage()); // TODO Like objects, this can take a while if there are a lot of track designs // its also really something really we might want to do in the background // as its not required until the player wants to place a new ride. _trackDesignRepository->Scan(_localisationService->GetCurrentLanguage()); _scenarioRepository->Scan(_localisationService->GetCurrentLanguage()); TitleSequenceManager::Scan(); if (!gOpenRCT2Headless) { Init(); PopulateDevices(); InitRideSoundsAndInfo(); gGameSoundsOff = !gConfigSound.master_sound_enabled; } network_set_env(_env); chat_init(); CopyOriginalUserFilesOver(); if (!gOpenRCT2NoGraphics) { if (!LoadBaseGraphics()) { return false; } #ifdef __ENABLE_LIGHTFX__ lightfx_init(); #endif } gScenarioTicks = 0; input_reset_place_obj_modifier(); viewport_init_all(); _gameState = std::make_unique(); _gameState->InitAll(150); _titleScreen = std::make_unique(*_gameState); _uiContext->Initialise(); return true; } void InitialiseDrawingEngine() final override { assert(_drawingEngine == nullptr); _drawingEngineType = gConfigGeneral.drawing_engine; auto drawingEngineFactory = _uiContext->GetDrawingEngineFactory(); auto drawingEngine = drawingEngineFactory->Create(_drawingEngineType, _uiContext); if (drawingEngine == nullptr) { if (_drawingEngineType == DrawingEngine::Software) { _drawingEngineType = DrawingEngine::None; log_fatal("Unable to create a drawing engine."); exit(-1); } else { log_error("Unable to create drawing engine. Falling back to software."); // Fallback to software gConfigGeneral.drawing_engine = DrawingEngine::Software; config_save_default(); drawing_engine_init(); } } else { try { drawingEngine->Initialise(); drawingEngine->SetVSync(gConfigGeneral.use_vsync); _drawingEngine = std::unique_ptr(std::move(drawingEngine)); } catch (const std::exception& ex) { if (_drawingEngineType == DrawingEngine::Software) { _drawingEngineType = DrawingEngine::None; log_error(ex.what()); log_fatal("Unable to initialise a drawing engine."); exit(-1); } else { log_error(ex.what()); log_error("Unable to initialise drawing engine. Falling back to software."); // Fallback to software gConfigGeneral.drawing_engine = DrawingEngine::Software; config_save_default(); drawing_engine_init(); } } } window_check_all_valid_zoom(); } void DisposeDrawingEngine() final override { _drawingEngine = nullptr; } bool LoadParkFromFile(const std::string& path, bool loadTitleScreenOnFail) final override { log_verbose("Context::LoadParkFromFile(%s)", path.c_str()); try { if (String::Equals(Path::GetExtension(path), ".sea", true)) { auto data = DecryptSea(fs::u8path(path)); auto ms = MemoryStream(data.data(), data.size(), MEMORY_ACCESS::READ); if (!LoadParkFromStream(&ms, path, loadTitleScreenOnFail)) { throw std::runtime_error(".sea file may have been renamed."); } return true; } else { auto fs = FileStream(path, FILE_MODE_OPEN); if (!LoadParkFromStream(&fs, path, loadTitleScreenOnFail)) { return false; } return true; } } catch (const std::exception& e) { Console::Error::WriteLine(e.what()); if (loadTitleScreenOnFail) { title_load(); } auto windowManager = _uiContext->GetWindowManager(); windowManager->ShowError(STR_FAILED_TO_LOAD_FILE_CONTAINS_INVALID_DATA, STR_NONE, {}); } return false; } bool LoadParkFromStream(IStream* stream, const std::string& path, bool loadTitleScreenFirstOnFail) final override { try { ClassifiedFileInfo info; if (!TryClassifyFile(stream, &info)) { throw std::runtime_error("Unable to detect file type"); } if (info.Type != FILE_TYPE::SAVED_GAME && info.Type != FILE_TYPE::SCENARIO) { throw std::runtime_error("Invalid file type."); } std::unique_ptr parkImporter; if (info.Version <= FILE_TYPE_S4_CUTOFF) { // Save is an S4 (RCT1 format) parkImporter = ParkImporter::CreateS4(); } else { // Save is an S6 (RCT2 format) parkImporter = ParkImporter::CreateS6(*_objectRepository); } auto result = parkImporter->LoadFromStream(stream, info.Type == FILE_TYPE::SCENARIO, false, path.c_str()); // From this point onwards the currently loaded park will be corrupted if loading fails // so reload the title screen if that happens. loadTitleScreenFirstOnFail = true; _objectManager->LoadObjects(result.RequiredObjects.data(), result.RequiredObjects.size()); parkImporter->Import(); gScenarioSavePath = path; gCurrentLoadedPath = path; gFirstTimeSaving = true; game_fix_save_vars(); AutoCreateMapAnimations(); EntityTweener::Get().Reset(); gScreenAge = 0; gLastAutoSaveUpdate = AUTOSAVE_PAUSE; bool sendMap = false; if (info.Type == FILE_TYPE::SAVED_GAME) { if (network_get_mode() == NETWORK_MODE_CLIENT) { network_close(); } game_load_init(); if (network_get_mode() == NETWORK_MODE_SERVER) { sendMap = true; } } else { scenario_begin(); if (network_get_mode() == NETWORK_MODE_SERVER) { sendMap = true; } if (network_get_mode() == NETWORK_MODE_CLIENT) { network_close(); } } // This ensures that the newly loaded save reflects the user's // 'show real names of guests' option, now that it's a global setting peep_update_names(gConfigGeneral.show_real_names_of_guests); if (sendMap) { network_send_map(); } #ifdef USE_BREAKPAD if (network_get_mode() == NETWORK_MODE_NONE) { start_silent_record(); } #endif return true; } catch (const ObjectLoadException& e) { // If loading the SV6 or SV4 failed return to the title screen if requested. if (loadTitleScreenFirstOnFail) { title_load(); } // The path needs to be duplicated as it's a const here // which the window function doesn't like auto intent = Intent(WC_OBJECT_LOAD_ERROR); intent.putExtra(INTENT_EXTRA_PATH, path); intent.putExtra(INTENT_EXTRA_LIST, const_cast(e.MissingObjects.data())); intent.putExtra(INTENT_EXTRA_LIST_COUNT, static_cast(e.MissingObjects.size())); auto windowManager = _uiContext->GetWindowManager(); windowManager->OpenIntent(&intent); } catch (const UnsupportedRCTCFlagException& e) { // If loading the SV6 or SV4 failed return to the title screen if requested. if (loadTitleScreenFirstOnFail) { title_load(); } auto windowManager = _uiContext->GetWindowManager(); auto ft = Formatter(); ft.Add(e.Flag); windowManager->ShowError(STR_FAILED_TO_LOAD_IMCOMPATIBLE_RCTC_FLAG, STR_NONE, ft); } catch (const UnsupportedRideTypeException&) { // If loading the SV6 or SV4 failed return to the title screen if requested. if (loadTitleScreenFirstOnFail) { title_load(); } auto windowManager = _uiContext->GetWindowManager(); windowManager->ShowError(STR_FILE_CONTAINS_UNSUPPORTED_RIDE_TYPES, STR_NONE, {}); } catch (const std::exception& e) { // If loading the SV6 or SV4 failed return to the title screen if requested. if (loadTitleScreenFirstOnFail) { title_load(); } Console::Error::WriteLine(e.what()); } return false; } private: std::string GetOrPromptRCT2Path() { auto result = std::string(); if (String::IsNullOrEmpty(gCustomRCT2DataPath)) { // Check install directory if (gConfigGeneral.rct2_path == nullptr || !platform_original_game_data_exists(gConfigGeneral.rct2_path)) { log_verbose("install directory does not exist or invalid directory selected, %s", gConfigGeneral.rct2_path); if (!config_find_or_browse_install_directory()) { utf8 path[MAX_PATH]; config_get_default_path(path, sizeof(path)); Console::Error::WriteLine( "An RCT2 install directory must be specified! Please edit \"game_path\" in %s.\n", path); return std::string(); } } result = std::string(gConfigGeneral.rct2_path); } else { result = std::string(gCustomRCT2DataPath); } return result; } bool LoadBaseGraphics() { if (!gfx_load_g1(*_env)) { return false; } gfx_load_g2(); gfx_load_csg(); font_sprite_initialise_characters(); return true; } /** * Launches the game, after command line arguments have been parsed and processed. */ void Launch() { if (!_versionCheckFuture.valid()) { _versionCheckFuture = std::async(std::launch::async, [this] { _newVersionInfo = get_latest_version(); if (!String::StartsWith(gVersionInfoTag, _newVersionInfo.tag)) { _hasNewVersionInfo = true; } }); } gIntroState = IntroState::None; if (gOpenRCT2Headless) { // NONE or OPEN are the only allowed actions for headless mode if (gOpenRCT2StartupAction != StartupAction::Open) { gOpenRCT2StartupAction = StartupAction::None; } } else { if ((gOpenRCT2StartupAction == StartupAction::Title) && gConfigGeneral.play_intro) { gOpenRCT2StartupAction = StartupAction::Intro; } } switch (gOpenRCT2StartupAction) { case StartupAction::Intro: gIntroState = IntroState::PublisherBegin; title_load(); break; case StartupAction::Title: title_load(); break; case StartupAction::Open: { // A path that includes "://" is illegal with all common filesystems, so it is almost certainly a URL // This way all cURL supported protocols, like http, ftp, scp and smb are automatically handled if (strstr(gOpenRCT2StartupActionPath, "://") != nullptr) { #ifndef DISABLE_HTTP // Download park and open it using its temporary filename auto data = DownloadPark(gOpenRCT2StartupActionPath); if (data.empty()) { title_load(); break; } auto ms = MemoryStream(data.data(), data.size(), MEMORY_ACCESS::READ); if (!LoadParkFromStream(&ms, gOpenRCT2StartupActionPath, true)) { Console::Error::WriteLine("Failed to load '%s'", gOpenRCT2StartupActionPath); title_load(); break; } #endif } else { try { if (!LoadParkFromFile(gOpenRCT2StartupActionPath, true)) { break; } } catch (const std::exception& ex) { Console::Error::WriteLine("Failed to load '%s'", gOpenRCT2StartupActionPath); Console::Error::WriteLine("%s", ex.what()); title_load(); break; } } gScreenFlags = SCREEN_FLAGS_PLAYING; #ifndef DISABLE_NETWORK if (gNetworkStart == NETWORK_MODE_SERVER) { if (gNetworkStartPort == 0) { gNetworkStartPort = gConfigNetwork.default_port; } if (gNetworkStartAddress.empty()) { gNetworkStartAddress = gConfigNetwork.listen_address; } if (String::IsNullOrEmpty(gCustomPassword)) { network_set_password(gConfigNetwork.default_password.c_str()); } else { network_set_password(gCustomPassword); } network_begin_server(gNetworkStartPort, gNetworkStartAddress); } else #endif // DISABLE_NETWORK { game_load_scripts(); } break; } case StartupAction::Edit: if (String::SizeOf(gOpenRCT2StartupActionPath) == 0) { Editor::Load(); } else if (!Editor::LoadLandscape(gOpenRCT2StartupActionPath)) { title_load(); } break; default: break; } #ifndef DISABLE_NETWORK if (gNetworkStart == NETWORK_MODE_CLIENT) { if (gNetworkStartPort == 0) { gNetworkStartPort = gConfigNetwork.default_port; } network_begin_client(gNetworkStartHost, gNetworkStartPort); } #endif // DISABLE_NETWORK _stdInOutConsole.Start(); RunGameLoop(); } bool ShouldRunVariableFrame() { if (!gConfigGeneral.uncap_fps) return false; if (gGameSpeed > 4) return false; if (gOpenRCT2Headless) return false; if (_uiContext->IsMinimised()) return false; return true; } /** * Run the main game loop until the finished flag is set. */ void RunGameLoop() { log_verbose("begin openrct2 loop"); _finished = false; #ifndef __EMSCRIPTEN__ _variableFrame = ShouldRunVariableFrame(); do { RunFrame(); } while (!_finished); #else emscripten_set_main_loop_arg( [](void* vctx) -> { auto ctx = reinterpret_cast(vctx); ctx->RunFrame(); }, this, 0, 1); #endif // __EMSCRIPTEN__ log_verbose("finish openrct2 loop"); } void RunFrame() { // Make sure we catch the state change and reset it. bool useVariableFrame = ShouldRunVariableFrame(); if (_variableFrame != useVariableFrame) { _lastTick = 0; _variableFrame = useVariableFrame; } if (useVariableFrame) { RunVariableFrame(); } else { RunFixedFrame(); } } void RunFixedFrame() { uint32_t currentTick = platform_get_ticks(); if (_lastTick == 0) { _lastTick = currentTick; } uint32_t elapsed = currentTick - _lastTick; _lastTick = currentTick; _accumulator = std::min(_accumulator + elapsed, static_cast(GAME_UPDATE_MAX_THRESHOLD)); _uiContext->ProcessMessages(); if (_accumulator < GAME_UPDATE_TIME_MS) { platform_sleep(GAME_UPDATE_TIME_MS - _accumulator - 1); return; } while (_accumulator >= GAME_UPDATE_TIME_MS) { Update(); _accumulator -= GAME_UPDATE_TIME_MS; } if (!_isWindowMinimised && !gOpenRCT2Headless) { _drawingEngine->BeginDraw(); _painter->Paint(*_drawingEngine); _drawingEngine->EndDraw(); _drawingEngine->UpdateWindows(); } } void RunVariableFrame() { uint32_t currentTick = platform_get_ticks(); auto& tweener = EntityTweener::Get(); bool draw = !_isWindowMinimised && !gOpenRCT2Headless; if (_lastTick == 0) { tweener.Reset(); _lastTick = currentTick; } uint32_t elapsed = currentTick - _lastTick; _lastTick = currentTick; _accumulator = std::min(_accumulator + elapsed, static_cast(GAME_UPDATE_MAX_THRESHOLD)); _uiContext->ProcessMessages(); while (_accumulator >= GAME_UPDATE_TIME_MS) { // Get the original position of each sprite if (draw) tweener.PreTick(); Update(); _accumulator -= GAME_UPDATE_TIME_MS; // Get the next position of each sprite if (draw) tweener.PostTick(); } if (draw) { const float alpha = std::min(static_cast(_accumulator) / GAME_UPDATE_TIME_MS, 1.0f); tweener.Tween(alpha); _drawingEngine->BeginDraw(); _painter->Paint(*_drawingEngine); _drawingEngine->EndDraw(); // Note: It's important to call UpdateWindows after restoring the sprite positions, not in between, // otherwise the window updates to positions of sprites could be reverted. // This can be observed when changing ride settings using the mouse wheel that removes all // vehicles and peeps from the ride: it can freeze the game. _drawingEngine->UpdateWindows(); } } void Update() { uint32_t currentUpdateTime = platform_get_ticks(); gCurrentDeltaTime = std::min(currentUpdateTime - _lastUpdateTime, 500); _lastUpdateTime = currentUpdateTime; if (game_is_not_paused()) { gPaletteEffectFrame += gCurrentDeltaTime; } date_update_real_time_of_day(); if (gIntroState != IntroState::None) { intro_update(); } else if ((gScreenFlags & SCREEN_FLAGS_TITLE_DEMO) && !gOpenRCT2Headless) { _titleScreen->Update(); } else { _gameState->Update(); } #ifdef __ENABLE_DISCORD__ if (_discordService != nullptr) { _discordService->Update(); } #endif chat_update(); #ifdef ENABLE_SCRIPTING _scriptEngine.Update(); #endif _stdInOutConsole.ProcessEvalQueue(); _uiContext->Update(); } /** * Ensure that the custom user content folders are present */ void EnsureUserContentDirectoriesExist() { EnsureDirectoriesExist( DIRBASE::USER, { DIRID::OBJECT, DIRID::SAVE, DIRID::SCENARIO, DIRID::TRACK, DIRID::LANDSCAPE, DIRID::HEIGHTMAP, DIRID::PLUGIN, DIRID::THEME, DIRID::SEQUENCE, DIRID::REPLAY, DIRID::LOG_DESYNCS, }); } void EnsureDirectoriesExist(const DIRBASE dirBase, const std::initializer_list& dirIds) { for (const auto& dirId : dirIds) { auto path = _env->GetDirectoryPath(dirBase, dirId); if (!platform_ensure_directory_exists(path.c_str())) log_error("Unable to create directory '%s'.", path.c_str()); } } /** * Copy saved games and landscapes to user directory */ void CopyOriginalUserFilesOver() { CopyOriginalUserFilesOver(DIRID::SAVE, "*.sv6"); CopyOriginalUserFilesOver(DIRID::LANDSCAPE, "*.sc6"); } void CopyOriginalUserFilesOver(DIRID dirid, const std::string& pattern) { auto src = _env->GetDirectoryPath(DIRBASE::RCT2, dirid); auto dst = _env->GetDirectoryPath(DIRBASE::USER, dirid); CopyOriginalUserFilesOver(src, dst, pattern); } void CopyOriginalUserFilesOver(const std::string& srcRoot, const std::string& dstRoot, const std::string& pattern) { log_verbose("CopyOriginalUserFilesOver('%s', '%s', '%s')", srcRoot.c_str(), dstRoot.c_str(), pattern.c_str()); auto scanPattern = Path::Combine(srcRoot, pattern); auto scanner = Path::ScanDirectory(scanPattern, true); while (scanner->Next()) { auto src = std::string(scanner->GetPath()); auto dst = Path::Combine(dstRoot, scanner->GetPathRelative()); auto dstDirectory = Path::GetDirectory(dst); // Create the directory if necessary if (!platform_directory_exists(dstDirectory.c_str())) { Console::WriteLine("Creating directory '%s'", dstDirectory.c_str()); if (!platform_ensure_directory_exists(dstDirectory.c_str())) { Console::Error::WriteLine("Could not create directory %s.", dstDirectory.c_str()); break; } } // Only copy the file if it doesn't already exist if (!File::Exists(dst)) { Console::WriteLine("Copying '%s' to '%s'", src.c_str(), dst.c_str()); if (!File::Copy(src, dst, false)) { Console::Error::WriteLine("Failed to copy '%s' to '%s'", src.c_str(), dst.c_str()); } } } delete scanner; } #ifndef DISABLE_HTTP std::vector DownloadPark(const std::string& url) { // Download park to buffer in memory Http::Request request; request.url = url; request.method = Http::Method::GET; Http::Response res; try { res = Do(request); if (res.status != Http::Status::Ok) throw std::runtime_error("bad http status"); } catch (std::exception& e) { Console::Error::WriteLine("Failed to download '%s', cause %s", request.url.c_str(), e.what()); return {}; } std::vector parkData; parkData.resize(res.body.size()); std::memcpy(parkData.data(), res.body.c_str(), parkData.size()); return parkData; } #endif bool HasNewVersionInfo() const override { return _hasNewVersionInfo; } const NewVersionInfo* GetNewVersionInfo() const override { return &_newVersionInfo; } }; Context* Context::Instance = nullptr; std::unique_ptr CreateContext() { return CreateContext(CreatePlatformEnvironment(), CreateDummyAudioContext(), CreateDummyUiContext()); } std::unique_ptr CreateContext( const std::shared_ptr& env, const std::shared_ptr& audioContext, const std::shared_ptr& uiContext) { return std::make_unique(env, audioContext, uiContext); } IContext* GetContext() { return Context::Instance; } } // namespace OpenRCT2 void context_init() { GetContext()->GetUiContext()->GetWindowManager()->Init(); } bool context_load_park_from_file(const utf8* path) { return GetContext()->LoadParkFromFile(path); } bool context_load_park_from_stream(void* stream) { return GetContext()->LoadParkFromStream(static_cast(stream), ""); } void openrct2_write_full_version_info(utf8* buffer, size_t bufferSize) { String::Set(buffer, bufferSize, gVersionInfoFull); } void openrct2_finish() { GetContext()->Finish(); } void context_setcurrentcursor(CursorID cursor) { GetContext()->GetUiContext()->SetCursor(cursor); } void context_update_cursor_scale() { GetContext()->GetUiContext()->SetCursorScale(static_cast(std::round(gConfigGeneral.window_scale))); } void context_hide_cursor() { GetContext()->GetUiContext()->SetCursorVisible(false); } void context_show_cursor() { GetContext()->GetUiContext()->SetCursorVisible(true); } ScreenCoordsXY context_get_cursor_position() { return GetContext()->GetUiContext()->GetCursorPosition(); } ScreenCoordsXY context_get_cursor_position_scaled() { auto cursorCoords = context_get_cursor_position(); // Compensate for window scaling. return { static_cast(std::ceil(cursorCoords.x / gConfigGeneral.window_scale)), static_cast(std::ceil(cursorCoords.y / gConfigGeneral.window_scale)) }; } void context_set_cursor_position(const ScreenCoordsXY& cursorPosition) { GetContext()->GetUiContext()->SetCursorPosition(cursorPosition); } const CursorState* context_get_cursor_state() { return GetContext()->GetUiContext()->GetCursorState(); } const uint8_t* context_get_keys_state() { return GetContext()->GetUiContext()->GetKeysState(); } const uint8_t* context_get_keys_pressed() { return GetContext()->GetUiContext()->GetKeysPressed(); } TextInputSession* context_start_text_input(utf8* buffer, size_t maxLength) { return GetContext()->GetUiContext()->StartTextInput(buffer, maxLength); } void context_stop_text_input() { GetContext()->GetUiContext()->StopTextInput(); } bool context_is_input_active() { return GetContext()->GetUiContext()->IsTextInputActive(); } void context_trigger_resize() { return GetContext()->GetUiContext()->TriggerResize(); } void context_set_fullscreen_mode(int32_t mode) { return GetContext()->GetUiContext()->SetFullscreenMode(static_cast(mode)); } void context_recreate_window() { GetContext()->GetUiContext()->RecreateWindow(); } int32_t context_get_width() { return GetContext()->GetUiContext()->GetWidth(); } int32_t context_get_height() { return GetContext()->GetUiContext()->GetHeight(); } bool context_has_focus() { return GetContext()->GetUiContext()->HasFocus(); } void context_set_cursor_trap(bool value) { GetContext()->GetUiContext()->SetCursorTrap(value); } rct_window* context_open_window(rct_windowclass wc) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); return windowManager->OpenWindow(wc); } rct_window* context_open_window_view(rct_windowclass wc) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); return windowManager->OpenView(wc); } rct_window* context_open_detail_window(uint8_t type, int32_t id) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); return windowManager->OpenDetails(type, id); } rct_window* context_open_intent(Intent* intent) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); return windowManager->OpenIntent(intent); } void context_broadcast_intent(Intent* intent) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); windowManager->BroadcastIntent(*intent); } void context_force_close_window_by_class(rct_windowclass windowClass) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); windowManager->ForceClose(windowClass); } rct_window* context_show_error(rct_string_id title, rct_string_id message, const Formatter& args) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); return windowManager->ShowError(title, message, args); } void context_update_map_tooltip() { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); windowManager->UpdateMapTooltip(); } void context_handle_input() { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); windowManager->HandleInput(); } void context_input_handle_keyboard(bool isTitle) { auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); windowManager->HandleKeyboard(isTitle); } void context_quit() { GetContext()->Quit(); } const utf8* context_get_path_legacy(int32_t pathId) { static utf8 result[MAX_PATH]; auto path = GetContext()->GetPathLegacy(pathId); String::Set(result, sizeof(result), path.c_str()); return result; } bool platform_open_common_file_dialog(utf8* outFilename, file_dialog_desc* desc, size_t outSize) { try { FileDialogDesc desc2; desc2.Type = static_cast(desc->type); desc2.Title = String::ToStd(desc->title); desc2.InitialDirectory = String::ToStd(desc->initial_directory); desc2.DefaultFilename = String::ToStd(desc->default_filename); for (const auto& filter : desc->filters) { if (filter.name != nullptr) { desc2.Filters.push_back({ String::ToStd(filter.name), String::ToStd(filter.pattern) }); } } std::string result = GetContext()->GetUiContext()->ShowFileDialog(desc2); String::Set(outFilename, outSize, result.c_str()); return !result.empty(); } catch (const std::exception& ex) { log_error(ex.what()); outFilename[0] = '\0'; return false; } } utf8* platform_open_directory_browser(const utf8* title) { try { std::string result = GetContext()->GetUiContext()->ShowDirectoryDialog(title); return String::Duplicate(result.c_str()); } catch (const std::exception& ex) { log_error(ex.what()); return nullptr; } } bool platform_place_string_on_clipboard(utf8* target) { return GetContext()->GetUiContext()->SetClipboardText(target); } /** * This function is deprecated. * Use IPlatformEnvironment instead. */ void platform_get_user_directory(utf8* outPath, const utf8* subDirectory, size_t outSize) { auto env = GetContext()->GetPlatformEnvironment(); auto path = env->GetDirectoryPath(DIRBASE::USER); if (!String::IsNullOrEmpty(subDirectory)) { path = Path::Combine(path, subDirectory); } String::Set(outPath, outSize, path.c_str()); } /** * This function is deprecated. * Use IPlatformEnvironment instead. */ void platform_get_openrct2_data_path(utf8* outPath, size_t outSize) { auto env = GetContext()->GetPlatformEnvironment(); auto path = env->GetDirectoryPath(DIRBASE::OPENRCT2); String::Set(outPath, outSize, path.c_str()); }