feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

View File

@@ -11,11 +11,15 @@ set(EDITOR_TEST_SOURCES
test_scene_viewport_scale_gizmo.cpp
test_scene_viewport_picker.cpp
test_scene_viewport_overlay_renderer.cpp
test_script_component_editor_utils.cpp
test_viewport_host_surface_utils.cpp
test_viewport_object_id_picker.cpp
test_viewport_render_targets.cpp
test_viewport_render_flow_utils.cpp
test_builtin_icon_layout_utils.cpp
test_editor_script_assembly_builder.cpp
test_editor_script_assembly_builder_utils.cpp
${CMAKE_SOURCE_DIR}/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
@@ -26,13 +30,19 @@ set(EDITOR_TEST_SOURCES
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp
)
if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies)
list(APPEND EDITOR_TEST_SOURCES
test_play_session_controller_scripting.cpp
)
endif()
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
if(MSVC)
set_target_properties(editor_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
target_compile_options(editor_tests PRIVATE /FS)
target_compile_options(editor_tests PRIVATE /FS /utf-8)
endif()
target_link_libraries(editor_tests PRIVATE
@@ -50,5 +60,28 @@ target_include_directories(editor_tests PRIVATE
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE)
file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE)
target_compile_definitions(editor_tests PRIVATE
XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE}"
XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE}"
)
if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies)
add_dependencies(editor_tests xcengine_managed_assemblies)
file(TO_CMAKE_PATH "${XCENGINE_MANAGED_OUTPUT_DIR}" XCENGINE_MANAGED_OUTPUT_DIR_CMAKE)
file(TO_CMAKE_PATH "${XCENGINE_SCRIPT_CORE_DLL}" XCENGINE_SCRIPT_CORE_DLL_CMAKE)
file(TO_CMAKE_PATH "${XCENGINE_GAME_SCRIPTS_DLL}" XCENGINE_GAME_SCRIPTS_DLL_CMAKE)
target_compile_definitions(editor_tests PRIVATE
XCENGINE_ENABLE_MONO_SCRIPTING
XCENGINE_TEST_MANAGED_OUTPUT_DIR="${XCENGINE_MANAGED_OUTPUT_DIR_CMAKE}"
XCENGINE_TEST_SCRIPT_CORE_DLL="${XCENGINE_SCRIPT_CORE_DLL_CMAKE}"
XCENGINE_TEST_GAME_SCRIPTS_DLL="${XCENGINE_GAME_SCRIPTS_DLL_CMAKE}"
)
endif()
include(GoogleTest)
gtest_discover_tests(editor_tests)

View File

