/*****************************************************************************
 * Copyright (c) 2014-2026 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 <cassert>
    #include <emscripten.h>
#endif // __EMSCRIPTEN__

#include "AssetPackManager.h"
#include "Context.h"
#include "Editor.h"
#include "FileClassifier.h"
#include "Game.h"
#include "GameState.h"
#include "GameStateSnapshots.h"
#include "Input.h"
#include "OpenRCT2.h"
#include "ParkImporter.h"
#include "PlatformEnvironment.h"
#include "ReplayManager.h"
#include "Version.h"
#include "actions/GameActionRunner.h"
#include "audio/Audio.h"
#include "audio/AudioContext.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 "core/Timer.hpp"
#include "drawing/ColourMap.h"
#include "drawing/Drawing.h"
#include "drawing/IDrawingEngine.h"
#include "drawing/Image.h"
#include "drawing/LightFX.h"
#include "entity/EntityTweener.h"
#include "entity/PatrolArea.h"
#include "interface/Chat.h"
#include "interface/StdInOutConsole.h"
#include "interface/Viewport.h"
#include "localisation/Formatter.h"
#include "localisation/Localisation.Date.h"
#include "localisation/LocalisationService.h"
#include "network/DiscordService.h"
#include "network/Network.h"
#include "network/NetworkBase.h"
#include "object/ObjectManager.h"
#include "object/ObjectRepository.h"
#include "paint/Painter.h"
#include "park/ParkFile.h"
#include "platform/Crash.h"
#include "platform/Platform.h"
#include "profiling/Profiling.h"
#include "rct2/RCT2.h"
#include "ride/TrackDesignRepository.h"
#include "scenario/Scenario.h"
#include "scenario/ScenarioRepository.h"
#include "scenes/SceneManager.h"
#include "scenes/game/GameScene.h"
#include "scenes/intro/IntroScene.h"
#include "scenes/preloader/PreloaderScene.h"
#include "scenes/title/TitleScene.h"
#include "scenes/title/TitleSequenceManager.h"
#include "scripting/HookEngine.h"
#include "scripting/ScriptEngine.h"
#include "ui/UiContext.h"
#include "ui/WindowManager.h"
#include "world/MapAnimation.h"
#include "world/MapSelection.h"

#include <chrono>
#include <cmath>
#include <exception>
#include <future>
#include <iterator>
#include <memory>
#include <string>

using namespace OpenRCT2;
using namespace OpenRCT2::Ui;

using OpenRCT2::Audio::IAudioContext;

namespace OpenRCT2
{
    namespace
    {
        using namespace std::chrono_literals;

        static constexpr auto kForcedUpdateInterval = 25ms;
    } // namespace

    class Context final : public IContext
    {
    private:
        // Dependencies
        std::unique_ptr<IPlatformEnvironment> const _env;
        std::unique_ptr<IAudioContext> const _audioContext;
        std::unique_ptr<IUiContext> const _uiContext;

        // Services
        std::unique_ptr<Localisation::LocalisationService> _localisationService;
        std::unique_ptr<IObjectRepository> _objectRepository;
        std::unique_ptr<IObjectManager> _objectManager;
        std::unique_ptr<ITrackDesignRepository> _trackDesignRepository;
        std::unique_ptr<IScenarioRepository> _scenarioRepository;
        std::unique_ptr<ISceneManager> _sceneManager;
        std::unique_ptr<IReplayManager> _replayManager;
        std::unique_ptr<IGameStateSnapshots> _gameStateSnapshots;
        std::unique_ptr<AssetPackManager> _assetPackManager;
#ifdef __ENABLE_DISCORD__
        std::unique_ptr<Network::DiscordService> _discordService;
#endif
        StdInOutConsole _stdInOutConsole;
#ifdef ENABLE_SCRIPTING
        Scripting::ScriptEngine _scriptEngine;
#endif
#ifndef DISABLE_NETWORK
        Network::NetworkBase _network;
#endif

        DrawingEngine _drawingEngineType = DrawingEngine::SoftwareWithHardwareDisplay;
        std::unique_ptr<Drawing::IDrawingEngine> _drawingEngine;
        std::unique_ptr<Paint::Painter> _painter;

        bool _initialised = false;

        Timer _timer;
        float _ticksAccumulator = 0.0f;
        float _realtimeAccumulator = 0.0f;
        float _timeScale = 1.0f;
        bool _variableFrame = false;

        // If set, will end the OpenRCT2 game loop. Intentionally private to this module so that the flag can not be set back to
        // false.
        bool _finished = false;

        std::future<void> _versionCheckFuture;
        NewVersionInfo _newVersionInfo;
        bool _hasNewVersionInfo = false;

        // We keep track of this to perform certain operations differently.
        std::thread::id _mainThreadId{};
        Timer _forcedUpdateTimer;

        BackgroundWorker _backgroundWorker;

    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(
            std::unique_ptr<IPlatformEnvironment>&& env, std::unique_ptr<IAudioContext>&& audioContext,
            std::unique_ptr<IUiContext>&& uiContext)
            : _env(std::move(env))
            , _audioContext(std::move(audioContext))
            , _uiContext(std::move(uiContext))
            , _localisationService(std::make_unique<Localisation::LocalisationService>(*_env))
            , _replayManager(CreateReplayManager())
            , _gameStateSnapshots(CreateGameStateSnapshots())
#ifdef ENABLE_SCRIPTING
            , _scriptEngine(_stdInOutConsole, *_env)
#endif
#ifndef DISABLE_NETWORK
            , _network(*this)
#endif
            , _painter(std::make_unique<Paint::Painter>(*_uiContext))
        {
            // Can't have more than one context currently.
            Guard::Assert(Instance == nullptr);

            Instance = this;
            _mainThreadId = std::this_thread::get_id();
        }

        ~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.

#ifdef ENABLE_SCRIPTING
            _scriptEngine.StopUnloadRegisterAllPlugins();
#endif

            GameActions::ClearQueue();
            _replayManager->StopRecording(true);
#ifndef DISABLE_NETWORK
            _network.Close();
#endif

            auto* windowMgr = GetWindowManager();
            windowMgr->Cleanup();

            // 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();
            }

            GfxObjectCheckAllImagesFreed();
            GfxUnloadCsg();
            GfxUnloadG2PalettesFontsTracks();
            GfxUnloadG1();
            Audio::Close();

            Instance = nullptr;
        }

        IAudioContext& GetAudioContext() override
        {
            return *_audioContext;
        }

        IUiContext& GetUiContext() override
        {
            return *_uiContext;
        }

#ifdef ENABLE_SCRIPTING
        Scripting::ScriptEngine& GetScriptEngine() override
        {
            return _scriptEngine;
        }
#endif

        IPlatformEnvironment& 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();
        }

        ISceneManager* GetSceneManager() override
        {
            return _sceneManager.get();
        }

        IReplayManager* GetReplayManager() override
        {
            return _replayManager.get();
        }

        IGameStateSnapshots* GetGameStateSnapshots() override
        {
            return _gameStateSnapshots.get();
        }

        AssetPackManager* GetAssetPackManager() override
        {
            return _assetPackManager.get();
        }

        DrawingEngine GetDrawingEngineType() override
        {
            return _drawingEngineType;
        }

        Drawing::IDrawingEngine* GetDrawingEngine() override
        {
            return _drawingEngine.get();
        }

        Paint::Painter* GetPainter() override
        {
            return _painter.get();
        }

#ifndef DISABLE_NETWORK
        Network::NetworkBase& GetNetwork() override
        {
            return _network;
        }
#endif

        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);
        }

        void WriteErrorLine(const std::string& s) override
        {
            _stdInOutConsole.WriteLineError(s);
        }

        /**
         * Causes the OpenRCT2 game loop to finish.
         */
        void Finish() override
        {
            _finished = true;
        }

        void Quit() override
        {
            gSavePromptMode = PromptMode::quit;
            ContextOpenWindow(WindowClass::savePrompt);
        }

        bool Initialise() final override
        {
            if (_initialised)
            {
                throw std::runtime_error("Context already initialised.");
            }
            _initialised = true;

            CrashInit();

            if (String::equals(Config::Get().general.lastRunVersion, kOpenRCT2Version))
            {
                gOpenRCT2ShowChangelog = false;
            }
            else
            {
                gOpenRCT2ShowChangelog = true;
                Config::Get().general.lastRunVersion = kOpenRCT2Version;
                Config::Save();
            }

            try
            {
                _localisationService->OpenLanguage(Config::Get().general.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();
#ifdef __ANDROID__
                    uiContext.ShowMessageBox(
                        "You need to copy some additional files to finish your install.\n\nSee "
                        "https://docs.openrct2.io/en/latest/installing/installing-on-android.html for more details.");
#else
                    uiContext.ShowMessageBox("Failed to load language file!\nYour installation may be damaged.");
#endif
                    return false;
                }
            }

            // TODO add configuration option to allow multiple instances
            // if (!gOpenRCT2Headless && !Platform::LockSingleInstance()) {
            //  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);
            }

            // The repositories are all dependent on the RCT2 path being set,
            // so they cannot be set in the constructor.
            _objectRepository = CreateObjectRepository(*_env);
            _objectManager = CreateObjectManager(*_objectRepository);
            _trackDesignRepository = CreateTrackDesignRepository(*_env);
            _scenarioRepository = CreateScenarioRepository(*_env);
            _sceneManager = createSceneManager(this);

            if (!gOpenRCT2Headless)
            {
                _assetPackManager = std::make_unique<AssetPackManager>();
            }
