You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
515 lines
16 KiB
C++
515 lines
16 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "LiveCodingConsole.h"
|
|
#include "RequiredProgramMainCPPInclude.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "StandaloneRenderer.h"
|
|
#include "LiveCodingConsoleStyle.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "SLogWidget.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "ILiveCodingServer.h"
|
|
#include "Features/IModularFeatures.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Windows/WindowsHWrapper.h"
|
|
#include "Misc/MonitoredProcess.h"
|
|
#include "LiveCodingManifest.h"
|
|
#include "SourceCodeAccess/Public/ISourceCodeAccessModule.h"
|
|
#include "Modules/ModuleInterface.h"
|
|
#include "Widgets/Input/SButton.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "LiveCodingConsole"
|
|
|
|
IMPLEMENT_APPLICATION(LiveCodingConsole, "LiveCodingConsole");
|
|
|
|
static void OnRequestExit()
|
|
{
|
|
RequestEngineExit(TEXT("LiveCoding console closed"));
|
|
}
|
|
|
|
class FLiveCodingConsoleApp
|
|
{
|
|
private:
|
|
FCriticalSection CriticalSection;
|
|
FSlateApplication& Slate;
|
|
ILiveCodingServer& Server;
|
|
TSharedPtr<SLogWidget> LogWidget;
|
|
TSharedPtr<SWindow> Window;
|
|
TSharedPtr<SNotificationItem> CompileNotification;
|
|
TArray<FSimpleDelegate> MainThreadTasks;
|
|
bool bRequestCancel;
|
|
FDateTime LastPatchTime;
|
|
FDateTime NextPatchStartTime;
|
|
|
|
public:
|
|
FLiveCodingConsoleApp(FSlateApplication& InSlate, ILiveCodingServer& InServer)
|
|
: Slate(InSlate)
|
|
, Server(InServer)
|
|
, bRequestCancel(false)
|
|
, LastPatchTime(FDateTime::MinValue())
|
|
, NextPatchStartTime(FDateTime::MinValue())
|
|
{
|
|
}
|
|
|
|
void Run()
|
|
{
|
|
// open up the app window
|
|
LogWidget = SNew(SLogWidget);
|
|
|
|
// Create the window
|
|
Window =
|
|
SNew(SWindow)
|
|
.Title(GetWindowTitle())
|
|
.ClientSize(FVector2D(1200.0f, 600.0f))
|
|
.ActivationPolicy(EWindowActivationPolicy::Never)
|
|
.IsInitiallyMaximized(false)
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder"))
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+ SVerticalBox::Slot()
|
|
.FillHeight(1.0f)
|
|
[
|
|
LogWidget.ToSharedRef()
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.HAlign(HAlign_Center)
|
|
.AutoHeight()
|
|
.Padding(0.0f, 6.0f, 0.0f, 4.0f)
|
|
[
|
|
SNew(SButton)
|
|
.Text(LOCTEXT("QuickRestart", "Quick Restart"))
|
|
.OnClicked(FOnClicked::CreateRaw(this, &FLiveCodingConsoleApp::RestartTargets))
|
|
]
|
|
]
|
|
];
|
|
|
|
// Add the window without showing it
|
|
Slate.AddWindow(Window.ToSharedRef(), false);
|
|
|
|
// Show the window without stealling focus
|
|
if (!FParse::Param(FCommandLine::Get(), TEXT("Hidden")))
|
|
{
|
|
HWND ForegroundWindow = GetForegroundWindow();
|
|
if (ForegroundWindow != nullptr)
|
|
{
|
|
::SetWindowPos((HWND)Window->GetNativeWindow()->GetOSWindowHandle(), ForegroundWindow, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
}
|
|
Window->ShowWindow();
|
|
}
|
|
|
|
// Get the server interface
|
|
Server.GetBringToFrontDelegate().BindRaw(this, &FLiveCodingConsoleApp::BringToFrontAsync);
|
|
Server.GetLogOutputDelegate().BindRaw(this, &FLiveCodingConsoleApp::AppendLogLine);
|
|
Server.GetShowConsoleDelegate().BindRaw(this, &FLiveCodingConsoleApp::BringToFrontAsync);
|
|
Server.GetSetVisibleDelegate().BindRaw(this, &FLiveCodingConsoleApp::SetVisibleAsync);
|
|
Server.GetCompileDelegate().BindRaw(this, &FLiveCodingConsoleApp::CompilePatch);
|
|
Server.GetCompileStartedDelegate().BindRaw(this, &FLiveCodingConsoleApp::OnCompileStartedAsync);
|
|
Server.GetCompileFinishedDelegate().BindLambda([this](ELiveCodingResult Result, const wchar_t* Message){ OnCompileFinishedAsync(Result, Message); });
|
|
Server.GetStatusChangeDelegate().BindLambda([this](const wchar_t* Status){ OnStatusChangedAsync(Status); });
|
|
|
|
// Start the server
|
|
FString ProcessGroupName;
|
|
if (FParse::Value(FCommandLine::Get(), TEXT("-Group="), ProcessGroupName))
|
|
{
|
|
Server.Start(*ProcessGroupName);
|
|
Window->SetRequestDestroyWindowOverride(FRequestDestroyWindowOverride::CreateLambda([this](const TSharedRef<SWindow>&){ SetVisible(false); }));
|
|
}
|
|
else
|
|
{
|
|
LogWidget->AppendLine(GetLogColor(ELiveCodingLogVerbosity::Warning), TEXT("Running in standalone mode. Server is disabled."));
|
|
}
|
|
|
|
// Setting focus seems to have to happen after the Window has been added
|
|
Slate.ClearKeyboardFocus(EFocusCause::Cleared);
|
|
|
|
// loop until the app is ready to quit
|
|
while (!IsEngineExitRequested())
|
|
{
|
|
BeginExitIfRequested();
|
|
|
|
Slate.PumpMessages();
|
|
Slate.Tick();
|
|
|
|
FPlatformProcess::Sleep(1.0f / 30.0f);
|
|
|
|
// Execute all the main thread tasks
|
|
FScopeLock Lock(&CriticalSection);
|
|
for (FSimpleDelegate& MainThreadTask : MainThreadTasks)
|
|
{
|
|
MainThreadTask.Execute();
|
|
}
|
|
MainThreadTasks.Empty();
|
|
}
|
|
|
|
// Make sure the window is hidden, because it might take a while for the background thread to finish.
|
|
Window->HideWindow();
|
|
|
|
// Shutdown the server
|
|
Server.Stop();
|
|
}
|
|
|
|
private:
|
|
FText GetWindowTitle()
|
|
{
|
|
FString ProjectName;
|
|
if (FParse::Value(FCommandLine::Get(), TEXT("-ProjectName="), ProjectName))
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("ProjectName"), FText::FromString(ProjectName));
|
|
return FText::Format(LOCTEXT("WindowTitleWithProject", "{ProjectName} - Live Coding"), Args);
|
|
}
|
|
return LOCTEXT("WindowTitle", "Live Coding");
|
|
}
|
|
|
|
void BringToFrontAsync()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MainThreadTasks.Add(FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::BringToFront));
|
|
}
|
|
|
|
void BringToFront()
|
|
{
|
|
HWND WindowHandle = (HWND)Window->GetNativeWindow()->GetOSWindowHandle();
|
|
if (IsIconic(WindowHandle))
|
|
{
|
|
ShowWindow(WindowHandle, SW_RESTORE);
|
|
}
|
|
::SetWindowPos(WindowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
::SetWindowPos(WindowHandle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
}
|
|
|
|
static FSlateColor GetLogColor(ELiveCodingLogVerbosity Verbosity)
|
|
{
|
|
switch (Verbosity)
|
|
{
|
|
case ELiveCodingLogVerbosity::Success:
|
|
return FSlateColor(FLinearColor::Green);
|
|
case ELiveCodingLogVerbosity::Failure:
|
|
return FSlateColor(FLinearColor::Red);
|
|
case ELiveCodingLogVerbosity::Warning:
|
|
return FSlateColor(FLinearColor::Yellow);
|
|
default:
|
|
return FSlateColor(FLinearColor::Gray);
|
|
}
|
|
}
|
|
|
|
void AppendLogLine(ELiveCodingLogVerbosity Verbosity, const TCHAR* Text)
|
|
{
|
|
// SLogWidget has its own synchronization
|
|
LogWidget->AppendLine(GetLogColor(Verbosity), MoveTemp(Text));
|
|
}
|
|
|
|
bool CompilePatch(const TArray<FString>& Targets, const TArray<FString>& ValidModules, TArray<FString>& RequiredModules, TMap<FString, TArray<FString>>& ModuleToObjectFiles)
|
|
{
|
|
// Update the compile start time. This gets copied into the last patch time once a patch has been confirmed to have been applied.
|
|
NextPatchStartTime = FDateTime::UtcNow();
|
|
|
|
// Get the UBT path
|
|
FString Executable;
|
|
FString Entry;
|
|
if( GConfig->GetString( TEXT("PlatformPaths"), TEXT("UnrealBuildTool"), Entry, GEngineIni ))
|
|
{
|
|
Executable = FPaths::ConvertRelativePathToFull(FPaths::RootDir() / Entry);
|
|
}
|
|
else
|
|
{
|
|
Executable = FPaths::EngineDir() / TEXT("Binaries/DotNET/UnrealBuildTool.exe");
|
|
}
|
|
FPaths::MakePlatformFilename(Executable);
|
|
|
|
// Write out the list of lazy-loaded modules for UBT to check
|
|
FString ModulesFileName = FPaths::ConvertRelativePathToFull(FPaths::EngineIntermediateDir() / TEXT("LiveCodingModules.txt"));
|
|
FFileHelper::SaveStringArrayToFile(ValidModules, *ModulesFileName);
|
|
|
|
// Delete the output file for non-whitelisted modules
|
|
FString ModulesOutputFileName = ModulesFileName + TEXT(".out");
|
|
IFileManager::Get().Delete(*ModulesOutputFileName);
|
|
|
|
// Delete any existing manifest
|
|
FString ManifestFileName = FPaths::ConvertRelativePathToFull(FPaths::EngineIntermediateDir() / TEXT("LiveCoding.json"));
|
|
IFileManager::Get().Delete(*ManifestFileName);
|
|
|
|
// Build the argument list
|
|
FString Arguments;
|
|
for (const FString& Target : Targets)
|
|
{
|
|
Arguments += FString::Printf(TEXT("-Target=\"%s\" "), *Target.Replace(TEXT("\""), TEXT("\"\"")));
|
|
}
|
|
Arguments += FString::Printf(TEXT("-LiveCoding -LiveCodingModules=\"%s\" -LiveCodingManifest=\"%s\" -WaitMutex"), *ModulesFileName, *ManifestFileName);
|
|
AppendLogLine(ELiveCodingLogVerbosity::Info, *FString::Printf(TEXT("Running %s %s"), *Executable, *Arguments));
|
|
|
|
// Spawn UBT and wait for it to complete (or the compile button to be pressed)
|
|
FMonitoredProcess Process(*Executable, *Arguments, true);
|
|
Process.OnOutput().BindLambda([this](const FString& Text){ AppendLogLine(ELiveCodingLogVerbosity::Info, *(FString(TEXT(" ")) + Text)); });
|
|
Process.Launch();
|
|
while(Process.Update())
|
|
{
|
|
if (HasCancelledBuild())
|
|
{
|
|
AppendLogLine(ELiveCodingLogVerbosity::Warning, TEXT("Build cancelled."));
|
|
return false;
|
|
}
|
|
FPlatformProcess::Sleep(0.1f);
|
|
}
|
|
|
|
int ReturnCode = Process.GetReturnCode();
|
|
if (ReturnCode != 0)
|
|
{
|
|
if (FPaths::FileExists(ModulesOutputFileName))
|
|
{
|
|
FFileHelper::LoadFileToStringArray(RequiredModules, *ModulesOutputFileName);
|
|
}
|
|
else
|
|
{
|
|
AppendLogLine(ELiveCodingLogVerbosity::Failure, TEXT("Build failed."));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Read the output manifest
|
|
FString ManifestFailReason;
|
|
FLiveCodingManifest Manifest;
|
|
if (!Manifest.Read(*ManifestFileName, ManifestFailReason))
|
|
{
|
|
AppendLogLine(ELiveCodingLogVerbosity::Failure, *ManifestFailReason);
|
|
return false;
|
|
}
|
|
|
|
// Override the linker path
|
|
Server.SetLinkerPath(*Manifest.LinkerPath, Manifest.LinkerEnvironment);
|
|
|
|
// Strip out all the files that haven't been modified
|
|
IFileManager& FileManager = IFileManager::Get();
|
|
for(TPair<FString, TArray<FString>>& Pair : Manifest.BinaryToObjectFiles)
|
|
{
|
|
FDateTime MinTimeStamp = FileManager.GetTimeStamp(*Pair.Key);
|
|
if(LastPatchTime > MinTimeStamp)
|
|
{
|
|
MinTimeStamp = LastPatchTime;
|
|
}
|
|
|
|
for (const FString& ObjectFileName : Pair.Value)
|
|
{
|
|
if (FileManager.GetTimeStamp(*ObjectFileName) > MinTimeStamp)
|
|
{
|
|
ModuleToObjectFiles.FindOrAdd(Pair.Key).Add(ObjectFileName);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CancelBuild()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
bRequestCancel = true;
|
|
}
|
|
|
|
bool HasCancelledBuild()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
return bRequestCancel;
|
|
}
|
|
|
|
FReply RestartTargets()
|
|
{
|
|
Server.RestartTargets();
|
|
return FReply::Handled();
|
|
}
|
|
|
|
void SetVisibleAsync(bool bVisible)
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, bVisible](){ SetVisible(bVisible); }));
|
|
}
|
|
|
|
void SetVisible(bool bVisible)
|
|
{
|
|
if (bVisible)
|
|
{
|
|
if (!Window->IsVisible())
|
|
{
|
|
Window->ShowWindow();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Window->IsVisible())
|
|
{
|
|
Window->HideWindow();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ShowConsole()
|
|
{
|
|
SetVisible(true);
|
|
BringToFront();
|
|
}
|
|
|
|
void OnCompileStartedAsync()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MainThreadTasks.Add(FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::OnCompileStarted));
|
|
bRequestCancel = false;
|
|
NextPatchStartTime = FDateTime::UtcNow();
|
|
}
|
|
|
|
void OnCompileStarted()
|
|
{
|
|
if (!CompileNotification.IsValid())
|
|
{
|
|
ShowConsole();
|
|
|
|
FNotificationInfo Info(FText::FromString(TEXT("Starting...")));
|
|
Info.bFireAndForget = false;
|
|
Info.FadeOutDuration = 0.0f;
|
|
Info.ExpireDuration = 0.0f;
|
|
Info.Hyperlink = FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::ShowConsole);
|
|
Info.HyperlinkText = LOCTEXT("BuildStatusShowConsole", "Show Console");
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(LOCTEXT("BuildStatusCancel", "Cancel"), FText(), FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::CancelBuild), SNotificationItem::CS_Pending));
|
|
|
|
CompileNotification = FSlateNotificationManager::Get().AddNotification(Info);
|
|
CompileNotification->SetCompletionState(SNotificationItem::CS_Pending);
|
|
}
|
|
}
|
|
|
|
void OnCompileFinishedAsync(ELiveCodingResult Result, const FString& Status)
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, Result, Status](){ OnCompileFinished(Result, Status); }));
|
|
|
|
if (Result == ELiveCodingResult::Success)
|
|
{
|
|
LastPatchTime = NextPatchStartTime;
|
|
}
|
|
}
|
|
|
|
void OnCompileFinished(ELiveCodingResult Result, const FString& Status)
|
|
{
|
|
if(CompileNotification.IsValid())
|
|
{
|
|
if(Result == ELiveCodingResult::Success)
|
|
{
|
|
CompileNotification->SetText(FText::FromString(Status));
|
|
CompileNotification->SetCompletionState(SNotificationItem::CS_Success);
|
|
CompileNotification->SetExpireDuration(1.5f);
|
|
CompileNotification->SetFadeOutDuration(0.4f);
|
|
}
|
|
else if(HasCancelledBuild())
|
|
{
|
|
CompileNotification->SetExpireDuration(0.0f);
|
|
CompileNotification->SetFadeOutDuration(0.1f);
|
|
}
|
|
else
|
|
{
|
|
CompileNotification->SetText(FText::FromString(Status));
|
|
CompileNotification->SetCompletionState(SNotificationItem::CS_Fail);
|
|
CompileNotification->SetExpireDuration(5.0f);
|
|
CompileNotification->SetFadeOutDuration(2.0f);
|
|
}
|
|
CompileNotification->ExpireAndFadeout();
|
|
CompileNotification.Reset();
|
|
}
|
|
}
|
|
|
|
void OnStatusChangedAsync(const FString& Status)
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, Status](){ OnCompileStatusChanged(Status); }));
|
|
}
|
|
|
|
void OnCompileStatusChanged(const FString& Status)
|
|
{
|
|
if (CompileNotification.IsValid())
|
|
{
|
|
CompileNotification->SetText(FText::FromString(Status));
|
|
}
|
|
}
|
|
};
|
|
|
|
bool LiveCodingConsoleMain(const TCHAR* CmdLine)
|
|
{
|
|
// start up the main loop
|
|
GEngineLoop.PreInit(CmdLine);
|
|
check(GConfig && GConfig->IsReadyForUse());
|
|
|
|
// Initialize high DPI mode
|
|
FSlateApplication::InitHighDPI(true);
|
|
|
|
{
|
|
// Create the platform slate application (what FSlateApplication::Get() returns)
|
|
TSharedRef<FSlateApplication> Slate = FSlateApplication::Create(MakeShareable(FPlatformApplicationMisc::CreateApplication()));
|
|
|
|
{
|
|
// Initialize renderer
|
|
TSharedRef<FSlateRenderer> SlateRenderer = GetStandardStandaloneRenderer();
|
|
|
|
// Try to initialize the renderer. It's possible that we launched when the driver crashed so try a few times before giving up.
|
|
bool bRendererInitialized = Slate->InitializeRenderer(SlateRenderer, true);
|
|
if (!bRendererInitialized)
|
|
{
|
|
// Close down the Slate application
|
|
FSlateApplication::Shutdown();
|
|
return false;
|
|
}
|
|
|
|
// Set the normal UE4 IsEngineExitRequested() when outer frame is closed
|
|
Slate->SetExitRequestedHandler(FSimpleDelegate::CreateStatic(&OnRequestExit));
|
|
|
|
// Prepare the custom Slate styles
|
|
FLiveCodingConsoleStyle::Initialize();
|
|
|
|
// Set the icon
|
|
FAppStyle::SetAppStyleSet(FLiveCodingConsoleStyle::Get());
|
|
|
|
// Load the source code access module
|
|
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>(FName("SourceCodeAccess"));
|
|
|
|
// Manually load in the source code access plugins, as standalone programs don't currently support plugins.
|
|
#if PLATFORM_MAC
|
|
IModuleInterface& XCodeSourceCodeAccessModule = FModuleManager::LoadModuleChecked<IModuleInterface>(FName("XCodeSourceCodeAccess"));
|
|
SourceCodeAccessModule.SetAccessor(FName("XCodeSourceCodeAccess"));
|
|
#elif PLATFORM_WINDOWS
|
|
IModuleInterface& VisualStudioSourceCodeAccessModule = FModuleManager::LoadModuleChecked<IModuleInterface>(FName("VisualStudioSourceCodeAccess"));
|
|
SourceCodeAccessModule.SetAccessor(FName("VisualStudioSourceCodeAccess"));
|
|
#endif
|
|
|
|
// Load the server module
|
|
FModuleManager::Get().LoadModuleChecked<ILiveCodingServerModule>(TEXT("LiveCodingServer"));
|
|
ILiveCodingServer& Server = IModularFeatures::Get().GetModularFeature<ILiveCodingServer>(LIVE_CODING_SERVER_FEATURE_NAME);
|
|
|
|
// Run the inner application loop
|
|
FLiveCodingConsoleApp App(Slate.Get(), Server);
|
|
App.Run();
|
|
|
|
// Unload the server module
|
|
FModuleManager::Get().UnloadModule(TEXT("LiveCodingServer"));
|
|
|
|
// Clean up the custom styles
|
|
FLiveCodingConsoleStyle::Shutdown();
|
|
}
|
|
|
|
// Close down the Slate application
|
|
FSlateApplication::Shutdown();
|
|
}
|
|
|
|
FEngineLoop::AppPreExit();
|
|
FModuleManager::Get().UnloadModulesAtShutdown();
|
|
FEngineLoop::AppExit();
|
|
return true;
|
|
}
|
|
|
|
int WINAPI WinMain(HINSTANCE hCurrInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
|
{
|
|
hInstance = hCurrInstance;
|
|
return LiveCodingConsoleMain(GetCommandLineW())? 0 : 1;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|