@@ -9,12 +9,14 @@
#include "Core/EditorContext.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
namespace fs = std::filesystem;
@@ -343,6 +345,17 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsPlayPauseResumeAndStepEven
m_context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(playStepSubscription);
}
TEST_F(EditorActionRoutingTest, ProjectCommandsReportWhenScriptAssembliesCanBeRebuilt) {
EXPECT_TRUE(Commands::CanRebuildScriptAssemblies(m_context));
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
m_context.SetProjectPath(std::string());
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
}
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
@@ -470,6 +483,68 @@ TEST_F(EditorActionRoutingTest, ProjectCommandsRenameAssetUpdatesSelectionAndPre
EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), renamedItem->fullPath);
}
TEST_F(EditorActionRoutingTest, ProjectCommandsMigrateSceneAssetReferencesRewritesLegacyScenePayloads) {
using ::XCEngine::Resources::ResourceManager;
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path scenesDir = assetsDir / "Scenes";
const fs::path materialPath = assetsDir / "runtime.material";
const fs::path scenePath = scenesDir / "LegacyScene.xc";
{
std::ofstream materialFile(materialPath.string(), std::ios::out | std::ios::trunc);
ASSERT_TRUE(materialFile.is_open());
materialFile << "{\n";
materialFile << " \"renderQueue\": \"geometry\",\n";
materialFile << " \"renderState\": {\n";
materialFile << " \"cull\": \"back\"\n";
materialFile << " }\n";
materialFile << "}\n";
}
{
std::ofstream sceneFile(scenePath.string(), std::ios::out | std::ios::trunc);
ASSERT_TRUE(sceneFile.is_open());
sceneFile << "# XCEngine Scene File\n";
sceneFile << "scene=Legacy Scene\n";
sceneFile << "active=1\n\n";
sceneFile << "gameobject_begin\n";
sceneFile << "id=1\n";
sceneFile << "uuid=1\n";
sceneFile << "name=Legacy Object\n";
sceneFile << "active=1\n";
sceneFile << "parent=0\n";
sceneFile << "transform=position=0,0,0;rotation=0,0,0,1;scale=1,1,1;\n";
sceneFile << "component=MeshFilter;mesh=builtin://meshes/cube;meshRef=;\n";
sceneFile << "component=MeshRenderer;materials=Assets/runtime.material;materialRefs=;castShadows=1;receiveShadows=1;renderLayer=0;\n";
sceneFile << "gameobject_end\n";
}
ASSERT_TRUE(Commands::CanMigrateSceneAssetReferences(m_context));
const IProjectManager::SceneAssetReferenceMigrationReport report =
Commands::MigrateSceneAssetReferences(m_context);
EXPECT_EQ(report.scannedSceneCount, 1u);
EXPECT_EQ(report.migratedSceneCount, 1u);
EXPECT_EQ(report.unchangedSceneCount, 0u);
EXPECT_EQ(report.failedSceneCount, 0u);
std::ifstream migratedScene(scenePath.string(), std::ios::in | std::ios::binary);
ASSERT_TRUE(migratedScene.is_open());
std::string migratedText((std::istreambuf_iterator<char>(migratedScene)),
std::istreambuf_iterator<char>());
EXPECT_NE(migratedText.find("meshPath=builtin://meshes/cube;"), std::string::npos);
EXPECT_EQ(migratedText.find("component=MeshFilter;mesh=builtin://meshes/cube;"), std::string::npos);
EXPECT_NE(migratedText.find("materialPaths=;"), std::string::npos);
EXPECT_NE(migratedText.find("materialRefs="), std::string::npos);
EXPECT_EQ(migratedText.find("materialRefs=;"), std::string::npos);
EXPECT_EQ(migratedText.find("component=MeshRenderer;materials="), std::string::npos);
ResourceManager::Get().SetResourceRoot("");
ResourceManager::Get().Shutdown();
}
TEST_F(EditorActionRoutingTest, ProjectItemContextRequestSelectsAssetAndStoresPopupTarget) {
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path filePath = assetsDir / "ContextAsset.txt";

View File

@@ -0,0 +1,113 @@
#include <gtest/gtest.h>
#include "Scripting/EditorScriptAssemblyBuilder.h"
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#endif
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
namespace XCEngine::Editor::Scripting {
namespace {
class EditorScriptAssemblyBuilderTest : public ::testing::Test {
protected:
static void WriteTextFile(const std::filesystem::path& path, const std::string& content) {
std::ofstream output(path, std::ios::out | std::ios::trunc);
ASSERT_TRUE(output.is_open());
output << content;
output.close();
ASSERT_TRUE(output.good());
}
void SetUp() override {
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
m_projectRoot = std::filesystem::temp_directory_path() / ("xc_script_builder_" + std::to_string(stamp));
std::filesystem::create_directories(m_projectRoot / "Assets" / "Scripts");
}
void TearDown() override {
std::error_code ec;
std::filesystem::remove_all(m_projectRoot, ec);
}
std::filesystem::path m_projectRoot;
};
TEST_F(EditorScriptAssemblyBuilderTest, RebuildsProjectScriptAssembliesIntoLibraryDirectory) {
const std::filesystem::path scriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs";
WriteTextFile(
scriptPath,
"using XCEngine;\n"
"namespace BuilderTests {\n"
" public sealed class BuilderProbe : MonoBehaviour {\n"
" public float Speed = 4.0f;\n"
" }\n"
"}\n");
const EditorScriptAssemblyBuildResult result =
EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
ASSERT_TRUE(result.succeeded) << result.message;
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "XCEngine.ScriptCore.dll"));
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "GameScripts.dll"));
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "mscorlib.dll"));
}
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
TEST_F(EditorScriptAssemblyBuilderTest, RebuildFailsWhileLoadedAssemblyIsStillHeldByMonoRuntime) {
const std::filesystem::path initialScriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs";
WriteTextFile(
initialScriptPath,
"using XCEngine;\n"
"namespace BuilderTests {\n"
" public sealed class BuilderProbe : MonoBehaviour {\n"
" public float Speed = 4.0f;\n"
" }\n"
"}\n");
EditorScriptAssemblyBuildResult result =
EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
ASSERT_TRUE(result.succeeded) << result.message;
XCEngine::Scripting::MonoScriptRuntime::Settings settings;
settings.assemblyDirectory = m_projectRoot / "Library" / "ScriptAssemblies";
settings.corlibDirectory = settings.assemblyDirectory;
settings.coreAssemblyPath = settings.assemblyDirectory / "XCEngine.ScriptCore.dll";
settings.appAssemblyPath = settings.assemblyDirectory / "GameScripts.dll";
auto runtime = std::make_unique<XCEngine::Scripting::MonoScriptRuntime>(settings);
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
const std::filesystem::path addedScriptPath = m_projectRoot / "Assets" / "Scripts" / "TickLogProbe.cs";
WriteTextFile(
addedScriptPath,
"using XCEngine;\n"
"namespace BuilderTests {\n"
" public sealed class TickLogProbe : MonoBehaviour {\n"
" public int TickCount;\n"
" }\n"
"}\n");
result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
EXPECT_FALSE(result.succeeded);
runtime.reset();
result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
ASSERT_TRUE(result.succeeded) << result.message;
runtime = std::make_unique<XCEngine::Scripting::MonoScriptRuntime>(settings);
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "BuilderProbe"));
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "TickLogProbe"));
}
#endif
} // namespace
} // namespace XCEngine::Editor::Scripting