#ifdef __ENABLE_DISCORD__
            if (!gOpenRCT2Headless)
            {
                _discordService = std::make_unique<Network::DiscordService>();
            }
#endif

#ifndef __HAIKU__ // Haiku's user is always root, skip warning them about it.
            if (Platform::ProcessIsElevated())
            {
                std::string elevationWarning = _localisationService->GetString(STR_ADMIN_NOT_RECOMMENDED);
                if (gOpenRCT2Headless)
                {
                    Console::Error::WriteLine(elevationWarning.c_str());
                }
                else
                {
                    _uiContext->ShowMessageBox(elevationWarning);
                }
            }
#endif

            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();

            if (!gOpenRCT2Headless)
            {
                Audio::Init();
                Audio::PopulateDevices();
                Audio::InitRideSoundsAndInfo();
                Audio::gGameSoundsOff = !Config::Get().sound.masterSoundEnabled;
            }

            ChatInit();
            CopyOriginalUserFilesOver();

            if (!gOpenRCT2NoGraphics)
            {
                if (!LoadBaseGraphics())
                {
                    return false;
                }
                Drawing::LightFx::Init();
            }

            ContextInit();
            ResetSubsystems();

            if (!gOpenRCT2Headless)
            {
                auto* preloaderScene = static_cast<PreloaderScene*>(_sceneManager->getPreloaderScene());
                _sceneManager->setActiveScene(preloaderScene);

                // TODO: preload the title scene in another (parallel) job.
                preloaderScene->AddJob([this]() { InitialiseRepositories(); });
            }
            else
            {
                InitialiseRepositories();
            }

#ifdef ENABLE_SCRIPTING
            // quickjs script engine is single-threaded and must be set up on the main thread
            _scriptEngine.Initialise();
            _uiContext->InitialiseScriptExtensions();
