tests: remove legacy test tree

This commit is contained in:
2026-04-22 00:22:32 +08:00
parent 8bfca5e8f2
commit bc47e6e5ac
754 changed files with 0 additions and 3517894 deletions

View File

@@ -1,107 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngine_ScriptingTests)
function(xcengine_attach_recursive_build_target target dependency_target)
add_custom_command(TARGET ${target} PRE_BUILD
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR}
--config $<CONFIG>
--target ${dependency_target}
--
/m:1
/p:BuildProjectReferences=false
VERBATIM
COMMENT "Building ${dependency_target} before ${target}")
endfunction()
set(SCRIPTING_TEST_SOURCES
test_script_field_storage.cpp
test_script_component.cpp
test_script_engine.cpp
)
if(XCENGINE_ENABLE_MONO_SCRIPTING)
list(APPEND SCRIPTING_TEST_SOURCES
test_mono_script_runtime.cpp
)
if(TARGET xcengine_test_project_managed_assemblies)
list(APPEND SCRIPTING_TEST_SOURCES
test_project_script_assembly.cpp
)
endif()
endif()
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})
if(MSVC)
set_target_properties(scripting_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(scripting_tests PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(scripting_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
)
if(WIN32 AND XCENGINE_ENABLE_PHYSX)
xcengine_copy_physx_runtime_dlls(scripting_tests)
endif()
if(TARGET xcengine_managed_assemblies)
add_dependencies(scripting_tests xcengine_managed_assemblies)
xcengine_attach_recursive_build_target(scripting_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(scripting_tests PRIVATE
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()
if(TARGET xcengine_test_project_managed_assemblies)
add_dependencies(scripting_tests xcengine_test_project_managed_assemblies)
xcengine_attach_recursive_build_target(
scripting_tests
xcengine_test_project_managed_assemblies)
file(
TO_CMAKE_PATH
"${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR}"
XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE)
file(
TO_CMAKE_PATH
"${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL}"
XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE)
file(
TO_CMAKE_PATH
"${XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL}"
XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE)
target_compile_definitions(scripting_tests PRIVATE
XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR=\"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE}\"
XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL=\"${XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE}\"
XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL=\"${XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE}\"
)
endif()
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
add_custom_command(TARGET scripting_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
$<TARGET_FILE_DIR:scripting_tests>/assimp-vc143-mt.dll
)
endif()
include(GoogleTest)
gtest_discover_tests(scripting_tests)

File diff suppressed because it is too large Load Diff

View File

@@ -1,746 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Rendering/Execution/CameraFrameRenderGraphFrameData.h>
#include <XCEngine/Rendering/Graph/RenderGraph.h>
#include <XCEngine/Rendering/Graph/RenderGraphCompiler.h>
#include <XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <filesystem>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
namespace {
std::filesystem::path GetExecutableDirectory() {
#ifdef _WIN32
std::wstring buffer(MAX_PATH, L'\0');
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0 || length >= buffer.size()) {
return std::filesystem::current_path();
}
buffer.resize(length);
return std::filesystem::path(buffer).parent_path();
#else
return std::filesystem::current_path();
#endif
}
std::filesystem::path ResolveProjectManagedOutputDirectory() {
constexpr const char* configuredDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR;
if (configuredDirectory[0] != '\0') {
return std::filesystem::path(configuredDirectory);
}
return (GetExecutableDirectory() / ".." / ".." / "managed" / "ProjectScriptAssemblies").lexically_normal();
}
std::filesystem::path ResolveProjectScriptCoreDllPath() {
constexpr const char* configuredPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL;
if (configuredPath[0] != '\0') {
return std::filesystem::path(configuredPath);
}
return ResolveProjectManagedOutputDirectory() / "XCEngine.ScriptCore.dll";
}
std::filesystem::path ResolveProjectGameScriptsDllPath() {
constexpr const char* configuredPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL;
if (configuredPath[0] != '\0') {
return std::filesystem::path(configuredPath);
}
return ResolveProjectManagedOutputDirectory() / "GameScripts.dll";
}
MonoScriptRuntime::Settings CreateProjectMonoSettings() {
MonoScriptRuntime::Settings settings;
settings.assemblyDirectory = ResolveProjectManagedOutputDirectory();
settings.corlibDirectory = settings.assemblyDirectory;
settings.coreAssemblyPath = ResolveProjectScriptCoreDllPath();
settings.appAssemblyPath = ResolveProjectGameScriptsDllPath();
return settings;
}
class ProjectScriptAssemblyTest : public ::testing::Test {
protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
engine->SetRuntimeFixedDeltaTime(
ScriptEngine::DefaultFixedDeltaTime);
XCEngine::Input::InputManager::Get().Shutdown();
XCEngine::Resources::ResourceManager::Get().Shutdown();
XCEngine::Rendering::Pipelines::
ClearConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(std::filesystem::exists(ResolveProjectScriptCoreDllPath()));
ASSERT_TRUE(std::filesystem::exists(ResolveProjectGameScriptsDllPath()));
MonoScriptRuntime::Settings settings =
CreateProjectMonoSettings();
std::string engineAssemblyError;
ASSERT_TRUE(
MonoScriptRuntime::DiscoverEngineAssemblies(
settings,
&engineAssemblyError))
<< engineAssemblyError;
ASSERT_FALSE(settings.engineAssemblies.empty());
for (const MonoScriptRuntime::ManagedAssemblyDescriptor& assembly :
settings.engineAssemblies) {
ASSERT_TRUE(std::filesystem::exists(assembly.path))
<< assembly.path.string();
}
runtime = std::make_unique<MonoScriptRuntime>(std::move(settings));
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
engine->SetRuntime(runtime.get());
}
void TearDown() override {
if (engine != nullptr) {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
}
XCEngine::Input::InputManager::Get().Shutdown();
XCEngine::Rendering::Pipelines::
ClearConfiguredManagedRenderPipelineAssetDescriptor();
runtime.reset();
scene.reset();
XCEngine::Resources::ResourceManager::Get().Shutdown();
}
Scene* CreateScene(const std::string& sceneName) {
scene = std::make_unique<Scene>(sceneName);
return scene.get();
}
ScriptComponent* AddProjectScript(
GameObject* gameObject,
const std::string& className) {
ScriptComponent* component =
gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass(
"GameScripts",
"ProjectScripts",
className);
return component;
}
ScriptEngine* engine = nullptr;
std::unique_ptr<MonoScriptRuntime> runtime;
std::unique_ptr<Scene> scene;
};
TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) {
EXPECT_TRUE(runtime->IsInitialized());
EXPECT_EQ(runtime->GetSettings().assemblyDirectory, ResolveProjectManagedOutputDirectory());
EXPECT_EQ(runtime->GetSettings().appAssemblyPath, ResolveProjectGameScriptsDllPath());
}
TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFieldMetadata) {
const std::vector<std::string> classNames = runtime->GetScriptClassNames("GameScripts");
std::vector<ScriptClassDescriptor> classDescriptors;
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classDescriptors));
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "ProjectScripts", "ProjectScriptProbe"));
EXPECT_NE(
std::find(classNames.begin(), classNames.end(), "ProjectScripts.ProjectScriptProbe"),
classNames.end());
EXPECT_NE(
std::find(
classDescriptors.begin(),
classDescriptors.end(),
ScriptClassDescriptor{"GameScripts", "ProjectScripts", "ProjectScriptProbe"}),
classDescriptors.end());
std::vector<ScriptFieldMetadata> fields;
ASSERT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "ProjectScripts", "ProjectScriptProbe", fields));
const std::vector<ScriptFieldMetadata> expectedFields = {
{"EnabledOnBoot", ScriptFieldType::Bool},
{"Label", ScriptFieldType::String},
{"Speed", ScriptFieldType::Float},
};
EXPECT_EQ(fields, expectedFields);
std::vector<ScriptFieldDefaultValue> defaultValues;
ASSERT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "ProjectScripts", "ProjectScriptProbe", defaultValues));
ASSERT_EQ(defaultValues.size(), 3u);
EXPECT_EQ(defaultValues[0].fieldName, "EnabledOnBoot");
EXPECT_EQ(defaultValues[0].type, ScriptFieldType::Bool);
EXPECT_EQ(std::get<bool>(defaultValues[0].value), true);
EXPECT_EQ(defaultValues[1].fieldName, "Label");
EXPECT_EQ(defaultValues[1].type, ScriptFieldType::String);
EXPECT_EQ(std::get<std::string>(defaultValues[1].value), "ProjectScriptProbe");
EXPECT_EQ(defaultValues[2].fieldName, "Speed");
EXPECT_EQ(defaultValues[2].type, ScriptFieldType::Float);
EXPECT_FLOAT_EQ(std::get<float>(defaultValues[2].value), 2.5f);
}
TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetRenderPipelineAssetClasses) {
std::vector<ScriptClassDescriptor> classes;
ASSERT_TRUE(runtime->TryGetAvailableRenderPipelineAssetClasses(classes));
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"ProjectScripts",
"ProjectUniversalFeaturePipelineAsset"}),
classes.end());
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"ProjectScripts",
"ProjectCustomRendererPipelineAsset"}),
classes.end());
}
TEST_F(
ProjectScriptAssemblyTest,
CreatesProjectAssetUniversalFeatureRuntimeThroughManagedBridge) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"ProjectScripts",
"ProjectUniversalFeaturePipelineAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
EXPECT_NE(assetRuntime->GetPipelineRendererAsset(), nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder> recorder =
assetRuntime->CreateStageRecorder();
ASSERT_NE(recorder, nullptr);
const XCEngine::Rendering::RenderContext renderContext = {};
ASSERT_TRUE(recorder->Initialize(renderContext));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::PostProcess));
XCEngine::Rendering::RenderGraph graph;
XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph);
XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {};
colorDesc.width = 64u;
colorDesc.height = 64u;
colorDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::R8G8B8A8_UNorm);
const XCEngine::Rendering::RenderGraphTextureHandle sourceColor =
graphBuilder.ImportTexture(
"ProjectManagedPostProcessSource",
colorDesc,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(901),
{});
const XCEngine::Rendering::RenderGraphTextureHandle outputColor =
graphBuilder.CreateTransientTexture(
"ProjectManagedPostProcessOutput",
colorDesc);
const XCEngine::Rendering::RenderSceneData sceneData = {};
const XCEngine::Rendering::RenderSurface surface(64u, 64u);
bool executionSucceeded = true;
XCEngine::Rendering::RenderGraphBlackboard blackboard = {};
XCEngine::Rendering::EmplaceCameraFrameRenderGraphFrameData(blackboard)
.resources.mainScene.color = sourceColor;
const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"ProjectManagedPostProcess",
XCEngine::Rendering::CameraFrameStage::PostProcess,
renderContext,
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ outputColor },
{},
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext));
XCEngine::Rendering::CompiledRenderGraph compiledGraph = {};
XCEngine::Containers::String errorMessage;
ASSERT_TRUE(
XCEngine::Rendering::RenderGraphCompiler::Compile(
graph,
compiledGraph,
&errorMessage))
<< errorMessage.CStr();
ASSERT_EQ(compiledGraph.GetPassCount(), 1u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ProjectManagedPostProcess");
recorder->Shutdown();
}
TEST_F(
ProjectScriptAssemblyTest,
CreatesProjectCustomRendererPipelineAssetThroughManagedBridge) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"ProjectScripts",
"ProjectCustomRendererPipelineAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
EXPECT_NE(assetRuntime->GetPipelineRendererAsset(), nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder> recorder =
assetRuntime->CreateStageRecorder();
ASSERT_NE(recorder, nullptr);
const XCEngine::Rendering::RenderContext renderContext = {};
ASSERT_TRUE(recorder->Initialize(renderContext));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
EXPECT_FALSE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::PostProcess));
XCEngine::Rendering::RenderGraph graph;
XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph);
XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {};
colorDesc.width = 64u;
colorDesc.height = 64u;
colorDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::R8G8B8A8_UNorm);
XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc;
depthDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderGraphTextureHandle colorTarget =
graphBuilder.CreateTransientTexture(
"ProjectCustomMainSceneColor",
colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture(
"ProjectCustomMainSceneDepth",
depthDesc);
const XCEngine::Rendering::RenderSceneData sceneData = {};
const XCEngine::Rendering::RenderSurface surface(64u, 64u);
bool executionSucceeded = true;
XCEngine::Rendering::RenderGraphBlackboard blackboard = {};
const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"ProjectCustomMainScene",
XCEngine::Rendering::CameraFrameStage::MainScene,
renderContext,
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ colorTarget },
depthTarget,
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext));
XCEngine::Rendering::CompiledRenderGraph compiledGraph = {};
XCEngine::Containers::String errorMessage;
ASSERT_TRUE(
XCEngine::Rendering::RenderGraphCompiler::Compile(
graph,
compiledGraph,
&errorMessage))
<< errorMessage.CStr();
ASSERT_EQ(compiledGraph.GetPassCount(), 1u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ProjectCustomMainScene.Opaque");
recorder->Shutdown();
}
TEST_F(
ProjectScriptAssemblyTest,
ProjectManagedBridgeRebuildsRendererAfterProjectRendererDataInvalidation) {
Scene* runtimeScene =
CreateScene("ProjectRendererInvalidationScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ProjectRendererInvalidationSelection");
ScriptComponent* selectionScript =
AddProjectScript(
selectionObject,
"ProjectRendererInvalidationRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* observationObject =
runtimeScene->CreateGameObject(
"ProjectRendererInvalidationObservation");
ScriptComponent* observationScript =
AddProjectScript(
observationObject,
"ProjectRendererInvalidationObservationProbe");
ASSERT_NE(observationScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
descriptor =
XCEngine::Rendering::Pipelines::
GetConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(descriptor.IsValid());
ASSERT_NE(descriptor.managedAssetHandle, 0u);
EXPECT_EQ(descriptor.assemblyName, "GameScripts");
EXPECT_EQ(descriptor.namespaceName, "ProjectScripts");
EXPECT_EQ(
descriptor.className,
"ProjectRendererInvalidationProbeAsset");
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
recorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(recorder, nullptr);
const XCEngine::Rendering::RenderContext context = {};
ASSERT_TRUE(recorder->Initialize(context));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
engine->OnUpdate(0.016f);
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreatePipelineCallCount = 0;
int observedDisposePipelineCallCount = 0;
int observedCreateRendererCallCount = 0;
int observedSetupRendererCallCount = 0;
int observedCreateFeatureCallCount = 0;
int observedDisposeRendererCallCount = 0;
int observedDisposeFeatureCallCount = 0;
int observedInvalidateRendererCallCount = 0;
int observedRuntimeResourceVersionBeforeInvalidation = 0;
int observedRuntimeResourceVersionAfterInvalidation = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposePipelineCallCount",
observedDisposePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedSetupRendererCallCount",
observedSetupRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreateFeatureCallCount",
observedCreateFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposeRendererCallCount",
observedDisposeRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposeFeatureCallCount",
observedDisposeFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedInvalidateRendererCallCount",
observedInvalidateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedRuntimeResourceVersionBeforeInvalidation",
observedRuntimeResourceVersionBeforeInvalidation));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedRuntimeResourceVersionAfterInvalidation",
observedRuntimeResourceVersionAfterInvalidation));
EXPECT_EQ(observedCreatePipelineCallCount, 2);
EXPECT_EQ(observedDisposePipelineCallCount, 1);
EXPECT_EQ(observedCreateRendererCallCount, 2);
EXPECT_EQ(observedSetupRendererCallCount, 2);
EXPECT_EQ(observedCreateFeatureCallCount, 2);
EXPECT_EQ(observedDisposeRendererCallCount, 1);
EXPECT_EQ(observedDisposeFeatureCallCount, 1);
EXPECT_EQ(observedInvalidateRendererCallCount, 1);
EXPECT_GT(observedRuntimeResourceVersionBeforeInvalidation, 0);
EXPECT_EQ(
observedRuntimeResourceVersionAfterInvalidation,
observedRuntimeResourceVersionBeforeInvalidation + 1);
recorder->Shutdown();
}
TEST_F(
ProjectScriptAssemblyTest,
ProjectManagedBridgeReleasesProjectRendererCachesAcrossInvalidationAndAssetRuntimeRelease) {
Scene* runtimeScene =
CreateScene("ProjectPersistentFeatureLifecycleScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ProjectPersistentFeatureSelection");
ScriptComponent* selectionScript =
AddProjectScript(
selectionObject,
"ProjectPersistentFeatureRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* observationObject =
runtimeScene->CreateGameObject(
"ProjectPersistentFeatureObservation");
ScriptComponent* observationScript =
AddProjectScript(
observationObject,
"ProjectPersistentFeatureObservationProbe");
ASSERT_NE(observationScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
descriptor =
XCEngine::Rendering::Pipelines::
GetConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(descriptor.IsValid());
ASSERT_NE(descriptor.managedAssetHandle, 0u);
EXPECT_EQ(descriptor.assemblyName, "GameScripts");
EXPECT_EQ(descriptor.namespaceName, "ProjectScripts");
EXPECT_EQ(
descriptor.className,
"ProjectPersistentFeatureProbeAsset");
{
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
recorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(recorder, nullptr);
const XCEngine::Rendering::RenderContext context = {};
ASSERT_TRUE(recorder->Initialize(context));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
engine->OnUpdate(0.016f);
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
recorder->Shutdown();
}
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreateRendererCallCount = 0;
int observedCreateFeatureRuntimeCallCount = 0;
int observedDisposeRendererCallCount = 0;
int observedDisposeFeatureCallCount = 0;
int observedInvalidateRendererCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreateFeatureRuntimeCallCount",
observedCreateFeatureRuntimeCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposeRendererCallCount",
observedDisposeRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposeFeatureCallCount",
observedDisposeFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedInvalidateRendererCallCount",
observedInvalidateRendererCallCount));
EXPECT_EQ(observedCreateRendererCallCount, 2);
EXPECT_EQ(observedCreateFeatureRuntimeCallCount, 2);
EXPECT_EQ(observedDisposeRendererCallCount, 2);
EXPECT_EQ(observedDisposeFeatureCallCount, 2);
EXPECT_EQ(observedInvalidateRendererCallCount, 1);
}
TEST_F(
ProjectScriptAssemblyTest,
ProjectManagedBridgeRebuildsPipelineAfterProjectAssetInvalidation) {
Scene* runtimeScene =
CreateScene("ProjectAssetInvalidationScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ProjectAssetInvalidationSelection");
ScriptComponent* selectionScript =
AddProjectScript(
selectionObject,
"ProjectAssetInvalidationRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* observationObject =
runtimeScene->CreateGameObject(
"ProjectAssetInvalidationObservation");
ScriptComponent* observationScript =
AddProjectScript(
observationObject,
"ProjectAssetInvalidationObservationProbe");
ASSERT_NE(observationScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
descriptor =
XCEngine::Rendering::Pipelines::
GetConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(descriptor.IsValid());
ASSERT_NE(descriptor.managedAssetHandle, 0u);
EXPECT_EQ(descriptor.assemblyName, "GameScripts");
EXPECT_EQ(descriptor.namespaceName, "ProjectScripts");
EXPECT_EQ(
descriptor.className,
"ProjectAssetInvalidationProbeAsset");
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
recorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(recorder, nullptr);
const XCEngine::Rendering::RenderContext context = {};
ASSERT_TRUE(recorder->Initialize(context));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
ASSERT_FALSE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::PostProcess));
engine->OnUpdate(0.016f);
ASSERT_FALSE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::PostProcess));
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreatePipelineCallCount = 0;
int observedDisposePipelineCallCount = 0;
int observedInvalidateAssetCallCount = 0;
int observedLastCreatedSupportedStage = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedDisposePipelineCallCount",
observedDisposePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedInvalidateAssetCallCount",
observedInvalidateAssetCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedLastCreatedSupportedStage",
observedLastCreatedSupportedStage));
EXPECT_EQ(observedCreatePipelineCallCount, 2);
EXPECT_EQ(observedDisposePipelineCallCount, 1);
EXPECT_EQ(observedInvalidateAssetCallCount, 1);
EXPECT_EQ(
observedLastCreatedSupportedStage,
static_cast<int>(
XCEngine::Rendering::CameraFrameStage::PostProcess));
recorder->Shutdown();
}
} // namespace