View File

@@ -0,0 +1,74 @@
#include <gtest/gtest.h>
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
#include <chrono>
#include <filesystem>
#include <fstream>
namespace XCEngine::Editor::Scripting {
namespace {
class EditorScriptAssemblyBuilderUtilsTest : public ::testing::Test {
protected:
void SetUp() override {
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
m_root = std::filesystem::temp_directory_path() / ("xc_script_builder_utils_" + std::to_string(stamp));
std::filesystem::create_directories(m_root);
}
void TearDown() override {
std::error_code ec;
std::filesystem::remove_all(m_root, ec);
}
std::filesystem::path m_root;
};
TEST_F(EditorScriptAssemblyBuilderUtilsTest, CollectsAndSortsCSharpSourceFilesRecursively) {
std::filesystem::create_directories(m_root / "B");
std::filesystem::create_directories(m_root / "A" / "Nested");
std::ofstream(m_root / "B" / "Second.cs").put('\n');
std::ofstream(m_root / "A" / "Nested" / "Third.cs").put('\n');
std::ofstream(m_root / "A" / "First.cs").put('\n');
std::ofstream(m_root / "Ignore.txt").put('\n');
const std::vector<std::filesystem::path> files = CollectCSharpSourceFiles(m_root);
ASSERT_EQ(files.size(), 3u);
EXPECT_EQ(files[0], (m_root / "A" / "First.cs").lexically_normal());
EXPECT_EQ(files[1], (m_root / "A" / "Nested" / "Third.cs").lexically_normal());
EXPECT_EQ(files[2], (m_root / "B" / "Second.cs").lexically_normal());
}
TEST(EditorScriptAssemblyBuilderUtils_StandaloneTest, ParsesLatestDotnetSdkVersionFromSdkListOutput) {
const std::string sdkListOutput =
"7.0.410 [C:\\Program Files\\dotnet\\sdk]\n"
"8.0.412 [C:\\Program Files\\dotnet\\sdk]\n"
"9.0.100 [C:\\Program Files\\dotnet\\sdk]\n";
EXPECT_EQ(ParseLatestDotnetSdkVersion(sdkListOutput), "9.0.100");
EXPECT_TRUE(ParseLatestDotnetSdkVersion(std::string()).empty());
}
TEST_F(EditorScriptAssemblyBuilderUtilsTest, CreatesPlaceholderProjectScriptSourceWhenNoScriptsExist) {
std::vector<std::filesystem::path> projectSources;
std::string error;
const std::filesystem::path placeholderPath = m_root / "Generated" / "EmptyProjectGameScripts.cs";
ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error;
ASSERT_EQ(projectSources.size(), 1u);
EXPECT_EQ(projectSources.front(), placeholderPath.lexically_normal());
EXPECT_TRUE(std::filesystem::exists(placeholderPath));
std::ifstream input(placeholderPath);
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
EXPECT_NE(content.find("EmptyProjectGameScriptsMarker"), std::string::npos);
error.clear();
ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error;
EXPECT_EQ(projectSources.size(), 1u);
}
} // namespace
} // namespace XCEngine::Editor::Scripting

View File