#endif

            return true;
        }

        /**
         *  rct2: 0x006E6EAC
         */
        void ResetSubsystems() override
        {
            if (!gOpenRCT2NoGraphics)
            {
                Drawing::initColourMaps();
            }

            WindowInitAll();

            gInputFlags.clearAll();
            InputSetState(InputState::reset);
            gPressedWidget.windowClassification = WindowClass::null;
            gPickupPeepImage = ImageId();
            ResetTooltipNotShown();
            gMapSelectFlags.clearAll();
            ClearPatrolAreaToRender();
            TextinputCancel();
        }

    private:
        void InitialiseRepositories()
        {
            if (!_initialised)
            {
                throw std::runtime_error("Context needs to be initialised first.");
            }

            auto currentLanguage = _localisationService->GetCurrentLanguage();

            OpenProgress(STR_CHECKING_OBJECT_FILES);
            _objectRepository->LoadOrConstruct(currentLanguage);

            // Asset packs need to be loaded before any of the objects they may override are.
            // This is especially important to keep in mind with intransient objects like Audio objects,
            // which are only loaded once.
            if (!gOpenRCT2Headless)
            {
                OpenProgress(STR_CHECKING_ASSET_PACKS);
                _assetPackManager->Scan();
                _assetPackManager->LoadEnabledAssetPacks();
                _assetPackManager->Reload();
            }

            OpenProgress(STR_LOADING_GENERIC);
            Audio::LoadAudioObjects();

            OpenProgress(STR_CHECKING_TRACK_DESIGN_FILES);
            _trackDesignRepository->Scan(currentLanguage);

            OpenProgress(STR_CHECKING_SCENARIO_FILES);
            _scenarioRepository->Scan(currentLanguage);

            OpenProgress(STR_CHECKING_TITLE_SEQUENCES);
            TitleSequenceManager::Scan();

            OpenProgress(STR_LOADING_GENERIC);
        }

    public:
        void InitialiseDrawingEngine() final override
        {
            assert(_drawingEngine == nullptr);

            const auto initializeEngine = [&](DrawingEngine engine) -> std::unique_ptr<Drawing::IDrawingEngine> {
                try
                {
                    auto drawingEngineFactory = _uiContext->GetDrawingEngineFactory();
                    auto drawingEngine = drawingEngineFactory->Create(engine, *_uiContext);
                    if (drawingEngine == nullptr)
                    {
                        LOG_FATAL("Unable to create a drawing engine.");
                        return nullptr;
                    }

                    drawingEngine->Initialise();
                    drawingEngine->SetVSync(Config::Get().general.useVSync);

                    return drawingEngine;
                }
                catch (std::exception& ex)
                {
                    LOG_ERROR(ex.what());
                    LOG_ERROR("Unable to initialise drawing engine.");
                }
                return nullptr;
            };

            auto drawingEngineType = Config::Get().general.drawingEngine;

            // Attempt to create drawing engine of the type specified in the config.
            {
                auto drawingEngine = initializeEngine(drawingEngineType);
                if (drawingEngine != nullptr)
                {
                    _drawingEngine = std::move(drawingEngine);
                }
                else
                {
                    // If the drawing engine creation failed, try to create a software engine.
                    if (drawingEngineType == DrawingEngine::OpenGL)
                    {
                        drawingEngineType = DrawingEngine::SoftwareWithHardwareDisplay;
                        LOG_ERROR("Trying fallback back to software...");

                        drawingEngine = initializeEngine(drawingEngineType);
                        if (drawingEngine != nullptr)
                        {
                            _drawingEngine = std::move(drawingEngine);
                        }
                        else
                        {
                            LOG_FATAL("Unable to create any renderer.");
                            exit(-1);
                        }
                    }
                }
            }

            _drawingEngineType = drawingEngineType;

            Config::Get().general.drawingEngine = drawingEngineType;
            Config::Save();

            WindowCheckAllValidZoom();
        }

        void DisposeDrawingEngine() final override
        {
            _drawingEngine = nullptr;
        }

        void OpenProgress(StringId captionStringId) override
        {
            auto captionString = _localisationService->GetString(captionStringId);
            auto intent = Intent(INTENT_ACTION_PROGRESS_OPEN);
            intent.PutExtra(INTENT_EXTRA_MESSAGE, captionString);
            ContextOpenIntent(&intent);
        }

        void SetProgress(uint32_t currentProgress, uint32_t totalCount, StringId format = kStringIdNone) override
        {
            if (_forcedUpdateTimer.GetElapsedTime() < kForcedUpdateInterval)
                return;

            _forcedUpdateTimer.Restart();

            auto intent = Intent(INTENT_ACTION_PROGRESS_SET);
            intent.PutExtra(INTENT_EXTRA_PROGRESS_OFFSET, currentProgress);
            intent.PutExtra(INTENT_EXTRA_PROGRESS_TOTAL, totalCount);
            intent.PutExtra(INTENT_EXTRA_STRING_ID, format);
            ContextOpenIntent(&intent);

            // When we call this from the main thread we can pump messages and redraw.
            const auto isMainThread = _mainThreadId == std::this_thread::get_id();

            if (!gOpenRCT2Headless && isMainThread)
            {
                _uiContext->ProcessMessages();
                auto* windowMgr = GetWindowManager();
                windowMgr->InvalidateByClass(WindowClass::progressWindow);
                Draw();
            }
        }

        void CloseProgress() override
        {
            auto intent = Intent(INTENT_ACTION_PROGRESS_CLOSE);
            ContextOpenIntent(&intent);
        }

        bool LoadParkFromFile(const u8string& path, bool loadTitleScreenOnFail = false, bool asScenario = false) final override
        {
            LOG_VERBOSE("Context::LoadParkFromFile(%s)", path.c_str());

            struct CrashAdditionalFileRegistration
            {
                CrashAdditionalFileRegistration(const std::string& path)
                {
                    // Register the file for crash upload if it asserts while loading.
                    CrashRegisterAdditionalFile("load_park", path);
                }
                ~CrashAdditionalFileRegistration()
                {
                    // Deregister park file in case it was processed without hitting an assert.
                    CrashUnregisterAdditionalFile("load_park");
                }
            } crash_additional_file_registration(path);

            try
            {
                if (String::iequals(Path::GetExtension(path), ".sea"))
                {
                    auto data = DecryptSea(fs::u8path(path));
                    auto ms = MemoryStream(data.data(), data.size(), MemoryAccess::read);
                    if (!LoadParkFromStream(&ms, path, loadTitleScreenOnFail, asScenario))
                    {
                        throw std::runtime_error(".sea file may have been renamed.");
                    }
                    return true;
                }

                auto fs = FileStream(path, FileMode::open);
                if (!LoadParkFromStream(&fs, path, loadTitleScreenOnFail, asScenario))
                {
                    return false;
                }
                return true;
            }
            catch (const std::exception& e)
            {
                Console::Error::WriteLine(e.what());
                if (loadTitleScreenOnFail)
                {
                    _sceneManager->setActiveScene(_sceneManager->getTitleScene());
                }
                auto windowManager = _uiContext->GetWindowManager();
                windowManager->ShowError(STR_FAILED_TO_LOAD_FILE_CONTAINS_INVALID_DATA, kStringIdNone, {});
            }
            return false;
        }

        bool LoadParkFromStream(
            IStream* stream, const std::string& path, bool loadTitleScreenFirstOnFail = false,
            bool asScenario = false) final override
        {
            try
            {
                ClassifiedFileInfo info;
                if (!TryClassifyFile(stream, &info))
                {
                    throw std::runtime_error("Unable to detect file type");
                }

                if (info.Type != FileType::park && info.Type != FileType::savedGame && info.Type != FileType::scenario)
                {
                    throw std::runtime_error("Invalid file type.");
                }

                std::unique_ptr<IParkImporter> parkImporter;
                if (info.Type == FileType::park)
                {
                    parkImporter = ParkImporter::CreateParkFile(*_objectRepository);
                }
                else if (info.Version <= kFileTypeS4Cutoff)
                {
                    // Save is an S4 (RCT1 format)
                    parkImporter = ParkImporter::CreateS4();
                }
                else
                {
                    // Save is an S6 (RCT2 format)
                    parkImporter = ParkImporter::CreateS6(*_objectRepository);
                }

                // Inhibit viewport rendering while we're loading
                WindowSetFlagForAllViewports(VIEWPORT_FLAG_RENDERING_INHIBITED, true);

                OpenProgress(asScenario ? STR_LOADING_SCENARIO : STR_LOADING_SAVED_GAME);
                SetProgress(0, 100, STR_STRING_M_PERCENT);

                auto result = parkImporter->LoadFromStream(stream, info.Type == FileType::scenario, false, path.c_str());
                SetProgress(10, 100, STR_STRING_M_PERCENT);

                // From this point onwards the currently loaded park will be corrupted if loading fails
                // so reload the title screen if that happens.
                loadTitleScreenFirstOnFail = true;

                GameUnloadScripts();
                _objectManager->LoadObjects(result.RequiredObjects, true);
                SetProgress(90, 100, STR_STRING_M_PERCENT);

                MapAnimations::ClearAll();
                // TODO: Have a separate GameState and exchange once loaded.
                auto& gameState = ::getGameState();
                parkImporter->Import(gameState);
                SetProgress(100, 100, STR_STRING_M_PERCENT);

                // Reset viewport rendering inhibition
                WindowSetFlagForAllViewports(VIEWPORT_FLAG_RENDERING_INHIBITED, false);

                gScenarioSavePath = path;
                gCurrentLoadedPath = path;
                gFirstTimeSaving = true;
                GameFixSaveVars();
                MapAnimations::MarkAllTiles();
                EntityTweener::Get().Reset();
                gScreenAge = 0;
                gLastAutoSaveUpdate = kAutosavePause;

#ifndef DISABLE_NETWORK
                bool sendMap = false;
#endif
                if (!asScenario && (info.Type == FileType::park || info.Type == FileType::savedGame))
                {
#ifndef DISABLE_NETWORK
                    if (_network.GetMode() == Network::Mode::client)
                    {
                        _network.Close();
                    }
#endif
                    GameLoadInit();
#ifndef DISABLE_NETWORK
                    if (_network.GetMode() == Network::Mode::server)
                    {
                        sendMap = true;
                    }
#endif
                }
                else
                {
                    ScenarioBegin(gameState);
#ifndef DISABLE_NETWORK
                    if (_network.GetMode() == Network::Mode::server)
                    {
                        sendMap = true;
                    }
                    if (_network.GetMode() == Network::Mode::client)
                    {
                        _network.Close();
                    }
#endif
                }
                // This ensures that the newly loaded save reflects the user's
                // 'show real names of guests' option, now that it's a global setting
                PeepUpdateNames();
#ifndef DISABLE_NETWORK
                if (sendMap)
                {
                    _network.ServerSendMap();
                }
#endif

#ifdef USE_BREAKPAD
                if (_network.GetMode() == Network::Mode::none)
                {
                    StartSilentRecord();
                }
#endif
                if (result.SemiCompatibleVersion)
                {
                    auto windowManager = _uiContext->GetWindowManager();
                    auto ft = Formatter();
                    ft.Add<uint32_t>(result.TargetVersion);
                    ft.Add<uint32_t>(kParkFileCurrentVersion);
                    windowManager->ShowError(STR_WARNING_PARK_VERSION_TITLE, STR_WARNING_PARK_VERSION_MESSAGE, ft);
                }
                else if (HasObjectsThatUseFallbackImages())
                {
                    Console::Error::WriteLine("Park has objects which require RCT1 linked. Fallback images will be used.");
                    auto windowManager = _uiContext->GetWindowManager();
                    windowManager->ShowError(STR_PARK_USES_FALLBACK_IMAGES_WARNING, kStringIdEmpty, Formatter());
                }

                CloseProgress();
                return true;
            }
            catch (const ObjectLoadException& e)
            {
                Console::Error::WriteLine("Unable to open park: missing objects");

                // If loading the SV6 or SV4 failed return to the title screen if requested.
                if (loadTitleScreenFirstOnFail)
                {
                    _sceneManager->setActiveScene(_sceneManager->getTitleScene());
                }
                // The path needs to be duplicated as it's a const here
                // which the window function doesn't like
                auto intent = Intent(WindowClass::objectLoadError);
                intent.PutExtra(INTENT_EXTRA_PATH, path);
                intent.PutExtra(INTENT_EXTRA_LIST, const_cast<ObjectEntryDescriptor*>(e.MissingObjects.data()));
                intent.PutExtra(INTENT_EXTRA_LIST_COUNT, static_cast<uint32_t>(e.MissingObjects.size()));

                auto windowManager = _uiContext->GetWindowManager();
                windowManager->OpenIntent(&intent);
            }
            catch (const UnsupportedRideTypeException&)
            {
                Console::Error::WriteLine("Unable to open park: unsupported ride types");

                // If loading the SV6 or SV4 failed return to the title screen if requested.
                if (loadTitleScreenFirstOnFail)
                {
                    _sceneManager->setActiveScene(_sceneManager->getTitleScene());
                }
                auto windowManager = _uiContext->GetWindowManager();
                windowManager->ShowError(STR_FILE_CONTAINS_UNSUPPORTED_RIDE_TYPES, kStringIdNone, {});
            }
            catch (const UnsupportedVersionException& e)
            {
                Console::Error::WriteLine("Unable to open park: unsupported park version");

                if (loadTitleScreenFirstOnFail)
                {
                    _sceneManager->setActiveScene(_sceneManager->getTitleScene());
                }
                auto windowManager = _uiContext->GetWindowManager();
                Formatter ft;
                /*if (e.TargetVersion < kParkFileMinSupportedVersion)
                {
                    ft.Add<uint32_t>(e.TargetVersion);
                    windowManager->ShowError(STR_ERROR_PARK_VERSION_TITLE, STR_ERROR_PARK_VERSION_TOO_OLD_MESSAGE, ft);
                }
                else*/
                {
                    if (e.MinVersion == e.TargetVersion)
                    {
                        ft.Add<uint32_t>(e.TargetVersion);
                        ft.Add<uint32_t>(kParkFileCurrentVersion);
                        windowManager->ShowError(STR_ERROR_PARK_VERSION_TITLE, STR_ERROR_PARK_VERSION_TOO_NEW_MESSAGE_2, ft);
                    }
                    else
                    {
                        ft.Add<uint32_t>(e.TargetVersion);
                        ft.Add<uint32_t>(e.MinVersion);
                        ft.Add<uint32_t>(kParkFileCurrentVersion);
                        windowManager->ShowError(STR_ERROR_PARK_VERSION_TITLE, STR_ERROR_PARK_VERSION_TOO_NEW_MESSAGE, ft);
                    }
                }
            }
            catch (const std::exception& e)
            {
                // If loading the SV6 or SV4 failed return to the title screen if requested.
                if (loadTitleScreenFirstOnFail)
                {
                    _sceneManager->setActiveScene(_sceneManager->getTitleScene());
                }
                Console::Error::WriteLine(e.what());
            }

            CloseProgress();
            WindowSetFlagForAllViewports(VIEWPORT_FLAG_RENDERING_INHIBITED, false);
            return false;
        }

    private:
        bool HasObjectsThatUseFallbackImages()
        {
            for (auto objectType : getAllObjectTypes())
            {
                auto maxObjectsOfType = static_cast<ObjectEntryIndex>(getObjectEntryGroupCount(objectType));
                for (ObjectEntryIndex i = 0; i < maxObjectsOfType; i++)
                {
                    auto obj = _objectManager->GetLoadedObject(objectType, i);
                    if (obj != nullptr)
                    {
                        if (obj->UsesFallbackImages())
                            return true;
                    }
                }
            }
            return false;
        }

        std::string GetOrPromptRCT2Path()
        {
            auto result = std::string();
            if (gCustomRCT2DataPath.empty())
            {
                // Check install directory
                if (Config::Get().general.rct2Path.empty() || !Platform::OriginalGameDataExists(Config::Get().general.rct2Path))
                {
                    LOG_VERBOSE(
                        "install directory does not exist or invalid directory selected, %s",
                        Config::Get().general.rct2Path.c_str());
                    if (!Config::FindOrBrowseInstallDirectory())
                    {
                        auto path = Config::GetDefaultPath();
                        Console::Error::WriteLine(
                            "An RCT2 install directory must be specified! Please edit \"game_path\" in %s.\n", path.c_str());
                        return std::string();
                    }
                }
                result = Config::Get().general.rct2Path;
            }
            else
            {
                result = gCustomRCT2DataPath;
            }
            return result;
        }

        // TODO: move function elsewhere?
        bool LoadBaseGraphics()
        {
            if (!GfxLoadG1(*_env))
            {
                return false;
            }
            GfxLoadG2PalettesFontsTracks();
            GfxLoadCsg();
            FontSpriteInitialiseCharacters();
            return true;
        }

        void SwitchToStartUpScene()
        {
            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) && Config::Get().general.playIntro)
                {
                    gOpenRCT2StartupAction = StartupAction::intro;
                }
            }

            IScene* nextScene{};
            switch (gOpenRCT2StartupAction)
            {
                case StartupAction::intro:
                {
                    nextScene = _sceneManager->getIntroScene();
                    break;
                }

                case StartupAction::title:
                {
                    nextScene = _sceneManager->getTitleScene();
                    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())
                        {
                            nextScene = _sceneManager->getTitleScene();
                            break;
                        }

                        auto ms = MemoryStream(data.data(), data.size(), MemoryAccess::read);
                        if (!LoadParkFromStream(&ms, gOpenRCT2StartupActionPath, true))
                        {
                            Console::Error::WriteLine("Failed to load '%s'", gOpenRCT2StartupActionPath);
                            nextScene = _sceneManager->getTitleScene();
                            break;
                        }
#endif
                    }
                    else
                    {
                        try
                        {
                            if (!LoadParkFromFile(gOpenRCT2StartupActionPath, true))
                            {
                                nextScene = _sceneManager->getTitleScene();
                                break;
                            }
                        }
                        catch (const std::exception& ex)
                        {
                            Console::Error::WriteLine("Failed to load '%s'", gOpenRCT2StartupActionPath);
                            Console::Error::WriteLine("%s", ex.what());
                            nextScene = _sceneManager->getTitleScene();
                            break;
                        }
                    }

                    // Successfully loaded a file
                    nextScene = _sceneManager->getGameScene();
                    break;
                }

                case StartupAction::edit:
                {
                    if (String::sizeOf(gOpenRCT2StartupActionPath) == 0)
                    {
                        Editor::Load();
                        nextScene = _sceneManager->getGameScene();
                    }
                    else if (Editor::LoadLandscape(gOpenRCT2StartupActionPath))
                    {
                        nextScene = _sceneManager->getGameScene();
                    }
                    else
                    {
                        nextScene = _sceneManager->getTitleScene();
                    }
                    break;
                }

                default:
                {
                    nextScene = _sceneManager->getTitleScene();
                }
            }

            _sceneManager->setActiveScene(nextScene);
            InitNetworkGame(nextScene == _sceneManager->getGameScene());
        }

        void InitNetworkGame(bool isGameScene)
        {
            if (isGameScene)
            {
#ifndef DISABLE_NETWORK
                if (gNetworkStart == Network::Mode::server)
                {
                    if (gNetworkStartPort == 0)
                    {
                        gNetworkStartPort = Config::Get().network.defaultPort;
                    }

                    if (gNetworkStartAddress.empty())
                    {
                        gNetworkStartAddress = Config::Get().network.listenAddress;
                    }

                    if (gCustomPassword.empty())
                    {
                        _network.SetPassword(Config::Get().network.defaultPassword.c_str());
                    }
                    else
                    {
                        _network.SetPassword(gCustomPassword);
                    }
                    _network.BeginServer(gNetworkStartPort, gNetworkStartAddress);
                }
                else
#endif // DISABLE_NETWORK
                {
                    GameLoadScripts();
                    GameNotifyMapChanged();
                }
            }

#ifndef DISABLE_NETWORK
            else if (gNetworkStart == Network::Mode::client)
            {
                if (gNetworkStartPort == 0)
                {
                    gNetworkStartPort = Config::Get().network.defaultPort;
                }
                _network.BeginClient(gNetworkStartHost, gNetworkStartPort);
            }
#endif // DISABLE_NETWORK
        }

        /**
         * 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 = GetLatestVersion();
                    if (!String::startsWith(gVersionInfoTag, _newVersionInfo.tag))
                    {
                        _hasNewVersionInfo = true;
                    }
                });
            }

            if (!gOpenRCT2Headless)
            {
                _sceneManager->getPreloaderScene()->SetOnComplete([&]() { SwitchToStartUpScene(); });
            }
            else
            {
                SwitchToStartUpScene();
            }
#ifdef __EMSCRIPTEN__
            emscripten_set_main_loop_arg(
                [](void* vctx) {
                    auto ctx = reinterpret_cast<Context*>(vctx);
                    if (ctx->_finished)
                    {
                        emscripten_cancel_main_loop();
                    }
                    ctx->RunFrame();
                },
                this, 0, 1);
#else
            _stdInOutConsole.Start();
            RunGameLoop();
#endif
        }

        bool ShouldDraw()
        {
            if (gOpenRCT2Headless)
                return false;
            if (_uiContext->IsMinimised())
                return false;
            return true;
        }

        bool ShouldRunVariableFrame()
        {
            if (!ShouldDraw())
                return false;
            if (!Config::Get().general.uncapFPS)
                return false;
            if (gGameSpeed > 4)
                return false;
            return true;
        }

        /**
         * Run the main game loop until the finished flag is set.
         */
