#include "RiderGameControl.hpp" #include "IRiderLink.hpp" #include "Model/Library/UE4Library/PlayState.Pregenerated.h" #include "Model/Library/UE4Library/RequestFailed.Pregenerated.h" #include "Model/Library/UE4Library/RequestSucceed.Pregenerated.h" #include "RdEditorModel/RdEditorModel.Pregenerated.h" #include "Async/Async.h" #include "Editor/UnrealEdEngine.h" #include "Framework/Application/SlateApplication.h" #include "Kismet2/DebuggerCommands.h" #include "LevelEditor.h" #include "LevelEditorActions.h" #include "Misc/FeedbackContext.h" #include "Modules/ModuleManager.h" #include "Settings/LevelEditorPlaySettings.h" #include "Editor.h" #include "Runtime/Launch/Resources/Version.h" #if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 23 #include "ILevelViewport.h" #include "LevelEditorViewport.h" #else #include "IAssetViewport.h" #include "EditorViewportClient.h" #endif #define LOCTEXT_NAMESPACE "RiderGameControl" DEFINE_LOG_CATEGORY(FLogRiderGameControlModule); IMPLEMENT_MODULE(FRiderGameControlModule, RiderGameControl); extern UNREALED_API class UUnrealEdEngine* GUnrealEd; static int NumberOfPlayers(int Mode) { return (Mode & 3) + 1; } static bool SpawnAtPlayerStart(int Mode) { return (Mode & 4) != 0; } static bool DedicatedServer(int Mode) { return (Mode & 8) != 0; } enum class Compile { Yes, No }; static Compile CompileBeforeRun(int Mode) { return (Mode & 128) != 0 ? Compile::Yes : Compile::No; } static EPlayModeType PlayModeFromInt(int ModeNumber) { switch (ModeNumber) { default: break; case 1: return PlayMode_InMobilePreview; case 2: return PlayMode_InEditorFloating; case 3: return PlayMode_InVR; case 4: return PlayMode_InNewProcess; case 5: return PlayMode_Simulate; case 6: return PlayMode_InVulkanPreview; } return PlayMode_InViewPort; } static int PlayModeToInt(EPlayModeType modeType) { switch (modeType) { default: break; case PlayMode_InTargetedMobilePreview: case PlayMode_InMobilePreview: return 1; case PlayMode_InEditorFloating: return 2; case PlayMode_InVR: return 3; case PlayMode_InNewProcess: return 4; case PlayMode_Simulate: return 5; case PlayMode_InVulkanPreview: return 6; } return 0; } FSlateApplication* SlateApplication = nullptr; struct FPlaySettings { EPlayModeType PlayMode; int32 NumberOfClients; bool bNetDedicated; bool bSpawnAtPlayerStart; static FPlaySettings UnpackFromMode(int32_t mode) { FPlaySettings settings = { PlayModeFromInt((mode & (16 + 32 + 64)) >> 4), NumberOfPlayers(mode), DedicatedServer(mode), SpawnAtPlayerStart(mode), }; return settings; } static int32_t PackToMode(const FPlaySettings& settings) { return (settings.NumberOfClients - 1) + (settings.bSpawnAtPlayerStart ? (1 << 2) : 0) + (settings.bNetDedicated ? (1 << 3) : 0) + (PlayModeToInt(settings.PlayMode) << 4); } }; static FPlaySettings RetrieveSettings(const ULevelEditorPlaySettings* PlayInSettings) { check(PlayInSettings); FPlaySettings settings; settings.PlayMode = PlayInSettings->LastExecutedPlayModeType; PlayInSettings->GetPlayNumberOfClients(settings.NumberOfClients); #if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 24 PlayInSettings->GetPlayNetDedicated(settings.bNetDedicated); #else settings.bNetDedicated = PlayInSettings->bLaunchSeparateServer; #endif settings.bSpawnAtPlayerStart = PlayInSettings->LastExecutedPlayModeLocation == PlayLocation_DefaultPlayerStart; return settings; } static void UpdateSettings(ULevelEditorPlaySettings* PlayInSettings, const FPlaySettings& settings) { check(PlayInSettings); PlayInSettings->SetPlayNumberOfClients(settings.NumberOfClients); #if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 24 PlayInSettings->SetPlayNetDedicated(settings.bNetDedicated); #else PlayInSettings->bLaunchSeparateServer = settings.bNetDedicated; #endif PlayInSettings->LastExecutedPlayModeLocation = settings.bSpawnAtPlayerStart ? PlayLocation_DefaultPlayerStart : PlayLocation_CurrentCameraLocation; PlayInSettings->LastExecutedPlayModeType = settings.PlayMode; PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); } struct FCachedCommandInfo { FName CommandName; TSharedPtr Command; }; class FRiderGameControlActionsCache { public: FRiderGameControlActionsCache(); ~FRiderGameControlActionsCache(); private: void UpdatePlayWorldCommandsCache(); public: FCachedCommandInfo PlayModeCommands[PlayMode_Count] = { {TEXT("PlayInViewport")}, {TEXT("PlayInEditorFloating")}, {TEXT("PlayInMobilePreview")}, {FName()}, {TEXT("PlayInVulkanPreview")}, {TEXT("PlayInNewProcess")}, {TEXT("PlayInVR")}, {TEXT("Simulate")}, }; FCachedCommandInfo ResumePlaySession = {TEXT("ResumePlaySession")}; FCachedCommandInfo PausePlaySession = {TEXT("PausePlaySession")}; FCachedCommandInfo StopPlaySession = {TEXT("StopPlaySession")}; FCachedCommandInfo SingleFrameAdvance = {TEXT("SingleFrameAdvance")}; private: FDelegateHandle CommandsChangedHandle; }; FRiderGameControlActionsCache::FRiderGameControlActionsCache() { const FName PlayWorldContextName = FName("PlayWorld"); TSharedPtr PlayWorldContext = FInputBindingManager::Get().GetContextByName(PlayWorldContextName); if (PlayWorldContext.IsValid()) { UpdatePlayWorldCommandsCache(); } CommandsChangedHandle = FBindingContext::CommandsChanged.AddLambda( [this, PlayWorldContextName](const FBindingContext& Ctx) { if (Ctx.GetContextName() == PlayWorldContextName) { UpdatePlayWorldCommandsCache(); } } ); } FRiderGameControlActionsCache::~FRiderGameControlActionsCache() { FBindingContext::CommandsChanged.Remove(CommandsChangedHandle); } void FRiderGameControlActionsCache::UpdatePlayWorldCommandsCache() { FInputBindingManager& BindingManager = FInputBindingManager::Get(); auto CacheCommand = [&] (FCachedCommandInfo &Cmd, const FName &ContextName) { Cmd.Command = BindingManager.FindCommandInContext(ContextName, Cmd.CommandName); }; const FName PlayWorldContextName = FName("PlayWorld"); for (FCachedCommandInfo& PlayModeCommand : PlayModeCommands) { if (PlayModeCommands->CommandName.IsNone()) continue; CacheCommand(PlayModeCommand, PlayWorldContextName); } CacheCommand(ResumePlaySession, PlayWorldContextName); CacheCommand(PausePlaySession, PlayWorldContextName); CacheCommand(StopPlaySession, PlayWorldContextName); CacheCommand(SingleFrameAdvance, PlayWorldContextName); } class FRiderGameControl { public: FRiderGameControl(rd::Lifetime Lifetime, JetBrains::EditorPlugin::RdEditorModel const &Model, FRiderGameControlActionsCache& ActionsCache); ~FRiderGameControl(); private: void RequestPlayWorldCommand(const FCachedCommandInfo& CommandInfo, int RequestID); void SendRequestSucceed(int RequestID); void SendRequestFailed(int RequestID, JetBrains::EditorPlugin::NotificationType Type, const FString& Message); void ScheduleModelAction(TFunction Action); private: FRiderGameControlActionsCache& Actions; JetBrains::EditorPlugin::RdEditorModel const &Model; int32_t playMode; FDelegateHandle BeginPIEHandle; FDelegateHandle EndPIEHandle; FDelegateHandle PausePIEHandle; FDelegateHandle ResumePIEHandle; FDelegateHandle SingleStepPIEHandle; FDelegateHandle OnObjectPropertyChangedHandle; }; void FRiderGameControl::SendRequestSucceed(int RequestID) { using namespace JetBrains::EditorPlugin; ScheduleModelAction([=](RdEditorModel const& EditorModel) { EditorModel.get_notificationReplyFromEditor().fire(RequestSucceed(RequestID)); }); } void FRiderGameControl::SendRequestFailed(int RequestID, JetBrains::EditorPlugin::NotificationType Type, const FString& Message) { using namespace JetBrains::EditorPlugin; ScheduleModelAction([=](RdEditorModel const& EditorModel) { EditorModel.get_notificationReplyFromEditor().fire(RequestFailed(Type, Message, RequestID)); }); } void FRiderGameControl::RequestPlayWorldCommand(const FCachedCommandInfo& CommandInfo, int RequestID) { using namespace JetBrains::EditorPlugin; if (!CommandInfo.Command.IsValid()) { const FString Message = FString::Format(TEXT("Command '{0}' was not executed.\nCommand was not registered in Unreal Engine"), {CommandInfo.CommandName.ToString()}); SendRequestFailed(RequestID, NotificationType::Error, Message); return; } AsyncTask(ENamedThreads::GameThread, [this, RequestID, CommandInfo]() { if (FPlayWorldCommands::GlobalPlayWorldActions->TryExecuteAction(CommandInfo.Command.ToSharedRef())) { SendRequestSucceed(RequestID); } else { const FString Message = FString::Format(TEXT("Command '{0}' was not executed.\nRejected by Unreal Engine"), {CommandInfo.CommandName.ToString()}); SendRequestFailed(RequestID, NotificationType::Message, Message); } }); } void FRiderGameControl::ScheduleModelAction(TFunction Action) { IRiderLinkModule& RiderLinkModule = IRiderLinkModule::Get(); RiderLinkModule.QueueAction([Action, this]() { Action(Model); }); } FRiderGameControl::FRiderGameControl(rd::Lifetime Lifetime, JetBrains::EditorPlugin::RdEditorModel const &Model, FRiderGameControlActionsCache& ActionsCache) : Actions(ActionsCache), Model(Model) { using namespace JetBrains::EditorPlugin; // Subscribe to Editor events Lifetime->bracket( [this]() { BeginPIEHandle = FEditorDelegates::BeginPIE.AddLambda([this](const bool) { ScheduleModelAction([](RdEditorModel const& model) { model.get_playStateFromEditor().fire(PlayState::Play); }); }); EndPIEHandle = FEditorDelegates::EndPIE.AddLambda([this](const bool) { ScheduleModelAction([](RdEditorModel const& model) { model.get_playStateFromEditor().fire(PlayState::Idle); }); }); PausePIEHandle = FEditorDelegates::PausePIE.AddLambda([this](const bool) { ScheduleModelAction([](RdEditorModel const& model) { model.get_playStateFromEditor().fire(PlayState::Pause); }); }); ResumePIEHandle = FEditorDelegates::ResumePIE.AddLambda([this](const bool) { ScheduleModelAction([](RdEditorModel const& model) { model.get_playStateFromEditor().fire(PlayState::Play); }); }); SingleStepPIEHandle = FEditorDelegates::SingleStepPIE.AddLambda([this](const bool) { ScheduleModelAction([](RdEditorModel const& model) { model.get_playStateFromEditor().fire(PlayState::Play); model.get_playStateFromEditor().fire(PlayState::Pause); }); }); OnObjectPropertyChangedHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddLambda( [this](UObject* obj, FPropertyChangedEvent& ev) { ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); if (!PlayInSettings || obj != PlayInSettings) return; const FPlaySettings Settings = RetrieveSettings(PlayInSettings); int PlayModeNew = FPlaySettings::PackToMode(Settings); if (PlayModeNew == playMode) return; playMode = PlayModeNew; ScheduleModelAction([PlayModeNew](RdEditorModel const& Model) { Model.get_playModeFromEditor().fire(PlayModeNew); }); } ); }, [this]() { FCoreUObjectDelegates::OnObjectPropertyChanged.Remove(OnObjectPropertyChangedHandle); FEditorDelegates::SingleStepPIE.Remove(SingleStepPIEHandle); FEditorDelegates::ResumePIE.Remove(ResumePIEHandle); FEditorDelegates::PausePIE.Remove(PausePIEHandle); FEditorDelegates::EndPIE.Remove(EndPIEHandle); FEditorDelegates::BeginPIE.Remove(BeginPIEHandle); } ); // Subscribe to model ScheduleModelAction([Lifetime, this](RdEditorModel const& Model) { Model.get_requestPlayFromRider() .advise(Lifetime, [this](int requestID) { const ULevelEditorPlaySettings* PlayInSettings = GetDefault(); check(PlayInSettings); const EPlayModeType PlayMode = PlayInSettings->LastExecutedPlayModeType; RequestPlayWorldCommand(Actions.PlayModeCommands[PlayMode], requestID); } ); Model.get_requestPauseFromRider() .advise(Lifetime, [this](int requestID) { RequestPlayWorldCommand(Actions.PausePlaySession, requestID); } ); Model.get_requestResumeFromRider() .advise(Lifetime, [this](int requestID) { RequestPlayWorldCommand(Actions.ResumePlaySession, requestID); } ); Model.get_requestStopFromRider() .advise(Lifetime, [this](int requestID) { RequestPlayWorldCommand(Actions.StopPlaySession, requestID); } ); Model.get_requestFrameSkipFromRider() .advise(Lifetime, [this](int requestID) { RequestPlayWorldCommand(Actions.SingleFrameAdvance, requestID); } ); Model.get_playModeFromRider() .advise(Lifetime, [this](int32_t mode) { ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); check(PlayInSettings); const FPlaySettings NewSettings = FPlaySettings::UnpackFromMode(mode); UpdateSettings(PlayInSettings, NewSettings); } ); }); // Initial sync. const ULevelEditorPlaySettings* PlayInSettings = GetDefault(); check(PlayInSettings); const FPlaySettings Settings = RetrieveSettings(PlayInSettings); playMode = FPlaySettings::PackToMode(Settings); ScheduleModelAction([lambdaPlayMode=playMode](RdEditorModel const &Model) { Model.get_playModeFromEditor().fire(lambdaPlayMode); }); // After all initialization finished/scheduled - mark that module was initialized Lifetime->bracket( [this]() { ScheduleModelAction([](RdEditorModel const& Model) { Model.get_isGameControlModuleInitialized().set(true); }); }, [this]() { ScheduleModelAction([](RdEditorModel const& Model) { Model.get_isGameControlModuleInitialized().set(false); }); } ); } FRiderGameControl::~FRiderGameControl() { } void FRiderGameControlModule::StartupModule() { using namespace JetBrains::EditorPlugin; UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("STARTUP START")); // Actions cache is not related to connection and its lifetimes ActionsCache = MakeUnique(); IRiderLinkModule& RiderLinkModule = IRiderLinkModule::Get(); ModuleLifetimeDefinition = RiderLinkModule.CreateNestedLifetimeDefinition(); rd::Lifetime ModuleLifetime = ModuleLifetimeDefinition.lifetime; RiderLinkModule.ViewModel( ModuleLifetime, [&](rd::Lifetime ModelLifetime, RdEditorModel const& Model) { ModelLifetime->add_action([&]() { GameControl.Reset(); }); GameControl = MakeUnique(ModelLifetime, Model, *ActionsCache); } ); UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("STARTUP FINISH")); } void FRiderGameControlModule::ShutdownModule() { UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("SHUTDOWN START")); ModuleLifetimeDefinition.terminate(); ActionsCache.Reset(); UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("SHUTDOWN FINISH")); } #undef LOCTEXT_NAMESPACE