@@ -5,16 +5,55 @@
#include "Core/PlaySessionController.h"
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Input/InputManager.h>
namespace XCEngine::Editor {
namespace {
GameViewInputFrameEvent CreateGameViewInputFrame(
bool focused,
bool hovered,
std::initializer_list<XCEngine::Input::KeyCode> keys = {},
std::initializer_list<XCEngine::Input::MouseButton> mouseButtons = {},
XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(),
XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(),
float mouseWheel = 0.0f) {
GameViewInputFrameEvent event = {};
event.focused = focused;
event.hovered = hovered;
event.mousePosition = mousePosition;
event.mouseDelta = mouseDelta;
event.mouseWheel = mouseWheel;
for (const XCEngine::Input::KeyCode key : keys) {
const size_t index = static_cast<size_t>(key);
if (index < event.keyDown.size()) {
event.keyDown[index] = true;
}
}
for (const XCEngine::Input::MouseButton button : mouseButtons) {
const size_t index = static_cast<size_t>(button);
if (index < event.mouseButtonDown.size()) {
event.mouseButtonDown[index] = true;
}
}
return event;
}
class PlaySessionControllerTest : public ::testing::Test {
protected:
void SetUp() override {
XCEngine::Input::InputManager::Get().Shutdown();
m_context.GetSceneManager().NewScene("Play Session Scene");
}
void TearDown() override {
m_controller.Detach(m_context);
XCEngine::Input::InputManager::Get().Shutdown();
}
EditorContext m_context;
PlaySessionController m_controller;
};
@@ -120,5 +159,82 @@ TEST_F(PlaySessionControllerTest, PauseResumeAndStepRequestsDrivePlayStateMachin
m_context.GetEventBus().Unsubscribe<PlayModeResumedEvent>(resumedSubscription);
}
TEST_F(PlaySessionControllerTest, GameViewInputFramesDoNotAffectInputManagerOutsidePlayMode) {
m_controller.Attach(m_context);
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::A},
{XCEngine::Input::MouseButton::Left},
XCEngine::Math::Vector2(120.0f, 48.0f),
XCEngine::Math::Vector2(3.0f, -2.0f),
1.0f));
m_controller.Update(m_context, 0.016f);
auto& inputManager = XCEngine::Input::InputManager::Get();
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2::Zero());
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f);
m_controller.Detach(m_context);
}
TEST_F(PlaySessionControllerTest, GameViewInputFramesDriveAndReleaseRuntimeInputDuringPlayMode) {
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
ASSERT_NE(editorEntity, nullptr);
m_controller.Attach(m_context);
ASSERT_TRUE(m_controller.StartPlay(m_context));
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space},
{XCEngine::Input::MouseButton::Left},
XCEngine::Math::Vector2(120.0f, 48.0f),
XCEngine::Math::Vector2(3.0f, -2.0f),
1.0f));
m_controller.Update(m_context, 0.016f);
auto& inputManager = XCEngine::Input::InputManager::Get();
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
EXPECT_TRUE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A));
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space));
EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
EXPECT_TRUE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left));
EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2(120.0f, 48.0f));
EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2(3.0f, -2.0f));
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 1.0f);
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space},
{XCEngine::Input::MouseButton::Left},
XCEngine::Math::Vector2(120.0f, 48.0f)));
m_controller.Update(m_context, 0.016f);
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
EXPECT_FALSE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A));
EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
EXPECT_FALSE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left));
EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2::Zero());
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f);
m_context.GetEventBus().Publish(GameViewInputFrameEvent{});
m_controller.Update(m_context, 0.016f);
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space));
EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
m_controller.Detach(m_context);
}
} // namespace
} // namespace XCEngine::Editor

View File