View File

@@ -1,147 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/ComponentFactoryRegistry.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <sstream>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
namespace {
TEST(ScriptComponent_Test, DefaultsAreInitializedForNativeDataLayer) {
ScriptComponent component;
EXPECT_EQ(component.GetName(), "ScriptComponent");
EXPECT_NE(component.GetScriptComponentUUID(), 0u);
EXPECT_EQ(component.GetAssemblyName(), "GameScripts");
EXPECT_TRUE(component.GetNamespaceName().empty());
EXPECT_TRUE(component.GetClassName().empty());
EXPECT_FALSE(component.HasScriptClass());
EXPECT_TRUE(component.GetFullClassName().empty());
EXPECT_EQ(component.GetFieldStorage().GetFieldCount(), 0u);
}
TEST(ScriptComponent_Test, FullClassNameComposesNamespaceAndClassName) {
ScriptComponent component;
component.SetScriptClass("Gameplay.Characters", "PlayerController");
EXPECT_EQ(component.GetFullClassName(), "Gameplay.Characters.PlayerController");
component.SetScriptClass("", "Mover");
EXPECT_EQ(component.GetFullClassName(), "Mover");
}
TEST(ScriptComponent_Test, SerializeRoundTripPreservesMetadataAndFields) {
ScriptComponent original;
original.SetScriptClass("GameScripts.Runtime", "Gameplay.Characters", "PlayerController");
ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("DisplayName", std::string("Hero=One;Ready|100%")));
ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("MoveSpeed", 6.25f));
ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("Target", GameObjectReference{9988}));
ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("TargetCamera", ComponentReference{9988, 0}));
std::ostringstream stream;
original.Serialize(stream);
ScriptComponent restored;
std::istringstream input(stream.str());
restored.Deserialize(input);
std::string displayName;
float moveSpeed = 0.0f;
GameObjectReference target;
ComponentReference targetCamera;
EXPECT_EQ(restored.GetScriptComponentUUID(), original.GetScriptComponentUUID());
EXPECT_EQ(restored.GetAssemblyName(), "GameScripts.Runtime");
EXPECT_EQ(restored.GetNamespaceName(), "Gameplay.Characters");
EXPECT_EQ(restored.GetClassName(), "PlayerController");
EXPECT_EQ(restored.GetFullClassName(), "Gameplay.Characters.PlayerController");
EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("DisplayName", displayName));
EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("MoveSpeed", moveSpeed));
EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("Target", target));
EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("TargetCamera", targetCamera));
EXPECT_EQ(displayName, "Hero=One;Ready|100%");
EXPECT_FLOAT_EQ(moveSpeed, 6.25f);
EXPECT_EQ(target, GameObjectReference{9988});
EXPECT_EQ(targetCamera, (ComponentReference{9988, 0}));
}
TEST(ScriptComponent_Test, ComponentFactoryRegistryCreatesScriptComponents) {
GameObject gameObject("ScriptHost");
auto& registry = ComponentFactoryRegistry::Get();
EXPECT_TRUE(registry.IsRegistered("ScriptComponent"));
ScriptComponent* created = dynamic_cast<ScriptComponent*>(registry.CreateComponent(&gameObject, "ScriptComponent"));
ASSERT_NE(created, nullptr);
EXPECT_EQ(gameObject.GetComponents<ScriptComponent>().size(), 1u);
}
TEST(ScriptComponent_Test, GameObjectCanOwnMultipleScriptComponents) {
GameObject gameObject("ScriptHost");
ScriptComponent* first = gameObject.AddComponent<ScriptComponent>();
ScriptComponent* second = gameObject.AddComponent<ScriptComponent>();
ASSERT_NE(first, nullptr);
ASSERT_NE(second, nullptr);
EXPECT_NE(first, second);
EXPECT_EQ(gameObject.GetComponents<ScriptComponent>().size(), 2u);
}
TEST(ScriptComponent_Test, SceneRoundTripPreservesMultipleScriptComponentsAndFields) {
Scene scene("Script Scene");
GameObject* host = scene.CreateGameObject("Host");
GameObject* targetObject = scene.CreateGameObject("Target");
ScriptComponent* movement = host->AddComponent<ScriptComponent>();
movement->SetScriptClass("GameScripts", "Gameplay", "MovementController");
ASSERT_TRUE(movement->GetFieldStorage().SetFieldValue("Speed", 4.25f));
ASSERT_TRUE(movement->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetObject->GetUUID()}));
ScriptComponent* inventory = host->AddComponent<ScriptComponent>();
inventory->SetScriptClass("GameScripts", "Gameplay.Inventory", "InventoryWatcher");
ASSERT_TRUE(inventory->GetFieldStorage().SetFieldValue("Label", std::string("Player|One;Ready=1")));
ASSERT_TRUE(inventory->GetFieldStorage().SetFieldValue("Slots", int32_t(24)));
const uint64_t movementUUID = movement->GetScriptComponentUUID();
const uint64_t inventoryUUID = inventory->GetScriptComponentUUID();
Scene loadedScene;
loadedScene.DeserializeFromString(scene.SerializeToString());
GameObject* loadedHost = loadedScene.Find("Host");
GameObject* loadedTarget = loadedScene.Find("Target");
ASSERT_NE(loadedHost, nullptr);
ASSERT_NE(loadedTarget, nullptr);
std::vector<ScriptComponent*> loadedScripts = loadedHost->GetComponents<ScriptComponent>();
ASSERT_EQ(loadedScripts.size(), 2u);
float speed = 0.0f;
GameObjectReference loadedReference;
std::string label;
int32_t slots = 0;
EXPECT_EQ(loadedScripts[0]->GetScriptComponentUUID(), movementUUID);
EXPECT_EQ(loadedScripts[0]->GetAssemblyName(), "GameScripts");
EXPECT_EQ(loadedScripts[0]->GetFullClassName(), "Gameplay.MovementController");
EXPECT_TRUE(loadedScripts[0]->GetFieldStorage().TryGetFieldValue("Speed", speed));
EXPECT_TRUE(loadedScripts[0]->GetFieldStorage().TryGetFieldValue("Target", loadedReference));
EXPECT_FLOAT_EQ(speed, 4.25f);
EXPECT_EQ(loadedReference, GameObjectReference{loadedTarget->GetUUID()});
EXPECT_EQ(loadedScripts[1]->GetScriptComponentUUID(), inventoryUUID);
EXPECT_EQ(loadedScripts[1]->GetAssemblyName(), "GameScripts");
EXPECT_EQ(loadedScripts[1]->GetFullClassName(), "Gameplay.Inventory.InventoryWatcher");
EXPECT_TRUE(loadedScripts[1]->GetFieldStorage().TryGetFieldValue("Label", label));
EXPECT_TRUE(loadedScripts[1]->GetFieldStorage().TryGetFieldValue("Slots", slots));
EXPECT_EQ(label, "Player|One;Ready=1");
EXPECT_EQ(slots, 24);
}
} // namespace

