/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include "EditorDefs.h" #include "GameEngine.h" // Qt #include #include // AzCore #include #include #include #include #include #include // AzFramework #include #include #include #include #include // Editor #include "IEditorImpl.h" #include "CryEditDoc.h" #include "Settings.h" // CryCommon #include // Editor #include "CryEdit.h" #include "ViewManager.h" #include "AnimationContext.h" #include "MainWindow.h" // Implementation of System Callback structure. struct SSystemUserCallback : public ISystemUserCallback { SSystemUserCallback(IInitializeUIInfo* logo) : m_threadErrorHandler(this) { m_pLogo = logo; }; void OnSystemConnect(ISystem* pSystem) override { ModuleInitISystem(pSystem, "Editor"); } bool OnError(const char* szErrorString) override { // since we show a message box, we have to use the GUI thread if (QThread::currentThread() != qApp->thread()) { bool result = false; QMetaObject::invokeMethod(&m_threadErrorHandler, "OnError", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const char*, szErrorString)); return result; } if (szErrorString) { Log(szErrorString); } if (GetIEditor()->IsInTestMode()) { exit(1); } char str[4096]; if (szErrorString) { azsnprintf(str, 4096, "%s\r\nSave Level Before Exiting the Editor?", szErrorString); } else { azsnprintf(str, 4096, "Unknown Error\r\nSave Level Before Exiting the Editor?"); } int res = IDNO; ICVar* pCVar = gEnv->pConsole ? gEnv->pConsole->GetCVar("sys_no_crash_dialog") : nullptr; if (!pCVar || pCVar->GetIVal() == 0) { res = QMessageBox::critical(QApplication::activeWindow(), QObject::tr("Engine Error"), str, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); } if (res == QMessageBox::Yes || res == QMessageBox::No) { if (res == QMessageBox::Yes) { if (GetIEditor()->SaveDocument()) { QMessageBox::information(QApplication::activeWindow(), QObject::tr("Save"), QObject::tr("Level has been successfully saved!\r\nPress Ok to terminate Editor.")); } } } return true; } bool OnSaveDocument() override { bool success = false; if (GetIEditor()) { // Turn off save backup as we force a backup before reaching this point bool prevSaveBackup = gSettings.bBackupOnSave; gSettings.bBackupOnSave = false; success = GetIEditor()->SaveDocument(); gSettings.bBackupOnSave = prevSaveBackup; } return success; } bool OnBackupDocument() override { CCryEditDoc* level = GetIEditor() ? GetIEditor()->GetDocument() : nullptr; if (level) { return level->BackupBeforeSave(true); } return false; } void OnProcessSwitch() override { if (GetIEditor()->IsInGameMode()) { GetIEditor()->SetInGameMode(false); } } void OnInitProgress(const char* sProgressMsg) override { if (m_pLogo) { m_pLogo->SetInfoText(sProgressMsg); } } void OnSplashScreenDone() { m_pLogo = nullptr; } private: IInitializeUIInfo* m_pLogo; ThreadedOnErrorHandler m_threadErrorHandler; }; ThreadedOnErrorHandler::ThreadedOnErrorHandler(ISystemUserCallback* callback) : m_userCallback(callback) { moveToThread(qApp->thread()); } ThreadedOnErrorHandler::~ThreadedOnErrorHandler() { } bool ThreadedOnErrorHandler::OnError(const char* error) { return m_userCallback->OnError(error); } //! This class will be used by CSystem to find out whether the negotiation with the assetprocessor failed class AssetProcessConnectionStatus : public AzFramework::AssetSystemConnectionNotificationsBus::Handler { public: AssetProcessConnectionStatus() { AzFramework::AssetSystemConnectionNotificationsBus::Handler::BusConnect(); }; ~AssetProcessConnectionStatus() override { AzFramework::AssetSystemConnectionNotificationsBus::Handler::BusDisconnect(); } //! Notifies listeners that connection to the Asset Processor failed void ConnectionFailed() override { m_connectionFailed = true; } void NegotiationFailed() override { m_negotiationFailed = true; } bool CheckConnectionFailed() { return m_connectionFailed; } bool CheckNegotiationFailed() { return m_negotiationFailed; } private: bool m_connectionFailed = false; bool m_negotiationFailed = false; }; AZ_PUSH_DISABLE_WARNING(4273, "-Wunknown-warning-option") CGameEngine::CGameEngine() : m_bIgnoreUpdates(false) , m_ePendingGameMode(ePGM_NotPending) , m_modalWindowDismisser(nullptr) AZ_POP_DISABLE_WARNING { m_pISystem = nullptr; m_bLevelLoaded = false; m_bInGameMode = false; m_bSimulationMode = false; m_bSyncPlayerPosition = true; m_hSystemHandle.reset(nullptr); m_bJustCreated = false; m_levelName = "Untitled"; m_levelExtension = EditorUtils::LevelFile::GetDefaultFileExtension(); m_playerViewTM.SetIdentity(); GetIEditor()->RegisterNotifyListener(this); } AZ_PUSH_DISABLE_WARNING(4273, "-Wunknown-warning-option") CGameEngine::~CGameEngine() { AZ_POP_DISABLE_WARNING GetIEditor()->UnregisterNotifyListener(this); IMovieSystem* movieSystem = AZ::Interface::Get(); if (movieSystem) { movieSystem->SetCallback(nullptr); } delete m_pISystem; m_pISystem = nullptr; m_hSystemHandle.reset(nullptr); delete m_pSystemUserCallback; } static int ed_killmemory_size; static int ed_indexfiles; void KillMemory(IConsoleCmdArgs* /* pArgs */) { while (true) { const int kLimit = 10000000; int size; if (ed_killmemory_size > 0) { size = ed_killmemory_size; } else { size = rand() * rand(); size = size > kLimit ? kLimit : size; } new uint8[size]; } } static void CmdGotoEditor(IConsoleCmdArgs* pArgs) { // Console commands are assumed to be in the culture invariant locale since they can come from data files. AZ::Locale::ScopedSerializationLocale scopedLocale; // feature is mostly useful for QA purposes, this works with the game "goto" command // this console command actually is used by the game command, the editor command shouldn't be used by the user int iArgCount = pArgs->GetArgCount(); CViewManager* pViewManager = GetIEditor()->GetViewManager(); CViewport* pRenderViewport = pViewManager->GetGameViewport(); if (!pRenderViewport) { return; } float x, y, z, wx, wy, wz; if (iArgCount == 7 && azsscanf(pArgs->GetArg(1), "%f", &x) == 1 && azsscanf(pArgs->GetArg(2), "%f", &y) == 1 && azsscanf(pArgs->GetArg(3), "%f", &z) == 1 && azsscanf(pArgs->GetArg(4), "%f", &wx) == 1 && azsscanf(pArgs->GetArg(5), "%f", &wy) == 1 && azsscanf(pArgs->GetArg(6), "%f", &wz) == 1) { Matrix34 tm = pRenderViewport->GetViewTM(); tm.SetTranslation(Vec3(x, y, z)); tm.SetRotation33(Matrix33::CreateRotationXYZ(DEG2RAD(Ang3(wx, wy, wz)))); pRenderViewport->SetViewTM(tm); } } AZ::Outcome CGameEngine::Init( bool bPreviewMode, bool bTestMode, const char* sInCmdLine, IInitializeUIInfo* logo, HWND hwndForInputSystem) { m_pSystemUserCallback = new SSystemUserCallback(logo); constexpr const char* crySystemLibraryName = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX "CrySystem" AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION; m_hSystemHandle = AZ::DynamicModuleHandle::Create(crySystemLibraryName); if (!m_hSystemHandle->Load(AZ::DynamicModuleHandle::LoadFlags::InitFuncRequired)) { auto errorMessage = AZStd::string::format("%s Loading Failed", crySystemLibraryName); Error(errorMessage.c_str()); return AZ::Failure(errorMessage); } PFNCREATESYSTEMINTERFACE pfnCreateSystemInterface = m_hSystemHandle->GetFunction("CreateSystemInterface"); SSystemInitParams sip; sip.bEditor = true; sip.bDedicatedServer = false; sip.bPreview = bPreviewMode; sip.bTestMode = bTestMode; sip.hInstance = nullptr; #ifdef AZ_PLATFORM_MAC // Create a hidden QWidget. Would show a black window on macOS otherwise. auto window = new QWidget(); QObject::connect(qApp, &QApplication::lastWindowClosed, window, &QWidget::deleteLater); sip.hWnd = (HWND)window->winId(); #else sip.hWnd = hwndForInputSystem; #endif sip.pLogCallback = &m_logFile; sip.sLogFileName = "@log@/Editor.log"; sip.pUserCallback = m_pSystemUserCallback; if (sInCmdLine) { azstrncpy(sip.szSystemCmdLine, AZ_COMMAND_LINE_LEN, sInCmdLine, AZ_COMMAND_LINE_LEN); if (strstr(sInCmdLine, "-export") || strstr(sInCmdLine, "/export") || strstr(sInCmdLine, "-autotest_mode")) { sip.bUnattendedMode = true; } } if (sip.bUnattendedMode) { m_modalWindowDismisser = AZStd::make_unique(); } AssetProcessConnectionStatus apConnectionStatus; m_pISystem = pfnCreateSystemInterface(sip); if (!gEnv) { gEnv = m_pISystem->GetGlobalEnvironment(); } if (!m_pISystem) { AZStd::string errorMessage = "Could not initialize CSystem. View the logs for more details."; gEnv = nullptr; Error("CreateSystemInterface Failed"); return AZ::Failure(errorMessage); } if (apConnectionStatus.CheckNegotiationFailed()) { auto errorMessage = AZStd::string::format("Negotiation with Asset Processor failed.\n" "Please ensure the Asset Processor is running on the same branch and try again."); gEnv = nullptr; return AZ::Failure(errorMessage); } if (apConnectionStatus.CheckConnectionFailed()) { AzFramework::AssetSystem::ConnectionSettings connectionSettings; AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings); auto errorMessage = AZStd::string::format("Unable to connect to the local Asset Processor.\n\n" "The Asset Processor is either not running locally or not accepting connections on port %hu. " "Check your remote_port settings in bootstrap.cfg or view the Asset Processor's \"Logs\" tab " "for any errors.", connectionSettings.m_assetProcessorPort); gEnv = nullptr; return AZ::Failure(errorMessage); } SetEditorCoreEnvironment(gEnv); IMovieSystem* movieSystem = AZ::Interface::Get(); if (movieSystem) { movieSystem->EnablePhysicsEvents(m_bSimulationMode); } CLogFile::AboutSystem(); REGISTER_CVAR(ed_killmemory_size, -1, VF_DUMPTODISK, "Sets the testing allocation size. -1 for random"); REGISTER_CVAR(ed_indexfiles, 1, VF_DUMPTODISK, "Index game resource files, 0 - inactive, 1 - active"); REGISTER_COMMAND("ed_killmemory", KillMemory, VF_NULL, ""); REGISTER_COMMAND("ed_goto", CmdGotoEditor, VF_CHEAT, "Internal command, used by the 'GOTO' console command\n"); // The editor needs to handle the quit command differently gEnv->pConsole->RemoveCommand("quit"); REGISTER_COMMAND("quit", CGameEngine::HandleQuitRequest, VF_RESTRICTEDMODE, "Quit/Shutdown the engine"); CrySystemEventBus::Broadcast(&CrySystemEventBus::Events::OnCryEditorInitialized); return AZ::Success(); } bool CGameEngine::InitGame(const char*) { m_pISystem->ExecuteCommandLine(); return true; } void CGameEngine::SetLevelPath(const QString& path) { QByteArray levelPath; levelPath.reserve(AZ_MAX_PATH_LEN); levelPath = Path::ToUnixPath(Path::RemoveBackslash(path)).toUtf8(); m_levelPath = levelPath; m_levelName = m_levelPath.mid(m_levelPath.lastIndexOf('/') + 1); const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension(); const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension(); // Store off if if (QFileInfo(path + oldExtension).exists()) { m_levelExtension = oldExtension; } else { m_levelExtension = defaultExtension; } } bool CGameEngine::LoadLevel( [[maybe_unused]] bool bDeleteAIGraph, [[maybe_unused]] bool bReleaseResources) { m_bLevelLoaded = false; CLogFile::FormatLine("Loading map '%s' into engine...", m_levelPath.toUtf8().data()); // Switch the current directory back to the Primary CD folder first. // The engine might have trouble to find some files when the current // directory is wrong QDir::setCurrent(GetIEditor()->GetPrimaryCDFolder()); m_bLevelLoaded = true; return true; } bool CGameEngine::ReloadLevel() { if (!LoadLevel(false, false)) { return false; } return true; } void CGameEngine::SwitchToInGame() { auto streamer = AZ::Interface::Get(); if (streamer) { AZStd::binary_semaphore wait; AZ::IO::FileRequestPtr flush = streamer->FlushCaches(); streamer->SetRequestCompleteCallback(flush, [&wait](AZ::IO::FileRequestHandle) { wait.release(); }); streamer->QueueRequest(flush); wait.acquire(); } GetIEditor()->Notify(eNotify_OnBeginGameMode); IMovieSystem* movieSystem = AZ::Interface::Get(); if (movieSystem) { movieSystem->EnablePhysicsEvents(true); } m_bInGameMode = true; if (movieSystem) { constexpr bool playOnReset = true; constexpr bool seekToStart = false; movieSystem->Reset(playOnReset, seekToStart); } // Transition to runtime entity context. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::StartPlayInEditor); if (!CCryEditApp::instance()->IsInAutotestMode()) { // Constrain and hide the system cursor (important to do this last) AzFramework::InputSystemCursorRequestBus::Event(AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::SetSystemCursorState, AzFramework::SystemCursorState::ConstrainedAndHidden); } Log("Entered game mode"); } void CGameEngine::SwitchToInEditor() { // Transition to editor entity context. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::StopPlayInEditor); // Reset movie system IMovieSystem* movieSystem = AZ::Interface::Get(); if (movieSystem) { for (int i = movieSystem->GetNumPlayingSequences(); --i >= 0;) { movieSystem->GetPlayingSequence(i)->Deactivate(); } constexpr bool playOnReset = false; constexpr bool seekToStart = false; movieSystem->Reset(playOnReset, seekToStart); } CViewport* pGameViewport = GetIEditor()->GetViewManager()->GetGameViewport(); if (movieSystem) { movieSystem->EnablePhysicsEvents(m_bSimulationMode); } m_bInGameMode = false; // Out of game in Editor mode. if (pGameViewport) { pGameViewport->SetViewTM(m_playerViewTM); } GetIEditor()->Notify(eNotify_OnEndGameMode); // Unconstrain the system cursor and make it visible (important to do this last) AzFramework::InputSystemCursorRequestBus::Event(AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::SetSystemCursorState, AzFramework::SystemCursorState::UnconstrainedAndVisible); Log("Exited game mode"); } void CGameEngine::HandleQuitRequest(IConsoleCmdArgs* /*args*/) { if (GetIEditor()->GetGameEngine()->IsInGameMode()) { GetIEditor()->GetGameEngine()->RequestSetGameMode(false); gEnv->pConsole->ShowConsole(false); } else { MainWindow::instance()->window()->close(); } } void CGameEngine::RequestSetGameMode(bool inGame) { m_ePendingGameMode = inGame ? ePGM_SwitchToInGame : ePGM_SwitchToInEditor; if (m_ePendingGameMode == ePGM_SwitchToInGame) { AzToolsFramework::EditorLegacyGameModeNotificationBus::Broadcast( &AzToolsFramework::EditorLegacyGameModeNotificationBus::Events::OnStartGameModeRequest); } else if (m_ePendingGameMode == ePGM_SwitchToInEditor) { AzToolsFramework::EditorLegacyGameModeNotificationBus::Broadcast( &AzToolsFramework::EditorLegacyGameModeNotificationBus::Events::OnStopGameModeRequest); } } void CGameEngine::SetGameMode(bool bInGame) { if (m_bInGameMode == bInGame) { return; } if (!GetIEditor()->GetDocument()) { return; } GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_GAME_MODE_SWITCH_START, bInGame, 0); // Enables engine to know about that. gEnv->SetIsEditorGameMode(bInGame); // Ignore updates while changing in and out of game mode m_bIgnoreUpdates = true; // Switching modes will destroy the current AzFramework::EntityConext which may contain // data the queued events hold on to, so execute all queued events before switching. ExecuteQueuedEvents(); if (bInGame) { SwitchToInGame(); } else { SwitchToInEditor(); } // Enables engine to know about that. if (MainWindow::instance()) { AzFramework::InputChannelRequestBus::Broadcast(&AzFramework::InputChannelRequests::ResetState); MainWindow::instance()->setFocus(); } GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_EDITOR_GAME_MODE_CHANGED, bInGame, 0); m_bIgnoreUpdates = false; GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_GAME_MODE_SWITCH_END, bInGame, 0); } void CGameEngine::SetSimulationMode(bool enabled, bool bOnlyPhysics) { if (m_bSimulationMode == enabled) { return; } IMovieSystem* movieSystem = AZ::Interface::Get(); if (movieSystem) { movieSystem->EnablePhysicsEvents(enabled); } if (enabled) { GetIEditor()->Notify(eNotify_OnBeginSimulationMode); } else { GetIEditor()->Notify(eNotify_OnEndSimulationMode); } m_bSimulationMode = enabled; // Enables engine to know about simulation mode. gEnv->SetIsEditorSimulationMode(enabled); // Execute all queued events before switching modes. ExecuteQueuedEvents(); // Transition back to editor entity context. // Symmetry is not critical. It's okay to call this even if we never called StartPlayInEditor // (bOnlyPhysics was true when we entered simulation mode). AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::StopPlayInEditor); if (m_bSimulationMode && !bOnlyPhysics) { // Transition to runtime entity context. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::StartPlayInEditor); } AzFramework::InputChannelRequestBus::Broadcast(&AzFramework::InputChannelRequests::ResetState); } void CGameEngine::SetPlayerViewMatrix(const Matrix34& tm, [[maybe_unused]] bool bEyePos) { m_playerViewTM = tm; } void CGameEngine::SyncPlayerPosition(bool bEnable) { m_bSyncPlayerPosition = bEnable; if (m_bSyncPlayerPosition) { SetPlayerViewMatrix(m_playerViewTM); } } void CGameEngine::SetCurrentMOD(const char* sMod) { m_MOD = sMod; } QString CGameEngine::GetCurrentMOD() const { return m_MOD; } void CGameEngine::Update() { if (m_bIgnoreUpdates) { return; } switch (m_ePendingGameMode) { case ePGM_SwitchToInGame: { SetGameMode(true); m_ePendingGameMode = ePGM_NotPending; break; } case ePGM_SwitchToInEditor: { bool wasInSimulationMode = GetIEditor()->GetGameEngine()->GetSimulationMode(); if (wasInSimulationMode) { GetIEditor()->GetGameEngine()->SetSimulationMode(false); } SetGameMode(false); if (wasInSimulationMode) { GetIEditor()->GetGameEngine()->SetSimulationMode(true); } m_ePendingGameMode = ePGM_NotPending; break; } } AZ::ComponentApplication* componentApplication = nullptr; AZ::ComponentApplicationBus::BroadcastResult(componentApplication, &AZ::ComponentApplicationBus::Events::GetApplication); if (m_bInGameMode) { if (gEnv->pSystem) { gEnv->pSystem->UpdatePreTickBus(); componentApplication->Tick(); gEnv->pSystem->UpdatePostTickBus(); } if (CViewport* pRenderViewport = GetIEditor()->GetViewManager()->GetGameViewport()) { pRenderViewport->Update(); } // Check for the Escape key to exit game mode here rather than in Qt, // because all Qt events are usually filtered out in game mode in // QtEditorApplication_.cpp nativeEventFilter() to prevent // using Editor menu actions and shortcuts that shouldn't trigger while // playing the game. // When the user opens the console, Qt events will be allowed // so the user can interact with limited Editor content like the console. const AzFramework::InputChannel* inputChannel = nullptr; const AzFramework::InputChannelId channelId(AzFramework::InputDeviceKeyboard::Key::Escape); AzFramework::InputChannelRequestBus::EventResult(inputChannel, channelId, &AzFramework::InputChannelRequests::GetInputChannel); if(inputChannel && inputChannel->GetState() == AzFramework::InputChannel::State::Began) { // leave game mode RequestSetGameMode(false); } } else { // [marco] check current sound and vis areas for music etc. // but if in game mode, 'cos is already done in the above call to game->update() unsigned int updateFlags = ESYSUPDATE_EDITOR; GetIEditor()->GetAnimation()->Update(); GetIEditor()->GetSystem()->UpdatePreTickBus(updateFlags); componentApplication->Tick(); GetIEditor()->GetSystem()->UpdatePostTickBus(updateFlags); } } void CGameEngine::OnEditorNotifyEvent(EEditorNotifyEvent event) { switch (event) { case eNotify_OnSplashScreenDestroyed: { if (m_pSystemUserCallback != nullptr) { m_pSystemUserCallback->OnSplashScreenDone(); } } break; } } void CGameEngine::OnAreaModified([[maybe_unused]] const AZ::Aabb& modifiedArea) { } void CGameEngine::ExecuteQueuedEvents() { AZ::Data::AssetBus::ExecuteQueuedEvents(); AZ::TickBus::ExecuteQueuedEvents(); AZ::MainThreadRenderRequestBus::ExecuteQueuedEvents(); } #include