@@ -0,0 +1,641 @@
#include <gtest/gtest.h>
#include "Core/EditorContext.h"
#include "Core/EditorRuntimeMode.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Debug/ILogSink.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <cstdint>
#include <memory>
#include <string>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
namespace XCEngine::Editor {
namespace {
MonoScriptRuntime::Settings CreateMonoSettings() {
MonoScriptRuntime::Settings settings;
settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR;
settings.corlibDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR;
settings.coreAssemblyPath = XCENGINE_TEST_SCRIPT_CORE_DLL;
settings.appAssemblyPath = XCENGINE_TEST_GAME_SCRIPTS_DLL;
return settings;
}
void ExpectVector3Near(const Math::Vector3& actual, const Math::Vector3& expected, float tolerance = 0.001f) {
EXPECT_NEAR(actual.x, expected.x, tolerance);
EXPECT_NEAR(actual.y, expected.y, tolerance);
EXPECT_NEAR(actual.z, expected.z, tolerance);
}
class CapturingLogSink final : public Debug::ILogSink {
public:
void Log(const Debug::LogEntry& entry) override {
entries.push_back(entry);
}
void Flush() override {
}
std::vector<std::string> CollectMessagesWithPrefix(const char* prefix) const {
std::vector<std::string> messages;
for (const Debug::LogEntry& entry : entries) {
const std::string message = entry.message.CStr();
if (message.rfind(prefix, 0) == 0) {
messages.push_back(message);
}
}
return messages;
}
std::vector<Debug::LogEntry> entries;
};
ScriptComponent* FindLifecycleProbe(GameObject* gameObject) {
if (!gameObject) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component) {
continue;
}
if (component->GetAssemblyName() == "GameScripts"
&& component->GetNamespaceName() == "Gameplay"
&& component->GetClassName() == "LifecycleProbe") {
return component;
}
}
return nullptr;
}
ScriptComponent* FindInputProbe(GameObject* gameObject) {
if (!gameObject) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component) {
continue;
}
if (component->GetAssemblyName() == "GameScripts"
&& component->GetNamespaceName() == "Gameplay"
&& component->GetClassName() == "InputProbe") {
return component;
}
}
return nullptr;
}
ScriptComponent* FindTickLogProbe(GameObject* gameObject) {
if (!gameObject) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component) {
continue;
}
if (component->GetAssemblyName() == "GameScripts"
&& component->GetNamespaceName() == "Gameplay"
&& component->GetClassName() == "TickLogProbe") {
return component;
}
}
return nullptr;
}
GameViewInputFrameEvent CreateGameViewInputFrame(
bool focused,
bool hovered,
std::initializer_list<XCEngine::Input::KeyCode> keys = {},
std::initializer_list<XCEngine::Input::MouseButton> mouseButtons = {},
XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(),
XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(),
float mouseWheel = 0.0f) {
GameViewInputFrameEvent event = {};
event.focused = focused;
event.hovered = hovered;
event.mousePosition = mousePosition;
event.mouseDelta = mouseDelta;
event.mouseWheel = mouseWheel;
for (const XCEngine::Input::KeyCode key : keys) {
const size_t index = static_cast<size_t>(key);
if (index < event.keyDown.size()) {
event.keyDown[index] = true;
}
}
for (const XCEngine::Input::MouseButton button : mouseButtons) {
const size_t index = static_cast<size_t>(button);
if (index < event.mouseButtonDown.size()) {
event.mouseButtonDown[index] = true;
}
}
return event;
}
class PlaySessionControllerScriptingTest : public ::testing::Test {
protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
engine->SetRuntime(runtime.get());
context.GetSceneManager().NewScene("Play Session Script Scene");
controller.Attach(context);
}
void TearDown() override {
controller.Detach(context);
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
runtime.reset();
}
ScriptComponent* AddLifecycleProbe(GameObject* gameObject) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", "Gameplay", "LifecycleProbe");
return component;
}
ScriptComponent* AddInputProbe(GameObject* gameObject) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", "Gameplay", "InputProbe");
return component;
}
ScriptComponent* AddTickLogProbe(GameObject* gameObject) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", "Gameplay", "TickLogProbe");
return component;
}
ScriptEngine* engine = nullptr;
std::unique_ptr<MonoScriptRuntime> runtime;
EditorContext context;
PlaySessionController controller;
};
TEST_F(PlaySessionControllerScriptingTest, StartPlayAndRuntimeTickDriveManagedLifecycleThroughPlayController) {
GameObject* host = context.GetSceneManager().CreateEntity("Host");
ASSERT_NE(host, nullptr);
ScriptComponent* script = AddLifecycleProbe(host);
ASSERT_NE(script, nullptr);
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
script->GetFieldStorage().SetFieldValue("Speed", 5.0f);
script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f));
const uint64_t hostId = host->GetID();
ASSERT_TRUE(controller.StartPlay(context));
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play);
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(runtimeHost, nullptr);
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
ASSERT_NE(runtimeScript, nullptr);
EXPECT_TRUE(engine->HasRuntimeInstance(runtimeScript));
EXPECT_TRUE(runtime->HasManagedInstance(runtimeScript));
int32_t awakeCount = 0;
int32_t enableCount = 0;
int32_t startCount = 0;
int32_t fixedUpdateCount = 0;
int32_t updateCount = 0;
int32_t lateUpdateCount = 0;
std::string label;
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "AwakeCount", awakeCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "EnableCount", enableCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Label", label));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(enableCount, 1);
EXPECT_EQ(startCount, 0);
EXPECT_EQ(fixedUpdateCount, 0);
EXPECT_EQ(updateCount, 0);
EXPECT_EQ(lateUpdateCount, 0);
EXPECT_EQ(label, "EditorLabel|Awake");
controller.Update(context, 0.036f);
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
float observedLateDeltaTime = 0.0f;
float speed = 0.0f;
Math::Vector3 spawnPoint;
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFixedDeltaTime", observedFixedDeltaTime));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLateDeltaTime", observedLateDeltaTime));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Speed", speed));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "SpawnPoint", spawnPoint));
EXPECT_EQ(startCount, 1);
EXPECT_EQ(fixedUpdateCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 1);
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.036f);
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.036f);
EXPECT_FLOAT_EQ(speed, 6.0f);
EXPECT_EQ(runtimeHost->GetName(), "Host_Managed");
ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f));
ExpectVector3Near(spawnPoint, Math::Vector3(3.0f, 4.0f, 6.0f));
}
TEST_F(PlaySessionControllerScriptingTest, PauseAndStepGateManagedUpdatesInPlayMode) {
GameObject* host = context.GetSceneManager().CreateEntity("Host");
ASSERT_NE(host, nullptr);
ScriptComponent* script = AddLifecycleProbe(host);
ASSERT_NE(script, nullptr);
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
const uint64_t hostId = host->GetID();
ASSERT_TRUE(controller.StartPlay(context));
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(runtimeHost, nullptr);
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
ASSERT_NE(runtimeScript, nullptr);
controller.Update(context, 0.02f);
int32_t startCount = 0;
int32_t fixedUpdateCount = 0;
int32_t updateCount = 0;
int32_t lateUpdateCount = 0;
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
EXPECT_EQ(startCount, 1);
EXPECT_EQ(fixedUpdateCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 1);
ASSERT_TRUE(controller.PausePlay(context));
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused);
controller.Update(context, 0.02f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
EXPECT_EQ(fixedUpdateCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 1);
ASSERT_TRUE(controller.StepPlay(context));
controller.Update(context, 0.02f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused);
EXPECT_EQ(fixedUpdateCount, 2);
EXPECT_EQ(updateCount, 2);
EXPECT_EQ(lateUpdateCount, 2);
ASSERT_TRUE(controller.ResumePlay(context));
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play);
controller.Update(context, 0.02f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
EXPECT_EQ(fixedUpdateCount, 3);
EXPECT_EQ(updateCount, 3);
EXPECT_EQ(lateUpdateCount, 3);
}
TEST_F(PlaySessionControllerScriptingTest, StopPlayDestroysManagedInstancesAndRestoresEditorSnapshot) {
GameObject* host = context.GetSceneManager().CreateEntity("Host");
ASSERT_NE(host, nullptr);
host->GetTransform()->SetLocalPosition(Math::Vector3(1.0f, 2.0f, 3.0f));
ScriptComponent* script = AddLifecycleProbe(host);
ASSERT_NE(script, nullptr);
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
script->GetFieldStorage().SetFieldValue("Speed", 5.0f);
script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f));
const uint64_t hostId = host->GetID();
const uint64_t scriptComponentUUID = script->GetScriptComponentUUID();
ASSERT_TRUE(controller.StartPlay(context));
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(runtimeHost, nullptr);
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
ASSERT_NE(runtimeScript, nullptr);
controller.Update(context, 0.02f);
EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u);
EXPECT_EQ(runtimeHost->GetName(), "Host_Managed");
ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f));
ASSERT_TRUE(controller.StopPlay(context));
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Edit);
EXPECT_FALSE(engine->IsRuntimeRunning());
EXPECT_EQ(runtime->GetManagedInstanceCount(), 0u);
GameObject* restoredHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(restoredHost, nullptr);
EXPECT_EQ(restoredHost->GetName(), "Host");
ExpectVector3Near(restoredHost->GetTransform()->GetLocalPosition(), Math::Vector3(1.0f, 2.0f, 3.0f));
ScriptComponent* restoredScript = FindLifecycleProbe(restoredHost);
ASSERT_NE(restoredScript, nullptr);
EXPECT_EQ(restoredScript->GetScriptComponentUUID(), scriptComponentUUID);
std::string label;
float speed = 0.0f;
Math::Vector3 spawnPoint;
int32_t awakeCount = 0;
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Label", label));
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Speed", speed));
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "SpawnPoint", spawnPoint));
EXPECT_EQ(label, "EditorLabel");
EXPECT_FLOAT_EQ(speed, 5.0f);
ExpectVector3Near(spawnPoint, Math::Vector3(2.0f, 4.0f, 6.0f));
EXPECT_FALSE(engine->TryGetScriptFieldValue(restoredScript, "AwakeCount", awakeCount));
EXPECT_FALSE(restoredScript->GetFieldStorage().Contains("AwakeCount"));
}
TEST_F(PlaySessionControllerScriptingTest, GameViewInputBridgeFeedsManagedInputApiDuringPlayMode) {
GameObject* host = context.GetSceneManager().CreateEntity("Host");
ASSERT_NE(host, nullptr);
ScriptComponent* script = AddInputProbe(host);
ASSERT_NE(script, nullptr);
const uint64_t hostId = host->GetID();
controller.Attach(context);
ASSERT_TRUE(controller.StartPlay(context));
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(runtimeHost, nullptr);
ScriptComponent* runtimeScript = FindInputProbe(runtimeHost);
ASSERT_NE(runtimeScript, nullptr);
context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl},
{XCEngine::Input::MouseButton::Left},
XCEngine::Math::Vector2(120.0f, 48.0f),
XCEngine::Math::Vector2(3.0f, -2.0f),
1.0f));
controller.Update(context, 0.016f);
int32_t updateCount = 0;
bool observedKeyA = false;
bool observedKeyADown = false;
bool observedKeyAUp = false;
bool observedKeySpace = false;
bool observedJump = false;
bool observedJumpDown = false;
bool observedJumpUp = false;
bool observedFire1 = false;
bool observedFire1Down = false;
bool observedFire1Up = false;
bool observedAnyKey = false;
bool observedAnyKeyDown = false;
bool observedLeftMouse = false;
bool observedLeftMouseDown = false;
bool observedLeftMouseUp = false;
float observedHorizontal = 0.0f;
float observedHorizontalRaw = 0.0f;
XCEngine::Math::Vector2 observedMouseScrollDelta;
XCEngine::Math::Vector3 observedMousePosition;
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMousePosition", observedMousePosition));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 1);
EXPECT_TRUE(observedKeyA);
EXPECT_TRUE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedKeySpace);
EXPECT_TRUE(observedJump);
EXPECT_TRUE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_TRUE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_TRUE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, -1.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f));
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f));
context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl},
{XCEngine::Input::MouseButton::Left},
XCEngine::Math::Vector2(120.0f, 48.0f)));
controller.Update(context, 0.016f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 2);
EXPECT_TRUE(observedKeyA);
EXPECT_FALSE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_FALSE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_FALSE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f));
context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{XCEngine::Input::KeyCode::Space},
{},
XCEngine::Math::Vector2(120.0f, 48.0f)));
controller.Update(context, 0.016f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_EQ(updateCount, 3);
EXPECT_FALSE(observedKeyA);
EXPECT_TRUE(observedKeyAUp);
EXPECT_TRUE(observedKeySpace);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpUp);
EXPECT_FALSE(observedFire1);
EXPECT_TRUE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_FALSE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, 0.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f);
context.GetEventBus().Publish(CreateGameViewInputFrame(
true,
true,
{},
{},
XCEngine::Math::Vector2(120.0f, 48.0f)));
controller.Update(context, 0.016f);
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_EQ(updateCount, 4);
EXPECT_FALSE(observedKeySpace);
EXPECT_FALSE(observedJump);
EXPECT_TRUE(observedJumpUp);
EXPECT_FALSE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
}
TEST_F(PlaySessionControllerScriptingTest, PlayModeTickWritesManagedDebugLogsToNativeLogger) {
auto sink = std::make_unique<CapturingLogSink>();
CapturingLogSink* sinkPtr = sink.get();
Debug::Logger::Get().AddSink(std::move(sink));
GameObject* host = context.GetSceneManager().CreateEntity("Host");
ASSERT_NE(host, nullptr);
ScriptComponent* script = AddTickLogProbe(host);
ASSERT_NE(script, nullptr);
const uint64_t hostId = host->GetID();
ASSERT_TRUE(controller.StartPlay(context));
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
ASSERT_NE(runtimeHost, nullptr);
ASSERT_NE(FindTickLogProbe(runtimeHost), nullptr);
controller.Update(context, 0.036f);
const std::vector<std::string> messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]");
const std::vector<std::string> expected = {
"[TickLogProbe] Awake",
"[TickLogProbe] FixedUpdate 1",
"[TickLogProbe] Start",
"[TickLogProbe] Update 1",
"[TickLogProbe] LateUpdate 1",
};
EXPECT_EQ(messages, expected);
Debug::Logger::Get().RemoveSink(sinkPtr);
}
} // namespace
} // namespace XCEngine::Editor