View File

@@ -1,873 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
namespace {
std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
switch (method) {
case ScriptLifecycleMethod::Awake: return "Awake";
case ScriptLifecycleMethod::OnEnable: return "OnEnable";
case ScriptLifecycleMethod::Start: return "Start";
case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate";
case ScriptLifecycleMethod::Update: return "Update";
case ScriptLifecycleMethod::LateUpdate: return "LateUpdate";
case ScriptLifecycleMethod::OnDisable: return "OnDisable";
case ScriptLifecycleMethod::OnDestroy: return "OnDestroy";
}
return "Unknown";
}
std::string PhysicsMessageToString(ScriptPhysicsMessage message) {
switch (message) {
case ScriptPhysicsMessage::CollisionEnter: return "CollisionEnter";
case ScriptPhysicsMessage::CollisionStay: return "CollisionStay";
case ScriptPhysicsMessage::CollisionExit: return "CollisionExit";
case ScriptPhysicsMessage::TriggerEnter: return "TriggerEnter";
case ScriptPhysicsMessage::TriggerStay: return "TriggerStay";
case ScriptPhysicsMessage::TriggerExit: return "TriggerExit";
}
return "Unknown";
}
class FakeScriptRuntime : public IScriptRuntime {
public:
void OnRuntimeStart(Scene* scene) override {
events.push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
}
void OnRuntimeStop(Scene* scene) override {
events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
}
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override {
outClasses = scriptClasses;
return true;
}
bool TryGetAvailableRenderPipelineAssetClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override {
outClasses = renderPipelineAssetClasses;
return true;
}
bool TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const override {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields = fieldMetadata;
return !outFields.empty();
}
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields = fieldDefaultValues;
return !outFields.empty();
}
bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) override {
(void)context;
managedFieldValues[fieldName] = value;
managedSetFieldNames.push_back(fieldName);
return true;
}
bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const override {
(void)context;
const auto it = managedFieldValues.find(fieldName);
if (it == managedFieldValues.end()) {
return false;
}
outValue = it->second;
return true;
}
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override {
(void)context;
}
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
events.push_back("Create:" + Describe(context));
return context.component != nullptr;
}
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
events.push_back("Destroy:" + Describe(context));
}
void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) override {
(void)deltaTime;
events.push_back(LifecycleMethodToString(method) + ":" + Describe(context));
}
void InvokePhysicsMessage(
const ScriptRuntimeContext& context,
ScriptPhysicsMessage message,
GameObject* other) override {
events.push_back(
PhysicsMessageToString(message)
+ ":"
+ Describe(context)
+ ":"
+ (other ? other->GetName() : std::string("null")));
}
void Clear() {
events.clear();
}
std::vector<std::string> events;
std::vector<ScriptClassDescriptor> scriptClasses;
std::vector<ScriptClassDescriptor> renderPipelineAssetClasses;
std::vector<ScriptFieldMetadata> fieldMetadata;
std::vector<ScriptFieldDefaultValue> fieldDefaultValues;
std::unordered_map<std::string, ScriptFieldValue> managedFieldValues;
std::vector<std::string> managedSetFieldNames;
private:
static std::string Describe(const ScriptRuntimeContext& context) {
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
const std::string className = context.component ? context.component->GetFullClassName() : "null";
return gameObjectName + ":" + className;
}
};
class ScriptEngineTest : public ::testing::Test {
protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
engine->SetRuntime(&runtime);
}
void TearDown() override {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
scene.reset();
}
ScriptComponent* AddScriptComponent(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", namespaceName, className);
return component;
}
Scene* CreateScene(const std::string& sceneName) {
scene = std::make_unique<Scene>(sceneName);
return scene.get();
}
ScriptEngine* engine = nullptr;
FakeScriptRuntime runtime;
std::unique_ptr<Scene> scene;
};
TEST_F(ScriptEngineTest, RuntimeStartTracksActiveScriptsAndInvokesAwakeThenOnEnable) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "PlayerController");
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->IsRuntimeRunning());
EXPECT_EQ(engine->GetRuntimeScene(), runtimeScene);
EXPECT_EQ(engine->GetTrackedScriptCount(), 1u);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_TRUE(engine->HasRuntimeInstance(component));
const std::vector<std::string> expected = {
"RuntimeStart:RuntimeScene",
"Create:Host:Gameplay.PlayerController",
"Awake:Host:Gameplay.PlayerController",
"OnEnable:Host:Gameplay.PlayerController"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, UpdateInvokesStartOnlyOnceAndRunsPerFrameMethods) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "Mover");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->OnUpdate(0.016f);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
const std::vector<std::string> expected = {
"Start:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"FixedUpdate:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"LateUpdate:Host:Gameplay.Mover"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, InactiveObjectDefersCreationUntilActivated) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "SpawnedLater");
host->SetActive(false);
engine->OnRuntimeStart(runtimeScene);
ASSERT_EQ(engine->GetTrackedScriptCount(), 1u);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_FALSE(engine->HasRuntimeInstance(component));
ASSERT_EQ(runtime.events.size(), 1u);
EXPECT_EQ(runtime.events[0], "RuntimeStart:RuntimeScene");
runtime.Clear();
host->SetActive(true);
engine->OnUpdate(0.016f);
const std::vector<std::string> expected = {
"Create:Host:Gameplay.SpawnedLater",
"Awake:Host:Gameplay.SpawnedLater",
"OnEnable:Host:Gameplay.SpawnedLater",
"Start:Host:Gameplay.SpawnedLater",
"Update:Host:Gameplay.SpawnedLater"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, RuntimeStopInvokesDisableDestroyAndRuntimeStop) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "ShutdownWatcher");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->OnRuntimeStop();
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.ShutdownWatcher",
"OnDestroy:Host:Gameplay.ShutdownWatcher",
"Destroy:Host:Gameplay.ShutdownWatcher",
"RuntimeStop:RuntimeScene"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_FALSE(engine->IsRuntimeRunning());
EXPECT_EQ(engine->GetRuntimeScene(), nullptr);
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
}
TEST_F(ScriptEngineTest, ReEnablingScriptDoesNotInvokeStartTwice) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "ToggleWatcher");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
runtime.Clear();
component->SetEnabled(false);
component->SetEnabled(true);
engine->OnUpdate(0.016f);
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.ToggleWatcher",
"OnEnable:Host:Gameplay.ToggleWatcher",
"Update:Host:Gameplay.ToggleWatcher"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, DestroyingGameObjectWhileRuntimeRunningDestroysTrackedScript) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "DestroyWatcher");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
runtimeScene->DestroyGameObject(host);
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.DestroyWatcher",
"OnDestroy:Host:Gameplay.DestroyWatcher",
"Destroy:Host:Gameplay.DestroyWatcher"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
}
TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndStartsOnNextUpdate) {
Scene* runtimeScene = CreateScene("RuntimeScene");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
GameObject* spawned = runtimeScene->CreateGameObject("Spawned");
ScriptComponent* component = AddScriptComponent(spawned, "Gameplay", "RuntimeSpawned");
EXPECT_EQ(engine->GetTrackedScriptCount(), 1u);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_TRUE(engine->HasRuntimeInstance(component));
const std::vector<std::string> expectedBeforeUpdate = {
"Create:Spawned:Gameplay.RuntimeSpawned",
"Awake:Spawned:Gameplay.RuntimeSpawned",
"OnEnable:Spawned:Gameplay.RuntimeSpawned"
};
EXPECT_EQ(runtime.events, expectedBeforeUpdate);
runtime.Clear();
engine->OnUpdate(0.016f);
const std::vector<std::string> expectedAfterUpdate = {
"Start:Spawned:Gameplay.RuntimeSpawned",
"Update:Spawned:Gameplay.RuntimeSpawned"
};
EXPECT_EQ(runtime.events, expectedAfterUpdate);
}
TEST_F(ScriptEngineTest, ScriptClassDiscoveryApiReturnsSortedDescriptorsAndSupportsAssemblyFilter) {
runtime.scriptClasses = {
{"Tools", "", "UtilityProbe"},
{"GameScripts", "Gameplay", "Zombie"},
{"GameScripts", "", "Bootstrap"},
{"GameScripts", "Gameplay", "Actor"},
{"Broken", "", ""}
};
std::vector<ScriptClassDescriptor> allClasses;
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(allClasses));
const std::vector<ScriptClassDescriptor> expectedAllClasses = {
{"GameScripts", "", "Bootstrap"},
{"GameScripts", "Gameplay", "Actor"},
{"GameScripts", "Gameplay", "Zombie"},
{"Tools", "", "UtilityProbe"}
};
EXPECT_EQ(allClasses, expectedAllClasses);
EXPECT_EQ(allClasses[0].GetFullName(), "Bootstrap");
EXPECT_EQ(allClasses[1].GetFullName(), "Gameplay.Actor");
std::vector<ScriptClassDescriptor> gameScriptClasses;
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(gameScriptClasses, "GameScripts"));
const std::vector<ScriptClassDescriptor> expectedGameScriptClasses = {
{"GameScripts", "", "Bootstrap"},
{"GameScripts", "Gameplay", "Actor"},
{"GameScripts", "Gameplay", "Zombie"}
};
EXPECT_EQ(gameScriptClasses, expectedGameScriptClasses);
}
TEST_F(ScriptEngineTest, ChangingScriptClassWhileRuntimeRunningRecreatesTrackedInstance) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
component->SetScriptClass("GameScripts", "Gameplay", "NewClass");
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.NewClass",
"OnDestroy:Host:Gameplay.NewClass",
"Destroy:Host:Gameplay.NewClass",
"Create:Host:Gameplay.NewClass",
"Awake:Host:Gameplay.NewClass",
"OnEnable:Host:Gameplay.NewClass"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_TRUE(engine->HasRuntimeInstance(component));
}
TEST_F(ScriptEngineTest, DispatchPhysicsMessageInvokesTrackedActiveScripts) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* other = runtimeScene->CreateGameObject("Other");
AddScriptComponent(host, "Gameplay", "TriggerListener");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->DispatchPhysicsMessage(host, ScriptPhysicsMessage::TriggerEnter, other);
const std::vector<std::string> expected = {
"TriggerEnter:Host:Gameplay.TriggerListener:Other"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, DispatchPhysicsMessageSkipsInactiveAndDisabledScripts) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* activeHost = runtimeScene->CreateGameObject("ActiveHost");
GameObject* inactiveHost = runtimeScene->CreateGameObject("InactiveHost");
GameObject* disabledHost = runtimeScene->CreateGameObject("DisabledHost");
GameObject* other = runtimeScene->CreateGameObject("Other");
AddScriptComponent(activeHost, "Gameplay", "ActiveListener");
AddScriptComponent(inactiveHost, "Gameplay", "InactiveListener");
ScriptComponent* disabledComponent = AddScriptComponent(disabledHost, "Gameplay", "DisabledListener");
inactiveHost->SetActive(false);
disabledComponent->SetEnabled(false);
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->DispatchPhysicsMessage(activeHost, ScriptPhysicsMessage::CollisionEnter, other);
engine->DispatchPhysicsMessage(inactiveHost, ScriptPhysicsMessage::CollisionEnter, other);
engine->DispatchPhysicsMessage(disabledHost, ScriptPhysicsMessage::CollisionEnter, other);
const std::vector<std::string> expected = {
"CollisionEnter:ActiveHost:Gameplay.ActiveListener:Other"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, ClearingScriptClassWhileRuntimeRunningDestroysTrackedInstance) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
component->ClearScriptClass();
const std::vector<std::string> expected = {
"OnDisable:Host:",
"OnDestroy:Host:",
"Destroy:Host:"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_FALSE(engine->HasTrackedScriptComponent(component));
EXPECT_FALSE(engine->HasRuntimeInstance(component));
EXPECT_FALSE(component->HasScriptClass());
}
TEST_F(ScriptEngineTest, FieldReadApiPrefersLiveManagedValueAndFallsBackToStoredValue) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeReadback");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
std::string label;
int32_t awakeCount = 0;
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "Label", label));
EXPECT_EQ(label, "Stored");
EXPECT_FALSE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
runtime.managedFieldValues["Label"] = std::string("Live");
runtime.managedFieldValues["AwakeCount"] = int32_t(1);
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "Label", label));
EXPECT_EQ(label, "Live");
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
EXPECT_EQ(awakeCount, 1);
}
TEST_F(ScriptEngineTest, FieldSnapshotApiCombinesMetadataStoredValuesAndLiveManagedValues) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeSnapshot");
runtime.fieldMetadata = {
{"AwakeCount", ScriptFieldType::Int32},
{"Label", ScriptFieldType::String}
};
component->GetFieldStorage().SetFieldValue("AwakeCount", uint64_t(7));
component->GetFieldStorage().SetFieldValue("Label", "Stored");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
std::vector<ScriptFieldSnapshot>& snapshots = model.fields;
ASSERT_EQ(snapshots.size(), 3u);
const auto findSnapshot = [&snapshots](const std::string& fieldName) -> const ScriptFieldSnapshot* {
const auto it = std::find_if(
snapshots.begin(),
snapshots.end(),
[&fieldName](const ScriptFieldSnapshot& snapshot) {
return snapshot.metadata.name == fieldName;
});
return it != snapshots.end() ? &(*it) : nullptr;
};
const ScriptFieldSnapshot* awakeSnapshot = findSnapshot("AwakeCount");
const ScriptFieldSnapshot* labelSnapshot = findSnapshot("Label");
const ScriptFieldSnapshot* legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(legacySnapshot, nullptr);
EXPECT_EQ(awakeSnapshot->metadata.type, ScriptFieldType::Int32);
EXPECT_TRUE(awakeSnapshot->declaredInClass);
EXPECT_TRUE(awakeSnapshot->hasDefaultValue);
EXPECT_FALSE(awakeSnapshot->hasValue);
EXPECT_EQ(awakeSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(awakeSnapshot->issue, ScriptFieldIssue::TypeMismatch);
EXPECT_TRUE(awakeSnapshot->hasStoredValue);
EXPECT_EQ(awakeSnapshot->storedType, ScriptFieldType::UInt64);
EXPECT_EQ(std::get<int32_t>(awakeSnapshot->defaultValue), 0);
EXPECT_EQ(std::get<int32_t>(awakeSnapshot->value), 0);
EXPECT_EQ(std::get<uint64_t>(awakeSnapshot->storedValue), 7u);
EXPECT_EQ(labelSnapshot->metadata.type, ScriptFieldType::String);
EXPECT_TRUE(labelSnapshot->declaredInClass);
EXPECT_TRUE(labelSnapshot->hasDefaultValue);
EXPECT_TRUE(labelSnapshot->hasValue);
EXPECT_EQ(labelSnapshot->valueSource, ScriptFieldValueSource::StoredValue);
EXPECT_EQ(labelSnapshot->issue, ScriptFieldIssue::None);
EXPECT_TRUE(labelSnapshot->hasStoredValue);
EXPECT_EQ(labelSnapshot->storedType, ScriptFieldType::String);
EXPECT_EQ(std::get<std::string>(labelSnapshot->defaultValue), "");
EXPECT_EQ(std::get<std::string>(labelSnapshot->value), "Stored");
EXPECT_EQ(std::get<std::string>(labelSnapshot->storedValue), "Stored");
EXPECT_EQ(legacySnapshot->metadata.type, ScriptFieldType::UInt64);
EXPECT_FALSE(legacySnapshot->declaredInClass);
EXPECT_FALSE(legacySnapshot->hasDefaultValue);
EXPECT_TRUE(legacySnapshot->hasValue);
EXPECT_EQ(legacySnapshot->valueSource, ScriptFieldValueSource::StoredValue);
EXPECT_EQ(legacySnapshot->issue, ScriptFieldIssue::StoredOnly);
EXPECT_TRUE(legacySnapshot->hasStoredValue);
EXPECT_EQ(legacySnapshot->storedType, ScriptFieldType::UInt64);
EXPECT_EQ(std::get<uint64_t>(legacySnapshot->value), 99u);
EXPECT_EQ(std::get<uint64_t>(legacySnapshot->storedValue), 99u);
runtime.managedFieldValues["AwakeCount"] = int32_t(1);
runtime.managedFieldValues["Label"] = std::string("Live");
engine->OnRuntimeStart(runtimeScene);
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
awakeSnapshot = findSnapshot("AwakeCount");
labelSnapshot = findSnapshot("Label");
legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(legacySnapshot, nullptr);
EXPECT_TRUE(awakeSnapshot->hasValue);
EXPECT_TRUE(awakeSnapshot->hasDefaultValue);
EXPECT_EQ(awakeSnapshot->valueSource, ScriptFieldValueSource::ManagedValue);
EXPECT_EQ(awakeSnapshot->issue, ScriptFieldIssue::TypeMismatch);
EXPECT_EQ(std::get<int32_t>(awakeSnapshot->defaultValue), 0);
EXPECT_EQ(std::get<int32_t>(awakeSnapshot->value), 1);
EXPECT_TRUE(labelSnapshot->hasValue);
EXPECT_TRUE(labelSnapshot->hasDefaultValue);
EXPECT_EQ(labelSnapshot->valueSource, ScriptFieldValueSource::ManagedValue);
EXPECT_EQ(labelSnapshot->issue, ScriptFieldIssue::None);
EXPECT_EQ(std::get<std::string>(labelSnapshot->defaultValue), "");
EXPECT_EQ(std::get<std::string>(labelSnapshot->value), "Live");
EXPECT_TRUE(legacySnapshot->hasValue);
EXPECT_FALSE(legacySnapshot->hasDefaultValue);
EXPECT_EQ(legacySnapshot->valueSource, ScriptFieldValueSource::StoredValue);
EXPECT_EQ(legacySnapshot->issue, ScriptFieldIssue::StoredOnly);
EXPECT_EQ(std::get<uint64_t>(legacySnapshot->value), 99u);
}
TEST_F(ScriptEngineTest, FieldModelReportsMissingScriptClassAndPreservesStoredFields) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "MissingScript");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Missing);
ASSERT_EQ(model.fields.size(), 1u);
const ScriptFieldSnapshot& field = model.fields[0];
EXPECT_EQ(field.metadata.name, "Label");
EXPECT_EQ(field.metadata.type, ScriptFieldType::String);
EXPECT_FALSE(field.declaredInClass);
EXPECT_FALSE(field.hasDefaultValue);
EXPECT_TRUE(field.hasValue);
EXPECT_EQ(field.valueSource, ScriptFieldValueSource::StoredValue);
EXPECT_EQ(field.issue, ScriptFieldIssue::StoredOnly);
EXPECT_TRUE(field.hasStoredValue);
EXPECT_EQ(field.storedType, ScriptFieldType::String);
EXPECT_EQ(std::get<std::string>(field.value), "Stored");
EXPECT_EQ(std::get<std::string>(field.storedValue), "Stored");
}
TEST_F(ScriptEngineTest, FieldModelUsesRuntimeClassDefaultValuesWhenAvailable) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeDefaults");
runtime.fieldMetadata = {
{"Health", ScriptFieldType::Int32},
{"Label", ScriptFieldType::String},
{"SpawnPoint", ScriptFieldType::Vector3}
};
runtime.fieldDefaultValues = {
{"Health", ScriptFieldType::Int32, ScriptFieldValue(int32_t(17))},
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Seeded"))},
{"SpawnPoint", ScriptFieldType::Vector3, ScriptFieldValue(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f))}
};
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
ASSERT_EQ(model.fields.size(), 3u);
const auto findSnapshot = [&model](const std::string& fieldName) -> const ScriptFieldSnapshot* {
const auto it = std::find_if(
model.fields.begin(),
model.fields.end(),
[&fieldName](const ScriptFieldSnapshot& snapshot) {
return snapshot.metadata.name == fieldName;
});
return it != model.fields.end() ? &(*it) : nullptr;
};
const ScriptFieldSnapshot* healthSnapshot = findSnapshot("Health");
const ScriptFieldSnapshot* labelSnapshot = findSnapshot("Label");
const ScriptFieldSnapshot* spawnPointSnapshot = findSnapshot("SpawnPoint");
ASSERT_NE(healthSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(spawnPointSnapshot, nullptr);
EXPECT_TRUE(healthSnapshot->hasDefaultValue);
EXPECT_FALSE(healthSnapshot->hasValue);
EXPECT_EQ(healthSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<int32_t>(healthSnapshot->defaultValue), 17);
EXPECT_EQ(std::get<int32_t>(healthSnapshot->value), 17);
EXPECT_TRUE(labelSnapshot->hasDefaultValue);
EXPECT_FALSE(labelSnapshot->hasValue);
EXPECT_EQ(labelSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<std::string>(labelSnapshot->defaultValue), "Seeded");
EXPECT_EQ(std::get<std::string>(labelSnapshot->value), "Seeded");
EXPECT_TRUE(spawnPointSnapshot->hasDefaultValue);
EXPECT_FALSE(spawnPointSnapshot->hasValue);
EXPECT_EQ(spawnPointSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<XCEngine::Math::Vector3>(spawnPointSnapshot->defaultValue), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
EXPECT_EQ(std::get<XCEngine::Math::Vector3>(spawnPointSnapshot->value), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
}
TEST_F(ScriptEngineTest, FieldWriteBatchApiReportsPerItemStatusAndAppliesValidValues) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeBatchWrite");
runtime.fieldMetadata = {
{"Speed", ScriptFieldType::Float},
{"Label", ScriptFieldType::String}
};
component->GetFieldStorage().SetFieldValue("Speed", uint64_t(7));
component->GetFieldStorage().SetFieldValue("Label", "Stored");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldWriteRequest> requests = {
{"Speed", ScriptFieldType::Float, ScriptFieldValue(5.0f)},
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Edited"))},
{"LegacyOnly", ScriptFieldType::UInt64, ScriptFieldValue(uint64_t(100))},
{"Missing", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))},
{"Speed", ScriptFieldType::String, ScriptFieldValue(std::string("wrong"))},
{"", ScriptFieldType::Int32, ScriptFieldValue(int32_t(2))},
{"Label", ScriptFieldType::String, ScriptFieldValue(int32_t(3))}
};
std::vector<ScriptFieldWriteResult> results;
EXPECT_FALSE(engine->ApplyScriptFieldWrites(component, requests, results));
ASSERT_EQ(results.size(), requests.size());
EXPECT_EQ(results[0].status, ScriptFieldWriteStatus::Applied);
EXPECT_EQ(results[1].status, ScriptFieldWriteStatus::Applied);
EXPECT_EQ(results[2].status, ScriptFieldWriteStatus::StoredOnlyField);
EXPECT_EQ(results[3].status, ScriptFieldWriteStatus::UnknownField);
EXPECT_EQ(results[4].status, ScriptFieldWriteStatus::TypeMismatch);
EXPECT_EQ(results[5].status, ScriptFieldWriteStatus::EmptyFieldName);
EXPECT_EQ(results[6].status, ScriptFieldWriteStatus::InvalidValue);
float speed = 0.0f;
std::string label;
uint64_t legacyOnly = 0;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", speed));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", label));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("LegacyOnly", legacyOnly));
EXPECT_FLOAT_EQ(speed, 5.0f);
EXPECT_EQ(label, "Edited");
EXPECT_EQ(legacyOnly, 99u);
ASSERT_EQ(runtime.managedSetFieldNames.size(), 2u);
EXPECT_EQ(runtime.managedSetFieldNames[0], "Speed");
EXPECT_EQ(runtime.managedSetFieldNames[1], "Label");
EXPECT_EQ(std::get<float>(runtime.managedFieldValues["Speed"]), 5.0f);
EXPECT_EQ(std::get<std::string>(runtime.managedFieldValues["Label"]), "Edited");
}
TEST_F(ScriptEngineTest, FieldWriteBatchApiAllowsStoredFieldsWhenScriptClassMetadataIsMissing) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "MissingScript");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
const std::vector<ScriptFieldWriteRequest> requests = {
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Retained"))},
{"Missing", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))}
};
std::vector<ScriptFieldWriteResult> results;
EXPECT_FALSE(engine->ApplyScriptFieldWrites(component, requests, results));
ASSERT_EQ(results.size(), requests.size());
EXPECT_EQ(results[0].status, ScriptFieldWriteStatus::Applied);
EXPECT_EQ(results[1].status, ScriptFieldWriteStatus::UnknownField);
std::string label;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", label));
EXPECT_EQ(label, "Retained");
}
TEST_F(ScriptEngineTest, FieldClearApiReportsPerItemStatusAndClearsStoredAndLiveValues) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeFieldClear");
runtime.fieldMetadata = {
{"Label", ScriptFieldType::String},
{"Speed", ScriptFieldType::Float}
};
runtime.fieldDefaultValues = {
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Seeded"))},
{"Speed", ScriptFieldType::Float, ScriptFieldValue(6.5f)}
};
runtime.managedFieldValues["Speed"] = ScriptFieldValue(41.0f);
runtime.managedFieldValues["Label"] = ScriptFieldValue(std::string("LiveValue"));
component->GetFieldStorage().SetFieldValue("Speed", 5.0f);
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldClearRequest> requests = {
{"Speed"},
{"Label"},
{"LegacyOnly"},
{"Missing"},
{""}
};
std::vector<ScriptFieldClearResult> results;
EXPECT_FALSE(engine->ClearScriptFieldOverrides(component, requests, results));
ASSERT_EQ(results.size(), requests.size());
EXPECT_EQ(results[0].status, ScriptFieldClearStatus::Applied);
EXPECT_EQ(results[1].status, ScriptFieldClearStatus::Applied);
EXPECT_EQ(results[2].status, ScriptFieldClearStatus::Applied);
EXPECT_EQ(results[3].status, ScriptFieldClearStatus::UnknownField);
EXPECT_EQ(results[4].status, ScriptFieldClearStatus::EmptyFieldName);
EXPECT_FALSE(component->GetFieldStorage().Contains("Speed"));
EXPECT_FALSE(component->GetFieldStorage().Contains("Label"));
EXPECT_FALSE(component->GetFieldStorage().Contains("LegacyOnly"));
ASSERT_EQ(runtime.managedSetFieldNames.size(), 2u);
EXPECT_EQ(runtime.managedSetFieldNames[0], "Speed");
EXPECT_EQ(runtime.managedSetFieldNames[1], "Label");
EXPECT_FLOAT_EQ(std::get<float>(runtime.managedFieldValues["Speed"]), 6.5f);
EXPECT_EQ(std::get<std::string>(runtime.managedFieldValues["Label"]), "Seeded");
}
TEST_F(ScriptEngineTest, FieldClearApiReportsNoValueToClearForDeclaredFieldWithoutStoredOrLiveValue) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "RuntimeFieldClear");
runtime.fieldMetadata = {
{"Speed", ScriptFieldType::Float}
};
const std::vector<ScriptFieldClearRequest> requests = {
{"Speed"}
};
std::vector<ScriptFieldClearResult> results;
EXPECT_FALSE(engine->ClearScriptFieldOverrides(component, requests, results));
ASSERT_EQ(results.size(), requests.size());
EXPECT_EQ(results[0].status, ScriptFieldClearStatus::NoValueToClear);
}
TEST_F(ScriptEngineTest, FieldClearApiAllowsRemovingStoredFieldsWhenScriptClassMetadataIsMissing) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "MissingScript");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
const std::vector<ScriptFieldClearRequest> requests = {
{"Label"},
{"Missing"}
};
std::vector<ScriptFieldClearResult> results;
EXPECT_FALSE(engine->ClearScriptFieldOverrides(component, requests, results));
ASSERT_EQ(results.size(), requests.size());
EXPECT_EQ(results[0].status, ScriptFieldClearStatus::Applied);
EXPECT_EQ(results[1].status, ScriptFieldClearStatus::UnknownField);
EXPECT_FALSE(component->GetFieldStorage().Contains("Label"));
}
} // namespace