#ifndef __EMSCRIPTEN__
        void RunGameLoop()
        {
            PROFILED_FUNCTION();

            LOG_VERBOSE("begin openrct2 loop");
            _finished = false;

            _variableFrame = ShouldRunVariableFrame();
            do
            {
                RunFrame();
            } while (!_finished);
            LOG_VERBOSE("finish openrct2 loop");
        }
#endif // __EMSCRIPTEN__

        void RunFrame()
        {
            PROFILED_FUNCTION();

            const auto deltaTime = _timer.GetElapsedTimeAndRestart().count();

            // Make sure we catch the state change and reset it.
            bool useVariableFrame = ShouldRunVariableFrame();
            if (_variableFrame != useVariableFrame)
            {
                _variableFrame = useVariableFrame;

                // Switching from variable to fixed frame requires reseting
                // of entity positions back to end of tick positions
                auto& tweener = EntityTweener::Get();
                tweener.Restore();
                tweener.Reset();
            }

            UpdateTimeAccumulators(deltaTime);

            Network::Update();

            if (useVariableFrame)
            {
                RunVariableFrame(deltaTime);
            }
            else
            {
                RunFixedFrame(deltaTime);
            }

            Network::Flush();
        }

        void UpdateTimeAccumulators(float deltaTime)
        {
            // Ticks
            float scaledDeltaTime = deltaTime * _timeScale;
            _ticksAccumulator = std::min(_ticksAccumulator + scaledDeltaTime, kGameUpdateMaxThreshold);

            // Real Time.
            _realtimeAccumulator = std::min(_realtimeAccumulator + deltaTime, kGameUpdateMaxThreshold);
            while (_realtimeAccumulator >= kGameUpdateTimeMS)
            {
                gCurrentRealTimeTicks++;
                _realtimeAccumulator -= kGameUpdateTimeMS;
            }
        }

        void RunFixedFrame(float deltaTime)
        {
            PROFILED_FUNCTION();

            _uiContext->ProcessMessages();

            if (_ticksAccumulator < kGameUpdateTimeMS)
            {
                const auto sleepTimeSec = std::min(kNetworkUpdateTimeMS, kGameUpdateTimeMS - _ticksAccumulator);
                Platform::Sleep(static_cast<uint32_t>(sleepTimeSec * 1000.f));
                return;
            }

            while (_ticksAccumulator >= kGameUpdateTimeMS)
            {
                Tick();

                _ticksAccumulator -= kGameUpdateTimeMS;
            }

            _backgroundWorker.dispatchCompleted();

            ContextHandleInput();
            WindowUpdateAll();

            if (ShouldDraw())
            {
                Draw();
            }
        }

        void RunVariableFrame(float deltaTime)
        {
            PROFILED_FUNCTION();

            const bool shouldDraw = ShouldDraw();
            auto& tweener = EntityTweener::Get();

            _uiContext->ProcessMessages();

            while (_ticksAccumulator >= kGameUpdateTimeMS)
            {
                // Get the original position of each sprite
                if (shouldDraw)
                    tweener.PreTick();

                Tick();

                _ticksAccumulator -= kGameUpdateTimeMS;

                // Get the next position of each sprite
                if (shouldDraw)
                    tweener.PostTick();
            }

            _backgroundWorker.dispatchCompleted();

            ContextHandleInput();
            WindowUpdateAll();

            if (shouldDraw)
            {
                const float alpha = std::min(_ticksAccumulator / kGameUpdateTimeMS, 1.0f);
                tweener.Tween(alpha);

                Draw();
            }
        }

        void Draw()
        {
            PROFILED_FUNCTION();

            _drawingEngine->BeginDraw();
            _painter->Paint(*_drawingEngine);
            _drawingEngine->EndDraw();
        }

        void Tick()
        {
            PROFILED_FUNCTION();

            // TODO: This variable has been never "variable" in time, some code expects
            // this to be 40Hz (25 ms). Refactor this once the UI is decoupled.
            gCurrentDeltaTime = static_cast<uint16_t>(kGameUpdateTimeMS * 1000.0f);

            if (GameIsNotPaused())
            {
                gPaletteEffectFrame += gCurrentDeltaTime;
            }

            DateUpdateRealTimeOfDay();

            if (auto* activeScene = _sceneManager->getActiveScene())
                activeScene->Tick();

#ifdef __ENABLE_DISCORD__
            if (_discordService != nullptr)
            {
                _discordService->Tick();
            }
#endif

            ChatUpdate();
#ifdef ENABLE_SCRIPTING
            if (auto* activeScene = _sceneManager->getActiveScene(); activeScene != _sceneManager->getPreloaderScene())
            {
                _scriptEngine.Tick();
            }
#endif
            _stdInOutConsole.ProcessEvalQueue();
            _uiContext->Tick();
        }

        /**
         * Ensure that the custom user content folders are present
         */
        void EnsureUserContentDirectoriesExist()
        {
            EnsureDirectoriesExist(
                DirBase::user,
                {
                    DirId::objects,
                    DirId::saves,
                    DirId::scenarios,
                    DirId::trackDesigns,
                    DirId::landscapes,
                    DirId::heightmaps,
                    DirId::plugins,
                    DirId::themes,
                    DirId::sequences,
                    DirId::replayRecordings,
                    DirId::desyncLogs,
                    DirId::crashDumps,
                });
        }

        void EnsureDirectoriesExist(const DirBase dirBase, const std::initializer_list<DirId>& dirIds)
        {
            for (const auto& dirId : dirIds)
            {
                auto path = _env->GetDirectoryPath(dirBase, dirId);
                if (!Path::CreateDirectory(path))
                    LOG_ERROR("Unable to create directory '%s'.", path.c_str());
            }
        }

        /**
         * Copy saved games and landscapes to user directory
         */
        void CopyOriginalUserFilesOver()
        {
            CopyOriginalUserFilesOver(DirId::saves, "*.sv6");
            CopyOriginalUserFilesOver(DirId::landscapes, "*.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 (!Path::CreateDirectory(dstDirectory))
                {
                    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());
                    }
                }
            }
        }