View File

@@ -159,16 +159,16 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe
TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) {
SceneViewportCameraController zoomedController;
zoomedController.Reset();
const Vector3 zoomedInitialPosition = zoomedController.GetPosition();
SceneViewportCameraController baselineController;
baselineController.Reset();
const Vector3 baselineInitialPosition = baselineController.GetPosition();
SceneViewportCameraInputState zoomInput = {};
zoomInput.viewportHeight = 720.0f;
zoomInput.zoomDelta = 8.0f;
zoomedController.ApplyInput(zoomInput);
const Vector3 zoomedPositionBeforeMove = zoomedController.GetPosition();
const Vector3 baselinePositionBeforeMove = baselineController.GetPosition();
SceneViewportCameraInputState moveInput = {};
moveInput.viewportHeight = 720.0f;
@@ -178,8 +178,8 @@ TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) {
baselineController.ApplyInput(moveInput);
EXPECT_FLOAT_EQ(zoomedController.GetFlySpeed(), baselineController.GetFlySpeed());
const float zoomedTravel = (zoomedController.GetPosition() - zoomedInitialPosition).Magnitude();
const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude();
const float zoomedTravel = (zoomedController.GetPosition() - zoomedPositionBeforeMove).Magnitude();
const float baselineTravel = (baselineController.GetPosition() - baselinePositionBeforeMove).Magnitude();
EXPECT_NEAR(zoomedTravel, baselineTravel, 1e-3f);
}