View File

@@ -1,100 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Scripting/ScriptFieldStorage.h>
using namespace XCEngine::Math;
using namespace XCEngine::Scripting;
namespace {
TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) {
ScriptFieldStorage storage;
EXPECT_TRUE(storage.SetFieldValue("Speed", 3.5f));
EXPECT_TRUE(storage.SetFieldValue("Accuracy", 0.875));
EXPECT_TRUE(storage.SetFieldValue("IsAlive", true));
EXPECT_TRUE(storage.SetFieldValue("Health", int32_t(125)));
EXPECT_TRUE(storage.SetFieldValue("Score", uint64_t(9001)));
EXPECT_TRUE(storage.SetFieldValue("DisplayName", std::string("Player One")));
EXPECT_TRUE(storage.SetFieldValue("Offset2D", Vector2(1.0f, 2.0f)));
EXPECT_TRUE(storage.SetFieldValue("Velocity", Vector3(3.0f, 4.0f, 5.0f)));
EXPECT_TRUE(storage.SetFieldValue("Tint", Vector4(0.1f, 0.2f, 0.3f, 1.0f)));
EXPECT_TRUE(storage.SetFieldValue("Target", GameObjectReference{42}));
EXPECT_TRUE(storage.SetFieldValue("TargetCamera", ComponentReference{42, 0}));
float speed = 0.0f;
double accuracy = 0.0;
bool isAlive = false;
int32_t health = 0;
uint64_t score = 0;
std::string displayName;
Vector2 offset2D;
Vector3 velocity;
Vector4 tint;
GameObjectReference target;
ComponentReference targetCamera;
EXPECT_EQ(storage.GetFieldCount(), 11u);
EXPECT_TRUE(storage.Contains("Velocity"));
EXPECT_TRUE(storage.TryGetFieldValue("Speed", speed));
EXPECT_TRUE(storage.TryGetFieldValue("Accuracy", accuracy));
EXPECT_TRUE(storage.TryGetFieldValue("IsAlive", isAlive));
EXPECT_TRUE(storage.TryGetFieldValue("Health", health));
EXPECT_TRUE(storage.TryGetFieldValue("Score", score));
EXPECT_TRUE(storage.TryGetFieldValue("DisplayName", displayName));
EXPECT_TRUE(storage.TryGetFieldValue("Offset2D", offset2D));
EXPECT_TRUE(storage.TryGetFieldValue("Velocity", velocity));
EXPECT_TRUE(storage.TryGetFieldValue("Tint", tint));
EXPECT_TRUE(storage.TryGetFieldValue("Target", target));
EXPECT_TRUE(storage.TryGetFieldValue("TargetCamera", targetCamera));
EXPECT_FLOAT_EQ(speed, 3.5f);
EXPECT_DOUBLE_EQ(accuracy, 0.875);
EXPECT_TRUE(isAlive);
EXPECT_EQ(health, 125);
EXPECT_EQ(score, 9001u);
EXPECT_EQ(displayName, "Player One");
EXPECT_EQ(offset2D, Vector2(1.0f, 2.0f));
EXPECT_EQ(velocity, Vector3(3.0f, 4.0f, 5.0f));
EXPECT_EQ(tint, Vector4(0.1f, 0.2f, 0.3f, 1.0f));
EXPECT_EQ(target, GameObjectReference{42});
EXPECT_EQ(targetCamera, (ComponentReference{42, 0}));
EXPECT_TRUE(storage.Remove("IsAlive"));
EXPECT_FALSE(storage.Contains("IsAlive"));
EXPECT_EQ(storage.GetFieldCount(), 10u);
}
TEST(ScriptFieldStorage_Test, SerializeRoundTripPreservesFieldValues) {
ScriptFieldStorage original;
ASSERT_TRUE(original.SetFieldValue("Message", std::string("Ready;Set=Go|100%\nLine2")));
ASSERT_TRUE(original.SetFieldValue("Scale", Vector3(2.0f, 3.0f, 4.0f)));
ASSERT_TRUE(original.SetFieldValue("Owner", GameObjectReference{778899}));
ASSERT_TRUE(original.SetFieldValue("View", ComponentReference{778899, 123456}));
ASSERT_TRUE(original.SetFieldValue("Counter", int32_t(-17)));
const std::string serialized = original.SerializeToString();
ScriptFieldStorage restored;
restored.DeserializeFromString(serialized);
std::string message;
Vector3 scale;
GameObjectReference owner;
ComponentReference view;
int32_t counter = 0;
EXPECT_TRUE(restored.TryGetFieldValue("Message", message));
EXPECT_TRUE(restored.TryGetFieldValue("Scale", scale));
EXPECT_TRUE(restored.TryGetFieldValue("Owner", owner));
EXPECT_TRUE(restored.TryGetFieldValue("View", view));
EXPECT_TRUE(restored.TryGetFieldValue("Counter", counter));
EXPECT_EQ(message, "Ready;Set=Go|100%\nLine2");
EXPECT_EQ(scale, Vector3(2.0f, 3.0f, 4.0f));
EXPECT_EQ(owner, GameObjectReference{778899});
EXPECT_EQ(view, (ComponentReference{778899, 123456}));
EXPECT_EQ(counter, -17);
}
} // namespace