#ifndef DISABLE_HTTP
        std::vector<uint8_t> 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<uint8_t> 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;
        }

        void SetTimeScale(float newScale) override
        {
            _timeScale = std::clamp(newScale, kGameMinTimeScale, kGameMaxTimeScale);
        }

        float GetTimeScale() const override
        {
            return _timeScale;
        }

        BackgroundWorker& GetBackgroundWorker() override
        {
            return _backgroundWorker;
        }
    };

    Context* Context::Instance = nullptr;

    std::unique_ptr<IContext> CreateContext()
    {
        return CreateContext(CreatePlatformEnvironment(), Audio::CreateDummyAudioContext(), CreateDummyUiContext());
    }

    std::unique_ptr<IContext> CreateContext(
        std::unique_ptr<IPlatformEnvironment>&& env, std::unique_ptr<IAudioContext>&& audioContext,
        std::unique_ptr<IUiContext>&& uiContext)
    {
        return std::make_unique<Context>(std::move(env), std::move(audioContext), std::move(uiContext));
    }

    IContext* GetContext()
    {
        return Context::Instance;
    }

    void ContextInit()
    {
        GetWindowManager()->Init();
    }

    void ContextResetSubsystems()
    {
        GetContext()->ResetSubsystems();
    }

    bool ContextLoadParkFromStream(void* stream)
    {
        return GetContext()->LoadParkFromStream(static_cast<IStream*>(stream), "");
    }

    void ContextSetCurrentCursor(CursorID cursor)
    {
        GetContext()->GetUiContext().SetCursor(cursor);
    }

    void ContextUpdateCursorScale()
    {
        GetContext()->GetUiContext().SetCursorScale(static_cast<uint8_t>(std::round(Config::Get().general.windowScale)));
    }

    void ContextHideCursor()
    {
        GetContext()->GetUiContext().SetCursorVisible(false);
    }

    void ContextShowCursor()
    {
        GetContext()->GetUiContext().SetCursorVisible(true);
    }

    ScreenCoordsXY ContextGetCursorPosition()
    {
        return GetContext()->GetUiContext().GetCursorPosition();
    }

    ScreenCoordsXY ContextGetCursorPositionScaled()
    {
        auto cursorCoords = ContextGetCursorPosition();
        // Compensate for window scaling.
        return { static_cast<int32_t>(std::ceil(cursorCoords.x / Config::Get().general.windowScale)),
                 static_cast<int32_t>(std::ceil(cursorCoords.y / Config::Get().general.windowScale)) };
    }

    void ContextSetCursorPosition(const ScreenCoordsXY& cursorPosition)
    {
        GetContext()->GetUiContext().SetCursorPosition(cursorPosition);
    }

    const CursorState* ContextGetCursorState()
    {
        return GetContext()->GetUiContext().GetCursorState();
    }

    const uint8_t* ContextGetKeysState()
    {
        return GetContext()->GetUiContext().GetKeysState();
    }

    const uint8_t* ContextGetKeysPressed()
    {
        return GetContext()->GetUiContext().GetKeysPressed();
    }

    TextInputSession* ContextStartTextInput(u8string& buffer, size_t maxLength)
    {
        return GetContext()->GetUiContext().StartTextInput(buffer, maxLength);
    }

    void ContextStopTextInput()
    {
        GetContext()->GetUiContext().StopTextInput();
    }

    bool ContextIsInputActive()
    {
        return GetContext()->GetUiContext().IsTextInputActive();
    }

    void ContextTriggerResize()
    {
        return GetContext()->GetUiContext().TriggerResize();
    }

    void ContextSetFullscreenMode(int32_t mode)
    {
        return GetContext()->GetUiContext().SetFullscreenMode(static_cast<FullscreenMode>(mode));
    }

    void ContextRecreateWindow()
    {
        GetContext()->GetUiContext().RecreateWindow();
    }

    int32_t ContextGetWidth()
    {
        return GetContext()->GetUiContext().GetWidth();
    }

    int32_t ContextGetHeight()
    {
        return GetContext()->GetUiContext().GetHeight();
    }

    bool ContextHasFocus()
    {
        return GetContext()->GetUiContext().HasFocus();
    }

    void ContextSetCursorTrap(bool value)
    {
        GetContext()->GetUiContext().SetCursorTrap(value);
    }

    WindowBase* ContextOpenWindow(WindowClass wc)
    {
        auto windowManager = GetWindowManager();
        return windowManager->OpenWindow(wc);
    }

    WindowBase* ContextOpenWindowView(WindowView view)
    {
        auto windowManager = GetWindowManager();
        return windowManager->openView(view);
    }

    WindowBase* ContextOpenDetailWindow(WindowDetail type, int32_t id)
    {
        auto windowManager = GetWindowManager();
        return windowManager->openDetails(type, id);
    }

    WindowBase* ContextOpenIntent(Intent* intent)
    {
        auto windowManager = GetWindowManager();
        return windowManager->OpenIntent(intent);
    }

    void ContextBroadcastIntent(Intent* intent)
    {
        auto windowManager = GetWindowManager();
        windowManager->BroadcastIntent(*intent);
    }

    void ContextForceCloseWindowByClass(WindowClass windowClass)
    {
        auto windowManager = GetWindowManager();
        windowManager->ForceClose(windowClass);
    }

    WindowBase* ContextShowError(StringId title, StringId message, const Formatter& args, const bool autoClose /* = false */)
    {
        auto windowManager = GetWindowManager();
        return windowManager->ShowError(title, message, args, autoClose);
    }

    void ContextHandleInput()
    {
        auto windowManager = GetWindowManager();
        windowManager->HandleInput();
    }

    void ContextInputHandleKeyboard(bool isTitle)
    {
        auto windowManager = GetWindowManager();
        windowManager->HandleKeyboard(isTitle);
    }

    void ContextQuit()
    {
        GetContext()->Quit();
    }

    u8string ContextOpenCommonFileDialog(FileDialogDesc& desc)
    {
        try
        {
            return GetContext()->GetUiContext().ShowFileDialog(desc);
        }
        catch (const std::exception& ex)
        {
            LOG_ERROR(ex.what());
            return u8string{};
        }
    }
} // namespace OpenRCT2