View File

@@ -0,0 +1,135 @@
#include <gtest/gtest.h>
#include "ComponentEditors/ScriptComponentEditorUtils.h"
#include <XCEngine/Scripting/ScriptComponent.h>
namespace XCEngine::Editor {
namespace {
TEST(ScriptComponentEditorUtils_Test, BuildsReadableScriptClassDisplayNames) {
const ::XCEngine::Scripting::ScriptClassDescriptor gameScript{
"GameScripts",
"Gameplay",
"PlayerController"
};
const ::XCEngine::Scripting::ScriptClassDescriptor toolScript{
"EditorTools",
"Gameplay",
"PlayerController"
};
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptClassDisplayName(gameScript),
"Gameplay.PlayerController");
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptClassDisplayName(toolScript),
"Gameplay.PlayerController (EditorTools)");
::XCEngine::Scripting::ScriptComponent component;
component.SetScriptClass("GameScripts", "ProjectScripts", "Mover");
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptClassDisplayName(component),
"ProjectScripts.Mover");
}
TEST(ScriptComponentEditorUtils_Test, FindsGameObjectIdsByUuidAcrossHierarchy) {
::XCEngine::Components::GameObject root("Root");
::XCEngine::Components::GameObject child("Child");
::XCEngine::Components::GameObject grandChild("GrandChild");
child.SetParent(&root);
grandChild.SetParent(&child);
const std::vector<::XCEngine::Components::GameObject*> roots = { &root };
EXPECT_EQ(
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, child.GetUUID()),
child.GetID());
EXPECT_EQ(
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, grandChild.GetUUID()),
grandChild.GetID());
EXPECT_EQ(
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, 0),
::XCEngine::Components::GameObject::INVALID_ID);
}
TEST(ScriptComponentEditorUtils_Test, ReportsFieldEditabilityClearabilityAndIssues) {
::XCEngine::Scripting::ScriptFieldSnapshot declaredField;
declaredField.metadata = { "Health", ::XCEngine::Scripting::ScriptFieldType::Int32 };
declaredField.declaredInClass = true;
::XCEngine::Scripting::ScriptFieldSnapshot storedOnlyField;
storedOnlyField.metadata = { "LegacyValue", ::XCEngine::Scripting::ScriptFieldType::String };
storedOnlyField.hasValue = true;
storedOnlyField.value = std::string("legacy");
storedOnlyField.valueSource = ::XCEngine::Scripting::ScriptFieldValueSource::StoredValue;
storedOnlyField.issue = ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly;
storedOnlyField.hasStoredValue = true;
storedOnlyField.storedType = ::XCEngine::Scripting::ScriptFieldType::String;
storedOnlyField.storedValue = std::string("legacy");
::XCEngine::Scripting::ScriptFieldSnapshot mismatchedField;
mismatchedField.metadata = { "Speed", ::XCEngine::Scripting::ScriptFieldType::Float };
mismatchedField.declaredInClass = true;
mismatchedField.issue = ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch;
mismatchedField.hasStoredValue = true;
mismatchedField.storedType = ::XCEngine::Scripting::ScriptFieldType::UInt64;
mismatchedField.storedValue = uint64_t(5);
EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField(
::XCEngine::Scripting::ScriptFieldClassStatus::Available,
declaredField));
EXPECT_FALSE(ScriptComponentEditorUtils::CanEditScriptField(
::XCEngine::Scripting::ScriptFieldClassStatus::Available,
storedOnlyField));
EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField(
::XCEngine::Scripting::ScriptFieldClassStatus::Missing,
storedOnlyField));
EXPECT_FALSE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(declaredField));
EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(storedOnlyField));
EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(mismatchedField));
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptFieldIssueText(storedOnlyField),
"Stored override is not declared by the selected script.");
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptFieldIssueText(mismatchedField),
"Stored override type is UInt64, but the script field expects Float.");
}
TEST(ScriptComponentEditorUtils_Test, BuildsRuntimeUnavailableHintsAndReloadAvailability) {
EditorScriptRuntimeStatus missingAssembliesStatus;
missingAssembliesStatus.backendEnabled = true;
missingAssembliesStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies";
missingAssembliesStatus.statusMessage =
"Script assemblies were not found in D:/Project/Library/ScriptAssemblies. "
"Script class discovery is disabled until the managed assemblies are built.";
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(missingAssembliesStatus),
missingAssembliesStatus.statusMessage);
EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(missingAssembliesStatus));
EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(missingAssembliesStatus));
EditorScriptRuntimeStatus fallbackStatus;
fallbackStatus.backendEnabled = true;
fallbackStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies";
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(fallbackStatus),
"No script assemblies are currently loaded from D:/Project/Library/ScriptAssemblies.");
EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(fallbackStatus));
EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(fallbackStatus));
EditorScriptRuntimeStatus backendDisabledStatus;
EXPECT_EQ(
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(backendDisabledStatus),
"This editor build does not include Mono scripting support.");
EXPECT_FALSE(ScriptComponentEditorUtils::CanReloadScriptRuntime(backendDisabledStatus));
EXPECT_FALSE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(backendDisabledStatus));
}
} // namespace
} // namespace XCEngine::Editor