diff --git a/CHANGELOG.md b/CHANGELOG.md index d800ece594..9878702a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [`0.6.0`] - 2019-07-09 ### Features: -- Automatic local deployment starting. Local deployments are now started automatically for you when pressing the 'Play' button. Local deployment start time has been reduced to around 5.5s~. If your schema has changed during a deployment, the next time you press play the local deployment will be automatically restarted. There is no longer a `spatial` cmd window for a local deployment, the Unreal output window will still contain logs. Runtime logs can be found at `spatial\logs\localdeployment\%timestamp%\runtime.log`. A new option `Show spatial service button` in the SpatialOS Settings menu allows you to turn the 'spatial service' on and off via the SpatialGDK Toolbar for debugging purposes. +- Automatic local deployment starting. Local deployments are now started automatically for you when pressing the 'Play' button. Local deployment start time has been reduced to around 5.5s~. If your schema has changed during a deployment, the next time you press play the local deployment will be automatically restarted. There is no longer a `spatial` cmd window for a local deployment, the Unreal output window will still contain logs. Runtime logs can be found at `spatial\logs\localdeployment\%timestamp%\runtime.log`. A new option `Show spatial service button` in the SpatialOS Settings menu allows you to turn the 'spatial service' on and off via the SpatialGDK Toolbar for debugging purposes. Each time you start the editor in any project the 'spatial service' will be restarted, this is to ensure the service is always running in the correct SpatialOS project. - Added external schema code-generation tool for [non-Unreal server-worker types]({{urlRoot}}/content/non-unreal-server-worker-types). If you create non-Unreal server-worker types using the [SpatialOS Worker SDK](https://docs.improbable.io/reference/13.8/shared/sdks-and-data-overview) outside of the GDK and your Unreal Engine, you manually create [schema]({{urlRoot}/content/glossary#schema). Use the new [helper-script]({{urlRoot}}/content/helper-scripts) to generate Unreal code from manually-created schema; it enables your Unreal game code to interoperate with non-Unreal server-worker types. - Added simulated player tools, which will allow you to create logic to simulate the behavior of real players. Simulated players can be used to scale test your game to hundreds of players. Simulated players can be launched locally as part of your development flow for quick iteration, as well as part of a separate "simulated player deployment" to connect a configurable amount of simulated players to your running game deployment. - Factored out writing of Linux worker start scripts into a library, and added a standalone `WriteLinuxScript.exe` to _just_ write the launch script (for use in custom build pipelines). diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index 97dc26c798..bc9f6446fb 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -271,14 +271,14 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuCont void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() { - ShowTaskStartNotification("Started snapshot generation"); + OnShowTaskStartNotification("Started snapshot generation"); const USpatialGDKEditorSettings* Settings = GetDefault(); SpatialGDKEditorInstance->GenerateSnapshot( GEditor->GetEditorWorldContext().World(), Settings->GetSpatialOSSnapshotFile(), - FSimpleDelegate::CreateLambda([this]() { ShowSuccessNotification("Snapshot successfully generated!"); }), - FSimpleDelegate::CreateLambda([this]() { ShowFailedNotification("Snapshot generation failed!"); }), + FSimpleDelegate::CreateLambda([this]() { OnShowSuccessNotification("Snapshot successfully generated!"); }), + FSimpleDelegate::CreateLambda([this]() { OnShowFailedNotification("Snapshot generation failed!"); }), FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { FMessageDialog::Debugf(FText::FromString(ErrorText)); })); } @@ -292,40 +292,62 @@ void FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked() GenerateSchema(true); } -void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& NotificationText) +void FSpatialGDKEditorToolbarModule::OnShowTaskStartNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [this, NotificationText] { - if (TaskNotificationPtr.IsValid()) + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { - TaskNotificationPtr.Pin()->ExpireAndFadeout(); + Module->ShowTaskStartNotification(NotificationText); } + }); +} - if (GEditor && ExecutionStartSound) - { - GEditor->PlayEditorSound(ExecutionStartSound); - } +void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& NotificationText) +{ + // If a task notification already exists then expire it. + if (TaskNotificationPtr.IsValid()) + { + TaskNotificationPtr.Pin()->ExpireAndFadeout(); + } - FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); - Info.Image = FSpatialGDKEditorToolbarStyle::Get().GetBrush(TEXT("SpatialGDKEditorToolbar.SpatialOSLogo")); - Info.ExpireDuration = 5.0f; - Info.bFireAndForget = false; + if (GEditor && ExecutionStartSound) + { + GEditor->PlayEditorSound(ExecutionStartSound); + } - TaskNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); + Info.Image = FSpatialGDKEditorToolbarStyle::Get().GetBrush(TEXT("SpatialGDKEditorToolbar.SpatialOSLogo")); + Info.ExpireDuration = 5.0f; + Info.bFireAndForget = false; - if (TaskNotificationPtr.IsValid()) + TaskNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + + if (TaskNotificationPtr.IsValid()) + { + TaskNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + } +} + +void FSpatialGDKEditorToolbarModule::OnShowSuccessNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { - TaskNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + Module->ShowSuccessNotification(NotificationText); } }); } void FSpatialGDKEditorToolbarModule::ShowSuccessNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [this, NotificationText]{ - TSharedPtr Notification = TaskNotificationPtr.Pin(); + TSharedPtr Notification = TaskNotificationPtr.Pin(); + if (Notification.IsValid()) + { Notification->SetFadeInDuration(0.1f); Notification->SetFadeOutDuration(0.5f); - Notification->SetExpireDuration(7.5f); + Notification->SetExpireDuration(5.0f); Notification->SetText(FText::AsCultureInvariant(NotificationText)); Notification->SetCompletionState(SNotificationItem::CS_Success); Notification->ExpireAndFadeout(); @@ -334,24 +356,37 @@ void FSpatialGDKEditorToolbarModule::ShowSuccessNotification(const FString& Noti { GEditor->PlayEditorSound(ExecutionSuccessSound); } + } +} + +void FSpatialGDKEditorToolbarModule::OnShowFailedNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + Module->ShowFailedNotification(NotificationText); + } }); } void FSpatialGDKEditorToolbarModule::ShowFailedNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [this, NotificationText]{ - TSharedPtr Notification = TaskNotificationPtr.Pin(); + TSharedPtr Notification = TaskNotificationPtr.Pin(); + if (Notification.IsValid()) + { + Notification->SetFadeInDuration(0.1f); + Notification->SetFadeOutDuration(0.5f); + Notification->SetExpireDuration(5.0); Notification->SetText(FText::AsCultureInvariant(NotificationText)); Notification->SetCompletionState(SNotificationItem::CS_Fail); - Notification->SetExpireDuration(5.0f); - Notification->ExpireAndFadeout(); if (GEditor && ExecutionFailSound) { GEditor->PlayEditorSound(ExecutionFailSound); } - }); + } } bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const @@ -448,18 +483,18 @@ void FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { FDateTime StartTime = FDateTime::Now(); - ShowTaskStartNotification(TEXT("Starting spatial service...")); + OnShowTaskStartNotification(TEXT("Starting spatial service...")); if (!LocalDeploymentManager->TryStartSpatialService()) { - ShowFailedNotification(TEXT("Spatial service failed to start")); + OnShowFailedNotification(TEXT("Spatial service failed to start")); UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not start spatial service.")); return; } FTimespan Span = FDateTime::Now() - StartTime; - ShowSuccessNotification(TEXT("Spatial service started!")); + OnShowSuccessNotification(TEXT("Spatial service started!")); UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service started in %f seconds."), Span.GetTotalSeconds()); }); } @@ -469,18 +504,18 @@ void FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { FDateTime StartTime = FDateTime::Now(); - ShowTaskStartNotification(TEXT("Stopping spatial service...")); + OnShowTaskStartNotification(TEXT("Stopping spatial service...")); if (!LocalDeploymentManager->TryStopSpatialService()) { - ShowFailedNotification(TEXT("Spatial service failed to stop")); + OnShowFailedNotification(TEXT("Spatial service failed to stop")); UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not stop spatial service.")); return; } FTimespan Span = FDateTime::Now() - StartTime; - ShowSuccessNotification(TEXT("Spatial service stopped!")); + OnShowSuccessNotification(TEXT("Spatial service stopped!")); UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service stopped in %f secoonds."), Span.GetTotalSeconds()); }); } @@ -529,10 +564,10 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() } // If schema has been regenerated then we need to restart spatial. - if (bRedeployRequired) + if (bRedeployRequired && LocalDeploymentManager->IsLocalDeploymentRunning()) { UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Schema has changed since last session. Local deployment must restart.")); - ShowTaskStartNotification(TEXT("Schema has changed. Local deployment restarting.")); + OnShowTaskStartNotification(TEXT("Schema has changed. Local deployment restarting.")); LocalDeploymentManager->TryStopLocalDeployment(); bRedeployRequired = false; } @@ -542,14 +577,14 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() return; } - ShowTaskStartNotification(TEXT("Starting local deployment...")); + OnShowTaskStartNotification(TEXT("Starting local deployment...")); if (LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, LaunchFlags)) { - ShowSuccessNotification(TEXT("Local deployment started!")); + OnShowSuccessNotification(TEXT("Local deployment started!")); } else { - ShowFailedNotification(TEXT("Local deployment failed to start")); + OnShowFailedNotification(TEXT("Local deployment failed to start")); } }); } @@ -563,14 +598,14 @@ void FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked() { AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { - ShowTaskStartNotification(TEXT("Stopping local deployment...")); + OnShowTaskStartNotification(TEXT("Stopping local deployment...")); if (LocalDeploymentManager->TryStopLocalDeployment()) { - ShowSuccessNotification(TEXT("Successfully stopped local deployment")); + OnShowSuccessNotification(TEXT("Successfully stopped local deployment")); } else { - ShowFailedNotification(TEXT("Failed to stop local deployment!")); + OnShowFailedNotification(TEXT("Failed to stop local deployment!")); } }); } @@ -808,41 +843,41 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) if (SpatialGDKEditorInstance->FullScanRequired()) { - ShowTaskStartNotification("Initial Schema Generation"); + OnShowTaskStartNotification("Initial Schema Generation"); if (SpatialGDKEditorInstance->GenerateSchema(true)) { - ShowSuccessNotification("Initial Schema Generation completed!"); + OnShowSuccessNotification("Initial Schema Generation completed!"); } else { - ShowFailedNotification("Initial Schema Generation failed"); + OnShowFailedNotification("Initial Schema Generation failed"); } } else if (bFullScan) { - ShowTaskStartNotification("Generating Schema (Full)"); + OnShowTaskStartNotification("Generating Schema (Full)"); if (SpatialGDKEditorInstance->GenerateSchema(true)) { - ShowSuccessNotification("Full Schema Generation completed!"); + OnShowSuccessNotification("Full Schema Generation completed!"); } else { - ShowFailedNotification("Full Schema Generation failed"); + OnShowFailedNotification("Full Schema Generation failed"); } } else { - ShowTaskStartNotification("Generating Schema (Incremental)"); + OnShowTaskStartNotification("Generating Schema (Incremental)"); if (SpatialGDKEditorInstance->GenerateSchema(false)) { - ShowSuccessNotification("Incremental Schema Generation completed!"); + OnShowSuccessNotification("Incremental Schema Generation completed!"); } else { - ShowFailedNotification("Incremental Schema Generation failed"); + OnShowFailedNotification("Incremental Schema Generation failed"); } } } diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index 9536b042a9..b33582ba87 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -84,8 +84,13 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TSharedRef CreateGenerateSchemaMenuContent(); + void OnShowTaskStartNotification(const FString& NotificationText); void ShowTaskStartNotification(const FString& NotificationText); + + void OnShowSuccessNotification(const FString& NotificationText); void ShowSuccessNotification(const FString& NotificationText); + + void OnShowFailedNotification(const FString& NotificationText); void ShowFailedNotification(const FString& NotificationText); bool ValidateGeneratedLaunchConfig() const; diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp index 9fddcb7cf9..a027192b27 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -42,8 +42,15 @@ FLocalDeploymentManager::FLocalDeploymentManager() // Watch the worker config directory for changes. StartUpWorkerConfigDirectoryWatcher(); - // Ensure we have an up to date state of the spatial service and local deployment. - RefreshServiceStatus(); + // Restart the spatial service so it is guaranteed to be running in the current project. + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + TryStopSpatialService(); + TryStartSpatialService(); + + // Ensure we have an up to date state of the spatial service and local deployment. + RefreshServiceStatus(); + }); } const FString FLocalDeploymentManager::GetSpotExe() @@ -87,16 +94,23 @@ FString FLocalDeploymentManager::GetProjectName() FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialDirectory, SpatialFileName)); TSharedPtr JsonParsedSpatialFile; - if (!ParseJson(SpatialFileResult, JsonParsedSpatialFile)) + if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spatialos.json failed. Can't get project name.")); + if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectName)) + { + return ProjectName; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + } } - - if (!JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectName)) + else { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spatialos.json failed. Can't get project name.")); } + ProjectName.Empty(); return ProjectName; } @@ -169,12 +183,16 @@ void FLocalDeploymentManager::RefreshServiceStatus() // Timers must be started on the game thread. AsyncTask(ENamedThreads::GameThread, [this] { - // Start checking for the service status. - FTimerHandle RefreshTimer; - GEditor->GetTimerManager()->SetTimer(RefreshTimer, [this]() + // It's possible that GEditor won't exist when shutting down. + if (GEditor != nullptr) { - RefreshServiceStatus(); - }, RefreshFrequency, false); + // Start checking for the service status. + FTimerHandle RefreshTimer; + GEditor->GetTimerManager()->SetTimer(RefreshTimer, [this]() + { + RefreshServiceStatus(); + }, RefreshFrequency, false); + } }); }); } @@ -200,12 +218,6 @@ bool FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr bStartingDeployment = true; - if (!bSpatialServiceInProjectDirectory) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service is running in a different project directory! Cannot start local deployment.")); - return false; - } - // If the service is not running then start it. if (!bSpatialServiceRunning) { @@ -376,12 +388,6 @@ bool FLocalDeploymentManager::TryStartSpatialService() bool FLocalDeploymentManager::TryStopSpatialService() { - if (!bSpatialServiceRunning) - { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Tried to stop spatial service but it's not running.")); - return false; - } - bStoppingSpatialService = true; FString SpatialServiceStartArgs = TEXT("service stop"); @@ -394,7 +400,7 @@ bool FLocalDeploymentManager::TryStopSpatialService() { UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service stopped!")); bSpatialServiceRunning = false; - bSpatialServiceInProjectDirectory = false; + bSpatialServiceInProjectDirectory = true; bLocalDeploymentRunning = false; return true; } @@ -403,7 +409,6 @@ bool FLocalDeploymentManager::TryStopSpatialService() UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to stop! %s"), *ServiceStopResult); } - IsSpatialServiceRunning(); return false; } @@ -488,6 +493,7 @@ bool FLocalDeploymentManager::GetServiceStatus() if (ServiceStatusResult.Contains(TEXT("Local API service is not running."))) { UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service not running.")); + bSpatialServiceInProjectDirectory = true; bSpatialServiceRunning = false; bLocalDeploymentRunning = false; return false; @@ -505,26 +511,31 @@ bool FLocalDeploymentManager::GetServiceStatus() bool FLocalDeploymentManager::IsServiceInCorrectDirectory(const FString& ServiceStatusResult) { // Get the project file path and ensure it matches the one for the currently running project. - FString SpatialServicePath; - if (ServiceStatusResult.Split(TEXT("project file path: "), nullptr, &SpatialServicePath)) + FString SpatialServiceProjectPath; + if (ServiceStatusResult.Split(TEXT("project file path: "), nullptr, &SpatialServiceProjectPath)) { // Remove the trailing '" cli-version' that comes with non-interactive 'spatial' calls. - SpatialServicePath.Split(TEXT("\" cli-version"), &SpatialServicePath, nullptr); + SpatialServiceProjectPath.Split(TEXT("\" cli-version"), &SpatialServiceProjectPath, nullptr); FString CurrentProjectSpatialPath = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("spatialos.json")); - FPaths::NormalizeDirectoryName(SpatialServicePath); - FPaths::RemoveDuplicateSlashes(SpatialServicePath); + FPaths::NormalizeDirectoryName(SpatialServiceProjectPath); + FPaths::RemoveDuplicateSlashes(SpatialServiceProjectPath); - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service running at path: %s "), *SpatialServicePath); + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service running at path: %s "), *SpatialServiceProjectPath); - if (CurrentProjectSpatialPath.Equals(SpatialServicePath, ESearchCase::IgnoreCase)) + if (CurrentProjectSpatialPath.Equals(SpatialServiceProjectPath, ESearchCase::IgnoreCase)) { return true; } + else if (SpatialServiceProjectPath.Contains(TEXT("not available"))) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service has hit an erroneous state! Please run 'spatial service stop'.")); + return false; + } else { UE_LOG(LogSpatialDeploymentManager, Error, - TEXT("Spatial service running in a different project! Please run 'spatial service stop' if you wish to launch deployments in the current project. Service at: %s"), *SpatialServicePath); + TEXT("Spatial service running in a different project! Please run 'spatial service stop' if you wish to launch deployments in the current project. Service at: %s"), *SpatialServiceProjectPath); return false; } }