Files
XCEngine/tests/scripting/test_mono_script_runtime.cpp
ssdfasd 58dde75d3d refactor(srp): add managed lifecycle cleanup seams
Invoke managed pipeline disposal and asset runtime cleanup from the native bridge lifecycle. Add Universal renderer and feature cleanup hooks plus regression probes to verify runtime cache teardown semantics.
2026-04-20 01:14:37 +08:00

6277 lines
274 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Components/BoxColliderComponent.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Components/RigidbodyComponent.h>
#include <XCEngine/Components/SphereColliderComponent.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/Core/Math/Vector4.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Physics/PhysicsWorld.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/Rendering/Execution/CameraFramePlan.h>
#include <XCEngine/Rendering/Execution/CameraFrameRenderGraphFrameData.h>
#include <XCEngine/Rendering/Execution/CameraRenderer.h>
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
#include <XCEngine/Rendering/Extraction/RenderSceneExtractor.h>
#include <XCEngine/Rendering/Extraction/RenderSceneUtility.h>
#include <XCEngine/Rendering/Graph/RenderGraph.h>
#include <XCEngine/Rendering/Graph/RenderGraphCompiler.h>
#include <XCEngine/Rendering/Planning/SceneRenderRequestPlanner.h>
#include <XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h>
#include <XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h>
#include <XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHICommandQueue.h>
#include <XCEngine/RHI/RHIDescriptorPool.h>
#include <XCEngine/RHI/RHIDescriptorSet.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHISampler.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scene/SceneRuntime.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "../Fixtures/RenderTestRhiStubs.h"
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
using namespace XCTest;
namespace {
void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::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);
}
void ExpectVector4Near(const XCEngine::Math::Vector4& actual, const XCEngine::Math::Vector4& 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);
EXPECT_NEAR(actual.w, expected.w, tolerance);
}
class CapturingLogSink final : public XCEngine::Debug::ILogSink {
public:
void Log(const XCEngine::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 XCEngine::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<XCEngine::Debug::LogEntry> entries;
};
ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
if (!gameObject) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component) {
continue;
}
if (component->GetNamespaceName() == namespaceName && component->GetClassName() == className) {
return component;
}
}
return nullptr;
}
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;
}
class MonoScriptRuntimeTest : 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();
std::string engineAssemblyError;
MonoScriptRuntime::Settings settings = CreateMonoSettings();
ASSERT_TRUE(
MonoScriptRuntime::DiscoverEngineAssemblies(
settings,
&engineAssemblyError))
<< engineAssemblyError;
runtime = std::make_unique<MonoScriptRuntime>(std::move(settings));
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
engine->SetRuntime(runtime.get());
}
void TearDown() override {
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* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", namespaceName, className);
return component;
}
ScriptEngine* engine = nullptr;
std::unique_ptr<MonoScriptRuntime> runtime;
std::unique_ptr<Scene> scene;
};
TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasses) {
const std::vector<std::string> classNames = runtime->GetScriptClassNames("GameScripts");
EXPECT_TRUE(runtime->IsInitialized());
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "Gameplay", "LifecycleProbe"));
EXPECT_NE(std::find(classNames.begin(), classNames.end(), "Gameplay.LifecycleProbe"), classNames.end());
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.AbstractLifecycleProbe"), classNames.end());
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end());
}
TEST_F(MonoScriptRuntimeTest, ScriptClassDescriptorApiReturnsConcreteManagedTypes) {
std::vector<ScriptClassDescriptor> classes;
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classes));
ASSERT_FALSE(classes.empty());
EXPECT_TRUE(std::is_sorted(
classes.begin(),
classes.end(),
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
if (lhs.assemblyName != rhs.assemblyName) {
return lhs.assemblyName < rhs.assemblyName;
}
if (lhs.namespaceName != rhs.namespaceName) {
return lhs.namespaceName < rhs.namespaceName;
}
return lhs.className < rhs.className;
}));
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "LifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "AbstractLifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "UtilityHelper"}),
classes.end());
}
TEST_F(
MonoScriptRuntimeTest,
RenderPipelineAssetClassDescriptorApiReturnsConcretePipelineAssets) {
std::vector<ScriptClassDescriptor> classes;
ASSERT_TRUE(runtime->TryGetAvailableRenderPipelineAssetClasses(classes));
ASSERT_FALSE(classes.empty());
EXPECT_TRUE(std::is_sorted(
classes.begin(),
classes.end(),
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
if (lhs.assemblyName != rhs.assemblyName) {
return lhs.assemblyName < rhs.assemblyName;
}
if (lhs.namespaceName != rhs.namespaceName) {
return lhs.namespaceName < rhs.namespaceName;
}
return lhs.className < rhs.className;
}));
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"XCEngine.RenderPipelines.Universal",
"XCEngine.Rendering.Universal",
"UniversalRenderPipelineAsset"}),
classes.end());
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"Gameplay",
"RenderPipelineApiProbeAsset"}),
classes.end());
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"}),
classes.end());
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"Gameplay",
"ManagedUniversalRenderPipelineProbeAsset"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{
"GameScripts",
"Gameplay",
"LegacyRenderPipelineApiProbeAsset"}),
classes.end());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) {
std::vector<ScriptFieldMetadata> fields;
EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "FieldMetadataProbe", fields));
const std::vector<ScriptFieldMetadata> expected = {
{"Health", ScriptFieldType::Int32},
{"HiddenFlag", ScriptFieldType::Bool},
{"Label", ScriptFieldType::String},
{"SpawnPoint", ScriptFieldType::Vector3},
{"Speed", ScriptFieldType::Float},
{"State", ScriptFieldType::Int32},
{"Target", ScriptFieldType::GameObject},
};
EXPECT_EQ(fields, expected);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataQueryFailsForUnknownClass) {
std::vector<ScriptFieldMetadata> fields = {
{"Sentinel", ScriptFieldType::Bool},
};
EXPECT_FALSE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "MissingProbe", fields));
EXPECT_TRUE(fields.empty());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsManagedInitializers) {
std::vector<ScriptFieldDefaultValue> fields;
EXPECT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "Gameplay", "RuntimeGameObjectProbe", fields));
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[](const ScriptFieldDefaultValue& field) {
return field.fieldName == "ObservedRootChildCountAfterDestroy";
});
ASSERT_NE(fieldIt, fields.end());
EXPECT_EQ(fieldIt->type, ScriptFieldType::Int32);
EXPECT_EQ(std::get<int32_t>(fieldIt->value), -1);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsEnumInitializersAsInt32) {
std::vector<ScriptFieldDefaultValue> fields;
EXPECT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "Gameplay", "FieldMetadataProbe", fields));
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[](const ScriptFieldDefaultValue& field) {
return field.fieldName == "State";
});
ASSERT_NE(fieldIt, fields.end());
EXPECT_EQ(fieldIt->type, ScriptFieldType::Int32);
EXPECT_EQ(std::get<int32_t>(fieldIt->value), 2);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers) {
std::vector<ScriptFieldDefaultValue> fields;
EXPECT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "Gameplay", "FieldMetadataProbe", fields));
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[](const ScriptFieldDefaultValue& field) {
return field.fieldName == "HiddenFlag";
});
ASSERT_NE(fieldIt, fields.end());
EXPECT_EQ(fieldIt->type, ScriptFieldType::Bool);
EXPECT_TRUE(std::get<bool>(fieldIt->value));
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsConcreteComponentReferenceFields) {
std::vector<ScriptFieldMetadata> fields;
EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "ComponentFieldMetadataProbe", fields));
const std::vector<ScriptFieldMetadata> expected = {
{"Pivot", ScriptFieldType::Component},
{"SceneCamera", ScriptFieldType::Component},
{"ScriptTarget", ScriptFieldType::Component},
};
EXPECT_EQ(fields, expected);
}
TEST_F(MonoScriptRuntimeTest, ManagedGraphicsSettingsRoundTripsRenderPipelineAssetSelection) {
Scene* runtimeScene = CreateScene("ManagedRenderPipelineSelectionScene");
GameObject* scriptObject = runtimeScene->CreateGameObject("RenderPipelineProbe");
ScriptComponent* script =
AddScript(scriptObject, "Gameplay", "RenderPipelineApiProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
EXPECT_EQ(descriptor.assemblyName, "GameScripts");
EXPECT_EQ(descriptor.namespaceName, "Gameplay");
EXPECT_EQ(descriptor.className, "RenderPipelineApiProbeAsset");
EXPECT_NE(descriptor.managedAssetHandle, 0u);
bool initialAssetWasNull = false;
bool selectionRoundTripSucceeded = false;
bool selectionReferencePreserved = false;
std::string selectedPipelineAssetTypeName;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"InitialAssetWasNull",
initialAssetWasNull));
EXPECT_TRUE(initialAssetWasNull);
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectionRoundTripSucceeded",
selectionRoundTripSucceeded));
EXPECT_TRUE(selectionRoundTripSucceeded);
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectionReferencePreserved",
selectionReferencePreserved));
EXPECT_TRUE(selectionReferencePreserved);
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectedPipelineAssetTypeName",
selectedPipelineAssetTypeName));
EXPECT_EQ(
selectedPipelineAssetTypeName,
"Gameplay.RenderPipelineApiProbeAsset");
}
TEST_F(
MonoScriptRuntimeTest,
ManagedGraphicsSettingsRoundTripsScriptCoreUniversalRenderPipelineAssetSelection) {
Scene* runtimeScene =
CreateScene("ScriptCoreUniversalRenderPipelineSelectionScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ScriptCoreUniversalRenderPipelineProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ScriptCoreUniversalRenderPipelineSelectionProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
EXPECT_EQ(descriptor.assemblyName, "XCEngine.RenderPipelines.Universal");
EXPECT_EQ(descriptor.namespaceName, "XCEngine.Rendering.Universal");
EXPECT_EQ(descriptor.className, "UniversalRenderPipelineAsset");
EXPECT_NE(descriptor.managedAssetHandle, 0u);
bool selectionRoundTripSucceeded = false;
std::string selectedPipelineAssetTypeName;
std::string selectedRendererDataTypeName;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectionRoundTripSucceeded",
selectionRoundTripSucceeded));
EXPECT_TRUE(selectionRoundTripSucceeded);
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectedPipelineAssetTypeName",
selectedPipelineAssetTypeName));
EXPECT_EQ(
selectedPipelineAssetTypeName,
"XCEngine.Rendering.Universal.UniversalRenderPipelineAsset");
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"SelectedRendererDataTypeName",
selectedRendererDataTypeName));
EXPECT_EQ(
selectedRendererDataTypeName,
"XCEngine.Rendering.Universal.UniversalRendererData");
}
TEST_F(
MonoScriptRuntimeTest,
ManagedGraphicsSettingsGetterMaterializesConfiguredAssetWithoutExistingHandle) {
Scene* runtimeScene =
CreateScene("ManagedRenderPipelineGetterMaterializationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("RenderPipelineGetterProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ProjectConfiguredRenderPipelineGetterProbe");
ASSERT_NE(script, nullptr);
XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
configuredDescriptor = {};
configuredDescriptor.assemblyName = "GameScripts";
configuredDescriptor.namespaceName = "Gameplay";
configuredDescriptor.className = "RenderPipelineApiProbeAsset";
XCEngine::Rendering::Pipelines::SetConfiguredManagedRenderPipelineAssetDescriptor(
configuredDescriptor);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
resolvedDescriptor =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
EXPECT_EQ(resolvedDescriptor.assemblyName, configuredDescriptor.assemblyName);
EXPECT_EQ(resolvedDescriptor.namespaceName, configuredDescriptor.namespaceName);
EXPECT_EQ(resolvedDescriptor.className, configuredDescriptor.className);
EXPECT_NE(resolvedDescriptor.managedAssetHandle, 0u);
EXPECT_NE(
runtime->GetExternalManagedObject(
resolvedDescriptor.managedAssetHandle),
nullptr);
bool observedAssetWasNull = true;
std::string observedPipelineAssetTypeName;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedAssetWasNull",
observedAssetWasNull));
EXPECT_FALSE(observedAssetWasNull);
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedPipelineAssetTypeName",
observedPipelineAssetTypeName));
EXPECT_EQ(
observedPipelineAssetTypeName,
"Gameplay.RenderPipelineApiProbeAsset");
}
TEST_F(
MonoScriptRuntimeTest,
RuntimeStopClearsManagedGraphicsSettingsSelection) {
Scene* runtimeScene = CreateScene("ManagedRenderPipelineStopClearsSelectionScene");
GameObject* scriptObject = runtimeScene->CreateGameObject("RenderPipelineProbe");
ScriptComponent* script =
AddScript(scriptObject, "Gameplay", "RenderPipelineApiProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptorBeforeStop =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(descriptorBeforeStop.IsValid());
ASSERT_NE(descriptorBeforeStop.managedAssetHandle, 0u);
ASSERT_NE(
runtime->GetExternalManagedObject(
descriptorBeforeStop.managedAssetHandle),
nullptr);
engine->OnRuntimeStop();
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptorAfterStop =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
EXPECT_FALSE(descriptorAfterStop.IsValid());
EXPECT_EQ(descriptorAfterStop.managedAssetHandle, 0u);
EXPECT_EQ(
runtime->GetExternalManagedObject(
descriptorBeforeStop.managedAssetHandle),
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
CameraRendererFallsBackAfterRuntimeStopClearsManagedSelection) {
Scene* runtimeScene = CreateScene("ManagedRenderPipelineStopFallbackScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ManagedRenderPipelineSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderPipelineRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Rendering::CameraRenderer renderer;
ASSERT_NE(
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset()),
nullptr);
engine->OnRuntimeStop();
EXPECT_EQ(
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset()),
nullptr);
auto* host =
dynamic_cast<XCEngine::Rendering::Pipelines::ScriptableRenderPipelineHost*>(
renderer.GetPipeline());
ASSERT_NE(host, nullptr);
EXPECT_EQ(host->GetStageRecorder(), nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
RuntimeSceneReplacePreservesManagedGraphicsSettingsSelection) {
Scene* firstScene = CreateScene("ManagedRenderPipelineFirstScene");
GameObject* selectionObject =
firstScene->CreateGameObject("ManagedRenderPipelineSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderPipelineRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
engine->OnRuntimeStart(firstScene);
engine->OnUpdate(0.016f);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptorBeforeReplace =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
ASSERT_TRUE(descriptorBeforeReplace.IsValid());
ASSERT_NE(descriptorBeforeReplace.managedAssetHandle, 0u);
MonoObject* const managedAssetBeforeReplace =
runtime->GetExternalManagedObject(
descriptorBeforeReplace.managedAssetHandle);
ASSERT_NE(managedAssetBeforeReplace, nullptr);
auto secondScene = std::make_unique<Scene>("ManagedRenderPipelineSecondScene");
engine->OnRuntimeSceneReplaced(secondScene.get());
scene = std::move(secondScene);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptorAfterReplace =
XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor();
EXPECT_EQ(descriptorAfterReplace.assemblyName, descriptorBeforeReplace.assemblyName);
EXPECT_EQ(descriptorAfterReplace.namespaceName, descriptorBeforeReplace.namespaceName);
EXPECT_EQ(descriptorAfterReplace.className, descriptorBeforeReplace.className);
EXPECT_EQ(
descriptorAfterReplace.managedAssetHandle,
descriptorBeforeReplace.managedAssetHandle);
EXPECT_EQ(
runtime->GetExternalManagedObject(
descriptorAfterReplace.managedAssetHandle),
managedAssetBeforeReplace);
XCEngine::Rendering::CameraRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
descriptorBeforeReplace.className);
engine->OnRuntimeStop();
}
TEST_F(
MonoScriptRuntimeTest,
DefaultCameraRendererUsesManagedGraphicsSelectionToRecordMainSceneGraph) {
Scene* runtimeScene = CreateScene("ManagedRenderPipelineDefaultRendererScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ManagedRenderPipelineSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderPipelineRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Rendering::CameraRenderer renderer;
const auto* asset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(asset, nullptr);
EXPECT_EQ(asset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(asset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(asset->GetDescriptor().className, "ManagedRenderPipelineProbeAsset");
auto* host =
dynamic_cast<XCEngine::Rendering::Pipelines::ScriptableRenderPipelineHost*>(
renderer.GetPipeline());
ASSERT_NE(host, nullptr);
ASSERT_NE(host->GetStageRecorder(), nullptr);
EXPECT_TRUE(
host->GetStageRecorder()->Initialize(
XCEngine::Rendering::RenderContext{}))
<< runtime->GetLastError();
EXPECT_TRUE(
host->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene))
<< runtime->GetLastError();
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("ManagedSelectedMainSceneColor", colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture("ManagedSelectedMainSceneDepth", 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,
"ManagedSelectedMainScene",
XCEngine::Rendering::CameraFrameStage::MainScene,
{},
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ colorTarget },
depthTarget,
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(host->GetStageRecorder()->RecordStageRenderGraph(graphContext))
<< runtime->GetLastError();
XCEngine::Rendering::CompiledRenderGraph compiledGraph = {};
XCEngine::Containers::String errorMessage;
ASSERT_TRUE(
XCEngine::Rendering::RenderGraphCompiler::Compile(
graph,
compiledGraph,
&errorMessage))
<< errorMessage.CStr();
ASSERT_EQ(compiledGraph.GetPassCount(), 3u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ManagedSelectedMainScene.Opaque");
EXPECT_STREQ(
compiledGraph.GetPassName(1).CStr(),
"ManagedSelectedMainScene.Skybox");
EXPECT_STREQ(
compiledGraph.GetPassName(2).CStr(),
"ManagedSelectedMainScene.Transparent");
host->GetStageRecorder()->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
DefaultCameraRendererUsesScriptCoreUniversalPipelineAssetAndRespectsRendererData) {
Scene* runtimeScene =
CreateScene("ScriptCoreUniversalRenderPipelineDefaultRendererScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ScriptCoreUniversalRenderPipelineSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ScriptCoreConfiguredUniversalRenderPipelineRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Rendering::CameraRenderer renderer;
const auto* asset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(asset, nullptr);
EXPECT_EQ(
asset->GetDescriptor().assemblyName,
"XCEngine.RenderPipelines.Universal");
EXPECT_EQ(
asset->GetDescriptor().namespaceName,
"XCEngine.Rendering.Universal");
EXPECT_EQ(asset->GetDescriptor().className, "UniversalRenderPipelineAsset");
auto* host =
dynamic_cast<XCEngine::Rendering::Pipelines::ScriptableRenderPipelineHost*>(
renderer.GetPipeline());
ASSERT_NE(host, nullptr);
ASSERT_NE(host->GetStageRecorder(), nullptr);
EXPECT_TRUE(
host->GetStageRecorder()->Initialize(
XCEngine::Rendering::RenderContext{}));
EXPECT_TRUE(
host->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
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(
"ScriptCoreUniversalMainSceneColor",
colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture(
"ScriptCoreUniversalMainSceneDepth",
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,
"ScriptCoreUniversalMainScene",
XCEngine::Rendering::CameraFrameStage::MainScene,
{},
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ colorTarget },
depthTarget,
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(host->GetStageRecorder()->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(),
"ScriptCoreUniversalMainScene.Opaque");
host->GetStageRecorder()->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
DefaultSceneRendererUsesManagedUniversalPipelineForPlannedMainSceneAndPostProcessRender) {
Scene* runtimeScene = CreateScene("ManagedUniversalRenderPipelineSceneRendererScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ManagedUniversalRenderPipelineSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedUniversalRenderPipelineRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(64u, 64u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(pipelineAsset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(pipelineAsset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
"ManagedUniversalRenderPipelineProbeAsset");
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
EXPECT_TRUE(plans[0].UsesGraphManagedSceneColor());
EXPECT_TRUE(
plans[0].IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_TRUE(plans[0].IsPostProcessStageValid());
ASSERT_TRUE(renderer.Render(plans));
EXPECT_EQ(commandList.drawCalls, 1u);
EXPECT_GT(device.createTextureCalls, 0u);
EXPECT_GT(device.createPipelineLayoutCalls, 0u);
EXPECT_GT(device.createPipelineStateCalls, 0u);
EXPECT_GT(device.createDescriptorPoolCalls, 0u);
engine->OnUpdate(0.016f);
int observedCreatePipelineCallCount = 0;
int observedSupportsMainSceneCallCount = 0;
int observedSupportsPostProcessCallCount = 0;
int observedRecordMainSceneCallCount = 0;
int observedRecordSceneCallCount = 0;
int observedRecordPostProcessCallCount = 0;
XCEngine::Math::Vector4 observedPostProcessScale = {};
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedSupportsMainSceneCallCount",
observedSupportsMainSceneCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedSupportsPostProcessCallCount",
observedSupportsPostProcessCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordMainSceneCallCount",
observedRecordMainSceneCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordSceneCallCount",
observedRecordSceneCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordPostProcessCallCount",
observedRecordPostProcessCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedPostProcessScale",
observedPostProcessScale));
EXPECT_EQ(observedCreatePipelineCallCount, 1);
EXPECT_GT(observedSupportsMainSceneCallCount, 0);
EXPECT_GT(observedSupportsPostProcessCallCount, 0);
EXPECT_EQ(observedRecordMainSceneCallCount, 1);
EXPECT_EQ(observedRecordSceneCallCount, 1);
EXPECT_EQ(observedRecordPostProcessCallCount, 1);
EXPECT_FLOAT_EQ(observedPostProcessScale.x, 1.11f);
EXPECT_FLOAT_EQ(observedPostProcessScale.y, 0.97f);
EXPECT_FLOAT_EQ(observedPostProcessScale.z, 0.93f);
EXPECT_FLOAT_EQ(observedPostProcessScale.w, 1.0f);
}
TEST_F(
MonoScriptRuntimeTest,
DefaultSceneRendererUsesScriptCoreUniversalRendererFeatureForPlannedPostProcessRender) {
Scene* runtimeScene =
CreateScene("ScriptCoreUniversalRendererFeatureSceneRendererScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ScriptCoreUniversalRendererFeatureSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ScriptCoreUniversalPostProcessRendererFeatureRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(64u, 64u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(
pipelineAsset->GetDescriptor().assemblyName,
"XCEngine.RenderPipelines.Universal");
EXPECT_EQ(pipelineAsset->GetDescriptor().namespaceName, "XCEngine.Rendering.Universal");
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
"UniversalRenderPipelineAsset");
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
EXPECT_TRUE(plans[0].UsesGraphManagedSceneColor());
EXPECT_TRUE(
plans[0].IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_TRUE(plans[0].IsPostProcessStageValid());
EXPECT_EQ(
plans[0].ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::PostProcess),
XCEngine::Rendering::CameraFrameColorSource::MainSceneColor);
ASSERT_TRUE(renderer.Render(plans));
EXPECT_EQ(commandList.drawCalls, 1u);
EXPECT_GT(device.createTextureCalls, 0u);
EXPECT_GT(device.createPipelineLayoutCalls, 0u);
EXPECT_GT(device.createPipelineStateCalls, 0u);
}
TEST_F(
MonoScriptRuntimeTest,
ScriptCoreUniversalRendererFeatureConfiguresCameraRequestPolicy) {
Scene* runtimeScene =
CreateScene("ScriptCoreUniversalRendererFeatureCameraRequestScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ScriptCoreUniversalCameraRequestSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ScriptCoreUniversalShadowlessRendererFeatureRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
GameObject* lightObject = runtimeScene->CreateGameObject("Light");
auto* light = lightObject->AddComponent<LightComponent>();
ASSERT_NE(light, nullptr);
light->SetLightType(LightType::Directional);
light->SetCastsShadows(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Rendering::CameraRenderRequest baselineRequest = {};
baselineRequest.scene = runtimeScene;
baselineRequest.camera = camera;
baselineRequest.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
XCEngine::Rendering::ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy(
baselineRequest,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
ASSERT_TRUE(baselineRequest.directionalShadow.IsValid());
XCEngine::Rendering::CameraRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(
pipelineAsset->GetDescriptor().assemblyName,
"XCEngine.RenderPipelines.Universal");
EXPECT_EQ(pipelineAsset->GetDescriptor().namespaceName, "XCEngine.Rendering.Universal");
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
"UniversalRenderPipelineAsset");
XCEngine::Rendering::CameraRenderRequest request = {};
request.scene = runtimeScene;
request.camera = camera;
request.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
pipelineAsset->ConfigureCameraRenderRequest(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
EXPECT_FALSE(request.directionalShadow.IsValid());
}
TEST_F(
MonoScriptRuntimeTest,
ScriptableRenderContextPublicApiSurfaceUsesDirectContextModel) {
Scene* runtimeScene =
CreateScene("ScriptableRenderContextApiSurfaceScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ScriptableRenderContextApiSurfaceSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ScriptableRenderContextApiSurfaceProbe");
ASSERT_NE(selectionScript, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasPublicContextRecordScene = false;
bool hasPublicContextRecordOpaqueScenePhase = false;
bool hasPublicContextRecordBeforeOpaqueInjection = false;
bool hasPublicContextRecordShaderVectorFullscreenPass = false;
bool hasPublicContextCameraData = false;
bool hasPublicContextLightingData = false;
bool hasPublicContextShadowData = false;
bool hasPublicContextEnvironmentData = false;
bool hasPublicContextFinalColorData = false;
bool hasPublicContextStageColorData = false;
bool hasPublicRequestContextHasDirectionalShadow = false;
bool hasPublicRequestContextClearDirectionalShadow = false;
bool hasUniversalContextRecordSceneExtension = false;
bool hasUniversalContextRecordOpaqueScenePhaseExtension = false;
bool hasUniversalContextRecordBeforeOpaqueInjectionExtension = false;
bool hasUniversalContextRecordShaderVectorFullscreenPassExtension = false;
bool hasUniversalRequestContextHasDirectionalShadowExtension = false;
bool hasUniversalRequestContextClearDirectionalShadowExtension = false;
bool hasPublicPipelineAssetConfigureCameraFramePlan = false;
bool hasPlanningContextType = false;
bool hasRendererFeatureConfigureCameraFramePlan = false;
bool hasRendererRecordingContextType = false;
bool hasRendererCameraRequestContextType = false;
bool hasRendererBackedRenderPipelineAssetType = false;
bool hasRendererBackedRenderPipelineType = false;
bool hasRendererDrivenRenderPipelineType = false;
bool hasPublicRendererSupportsStageRenderGraph = false;
bool hasPublicRendererRecordStageRenderGraph = false;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextRecordScene",
hasPublicContextRecordScene));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextRecordOpaqueScenePhase",
hasPublicContextRecordOpaqueScenePhase));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextRecordBeforeOpaqueInjection",
hasPublicContextRecordBeforeOpaqueInjection));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextRecordShaderVectorFullscreenPass",
hasPublicContextRecordShaderVectorFullscreenPass));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextCameraData",
hasPublicContextCameraData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextLightingData",
hasPublicContextLightingData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextShadowData",
hasPublicContextShadowData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextEnvironmentData",
hasPublicContextEnvironmentData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextFinalColorData",
hasPublicContextFinalColorData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicContextStageColorData",
hasPublicContextStageColorData));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicRequestContextHasDirectionalShadow",
hasPublicRequestContextHasDirectionalShadow));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicRequestContextClearDirectionalShadow",
hasPublicRequestContextClearDirectionalShadow));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalContextRecordSceneExtension",
hasUniversalContextRecordSceneExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalContextRecordOpaqueScenePhaseExtension",
hasUniversalContextRecordOpaqueScenePhaseExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalContextRecordBeforeOpaqueInjectionExtension",
hasUniversalContextRecordBeforeOpaqueInjectionExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalContextRecordShaderVectorFullscreenPassExtension",
hasUniversalContextRecordShaderVectorFullscreenPassExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalRequestContextHasDirectionalShadowExtension",
hasUniversalRequestContextHasDirectionalShadowExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasUniversalRequestContextClearDirectionalShadowExtension",
hasUniversalRequestContextClearDirectionalShadowExtension));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicPipelineAssetConfigureCameraFramePlan",
hasPublicPipelineAssetConfigureCameraFramePlan));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPlanningContextType",
hasPlanningContextType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererFeatureConfigureCameraFramePlan",
hasRendererFeatureConfigureCameraFramePlan));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererRecordingContextType",
hasRendererRecordingContextType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererCameraRequestContextType",
hasRendererCameraRequestContextType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererBackedRenderPipelineAssetType",
hasRendererBackedRenderPipelineAssetType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererBackedRenderPipelineType",
hasRendererBackedRenderPipelineType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasRendererDrivenRenderPipelineType",
hasRendererDrivenRenderPipelineType));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicRendererSupportsStageRenderGraph",
hasPublicRendererSupportsStageRenderGraph));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"HasPublicRendererRecordStageRenderGraph",
hasPublicRendererRecordStageRenderGraph));
EXPECT_FALSE(hasPublicContextRecordScene);
EXPECT_FALSE(hasPublicContextRecordOpaqueScenePhase);
EXPECT_FALSE(hasPublicContextRecordBeforeOpaqueInjection);
EXPECT_FALSE(hasPublicContextRecordShaderVectorFullscreenPass);
EXPECT_FALSE(hasPublicContextCameraData);
EXPECT_FALSE(hasPublicContextLightingData);
EXPECT_FALSE(hasPublicContextShadowData);
EXPECT_FALSE(hasPublicContextEnvironmentData);
EXPECT_FALSE(hasPublicContextFinalColorData);
EXPECT_FALSE(hasPublicContextStageColorData);
EXPECT_FALSE(hasPublicRequestContextHasDirectionalShadow);
EXPECT_FALSE(hasPublicRequestContextClearDirectionalShadow);
EXPECT_FALSE(hasUniversalContextRecordSceneExtension);
EXPECT_FALSE(hasUniversalContextRecordOpaqueScenePhaseExtension);
EXPECT_FALSE(hasUniversalContextRecordBeforeOpaqueInjectionExtension);
EXPECT_FALSE(hasUniversalContextRecordShaderVectorFullscreenPassExtension);
EXPECT_FALSE(hasUniversalRequestContextHasDirectionalShadowExtension);
EXPECT_FALSE(hasUniversalRequestContextClearDirectionalShadowExtension);
EXPECT_FALSE(hasPublicPipelineAssetConfigureCameraFramePlan);
EXPECT_FALSE(hasPlanningContextType);
EXPECT_FALSE(hasRendererFeatureConfigureCameraFramePlan);
EXPECT_FALSE(hasRendererRecordingContextType);
EXPECT_FALSE(hasRendererCameraRequestContextType);
EXPECT_FALSE(hasRendererBackedRenderPipelineAssetType);
EXPECT_FALSE(hasRendererBackedRenderPipelineType);
EXPECT_FALSE(hasRendererDrivenRenderPipelineType);
EXPECT_FALSE(hasPublicRendererSupportsStageRenderGraph);
EXPECT_FALSE(hasPublicRendererRecordStageRenderGraph);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesCameraDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextCameraDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject("ManagedRenderContextCameraDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextCameraDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
camera->SetProjectionType(CameraProjectionType::Orthographic);
camera->SetFieldOfView(75.0f);
camera->SetOrthographicSize(7.5f);
camera->SetNearClipPlane(0.25f);
camera->SetFarClipPlane(512.0f);
camera->SetClearMode(CameraClearMode::DepthOnly);
camera->SetClearColor(
XCEngine::Math::Color(0.12f, 0.34f, 0.56f, 0.78f));
cameraObject->GetTransform()->SetLocalPosition(
XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(160u, 90u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(pipelineAsset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(pipelineAsset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
"ManagedRenderContextCameraDataProbeAsset");
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
const XCEngine::Rendering::RenderCameraData expectedCameraData =
XCEngine::Rendering::BuildRenderCameraData(
*camera,
160u,
90u);
XCEngine::Math::Vector4 observedViewRow0 = {};
XCEngine::Math::Vector4 observedViewRow1 = {};
XCEngine::Math::Vector4 observedViewRow2 = {};
XCEngine::Math::Vector4 observedViewRow3 = {};
XCEngine::Math::Vector4 observedProjectionRow0 = {};
XCEngine::Math::Vector4 observedProjectionRow1 = {};
XCEngine::Math::Vector4 observedProjectionRow2 = {};
XCEngine::Math::Vector4 observedProjectionRow3 = {};
XCEngine::Math::Vector4 observedViewProjectionRow0 = {};
XCEngine::Math::Vector4 observedViewProjectionRow1 = {};
XCEngine::Math::Vector4 observedViewProjectionRow2 = {};
XCEngine::Math::Vector4 observedViewProjectionRow3 = {};
XCEngine::Math::Vector3 observedWorldPosition = {};
XCEngine::Math::Vector4 observedClearColor = {};
int observedClearFlags = 0;
bool observedPerspectiveProjection = true;
float observedVerticalFovRadians = 0.0f;
float observedOrthographicSize = 0.0f;
float observedAspectRatio = 0.0f;
float observedNearClipPlane = 0.0f;
float observedFarClipPlane = 0.0f;
int observedViewportWidth = 0;
int observedViewportHeight = 0;
int observedRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewRow0",
observedViewRow0));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewRow1",
observedViewRow1));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewRow2",
observedViewRow2));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewRow3",
observedViewRow3));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedProjectionRow0",
observedProjectionRow0));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedProjectionRow1",
observedProjectionRow1));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedProjectionRow2",
observedProjectionRow2));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedProjectionRow3",
observedProjectionRow3));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewProjectionRow0",
observedViewProjectionRow0));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewProjectionRow1",
observedViewProjectionRow1));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewProjectionRow2",
observedViewProjectionRow2));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewProjectionRow3",
observedViewProjectionRow3));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedWorldPosition",
observedWorldPosition));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedClearColor",
observedClearColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedClearFlags",
observedClearFlags));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedPerspectiveProjection",
observedPerspectiveProjection));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedVerticalFovRadians",
observedVerticalFovRadians));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedOrthographicSize",
observedOrthographicSize));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedAspectRatio",
observedAspectRatio));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedNearClipPlane",
observedNearClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFarClipPlane",
observedFarClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewportWidth",
observedViewportWidth));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedViewportHeight",
observedViewportHeight));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordCallCount",
observedRecordCallCount));
const auto expectObservedRowMatchesMatrix = [](
const XCEngine::Math::Vector4& observedRow,
const XCEngine::Math::Matrix4x4& expectedMatrix,
int rowIndex) {
ExpectVector4Near(
observedRow,
XCEngine::Math::Vector4(
expectedMatrix.m[rowIndex][0],
expectedMatrix.m[rowIndex][1],
expectedMatrix.m[rowIndex][2],
expectedMatrix.m[rowIndex][3]));
};
EXPECT_EQ(observedRecordCallCount, 1);
expectObservedRowMatchesMatrix(
observedViewRow0,
expectedCameraData.view,
0);
expectObservedRowMatchesMatrix(
observedViewRow1,
expectedCameraData.view,
1);
expectObservedRowMatchesMatrix(
observedViewRow2,
expectedCameraData.view,
2);
expectObservedRowMatchesMatrix(
observedViewRow3,
expectedCameraData.view,
3);
expectObservedRowMatchesMatrix(
observedProjectionRow0,
expectedCameraData.projection,
0);
expectObservedRowMatchesMatrix(
observedProjectionRow1,
expectedCameraData.projection,
1);
expectObservedRowMatchesMatrix(
observedProjectionRow2,
expectedCameraData.projection,
2);
expectObservedRowMatchesMatrix(
observedProjectionRow3,
expectedCameraData.projection,
3);
expectObservedRowMatchesMatrix(
observedViewProjectionRow0,
expectedCameraData.viewProjection,
0);
expectObservedRowMatchesMatrix(
observedViewProjectionRow1,
expectedCameraData.viewProjection,
1);
expectObservedRowMatchesMatrix(
observedViewProjectionRow2,
expectedCameraData.viewProjection,
2);
expectObservedRowMatchesMatrix(
observedViewProjectionRow3,
expectedCameraData.viewProjection,
3);
EXPECT_FLOAT_EQ(observedWorldPosition.x, 2.0f);
EXPECT_FLOAT_EQ(observedWorldPosition.y, 4.0f);
EXPECT_FLOAT_EQ(observedWorldPosition.z, 6.0f);
EXPECT_FLOAT_EQ(observedClearColor.x, 0.12f);
EXPECT_FLOAT_EQ(observedClearColor.y, 0.34f);
EXPECT_FLOAT_EQ(observedClearColor.z, 0.56f);
EXPECT_FLOAT_EQ(observedClearColor.w, 0.78f);
EXPECT_EQ(
observedClearFlags,
static_cast<int>(XCEngine::Rendering::RenderClearFlags::Depth));
EXPECT_FALSE(observedPerspectiveProjection);
EXPECT_FLOAT_EQ(
observedVerticalFovRadians,
75.0f * XCEngine::Math::DEG_TO_RAD);
EXPECT_FLOAT_EQ(observedOrthographicSize, 7.5f);
EXPECT_FLOAT_EQ(observedAspectRatio, 160.0f / 90.0f);
EXPECT_FLOAT_EQ(observedNearClipPlane, 0.25f);
EXPECT_FLOAT_EQ(observedFarClipPlane, 512.0f);
EXPECT_EQ(observedViewportWidth, 160);
EXPECT_EQ(observedViewportHeight, 90);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesLightingDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextLightingDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedRenderContextLightingDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextLightingDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
GameObject* directionalLightObject =
runtimeScene->CreateGameObject("DirectionalLight");
auto* directionalLight =
directionalLightObject->AddComponent<LightComponent>();
ASSERT_NE(directionalLight, nullptr);
directionalLight->SetLightType(LightType::Directional);
directionalLight->SetColor(
XCEngine::Math::Color(0.25f, 0.50f, 0.75f, 1.0f));
directionalLight->SetIntensity(2.75f);
directionalLight->SetCastsShadows(true);
directionalLightObject->GetTransform()->SetLocalEulerAngles(
XCEngine::Math::Vector3(25.0f, -40.0f, 0.0f));
GameObject* pointLightObject =
runtimeScene->CreateGameObject("PointLight");
auto* pointLight =
pointLightObject->AddComponent<LightComponent>();
ASSERT_NE(pointLight, nullptr);
pointLight->SetLightType(LightType::Point);
pointLight->SetColor(
XCEngine::Math::Color(0.9f, 0.3f, 0.2f, 1.0f));
pointLight->SetIntensity(1.25f);
pointLight->SetRange(15.0f);
pointLight->SetCastsShadows(false);
pointLightObject->GetTransform()->SetLocalPosition(
XCEngine::Math::Vector3(3.0f, 2.0f, -1.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(192u, 108u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const auto* pipelineAsset =
dynamic_cast<
const XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(pipelineAsset, nullptr);
EXPECT_EQ(pipelineAsset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(pipelineAsset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(
pipelineAsset->GetDescriptor().className,
"ManagedRenderContextCameraDataProbeAsset");
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
XCEngine::Rendering::RenderSceneExtractor extractor;
const XCEngine::Rendering::RenderSceneData expectedSceneData =
extractor.ExtractForCamera(
*runtimeScene,
*camera,
192u,
108u);
ASSERT_TRUE(expectedSceneData.HasCamera());
bool observedMainDirectionalLightEnabled = false;
bool observedMainDirectionalLightCastsShadows = false;
XCEngine::Math::Vector3 observedMainDirectionalLightDirection = {};
XCEngine::Math::Vector4 observedMainDirectionalLightColor = {};
float observedMainDirectionalLightIntensity = 0.0f;
bool observedHasMainDirectionalShadow = false;
int observedAdditionalLightCount = 0;
int observedRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainDirectionalLightEnabled",
observedMainDirectionalLightEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainDirectionalLightCastsShadows",
observedMainDirectionalLightCastsShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainDirectionalLightDirection",
observedMainDirectionalLightDirection));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainDirectionalLightColor",
observedMainDirectionalLightColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainDirectionalLightIntensity",
observedMainDirectionalLightIntensity));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedHasMainDirectionalShadow",
observedHasMainDirectionalShadow));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedAdditionalLightCount",
observedAdditionalLightCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordCallCount",
observedRecordCallCount));
EXPECT_EQ(observedRecordCallCount, 1);
EXPECT_EQ(
observedMainDirectionalLightEnabled,
expectedSceneData.lighting.mainDirectionalLight.enabled);
EXPECT_EQ(
observedMainDirectionalLightCastsShadows,
expectedSceneData.lighting.mainDirectionalLight.castsShadows);
ExpectVector3Near(
observedMainDirectionalLightDirection,
expectedSceneData.lighting.mainDirectionalLight.direction);
ExpectVector4Near(
observedMainDirectionalLightColor,
XCEngine::Math::Vector4(
expectedSceneData.lighting.mainDirectionalLight.color.r,
expectedSceneData.lighting.mainDirectionalLight.color.g,
expectedSceneData.lighting.mainDirectionalLight.color.b,
expectedSceneData.lighting.mainDirectionalLight.color.a));
EXPECT_FLOAT_EQ(
observedMainDirectionalLightIntensity,
expectedSceneData.lighting.mainDirectionalLight.intensity);
EXPECT_EQ(
observedHasMainDirectionalShadow,
plans[0].directionalShadow.IsValid());
EXPECT_EQ(
observedAdditionalLightCount,
static_cast<int>(expectedSceneData.lighting.additionalLightCount));
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesShadowDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextShadowDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedRenderContextShadowDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextShadowDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
GameObject* directionalLightObject =
runtimeScene->CreateGameObject("DirectionalLight");
auto* directionalLight =
directionalLightObject->AddComponent<LightComponent>();
ASSERT_NE(directionalLight, nullptr);
directionalLight->SetLightType(LightType::Directional);
directionalLight->SetColor(
XCEngine::Math::Color(0.25f, 0.50f, 0.75f, 1.0f));
directionalLight->SetIntensity(2.75f);
directionalLight->SetCastsShadows(true);
directionalLightObject->GetTransform()->SetLocalEulerAngles(
XCEngine::Math::Vector3(25.0f, -40.0f, 0.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(192u, 108u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(plans[0].directionalShadow.IsValid());
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
bool observedShadowEnabled = false;
XCEngine::Math::Vector4 observedShadowViewProjectionRow0 = {};
XCEngine::Math::Vector4 observedShadowViewProjectionRow1 = {};
XCEngine::Math::Vector4 observedShadowViewProjectionRow2 = {};
XCEngine::Math::Vector4 observedShadowViewProjectionRow3 = {};
float observedShadowOrthographicHalfExtent = 0.0f;
float observedShadowNearClipPlane = 0.0f;
float observedShadowFarClipPlane = 0.0f;
int observedShadowMapWidth = 0;
int observedShadowMapHeight = 0;
float observedShadowWorldTexelSize = 0.0f;
float observedShadowReceiverDepthBias = 0.0f;
float observedShadowNormalBiasScale = 0.0f;
float observedShadowStrength = 0.0f;
float observedShadowDepthBiasFactor = 0.0f;
int observedShadowDepthBiasUnits = 0;
int observedRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowEnabled",
observedShadowEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowViewProjectionRow0",
observedShadowViewProjectionRow0));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowViewProjectionRow1",
observedShadowViewProjectionRow1));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowViewProjectionRow2",
observedShadowViewProjectionRow2));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowViewProjectionRow3",
observedShadowViewProjectionRow3));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowOrthographicHalfExtent",
observedShadowOrthographicHalfExtent));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowNearClipPlane",
observedShadowNearClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowFarClipPlane",
observedShadowFarClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowMapWidth",
observedShadowMapWidth));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowMapHeight",
observedShadowMapHeight));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowWorldTexelSize",
observedShadowWorldTexelSize));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowReceiverDepthBias",
observedShadowReceiverDepthBias));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowNormalBiasScale",
observedShadowNormalBiasScale));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowStrength",
observedShadowStrength));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowDepthBiasFactor",
observedShadowDepthBiasFactor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedShadowDepthBiasUnits",
observedShadowDepthBiasUnits));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordCallCount",
observedRecordCallCount));
const XCEngine::Rendering::DirectionalShadowRenderPlan&
expectedShadowPlan = plans[0].directionalShadow;
const auto expectObservedRowMatchesMatrix = [](
const XCEngine::Math::Vector4& observedRow,
const XCEngine::Math::Matrix4x4& expectedMatrix,
int rowIndex) {
ExpectVector4Near(
observedRow,
XCEngine::Math::Vector4(
expectedMatrix.m[rowIndex][0],
expectedMatrix.m[rowIndex][1],
expectedMatrix.m[rowIndex][2],
expectedMatrix.m[rowIndex][3]));
};
EXPECT_EQ(observedRecordCallCount, 1);
EXPECT_EQ(observedShadowEnabled, expectedShadowPlan.enabled);
expectObservedRowMatchesMatrix(
observedShadowViewProjectionRow0,
expectedShadowPlan.cameraData.viewProjection,
0);
expectObservedRowMatchesMatrix(
observedShadowViewProjectionRow1,
expectedShadowPlan.cameraData.viewProjection,
1);
expectObservedRowMatchesMatrix(
observedShadowViewProjectionRow2,
expectedShadowPlan.cameraData.viewProjection,
2);
expectObservedRowMatchesMatrix(
observedShadowViewProjectionRow3,
expectedShadowPlan.cameraData.viewProjection,
3);
EXPECT_FLOAT_EQ(
observedShadowOrthographicHalfExtent,
expectedShadowPlan.orthographicHalfExtent);
EXPECT_FLOAT_EQ(
observedShadowNearClipPlane,
expectedShadowPlan.nearClipPlane);
EXPECT_FLOAT_EQ(
observedShadowFarClipPlane,
expectedShadowPlan.farClipPlane);
EXPECT_EQ(observedShadowMapWidth, static_cast<int>(expectedShadowPlan.mapWidth));
EXPECT_EQ(observedShadowMapHeight, static_cast<int>(expectedShadowPlan.mapHeight));
EXPECT_FLOAT_EQ(
observedShadowWorldTexelSize,
expectedShadowPlan.texelWorldSize);
EXPECT_FLOAT_EQ(
observedShadowReceiverDepthBias,
expectedShadowPlan.sampling.receiverDepthBias);
EXPECT_FLOAT_EQ(
observedShadowNormalBiasScale,
expectedShadowPlan.sampling.normalBiasScale);
EXPECT_FLOAT_EQ(
observedShadowStrength,
expectedShadowPlan.sampling.shadowStrength);
EXPECT_FLOAT_EQ(
observedShadowDepthBiasFactor,
expectedShadowPlan.casterBias.depthBiasFactor);
EXPECT_EQ(
observedShadowDepthBiasUnits,
expectedShadowPlan.casterBias.depthBiasUnits);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesEnvironmentDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextEnvironmentDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedRenderContextEnvironmentDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextEnvironmentDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
camera->SetProjectionType(CameraProjectionType::Perspective);
camera->SetClearMode(CameraClearMode::ColorAndDepth);
camera->SetSkyboxEnabled(true);
camera->SetSkyboxTopColor(
XCEngine::Math::Color(0.11f, 0.22f, 0.33f, 1.0f));
camera->SetSkyboxHorizonColor(
XCEngine::Math::Color(0.44f, 0.55f, 0.66f, 1.0f));
camera->SetSkyboxBottomColor(
XCEngine::Math::Color(0.77f, 0.88f, 0.99f, 1.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(200u, 120u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
int observedEnvironmentMode = 0;
XCEngine::Math::Vector4 observedSkyboxTopColor = {};
XCEngine::Math::Vector4 observedSkyboxHorizonColor = {};
XCEngine::Math::Vector4 observedSkyboxBottomColor = {};
int observedRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedEnvironmentMode",
observedEnvironmentMode));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedSkyboxTopColor",
observedSkyboxTopColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedSkyboxHorizonColor",
observedSkyboxHorizonColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedSkyboxBottomColor",
observedSkyboxBottomColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordCallCount",
observedRecordCallCount));
EXPECT_EQ(observedRecordCallCount, 1);
EXPECT_EQ(
observedEnvironmentMode,
static_cast<int>(
XCEngine::Rendering::RenderEnvironmentMode::ProceduralSkybox));
ExpectVector4Near(
observedSkyboxTopColor,
XCEngine::Math::Vector4(0.11f, 0.22f, 0.33f, 1.0f));
ExpectVector4Near(
observedSkyboxHorizonColor,
XCEngine::Math::Vector4(0.44f, 0.55f, 0.66f, 1.0f));
ExpectVector4Near(
observedSkyboxBottomColor,
XCEngine::Math::Vector4(0.77f, 0.88f, 0.99f, 1.0f));
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesFinalColorDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextFinalColorDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedRenderContextFinalColorDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextFinalColorDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
XCEngine::Rendering::FinalColorOverrideSettings finalColorOverrides = {};
finalColorOverrides.overrideExposureValue = true;
finalColorOverrides.exposureValue = 2.5f;
finalColorOverrides.overrideToneMappingMode = true;
finalColorOverrides.toneMappingMode =
XCEngine::Rendering::FinalColorToneMappingMode::Neutral;
finalColorOverrides.overrideFinalColorScale = true;
finalColorOverrides.finalColorScale =
XCEngine::Math::Vector4(0.80f, 0.95f, 1.05f, 1.0f);
camera->SetFinalColorOverrides(finalColorOverrides);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(160u, 90u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
const XCEngine::Rendering::ResolvedFinalColorPolicy&
expectedFinalColorPolicy = plans[0].finalColorPolicy;
int observedFinalColorOutputTransferMode = 0;
int observedFinalColorExposureMode = 0;
float observedFinalColorExposureValue = 0.0f;
int observedFinalColorToneMappingMode = 0;
XCEngine::Math::Vector4 observedFinalColorScale = {};
bool observedFinalColorHasPipelineDefaults = false;
bool observedFinalColorHasCameraOverrides = false;
bool observedFinalColorRequiresProcessing = false;
int observedRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorOutputTransferMode",
observedFinalColorOutputTransferMode));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorExposureMode",
observedFinalColorExposureMode));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorExposureValue",
observedFinalColorExposureValue));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorToneMappingMode",
observedFinalColorToneMappingMode));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorScale",
observedFinalColorScale));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorHasPipelineDefaults",
observedFinalColorHasPipelineDefaults));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorHasCameraOverrides",
observedFinalColorHasCameraOverrides));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalColorRequiresProcessing",
observedFinalColorRequiresProcessing));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedRecordCallCount",
observedRecordCallCount));
EXPECT_EQ(observedRecordCallCount, 1);
EXPECT_EQ(
observedFinalColorOutputTransferMode,
static_cast<int>(expectedFinalColorPolicy.outputTransferMode));
EXPECT_EQ(
observedFinalColorExposureMode,
static_cast<int>(expectedFinalColorPolicy.exposureMode));
EXPECT_FLOAT_EQ(
observedFinalColorExposureValue,
expectedFinalColorPolicy.exposureValue);
EXPECT_EQ(
observedFinalColorToneMappingMode,
static_cast<int>(expectedFinalColorPolicy.toneMappingMode));
ExpectVector4Near(
observedFinalColorScale,
expectedFinalColorPolicy.finalColorScale);
EXPECT_EQ(
observedFinalColorHasPipelineDefaults,
expectedFinalColorPolicy.hasPipelineDefaults);
EXPECT_EQ(
observedFinalColorHasCameraOverrides,
expectedFinalColorPolicy.hasCameraOverrides);
EXPECT_EQ(
observedFinalColorRequiresProcessing,
expectedFinalColorPolicy.RequiresProcessing());
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderContextExposesStageColorDataThroughRenderingData) {
Scene* runtimeScene =
CreateScene("ManagedRenderContextStageColorDataScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedRenderContextStageColorDataSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedRenderContextStageColorDataRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
TestRenderDevice device;
TestRenderCommandList commandList;
TestRenderCommandQueue commandQueue;
TestRenderResourceView colorView(
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::R8G8B8A8_UNorm);
TestRenderResourceView depthView(
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::ResourceViewDimension::Texture2D,
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderContext context =
CreateRenderContext(
device,
commandList,
commandQueue);
XCEngine::Rendering::RenderSurface surface(160u, 90u);
surface.SetColorAttachment(&colorView);
surface.SetDepthAttachment(&depthView);
XCEngine::Rendering::SceneRenderer renderer;
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
renderer.BuildFramePlans(
*runtimeScene,
nullptr,
context,
surface);
ASSERT_EQ(plans.size(), 1u);
EXPECT_TRUE(
plans[0].IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_TRUE(
plans[0].IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::FinalOutput));
ASSERT_TRUE(renderer.Render(plans));
engine->OnUpdate(0.016f);
int observedMainSceneSource = 0;
bool observedMainSceneUsesGraphManagedOutputColor = false;
int observedPostProcessSource = 0;
bool observedPostProcessUsesGraphManagedOutputColor = false;
int observedFinalOutputSource = 0;
bool observedFinalOutputUsesGraphManagedOutputColor = false;
int observedMainSceneRecordCallCount = 0;
int observedPostProcessRecordCallCount = 0;
int observedFinalOutputRecordCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainSceneSource",
observedMainSceneSource));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainSceneUsesGraphManagedOutputColor",
observedMainSceneUsesGraphManagedOutputColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedPostProcessSource",
observedPostProcessSource));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedPostProcessUsesGraphManagedOutputColor",
observedPostProcessUsesGraphManagedOutputColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalOutputSource",
observedFinalOutputSource));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalOutputUsesGraphManagedOutputColor",
observedFinalOutputUsesGraphManagedOutputColor));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedMainSceneRecordCallCount",
observedMainSceneRecordCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedPostProcessRecordCallCount",
observedPostProcessRecordCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
selectionScript,
"ObservedFinalOutputRecordCallCount",
observedFinalOutputRecordCallCount));
EXPECT_EQ(observedMainSceneRecordCallCount, 1);
EXPECT_EQ(observedPostProcessRecordCallCount, 1);
EXPECT_EQ(observedFinalOutputRecordCallCount, 1);
EXPECT_EQ(
observedMainSceneSource,
static_cast<int>(
plans[0].ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::MainScene)));
EXPECT_EQ(
observedMainSceneUsesGraphManagedOutputColor,
plans[0].UsesGraphManagedOutputColor(
XCEngine::Rendering::CameraFrameStage::MainScene));
EXPECT_EQ(
observedPostProcessSource,
static_cast<int>(
plans[0].ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::PostProcess)));
EXPECT_EQ(
observedPostProcessUsesGraphManagedOutputColor,
plans[0].UsesGraphManagedOutputColor(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_EQ(
observedFinalOutputSource,
static_cast<int>(
plans[0].ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::FinalOutput)));
EXPECT_EQ(
observedFinalOutputUsesGraphManagedOutputColor,
plans[0].UsesGraphManagedOutputColor(
XCEngine::Rendering::CameraFrameStage::FinalOutput));
}
TEST_F(
MonoScriptRuntimeTest,
RegistersManagedRenderPipelineBridgeThatCreatesManagedStageRecorders) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
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 = {};
EXPECT_TRUE(recorder->Initialize(context));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
EXPECT_FALSE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::PostProcess));
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeSharesManagedPipelineInstanceAcrossStageRecorders) {
Scene* runtimeScene =
CreateScene("ManagedPipelineCreateCountObservationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ManagedPipelineCreateCountObservationProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ManagedRenderPipelineCreateCountObservationProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const XCEngine::Rendering::RenderContext context = {};
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
firstRecorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(firstRecorder, nullptr);
ASSERT_TRUE(firstRecorder->Initialize(context));
EXPECT_TRUE(
firstRecorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
secondRecorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(secondRecorder, nullptr);
ASSERT_TRUE(secondRecorder->Initialize(context));
EXPECT_TRUE(
secondRecorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
firstRecorder->Shutdown();
EXPECT_TRUE(
secondRecorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
engine->OnUpdate(0.016f);
int observedCreatePipelineCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_EQ(observedCreatePipelineCallCount, 1);
secondRecorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeInvokesManagedPipelineDisposeBeforeAssetRuntimeRelease) {
Scene* runtimeScene =
CreateScene("ManagedLifecycleDisposeObservationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ManagedLifecycleObservationProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ManagedLifecycleObservationProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
{
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedPipelineDisposeProbeAsset"
};
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));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
recorder->Shutdown();
}
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreatePipelineCallCount = 0;
int observedCreateRendererCallCount = 0;
int observedCreateFeatureCallCount = 0;
int observedReleaseRendererDataRuntimeResourcesCallCount = 0;
int observedDisposePipelineCallCount = 0;
int observedReleaseAssetRuntimeResourcesCallCount = 0;
int observedDisposeRendererCallCount = 0;
int observedDisposeFeatureCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateFeatureCallCount",
observedCreateFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseRendererDataRuntimeResourcesCallCount",
observedReleaseRendererDataRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposePipelineCallCount",
observedDisposePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseAssetRuntimeResourcesCallCount",
observedReleaseAssetRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeRendererCallCount",
observedDisposeRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeFeatureCallCount",
observedDisposeFeatureCallCount));
EXPECT_EQ(observedCreatePipelineCallCount, 1);
EXPECT_EQ(observedCreateRendererCallCount, 0);
EXPECT_EQ(observedCreateFeatureCallCount, 0);
EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 0);
EXPECT_EQ(observedDisposePipelineCallCount, 1);
EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 0);
EXPECT_EQ(observedDisposeRendererCallCount, 0);
EXPECT_EQ(observedDisposeFeatureCallCount, 0);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeReleasesUniversalRendererCachesOnAssetRuntimeRelease) {
Scene* runtimeScene =
CreateScene("ManagedUniversalLifecycleObservationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ManagedUniversalLifecycleSelectionProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ManagedUniversalLifecycleRuntimeSelectionProbe");
ASSERT_NE(script, 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, "Gameplay");
EXPECT_EQ(descriptor.className, "ManagedUniversalLifecycleProbeAsset");
{
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));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
recorder->Shutdown();
}
{
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));
EXPECT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
recorder->Shutdown();
}
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreatePipelineCallCount = 0;
int observedCreateRendererCallCount = 0;
int observedCreateFeatureCallCount = 0;
int observedReleaseRendererDataRuntimeResourcesCallCount = 0;
int observedDisposePipelineCallCount = 0;
int observedReleaseAssetRuntimeResourcesCallCount = 0;
int observedDisposeRendererCallCount = 0;
int observedDisposeFeatureCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateFeatureCallCount",
observedCreateFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseRendererDataRuntimeResourcesCallCount",
observedReleaseRendererDataRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposePipelineCallCount",
observedDisposePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseAssetRuntimeResourcesCallCount",
observedReleaseAssetRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeRendererCallCount",
observedDisposeRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeFeatureCallCount",
observedDisposeFeatureCallCount));
EXPECT_EQ(observedCreatePipelineCallCount, 2);
EXPECT_EQ(observedCreateRendererCallCount, 2);
EXPECT_EQ(observedCreateFeatureCallCount, 2);
EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 2);
EXPECT_EQ(observedDisposePipelineCallCount, 0);
EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 2);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeReleasesUniversalFeatureCachesWithoutPipelineCreation) {
Scene* runtimeScene =
CreateScene("ManagedUniversalLifecycleRequestObservationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ManagedUniversalLifecycleSelectionProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ManagedUniversalLifecycleRuntimeSelectionProbe");
ASSERT_NE(script, 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, "Gameplay");
EXPECT_EQ(descriptor.className, "ManagedUniversalLifecycleProbeAsset");
{
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);
XCEngine::Rendering::CameraRenderRequest request = {};
assetRuntime->ConfigureCameraRenderRequest(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
}
{
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);
XCEngine::Rendering::CameraRenderRequest request = {};
assetRuntime->ConfigureCameraRenderRequest(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
}
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
int observedCreatePipelineCallCount = 0;
int observedCreateRendererCallCount = 0;
int observedCreateFeatureCallCount = 0;
int observedReleaseRendererDataRuntimeResourcesCallCount = 0;
int observedDisposePipelineCallCount = 0;
int observedReleaseAssetRuntimeResourcesCallCount = 0;
int observedDisposeRendererCallCount = 0;
int observedDisposeFeatureCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreatePipelineCallCount",
observedCreatePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateFeatureCallCount",
observedCreateFeatureCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseRendererDataRuntimeResourcesCallCount",
observedReleaseRendererDataRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposePipelineCallCount",
observedDisposePipelineCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedReleaseAssetRuntimeResourcesCallCount",
observedReleaseAssetRuntimeResourcesCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeRendererCallCount",
observedDisposeRendererCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedDisposeFeatureCallCount",
observedDisposeFeatureCallCount));
EXPECT_EQ(observedCreatePipelineCallCount, 0);
EXPECT_EQ(observedCreateRendererCallCount, 0);
EXPECT_EQ(observedCreateFeatureCallCount, 2);
EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 2);
EXPECT_EQ(observedDisposePipelineCallCount, 0);
EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 2);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeRuntimeExposesBuiltinForwardRendererAsset) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(pipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
pipeline.get()),
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ScriptCoreUniversalRenderPipelineAssetExposesBuiltinForwardRendererAsset) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"XCEngine.RenderPipelines.Universal",
"XCEngine.Rendering.Universal",
"UniversalRenderPipelineAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(pipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
pipeline.get()),
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeUsesDefaultRendererSelectionForNativeBackendAsset) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedDefaultRendererSelectionProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(pipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
pipeline.get()),
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeFallsBackToFirstRendererWhenDefaultRendererIndexIsInvalid) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedInvalidDefaultRendererSelectionProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(pipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
pipeline.get()),
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeFallsBackToDefaultSceneRecorderWhenBackendKeyIsUnknown) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedUnknownBackendRenderPipelineProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
EXPECT_EQ(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));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
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("ManagedUnknownBackendColor", colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture("ManagedUnknownBackendDepth", 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,
"ManagedUnknownBackendMainScene",
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(), 3u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ManagedUnknownBackendMainScene.Opaque");
EXPECT_STREQ(
compiledGraph.GetPassName(1).CStr(),
"ManagedUnknownBackendMainScene.Skybox");
EXPECT_STREQ(
compiledGraph.GetPassName(2).CStr(),
"ManagedUnknownBackendMainScene.Transparent");
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeReusesRendererInstanceAcrossManagedPipelineCreations) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRendererReuseProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const XCEngine::Rendering::RenderContext context = {};
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
firstRecorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(firstRecorder, nullptr);
ASSERT_TRUE(firstRecorder->Initialize(context));
ASSERT_TRUE(
firstRecorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
secondRecorder = assetRuntime->CreateStageRecorder();
ASSERT_NE(secondRecorder, nullptr);
ASSERT_TRUE(secondRecorder->Initialize(context));
ASSERT_TRUE(
secondRecorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
Scene* runtimeScene =
CreateScene("ManagedRendererReuseObservationScene");
GameObject* scriptObject =
runtimeScene->CreateGameObject("ManagedRendererReuseObservationProbe");
ScriptComponent* script =
AddScript(
scriptObject,
"Gameplay",
"ManagedRendererReuseObservationProbe");
ASSERT_NE(script, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int observedCreateRendererCallCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(
script,
"ObservedCreateRendererCallCount",
observedCreateRendererCallCount));
EXPECT_EQ(observedCreateRendererCallCount, 1);
firstRecorder->Shutdown();
secondRecorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
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 renderContext = {};
ASSERT_TRUE(recorder->Initialize(renderContext));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
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("ManagedMainSceneColor", colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture("ManagedMainSceneDepth", 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,
"ManagedMainScene",
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(), 3u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ManagedMainScene.Opaque");
EXPECT_STREQ(
compiledGraph.GetPassName(1).CStr(),
"ManagedMainScene.Skybox");
EXPECT_STREQ(
compiledGraph.GetPassName(2).CStr(),
"ManagedMainScene.Transparent");
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedStageRecorderRecordsShaderVectorPostProcessThroughScriptableRenderContext) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedPostProcessRenderPipelineProbeAsset"
};
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 renderContext = {};
ASSERT_TRUE(recorder->Initialize(renderContext));
EXPECT_FALSE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
ASSERT_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(
"ManagedPostProcessSource",
colorDesc,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(801),
{});
const XCEngine::Rendering::RenderGraphTextureHandle outputColor =
graphBuilder.CreateTransientTexture("ManagedPostProcessOutput", 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,
"ManagedPostProcess",
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(), 2u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ManagedPostProcess.Pass0");
EXPECT_STREQ(
compiledGraph.GetPassName(1).CStr(),
"ManagedPostProcess.Pass1");
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineAssetPlansFullscreenStagesFromPipelineStageSupport) {
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedPlannedFullscreenRenderPipelineProbeAsset"
};
auto asset =
std::make_shared<
XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset>(
descriptor);
XCEngine::Rendering::CameraRenderRequest request = {};
request.context = {};
request.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
request.surface.SetColorAttachment(
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(1));
request.surface.SetDepthAttachment(
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
XCEngine::Rendering::RenderPipelineHost host(asset);
const std::vector<XCEngine::Rendering::CameraFramePlan> plans =
host.BuildFramePlans({ request });
ASSERT_EQ(plans.size(), 1u);
const XCEngine::Rendering::CameraFramePlan& plan = plans[0];
EXPECT_TRUE(
plan.IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_TRUE(
plan.IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::FinalOutput));
EXPECT_EQ(plan.postProcess.passes, nullptr);
EXPECT_EQ(plan.finalOutput.passes, nullptr);
EXPECT_TRUE(plan.UsesGraphManagedSceneColor());
EXPECT_TRUE(
plan.UsesGraphManagedOutputColor(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_EQ(
plan.ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::PostProcess),
XCEngine::Rendering::CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
plan.ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::FinalOutput),
XCEngine::Rendering::CameraFrameColorSource::PostProcessColor);
EXPECT_TRUE(plan.IsPostProcessStageValid());
EXPECT_TRUE(plan.IsFinalOutputStageValid());
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineAssetConfiguresCameraRequestsThroughRequestContext) {
Scene* runtimeScene = CreateScene("ManagedRenderPipelineCameraRequestScene");
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
GameObject* lightObject = runtimeScene->CreateGameObject("Light");
auto* light = lightObject->AddComponent<LightComponent>();
light->SetLightType(LightType::Directional);
light->SetCastsShadows(true);
XCEngine::Rendering::CameraRenderRequest baselineRequest = {};
baselineRequest.scene = runtimeScene;
baselineRequest.camera = camera;
baselineRequest.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
XCEngine::Rendering::ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy(
baselineRequest,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
ASSERT_TRUE(baselineRequest.directionalShadow.IsValid());
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedCameraRequestConfiguredRenderPipelineProbeAsset"
};
XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset asset(
descriptor);
XCEngine::Rendering::CameraRenderRequest request = {};
request.scene = runtimeScene;
request.camera = camera;
request.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
asset.ConfigureCameraRenderRequest(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
EXPECT_FALSE(request.directionalShadow.IsValid());
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineAssetReturnsManagedDefaultFinalColorSettings) {
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedFinalColorRenderPipelineProbeAsset"
};
XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset asset(
descriptor);
const XCEngine::Rendering::FinalColorSettings settings =
asset.GetDefaultFinalColorSettings();
EXPECT_EQ(
settings.outputTransferMode,
XCEngine::Rendering::FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_EQ(
settings.exposureMode,
XCEngine::Rendering::FinalColorExposureMode::Fixed);
EXPECT_FLOAT_EQ(settings.exposureValue, 1.75f);
EXPECT_EQ(
settings.toneMappingMode,
XCEngine::Rendering::FinalColorToneMappingMode::ACES);
EXPECT_FLOAT_EQ(settings.finalColorScale.x, 0.90f);
EXPECT_FLOAT_EQ(settings.finalColorScale.y, 1.10f);
EXPECT_FLOAT_EQ(settings.finalColorScale.z, 1.20f);
EXPECT_FLOAT_EQ(settings.finalColorScale.w, 1.0f);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsNullComponentReferences) {
std::vector<ScriptFieldDefaultValue> fields;
EXPECT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "Gameplay", "ComponentFieldMetadataProbe", fields));
const auto expectNullComponentField = [&](const char* fieldName) {
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[fieldName](const ScriptFieldDefaultValue& field) {
return field.fieldName == fieldName;
});
ASSERT_NE(fieldIt, fields.end());
EXPECT_EQ(fieldIt->type, ScriptFieldType::Component);
EXPECT_EQ(std::get<ComponentReference>(fieldIt->value), (ComponentReference{}));
};
expectNullComponentField("Pivot");
expectNullComponentField("SceneCamera");
expectNullComponentField("ScriptTarget");
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycleMethods) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("Speed", 5.0f);
component->GetFieldStorage().SetFieldValue("Label", "Configured");
component->GetFieldStorage().SetFieldValue("Target", GameObjectReference{target->GetUUID()});
component->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
EXPECT_TRUE(runtime->HasManagedInstance(component));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u);
int32_t awakeCount = 0;
int32_t enableCount = 0;
int32_t startCount = 0;
int32_t fixedUpdateCount = 0;
int32_t updateCount = 0;
int32_t lateUpdateCount = 0;
bool wasAwakened = false;
bool warningLogged = false;
bool errorLogged = false;
bool hasTransform = false;
bool transformLookupSucceeded = false;
bool hasUnsupportedComponent = true;
bool unsupportedComponentLookupReturnedNull = false;
bool targetResolved = false;
bool rotationAccessed = false;
bool scaleAccessed = false;
bool transformAccessed = false;
bool observedEnabled = false;
bool observedActiveSelf = false;
bool observedActiveInHierarchy = false;
bool observedIsActiveAndEnabled = false;
float speed = 0.0f;
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
float observedLateDeltaTime = 0.0f;
std::string label;
std::string observedGameObjectName;
std::string observedTargetName;
GameObjectReference targetReference;
GameObjectReference selfReference;
XCEngine::Math::Vector4 observedLocalRotation;
XCEngine::Math::Vector3 observedLocalPosition;
XCEngine::Math::Vector3 observedLocalScale;
XCEngine::Math::Vector3 spawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "FixedUpdateCount", fixedUpdateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "WasAwakened", wasAwakened));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "WarningLogged", warningLogged));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ErrorLogged", errorLogged));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTransform", hasTransform));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformLookupSucceeded", transformLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasUnsupportedComponent", hasUnsupportedComponent));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UnsupportedComponentLookupReturnedNull", unsupportedComponentLookupReturnedNull));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RotationAccessed", rotationAccessed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ScaleAccessed", scaleAccessed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformAccessed", transformAccessed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", speed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedGameObjectName", observedGameObjectName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Target", targetReference));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SelfReference", selfReference));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalRotation", observedLocalRotation));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalPosition", observedLocalPosition));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalScale", observedLocalScale));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", spawnPoint));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(enableCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_EQ(fixedUpdateCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 1);
EXPECT_TRUE(wasAwakened);
EXPECT_TRUE(warningLogged);
EXPECT_TRUE(errorLogged);
EXPECT_TRUE(hasTransform);
EXPECT_TRUE(transformLookupSucceeded);
EXPECT_FALSE(hasUnsupportedComponent);
EXPECT_TRUE(unsupportedComponentLookupReturnedNull);
EXPECT_TRUE(targetResolved);
EXPECT_TRUE(rotationAccessed);
EXPECT_TRUE(scaleAccessed);
EXPECT_TRUE(transformAccessed);
EXPECT_TRUE(observedEnabled);
EXPECT_TRUE(observedActiveSelf);
EXPECT_TRUE(observedActiveInHierarchy);
EXPECT_TRUE(observedIsActiveAndEnabled);
EXPECT_FLOAT_EQ(speed, 6.0f);
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f);
EXPECT_EQ(label, "Configured|Awake");
EXPECT_EQ(observedGameObjectName, "Host_Managed");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(targetReference, GameObjectReference{target->GetUUID()});
EXPECT_EQ(selfReference, GameObjectReference{host->GetUUID()});
EXPECT_EQ(observedLocalRotation, XCEngine::Math::Vector4(0.0f, 0.5f, 0.0f, 0.8660254f));
EXPECT_EQ(observedLocalPosition, XCEngine::Math::Vector3(8.0f, 8.0f, 9.0f));
EXPECT_EQ(observedLocalScale, XCEngine::Math::Vector3(3.0f, 3.0f, 4.0f));
EXPECT_EQ(spawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f));
EXPECT_EQ(host->GetName(), "Host_Managed");
EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(8.0f, 8.0f, 9.0f));
EXPECT_EQ(host->GetTransform()->GetLocalScale(), XCEngine::Math::Vector3(3.0f, 3.0f, 4.0f));
const XCEngine::Math::Quaternion& localRotation = host->GetTransform()->GetLocalRotation();
EXPECT_FLOAT_EQ(localRotation.x, 0.0f);
EXPECT_FLOAT_EQ(localRotation.y, 0.5f);
EXPECT_FLOAT_EQ(localRotation.z, 0.0f);
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
}
TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossFixedAndVariableUpdates) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->SetRuntimeFixedDeltaTime(0.05f);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.05f);
engine->OnUpdate(0.016f);
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.05f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
}
TEST_F(MonoScriptRuntimeTest, SceneRuntimeDispatchesManagedTriggerMessagesWithGameObjectArgumentsAndFallbacks) {
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX is not available in this configuration.";
}
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* mover = runtimeScene->CreateGameObject("Mover");
mover->AddComponent<SphereColliderComponent>();
RigidbodyComponent* rigidbody = mover->AddComponent<RigidbodyComponent>();
rigidbody->SetUseGravity(false);
ScriptComponent* component = AddScript(mover, "Gameplay", "PhysicsEventProbe");
mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f));
GameObject* triggerZone = runtimeScene->CreateGameObject("TriggerZone");
BoxColliderComponent* triggerCollider = triggerZone->AddComponent<BoxColliderComponent>();
triggerCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
triggerCollider->SetTrigger(true);
SceneRuntime sceneRuntime;
sceneRuntime.Start(runtimeScene);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
sceneRuntime.FixedUpdate(0.02f);
int32_t triggerEnterCount = 0;
int32_t triggerStayCount = 0;
int32_t triggerExitCount = 0;
bool triggerStayFallbackInvoked = false;
std::string lastTriggerOtherName;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerEnterCount", triggerEnterCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerStayCount", triggerStayCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerExitCount", triggerExitCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerStayFallbackInvoked", triggerStayFallbackInvoked));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LastTriggerOtherName", lastTriggerOtherName));
EXPECT_EQ(triggerEnterCount, 1);
EXPECT_EQ(triggerStayCount, 2);
EXPECT_EQ(triggerExitCount, 1);
EXPECT_TRUE(triggerStayFallbackInvoked);
EXPECT_EQ(lastTriggerOtherName, "TriggerZone");
sceneRuntime.Stop();
}
TEST_F(MonoScriptRuntimeTest, SceneRuntimeDispatchesManagedCollisionMessagesWithGameObjectArgumentsAndFallbacks) {
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX is not available in this configuration.";
}
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* mover = runtimeScene->CreateGameObject("Mover");
mover->AddComponent<SphereColliderComponent>();
RigidbodyComponent* rigidbody = mover->AddComponent<RigidbodyComponent>();
rigidbody->SetUseGravity(false);
ScriptComponent* component = AddScript(mover, "Gameplay", "PhysicsEventProbe");
mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f));
GameObject* wall = runtimeScene->CreateGameObject("Wall");
BoxColliderComponent* wallCollider = wall->AddComponent<BoxColliderComponent>();
wallCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
SceneRuntime sceneRuntime;
sceneRuntime.Start(runtimeScene);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(-150.0f, 0.0f, 0.0f));
sceneRuntime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
sceneRuntime.FixedUpdate(0.02f);
int32_t collisionEnterCount = 0;
int32_t collisionStayCount = 0;
int32_t collisionExitCount = 0;
bool collisionStayFallbackInvoked = false;
std::string lastCollisionOtherName;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionEnterCount", collisionEnterCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionStayCount", collisionStayCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionExitCount", collisionExitCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionStayFallbackInvoked", collisionStayFallbackInvoked));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LastCollisionOtherName", lastCollisionOtherName));
EXPECT_EQ(collisionEnterCount, 1);
EXPECT_EQ(collisionStayCount, 2);
EXPECT_EQ(collisionExitCount, 1);
EXPECT_TRUE(collisionStayFallbackInvoked);
EXPECT_EQ(lastCollisionOtherName, "Wall");
sceneRuntime.Stop();
}
TEST_F(MonoScriptRuntimeTest, ManagedRigidbodyWrapperAndPhysicsRaycastOperateAgainstRuntimePhysicsWorld) {
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX is not available in this configuration.";
}
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<SphereColliderComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "PhysicsApiProbe");
GameObject* rayTarget = runtimeScene->CreateGameObject("RayTarget");
rayTarget->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 3.0f, -4.0f));
BoxColliderComponent* targetCollider = rayTarget->AddComponent<BoxColliderComponent>();
targetCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 0.5f));
SceneRuntime sceneRuntime;
sceneRuntime.Start(runtimeScene);
sceneRuntime.Update(0.016f);
sceneRuntime.FixedUpdate(0.02f);
bool initialHasRigidbody = true;
bool addedRigidbody = false;
bool hasRigidbodyAfterAdd = false;
bool rigidbodyLookupSucceeded = false;
int32_t observedBodyType = -1;
float observedMass = 0.0f;
float observedLinearDamping = 0.0f;
float observedAngularDamping = 0.0f;
bool observedUseGravity = true;
bool observedEnableCCD = false;
XCEngine::Math::Vector3 observedLinearVelocity;
XCEngine::Math::Vector3 observedAngularVelocity;
bool raycastHitDetected = false;
std::string raycastHitName;
float raycastHitDistance = 0.0f;
bool raycastHitWasTrigger = true;
XCEngine::Math::Vector3 raycastHitPoint;
XCEngine::Math::Vector3 raycastHitNormal;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasRigidbody", initialHasRigidbody));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedRigidbody", addedRigidbody));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasRigidbodyAfterAdd", hasRigidbodyAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RigidbodyLookupSucceeded", rigidbodyLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedBodyType", observedBodyType));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMass", observedMass));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLinearDamping", observedLinearDamping));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAngularDamping", observedAngularDamping));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUseGravity", observedUseGravity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnableCCD", observedEnableCCD));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLinearVelocity", observedLinearVelocity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAngularVelocity", observedAngularVelocity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitDetected", raycastHitDetected));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitName", raycastHitName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitDistance", raycastHitDistance));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitWasTrigger", raycastHitWasTrigger));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitPoint", raycastHitPoint));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "RaycastHitNormal", raycastHitNormal));
EXPECT_FALSE(initialHasRigidbody);
EXPECT_TRUE(addedRigidbody);
EXPECT_TRUE(hasRigidbodyAfterAdd);
EXPECT_TRUE(rigidbodyLookupSucceeded);
EXPECT_EQ(observedBodyType, static_cast<int32_t>(XCEngine::Physics::PhysicsBodyType::Dynamic));
EXPECT_FLOAT_EQ(observedMass, 2.5f);
EXPECT_FLOAT_EQ(observedLinearDamping, 0.25f);
EXPECT_FLOAT_EQ(observedAngularDamping, 0.5f);
EXPECT_FALSE(observedUseGravity);
EXPECT_TRUE(observedEnableCCD);
ExpectVector3Near(observedLinearVelocity, XCEngine::Math::Vector3(0.0f, 0.0f, 0.0f));
ExpectVector3Near(observedAngularVelocity, XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f));
EXPECT_TRUE(raycastHitDetected);
EXPECT_EQ(raycastHitName, "RayTarget");
EXPECT_NEAR(raycastHitDistance, 3.75f, 0.05f);
EXPECT_FALSE(raycastHitWasTrigger);
ExpectVector3Near(raycastHitPoint, XCEngine::Math::Vector3(0.0f, 3.0f, -3.75f), 0.05f);
ExpectVector3Near(raycastHitNormal, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f), 0.01f);
EXPECT_NE(sceneRuntime.GetPhysicsWorld(), nullptr);
EXPECT_EQ(sceneRuntime.GetPhysicsWorld()->GetTrackedRigidbodyCount(), 1u);
RigidbodyComponent* rigidbody = host->GetComponent<RigidbodyComponent>();
ASSERT_NE(rigidbody, nullptr);
EXPECT_EQ(host->GetComponents<RigidbodyComponent>().size(), 1u);
EXPECT_EQ(rigidbody->GetBodyType(), XCEngine::Physics::PhysicsBodyType::Dynamic);
EXPECT_FLOAT_EQ(rigidbody->GetMass(), 2.5f);
EXPECT_FLOAT_EQ(rigidbody->GetLinearDamping(), 0.25f);
EXPECT_FLOAT_EQ(rigidbody->GetAngularDamping(), 0.5f);
EXPECT_FALSE(rigidbody->GetUseGravity());
EXPECT_TRUE(rigidbody->GetEnableCCD());
EXPECT_GT(rigidbody->GetLinearVelocity().x, 0.0f);
const XCEngine::Math::Vector3 hostPosition = host->GetTransform()->GetPosition();
EXPECT_GT(hostPosition.x, 0.01f);
EXPECT_NEAR(hostPosition.y, 0.0f, 0.01f);
sceneRuntime.Stop();
}
TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) {
XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get();
inputManager.Initialize(nullptr);
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "InputProbe");
engine->OnRuntimeStart(runtimeScene);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::A, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::Space, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, true, 120, 48);
inputManager.ProcessMouseMove(120, 48, 3, -2);
inputManager.ProcessMouseWheel(1.0f, 120, 48);
engine->OnUpdate(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;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMousePosition", observedMousePosition));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "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));
inputManager.Update(0.016f);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "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));
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::A, false, false, false, false);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, false, 120, 48);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_EQ(updateCount, 3);
EXPECT_FALSE(observedKeyA);
EXPECT_TRUE(observedKeyAUp);
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);
inputManager.Update(0.016f);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::Space, false, false, false, false);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_EQ(updateCount, 4);
EXPECT_FALSE(observedKeySpace);
EXPECT_FALSE(observedJump);
EXPECT_TRUE(observedJumpUp);
EXPECT_FALSE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
}
TEST_F(MonoScriptRuntimeTest, ManagedDebugLogBridgeWritesLifecycleTickMessagesToNativeLogger) {
auto sink = std::make_unique<CapturingLogSink>();
CapturingLogSink* sinkPtr = sink.get();
XCEngine::Debug::Logger::Get().AddSink(std::move(sink));
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TickLogProbe");
ASSERT_NE(component, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
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);
XCEngine::Debug::Logger::Get().RemoveSink(sinkPtr);
}
TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) {
Scene originalScene("SerializedMonoScene");
GameObject* hostA = originalScene.CreateGameObject("HostA");
GameObject* hostB = originalScene.CreateGameObject("HostB");
GameObject* targetA = originalScene.CreateGameObject("TargetA");
GameObject* targetB = originalScene.CreateGameObject("TargetB");
ScriptComponent* originalComponentA = AddScript(hostA, "Gameplay", "LifecycleProbe");
ScriptComponent* originalComponentB = AddScript(hostB, "Gameplay", "LifecycleProbe");
originalComponentA->GetFieldStorage().SetFieldValue("Speed", 2.5f);
originalComponentA->GetFieldStorage().SetFieldValue("Label", "Alpha");
originalComponentA->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetA->GetUUID()});
originalComponentA->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
originalComponentB->GetFieldStorage().SetFieldValue("Speed", 9.0f);
originalComponentB->GetFieldStorage().SetFieldValue("Label", "Beta");
originalComponentB->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetB->GetUUID()});
originalComponentB->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f));
const uint64_t originalUUIDA = originalComponentA->GetScriptComponentUUID();
const uint64_t originalUUIDB = originalComponentB->GetScriptComponentUUID();
const std::string serializedScene = originalScene.SerializeToString();
scene = std::make_unique<Scene>("LoadedMonoScene");
scene->DeserializeFromString(serializedScene);
Scene* runtimeScene = scene.get();
GameObject* loadedHostA = runtimeScene->Find("HostA");
GameObject* loadedHostB = runtimeScene->Find("HostB");
GameObject* loadedTargetA = runtimeScene->Find("TargetA");
GameObject* loadedTargetB = runtimeScene->Find("TargetB");
ASSERT_NE(loadedHostA, nullptr);
ASSERT_NE(loadedHostB, nullptr);
ASSERT_NE(loadedTargetA, nullptr);
ASSERT_NE(loadedTargetB, nullptr);
ScriptComponent* loadedComponentA = FindScriptComponentByClass(loadedHostA, "Gameplay", "LifecycleProbe");
ScriptComponent* loadedComponentB = FindScriptComponentByClass(loadedHostB, "Gameplay", "LifecycleProbe");
ASSERT_NE(loadedComponentA, nullptr);
ASSERT_NE(loadedComponentB, nullptr);
EXPECT_EQ(loadedComponentA->GetScriptComponentUUID(), originalUUIDA);
EXPECT_EQ(loadedComponentB->GetScriptComponentUUID(), originalUUIDB);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
EXPECT_TRUE(runtime->HasManagedInstance(loadedComponentA));
EXPECT_TRUE(runtime->HasManagedInstance(loadedComponentB));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u);
int32_t awakeCountA = 0;
int32_t startCountA = 0;
int32_t awakeCountB = 0;
int32_t startCountB = 0;
bool targetResolvedA = false;
bool targetResolvedB = false;
float speedA = 0.0f;
float speedB = 0.0f;
std::string labelA;
std::string labelB;
std::string observedTargetNameA;
std::string observedTargetNameB;
GameObjectReference targetReferenceA;
GameObjectReference targetReferenceB;
GameObjectReference selfReferenceA;
GameObjectReference selfReferenceB;
XCEngine::Math::Vector3 spawnPointA;
XCEngine::Math::Vector3 spawnPointB;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "AwakeCount", awakeCountA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "StartCount", startCountA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "TargetResolved", targetResolvedA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Speed", speedA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Label", labelA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "ObservedTargetName", observedTargetNameA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Target", targetReferenceA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "SelfReference", selfReferenceA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "SpawnPoint", spawnPointA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "AwakeCount", awakeCountB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "StartCount", startCountB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "TargetResolved", targetResolvedB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Speed", speedB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Label", labelB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "ObservedTargetName", observedTargetNameB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Target", targetReferenceB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "SelfReference", selfReferenceB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "SpawnPoint", spawnPointB));
EXPECT_EQ(awakeCountA, 1);
EXPECT_EQ(startCountA, 1);
EXPECT_TRUE(targetResolvedA);
EXPECT_FLOAT_EQ(speedA, 3.5f);
EXPECT_EQ(labelA, "Alpha|Awake");
EXPECT_EQ(observedTargetNameA, "TargetA");
EXPECT_EQ(targetReferenceA, GameObjectReference{loadedTargetA->GetUUID()});
EXPECT_EQ(selfReferenceA, GameObjectReference{loadedHostA->GetUUID()});
EXPECT_EQ(spawnPointA, XCEngine::Math::Vector3(2.0f, 2.0f, 3.0f));
EXPECT_EQ(awakeCountB, 1);
EXPECT_EQ(startCountB, 1);
EXPECT_TRUE(targetResolvedB);
EXPECT_FLOAT_EQ(speedB, 10.0f);
EXPECT_EQ(labelB, "Beta|Awake");
EXPECT_EQ(observedTargetNameB, "TargetB");
EXPECT_EQ(targetReferenceB, GameObjectReference{loadedTargetB->GetUUID()});
EXPECT_EQ(selfReferenceB, GameObjectReference{loadedHostB->GetUUID()});
EXPECT_EQ(spawnPointB, XCEngine::Math::Vector3(5.0f, 5.0f, 6.0f));
EXPECT_EQ(loadedHostA->GetName(), "HostA_Managed");
EXPECT_EQ(loadedHostB->GetName(), "HostB_Managed");
}
TEST_F(MonoScriptRuntimeTest, ManagedFieldChangesWriteBackToStoredCacheAndPersistAcrossSceneRoundTrip) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("Speed", 5.0f);
component->GetFieldStorage().SetFieldValue("Label", "Configured");
component->GetFieldStorage().SetFieldValue("Target", GameObjectReference{target->GetUUID()});
component->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
float storedSpeed = 0.0f;
std::string storedLabel;
GameObjectReference storedTarget;
XCEngine::Math::Vector3 storedSpawnPoint;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeed));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabel));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTarget));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPoint));
EXPECT_FLOAT_EQ(storedSpeed, 6.0f);
EXPECT_EQ(storedLabel, "Configured|Awake");
EXPECT_EQ(storedTarget, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f));
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
const std::string persistedHostName = host->GetName();
const std::string serializedScene = runtimeScene->SerializeToString();
engine->OnRuntimeStop();
scene = std::make_unique<Scene>("ReloadedMonoScene");
scene->DeserializeFromString(serializedScene);
Scene* reloadedScene = scene.get();
GameObject* loadedHost = reloadedScene->Find(persistedHostName);
GameObject* loadedTarget = reloadedScene->Find("Target");
ASSERT_NE(loadedHost, nullptr);
ASSERT_NE(loadedTarget, nullptr);
ScriptComponent* loadedComponent = FindScriptComponentByClass(loadedHost, "Gameplay", "LifecycleProbe");
ASSERT_NE(loadedComponent, nullptr);
float loadedStoredSpeed = 0.0f;
std::string loadedStoredLabel;
GameObjectReference loadedStoredTarget;
XCEngine::Math::Vector3 loadedStoredSpawnPoint;
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Speed", loadedStoredSpeed));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Label", loadedStoredLabel));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Target", loadedStoredTarget));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("SpawnPoint", loadedStoredSpawnPoint));
EXPECT_FLOAT_EQ(loadedStoredSpeed, 6.0f);
EXPECT_EQ(loadedStoredLabel, "Configured|Awake");
EXPECT_EQ(loadedStoredTarget, GameObjectReference{loadedTarget->GetUUID()});
EXPECT_EQ(loadedStoredSpawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f));
EXPECT_FALSE(loadedComponent->GetFieldStorage().Contains("AwakeCount"));
engine->OnRuntimeStart(reloadedScene);
engine->OnUpdate(0.016f);
int32_t awakeCount = 0;
int32_t startCount = 0;
int32_t updateCount = 0;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedTargetName", observedTargetName));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_FLOAT_EQ(runtimeSpeed, 7.0f);
EXPECT_EQ(runtimeLabel, "Configured|Awake|Awake");
EXPECT_EQ(observedTargetName, "Target");
}
TEST_F(MonoScriptRuntimeTest, EnumScriptFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "EnumFieldProbe");
component->GetFieldStorage().SetFieldValue("State", int32_t(5));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int32_t observedInitialState = 0;
bool observedStoredStateApplied = false;
int32_t observedUpdatedState = 0;
int32_t runtimeState = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialState", observedInitialState));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedStoredStateApplied", observedStoredStateApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedState", observedUpdatedState));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "State", runtimeState));
EXPECT_EQ(observedInitialState, 5);
EXPECT_TRUE(observedStoredStateApplied);
EXPECT_EQ(observedUpdatedState, 9);
EXPECT_EQ(runtimeState, 9);
int32_t storedState = 0;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("State", storedState));
EXPECT_EQ(storedState, 9);
const std::string serializedScene = runtimeScene->SerializeToString();
engine->OnRuntimeStop();
scene = std::make_unique<Scene>("ReloadedEnumScene");
scene->DeserializeFromString(serializedScene);
Scene* reloadedScene = scene.get();
GameObject* loadedHost = reloadedScene->Find("Host");
ASSERT_NE(loadedHost, nullptr);
ScriptComponent* loadedComponent = FindScriptComponentByClass(loadedHost, "Gameplay", "EnumFieldProbe");
ASSERT_NE(loadedComponent, nullptr);
int32_t loadedStoredState = 0;
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("State", loadedStoredState));
EXPECT_EQ(loadedStoredState, 9);
engine->OnRuntimeStart(reloadedScene);
engine->OnUpdate(0.016f);
int32_t loadedObservedInitialState = 0;
int32_t loadedRuntimeState = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedInitialState", loadedObservedInitialState));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "State", loadedRuntimeState));
EXPECT_EQ(loadedObservedInitialState, 9);
EXPECT_EQ(loadedRuntimeState, 9);
}
TEST_F(MonoScriptRuntimeTest, SerializeFieldPrivateFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "SerializeFieldProbe");
component->GetFieldStorage().SetFieldValue("HiddenCounter", int32_t(42));
component->GetFieldStorage().SetFieldValue("HiddenEnabled", false);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int32_t observedInitialHiddenCounter = 0;
bool observedInitialHiddenEnabled = true;
bool observedStoredValuesApplied = false;
int32_t observedUpdatedHiddenCounter = 0;
bool observedUpdatedHiddenEnabled = false;
bool observedIgnoredPrivateCounterUntouched = false;
int32_t runtimeHiddenCounter = 0;
bool runtimeHiddenEnabled = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialHiddenCounter", observedInitialHiddenCounter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialHiddenEnabled", observedInitialHiddenEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedStoredValuesApplied", observedStoredValuesApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedHiddenCounter", observedUpdatedHiddenCounter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedHiddenEnabled", observedUpdatedHiddenEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIgnoredPrivateCounterUntouched", observedIgnoredPrivateCounterUntouched));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HiddenCounter", runtimeHiddenCounter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HiddenEnabled", runtimeHiddenEnabled));
EXPECT_EQ(observedInitialHiddenCounter, 42);
EXPECT_FALSE(observedInitialHiddenEnabled);
EXPECT_TRUE(observedStoredValuesApplied);
EXPECT_EQ(observedUpdatedHiddenCounter, 43);
EXPECT_TRUE(observedUpdatedHiddenEnabled);
EXPECT_TRUE(observedIgnoredPrivateCounterUntouched);
EXPECT_EQ(runtimeHiddenCounter, 43);
EXPECT_TRUE(runtimeHiddenEnabled);
EXPECT_FALSE(component->GetFieldStorage().Contains("IgnoredPrivateCounter"));
int32_t storedHiddenCounter = 0;
bool storedHiddenEnabled = false;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("HiddenCounter", storedHiddenCounter));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("HiddenEnabled", storedHiddenEnabled));
EXPECT_EQ(storedHiddenCounter, 43);
EXPECT_TRUE(storedHiddenEnabled);
const std::string serializedScene = runtimeScene->SerializeToString();
engine->OnRuntimeStop();
scene = std::make_unique<Scene>("ReloadedSerializeFieldScene");
scene->DeserializeFromString(serializedScene);
Scene* reloadedScene = scene.get();
GameObject* loadedHost = reloadedScene->Find("Host");
ASSERT_NE(loadedHost, nullptr);
ScriptComponent* loadedComponent = FindScriptComponentByClass(loadedHost, "Gameplay", "SerializeFieldProbe");
ASSERT_NE(loadedComponent, nullptr);
int32_t loadedStoredHiddenCounter = 0;
bool loadedStoredHiddenEnabled = false;
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("HiddenCounter", loadedStoredHiddenCounter));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("HiddenEnabled", loadedStoredHiddenEnabled));
EXPECT_EQ(loadedStoredHiddenCounter, 43);
EXPECT_TRUE(loadedStoredHiddenEnabled);
engine->OnRuntimeStart(reloadedScene);
engine->OnUpdate(0.016f);
int32_t loadedObservedInitialHiddenCounter = 0;
bool loadedObservedInitialHiddenEnabled = false;
int32_t loadedRuntimeHiddenCounter = 0;
bool loadedRuntimeHiddenEnabled = false;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedInitialHiddenCounter", loadedObservedInitialHiddenCounter));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedInitialHiddenEnabled", loadedObservedInitialHiddenEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "HiddenCounter", loadedRuntimeHiddenCounter));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "HiddenEnabled", loadedRuntimeHiddenEnabled));
EXPECT_EQ(loadedObservedInitialHiddenCounter, 43);
EXPECT_TRUE(loadedObservedInitialHiddenEnabled);
EXPECT_EQ(loadedRuntimeHiddenCounter, 44);
EXPECT_FALSE(loadedRuntimeHiddenEnabled);
}
TEST_F(MonoScriptRuntimeTest, ComponentReferenceFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<CameraComponent>();
GameObject* pivotTarget = runtimeScene->CreateGameObject("PivotTarget");
GameObject* cameraTarget = runtimeScene->CreateGameObject("CameraTarget");
cameraTarget->AddComponent<CameraComponent>();
GameObject* scriptTargetHost = runtimeScene->CreateGameObject("ScriptTarget");
ScriptComponent* referencedScript = AddScript(scriptTargetHost, "Gameplay", "ScriptComponentTargetProbe");
ScriptComponent* component = AddScript(host, "Gameplay", "ComponentFieldProbe");
component->GetFieldStorage().SetFieldValue("Pivot", ComponentReference{pivotTarget->GetUUID(), 0});
component->GetFieldStorage().SetFieldValue("SceneCamera", ComponentReference{cameraTarget->GetUUID(), 0});
component->GetFieldStorage().SetFieldValue(
"ScriptTarget",
ComponentReference{scriptTargetHost->GetUUID(), referencedScript->GetScriptComponentUUID()});
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool observedStoredPivotApplied = false;
bool observedStoredCameraApplied = false;
bool observedStoredScriptApplied = false;
std::string observedPivotName;
std::string observedCameraName;
std::string observedScriptName;
int32_t observedScriptAwakeCount = -1;
int32_t observedScriptHostCallCount = -1;
bool observedUpdatedPivotAssigned = false;
bool observedUpdatedCameraAssigned = false;
bool observedUpdatedScriptAssigned = false;
std::string observedUpdatedPivotName;
std::string observedUpdatedCameraName;
std::string observedUpdatedScriptName;
ComponentReference runtimePivot;
ComponentReference runtimeCamera;
ComponentReference runtimeScript;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedStoredPivotApplied", observedStoredPivotApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedStoredCameraApplied", observedStoredCameraApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedStoredScriptApplied", observedStoredScriptApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPivotName", observedPivotName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCameraName", observedCameraName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedScriptName", observedScriptName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedScriptAwakeCount", observedScriptAwakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedScriptHostCallCount", observedScriptHostCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedPivotAssigned", observedUpdatedPivotAssigned));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedCameraAssigned", observedUpdatedCameraAssigned));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedScriptAssigned", observedUpdatedScriptAssigned));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedPivotName", observedUpdatedPivotName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedCameraName", observedUpdatedCameraName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedScriptName", observedUpdatedScriptName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Pivot", runtimePivot));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SceneCamera", runtimeCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ScriptTarget", runtimeScript));
EXPECT_TRUE(observedStoredPivotApplied);
EXPECT_TRUE(observedStoredCameraApplied);
EXPECT_TRUE(observedStoredScriptApplied);
EXPECT_EQ(observedPivotName, "PivotTarget");
EXPECT_EQ(observedCameraName, "CameraTarget");
EXPECT_EQ(observedScriptName, "ScriptTarget");
EXPECT_EQ(observedScriptAwakeCount, 1);
EXPECT_EQ(observedScriptHostCallCount, 1);
EXPECT_TRUE(observedUpdatedPivotAssigned);
EXPECT_TRUE(observedUpdatedCameraAssigned);
EXPECT_TRUE(observedUpdatedScriptAssigned);
EXPECT_EQ(observedUpdatedPivotName, "Host");
EXPECT_EQ(observedUpdatedCameraName, "Host");
EXPECT_EQ(observedUpdatedScriptName, "Host");
ScriptComponent* assignedHostScript = FindScriptComponentByClass(host, "Gameplay", "ScriptComponentTargetProbe");
ASSERT_NE(assignedHostScript, nullptr);
EXPECT_EQ(runtimePivot, (ComponentReference{host->GetUUID(), 0}));
EXPECT_EQ(runtimeCamera, (ComponentReference{host->GetUUID(), 0}));
EXPECT_EQ(
runtimeScript,
(ComponentReference{host->GetUUID(), assignedHostScript->GetScriptComponentUUID()}));
ComponentReference storedPivot;
ComponentReference storedCamera;
ComponentReference storedScript;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Pivot", storedPivot));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SceneCamera", storedCamera));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("ScriptTarget", storedScript));
EXPECT_EQ(storedPivot, runtimePivot);
EXPECT_EQ(storedCamera, runtimeCamera);
EXPECT_EQ(storedScript, runtimeScript);
const std::string serializedScene = runtimeScene->SerializeToString();
engine->OnRuntimeStop();
scene = std::make_unique<Scene>("ReloadedComponentFieldScene");
scene->DeserializeFromString(serializedScene);
Scene* reloadedScene = scene.get();
GameObject* loadedHost = reloadedScene->Find("Host");
ASSERT_NE(loadedHost, nullptr);
ScriptComponent* loadedComponent = FindScriptComponentByClass(loadedHost, "Gameplay", "ComponentFieldProbe");
ScriptComponent* loadedAssignedHostScript =
FindScriptComponentByClass(loadedHost, "Gameplay", "ScriptComponentTargetProbe");
ASSERT_NE(loadedComponent, nullptr);
ASSERT_NE(loadedAssignedHostScript, nullptr);
ComponentReference loadedStoredPivot;
ComponentReference loadedStoredCamera;
ComponentReference loadedStoredScript;
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Pivot", loadedStoredPivot));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("SceneCamera", loadedStoredCamera));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("ScriptTarget", loadedStoredScript));
EXPECT_EQ(loadedStoredPivot, (ComponentReference{loadedHost->GetUUID(), 0}));
EXPECT_EQ(loadedStoredCamera, (ComponentReference{loadedHost->GetUUID(), 0}));
EXPECT_EQ(
loadedStoredScript,
(ComponentReference{loadedHost->GetUUID(), loadedAssignedHostScript->GetScriptComponentUUID()}));
engine->OnRuntimeStart(reloadedScene);
engine->OnUpdate(0.016f);
bool loadedObservedStoredPivotApplied = false;
bool loadedObservedStoredCameraApplied = false;
bool loadedObservedStoredScriptApplied = false;
std::string loadedObservedPivotName;
std::string loadedObservedCameraName;
std::string loadedObservedScriptName;
ComponentReference loadedRuntimePivot;
ComponentReference loadedRuntimeCamera;
ComponentReference loadedRuntimeScript;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedStoredPivotApplied", loadedObservedStoredPivotApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedStoredCameraApplied", loadedObservedStoredCameraApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedStoredScriptApplied", loadedObservedStoredScriptApplied));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedPivotName", loadedObservedPivotName));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedCameraName", loadedObservedCameraName));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedScriptName", loadedObservedScriptName));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "Pivot", loadedRuntimePivot));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "SceneCamera", loadedRuntimeCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ScriptTarget", loadedRuntimeScript));
EXPECT_TRUE(loadedObservedStoredPivotApplied);
EXPECT_TRUE(loadedObservedStoredCameraApplied);
EXPECT_TRUE(loadedObservedStoredScriptApplied);
EXPECT_EQ(loadedObservedPivotName, "Host");
EXPECT_EQ(loadedObservedCameraName, "Host");
EXPECT_EQ(loadedObservedScriptName, "Host");
EXPECT_EQ(loadedRuntimePivot, (ComponentReference{loadedHost->GetUUID(), 0}));
EXPECT_EQ(loadedRuntimeCamera, (ComponentReference{loadedHost->GetUUID(), 0}));
EXPECT_EQ(
loadedRuntimeScript,
(ComponentReference{loadedHost->GetUUID(), loadedAssignedHostScript->GetScriptComponentUUID()}));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiUpdatesLiveManagedInstanceAndStoredCache) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 41.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "Edited"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f)));
EXPECT_FALSE(engine->TrySetScriptFieldValue(component, "DoesNotExist", int32_t(1)));
EXPECT_FALSE(engine->TrySetScriptFieldValue(component, "Speed", std::string("wrong")));
float storedSpeedBeforeUpdate = 0.0f;
std::string storedLabelBeforeUpdate;
GameObjectReference storedTargetBeforeUpdate;
XCEngine::Math::Vector3 storedSpawnPointBeforeUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeUpdate));
EXPECT_FLOAT_EQ(storedSpeedBeforeUpdate, 41.0f);
EXPECT_EQ(storedLabelBeforeUpdate, "Edited");
EXPECT_EQ(storedTargetBeforeUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeUpdate, XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f));
EXPECT_FALSE(component->GetFieldStorage().Contains("DoesNotExist"));
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 42.0f);
EXPECT_EQ(runtimeLabel, "Edited");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 42.0f);
EXPECT_EQ(storedLabelAfterUpdate, "Edited");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiCachesValuesBeforeRuntimeStartAndAppliesThemOnFirstInstance) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
EXPECT_FALSE(runtime->HasManagedInstance(component));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 9.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "PreStart"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f)));
float storedSpeedBeforeRuntime = 0.0f;
std::string storedLabelBeforeRuntime;
GameObjectReference storedTargetBeforeRuntime;
XCEngine::Math::Vector3 storedSpawnPointBeforeRuntime;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeRuntime));
EXPECT_FLOAT_EQ(storedSpeedBeforeRuntime, 9.0f);
EXPECT_EQ(storedLabelBeforeRuntime, "PreStart");
EXPECT_EQ(storedTargetBeforeRuntime, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeRuntime, XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f));
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(runtime->HasManagedInstance(component));
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t awakeCount = 0;
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 10.0f);
EXPECT_EQ(runtimeLabel, "PreStart|Awake");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(4.0f, 4.0f, 5.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 10.0f);
EXPECT_EQ(storedLabelAfterUpdate, "PreStart|Awake");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(4.0f, 4.0f, 5.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiCachesValuesForInactiveRuntimeScriptUntilActivation) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
host->SetActive(false);
engine->OnRuntimeStart(runtimeScene);
EXPECT_FALSE(runtime->HasManagedInstance(component));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 12.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "Dormant"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(5.0f, 6.0f, 7.0f)));
float storedSpeedBeforeActivation = 0.0f;
std::string storedLabelBeforeActivation;
GameObjectReference storedTargetBeforeActivation;
XCEngine::Math::Vector3 storedSpawnPointBeforeActivation;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeActivation));
EXPECT_FLOAT_EQ(storedSpeedBeforeActivation, 12.0f);
EXPECT_EQ(storedLabelBeforeActivation, "Dormant");
EXPECT_EQ(storedTargetBeforeActivation, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeActivation, XCEngine::Math::Vector3(5.0f, 6.0f, 7.0f));
host->SetActive(true);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
EXPECT_TRUE(runtime->HasManagedInstance(component));
int32_t awakeCount = 0;
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 13.0f);
EXPECT_EQ(runtimeLabel, "Dormant|Awake");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(6.0f, 6.0f, 7.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 13.0f);
EXPECT_EQ(storedLabelAfterUpdate, "Dormant|Awake");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(6.0f, 6.0f, 7.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldBatchApiUpdatesLiveManagedInstanceAndReportsPerFieldStatus) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldWriteRequest> requests = {
{"Speed", ScriptFieldType::Float, ScriptFieldValue(41.0f)},
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("BatchEdited"))},
{"Target", ScriptFieldType::GameObject, ScriptFieldValue(GameObjectReference{target->GetUUID()})},
{"SpawnPoint", ScriptFieldType::Vector3, ScriptFieldValue(XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f))},
{"LegacyOnly", ScriptFieldType::UInt64, ScriptFieldValue(uint64_t(100))},
{"DoesNotExist", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))},
{"Speed", ScriptFieldType::String, ScriptFieldValue(std::string("wrong"))},
{"Label", ScriptFieldType::String, ScriptFieldValue(int32_t(5))}
};
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::Applied);
EXPECT_EQ(results[3].status, ScriptFieldWriteStatus::Applied);
EXPECT_EQ(results[4].status, ScriptFieldWriteStatus::StoredOnlyField);
EXPECT_EQ(results[5].status, ScriptFieldWriteStatus::UnknownField);
EXPECT_EQ(results[6].status, ScriptFieldWriteStatus::TypeMismatch);
EXPECT_EQ(results[7].status, ScriptFieldWriteStatus::InvalidValue);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 42.0f);
EXPECT_EQ(runtimeLabel, "BatchEdited");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
float storedSpeed = 0.0f;
std::string storedLabel;
GameObjectReference storedTarget;
XCEngine::Math::Vector3 storedSpawnPoint;
uint64_t storedLegacyOnly = 0;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeed));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabel));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTarget));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPoint));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("LegacyOnly", storedLegacyOnly));
EXPECT_FLOAT_EQ(storedSpeed, 42.0f);
EXPECT_EQ(storedLabel, "BatchEdited");
EXPECT_EQ(storedTarget, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
EXPECT_EQ(storedLegacyOnly, 99u);
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldModelUsesManagedClassDefaultValuesBeforeRuntimeStart) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe");
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
const auto fieldIt = std::find_if(
model.fields.begin(),
model.fields.end(),
[](const ScriptFieldSnapshot& field) {
return field.metadata.name == "ObservedRootChildCountAfterDestroy";
});
ASSERT_NE(fieldIt, model.fields.end());
EXPECT_TRUE(fieldIt->declaredInClass);
EXPECT_TRUE(fieldIt->hasDefaultValue);
EXPECT_FALSE(fieldIt->hasValue);
EXPECT_EQ(fieldIt->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<int32_t>(fieldIt->defaultValue), -1);
EXPECT_EQ(std::get<int32_t>(fieldIt->value), -1);
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldClearApiRestoresManagedClassDefaultValuesAndRemovesStoredOverrides) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe");
component->GetFieldStorage().SetFieldValue("ObservedRootChildCountAfterDestroy", int32_t(7));
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldClearRequest> requests = {
{"ObservedRootChildCountAfterDestroy"},
{"LegacyOnly"},
{"DoesNotExist"},
{""}
};
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::UnknownField);
EXPECT_EQ(results[3].status, ScriptFieldClearStatus::EmptyFieldName);
EXPECT_FALSE(component->GetFieldStorage().Contains("ObservedRootChildCountAfterDestroy"));
EXPECT_FALSE(component->GetFieldStorage().Contains("LegacyOnly"));
int32_t runtimeObservedCountAfterDestroy = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountAfterDestroy", runtimeObservedCountAfterDestroy));
EXPECT_EQ(runtimeObservedCountAfterDestroy, -1);
EXPECT_FALSE(component->GetFieldStorage().Contains("ObservedRootChildCountAfterDestroy"));
EXPECT_FALSE(component->GetFieldStorage().Contains("LegacyOnly"));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldReadApiReturnsManagedOnlyRuntimeValues) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
float speed = 0.0f;
int32_t awakeCount = 0;
EXPECT_FALSE(component->GetFieldStorage().Contains("Speed"));
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
EXPECT_FALSE(engine->TryGetScriptFieldValue(component, "Speed", speed));
EXPECT_FALSE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
EXPECT_EQ(awakeCount, 1);
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
engine->OnUpdate(0.016f);
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "Speed", speed));
EXPECT_FLOAT_EQ(speed, 1.0f);
EXPECT_FALSE(component->GetFieldStorage().Contains("Speed"));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldSnapshotApiReportsMetadataAndCurrentValues) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
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;
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* speedSnapshot = findSnapshot("Speed");
const ScriptFieldSnapshot* legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(speedSnapshot, 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(speedSnapshot->metadata.type, ScriptFieldType::Float);
EXPECT_TRUE(speedSnapshot->declaredInClass);
EXPECT_TRUE(speedSnapshot->hasDefaultValue);
EXPECT_FALSE(speedSnapshot->hasValue);
EXPECT_EQ(speedSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(speedSnapshot->issue, ScriptFieldIssue::None);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->defaultValue), 0.0f);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->value), 0.0f);
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);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
awakeSnapshot = findSnapshot("AwakeCount");
labelSnapshot = findSnapshot("Label");
speedSnapshot = findSnapshot("Speed");
legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(speedSnapshot, 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), "Stored|Awake");
EXPECT_TRUE(speedSnapshot->hasValue);
EXPECT_TRUE(speedSnapshot->hasDefaultValue);
EXPECT_EQ(speedSnapshot->valueSource, ScriptFieldValueSource::ManagedValue);
EXPECT_EQ(speedSnapshot->issue, ScriptFieldIssue::None);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->defaultValue), 0.0f);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->value), 1.0f);
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(MonoScriptRuntimeTest, ScriptEngineFieldModelReportsMissingScriptClassAndStoredFields) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "MissingLifecycleProbe");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(9));
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Missing);
ASSERT_EQ(model.fields.size(), 2u);
EXPECT_EQ(model.fields[0].metadata.name, "Label");
EXPECT_EQ(model.fields[1].metadata.name, "LegacyOnly");
for (const ScriptFieldSnapshot& field : model.fields) {
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.metadata.type, field.storedType);
EXPECT_EQ(field.value, field.storedValue);
}
}
TEST_F(MonoScriptRuntimeTest, GameObjectComponentApiResolvesTransformAndRejectsUnsupportedManagedTypes) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasTransform = false;
bool transformLookupSucceeded = false;
bool hasUnsupportedComponent = true;
bool unsupportedComponentLookupReturnedNull = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTransform", hasTransform));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformLookupSucceeded", transformLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasUnsupportedComponent", hasUnsupportedComponent));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UnsupportedComponentLookupReturnedNull", unsupportedComponentLookupReturnedNull));
EXPECT_TRUE(hasTransform);
EXPECT_TRUE(transformLookupSucceeded);
EXPECT_FALSE(hasUnsupportedComponent);
EXPECT_TRUE(unsupportedComponentLookupReturnedNull);
EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f));
}
TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentWrappersReadAndWriteCameraAndLight) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
CameraComponent* camera = host->AddComponent<CameraComponent>();
LightComponent* light = host->AddComponent<LightComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "BuiltinComponentProbe");
camera->SetFieldOfView(52.0f);
camera->SetNearClipPlane(0.2f);
camera->SetFarClipPlane(300.0f);
camera->SetDepth(1.5f);
camera->SetPrimary(true);
light->SetIntensity(1.25f);
light->SetRange(12.0f);
light->SetSpotAngle(33.0f);
light->SetCastsShadows(false);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasCamera = false;
bool hasLight = false;
bool cameraLookupSucceeded = false;
bool lightLookupSucceeded = false;
bool observedPrimary = false;
bool observedCastsShadows = true;
float observedFieldOfView = 0.0f;
float observedNearClipPlane = 0.0f;
float observedFarClipPlane = 0.0f;
float observedDepth = 0.0f;
float observedIntensity = 0.0f;
float observedRange = 0.0f;
float observedSpotAngle = 0.0f;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasCamera", hasCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasLight", hasLight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFieldOfView", observedFieldOfView));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedNearClipPlane", observedNearClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFarClipPlane", observedFarClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedDepth", observedDepth));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPrimary", observedPrimary));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIntensity", observedIntensity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRange", observedRange));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedSpotAngle", observedSpotAngle));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastsShadows", observedCastsShadows));
EXPECT_TRUE(hasCamera);
EXPECT_TRUE(hasLight);
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_TRUE(lightLookupSucceeded);
EXPECT_FLOAT_EQ(observedFieldOfView, 52.0f);
EXPECT_FLOAT_EQ(observedNearClipPlane, 0.2f);
EXPECT_FLOAT_EQ(observedFarClipPlane, 300.0f);
EXPECT_FLOAT_EQ(observedDepth, 1.5f);
EXPECT_TRUE(observedPrimary);
EXPECT_FLOAT_EQ(observedIntensity, 1.25f);
EXPECT_FLOAT_EQ(observedRange, 12.0f);
EXPECT_FLOAT_EQ(observedSpotAngle, 33.0f);
EXPECT_FALSE(observedCastsShadows);
EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 75.0f);
EXPECT_FLOAT_EQ(camera->GetNearClipPlane(), 0.3f);
EXPECT_FLOAT_EQ(camera->GetFarClipPlane(), 500.0f);
EXPECT_FLOAT_EQ(camera->GetDepth(), 3.0f);
EXPECT_FALSE(camera->IsPrimary());
EXPECT_FLOAT_EQ(light->GetIntensity(), 2.5f);
EXPECT_FLOAT_EQ(light->GetRange(), 42.0f);
EXPECT_FLOAT_EQ(light->GetSpotAngle(), 55.0f);
EXPECT_TRUE(light->GetCastsShadows());
}
TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentLookupOnlyCameraAndLight) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<CameraComponent>();
host->AddComponent<LightComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "CameraLightLookupProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasCamera = false;
bool hasLight = false;
bool cameraLookupSucceeded = false;
bool lightLookupSucceeded = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasCamera", hasCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasLight", hasLight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(hasCamera);
EXPECT_TRUE(hasLight);
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_TRUE(lightLookupSucceeded);
}
TEST_F(MonoScriptRuntimeTest, ManagedCameraWrapperReadAndWriteProperties) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
CameraComponent* camera = host->AddComponent<CameraComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "CameraPropertyProbe");
camera->SetFieldOfView(52.0f);
camera->SetNearClipPlane(0.2f);
camera->SetFarClipPlane(300.0f);
camera->SetDepth(1.5f);
camera->SetPrimary(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool cameraLookupSucceeded = false;
bool observedPrimary = false;
float observedFieldOfView = 0.0f;
float observedNearClipPlane = 0.0f;
float observedFarClipPlane = 0.0f;
float observedDepth = 0.0f;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFieldOfView", observedFieldOfView));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedNearClipPlane", observedNearClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFarClipPlane", observedFarClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedDepth", observedDepth));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPrimary", observedPrimary));
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_FLOAT_EQ(observedFieldOfView, 52.0f);
EXPECT_FLOAT_EQ(observedNearClipPlane, 0.2f);
EXPECT_FLOAT_EQ(observedFarClipPlane, 300.0f);
EXPECT_FLOAT_EQ(observedDepth, 1.5f);
EXPECT_TRUE(observedPrimary);
EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 75.0f);
EXPECT_FLOAT_EQ(camera->GetNearClipPlane(), 0.3f);
EXPECT_FLOAT_EQ(camera->GetFarClipPlane(), 500.0f);
EXPECT_FLOAT_EQ(camera->GetDepth(), 3.0f);
EXPECT_FALSE(camera->IsPrimary());
}
TEST_F(MonoScriptRuntimeTest, ManagedLightWrapperReadAndWriteProperties) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
LightComponent* light = host->AddComponent<LightComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "LightPropertyProbe");
light->SetIntensity(1.25f);
light->SetRange(12.0f);
light->SetSpotAngle(33.0f);
light->SetCastsShadows(false);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool lightLookupSucceeded = false;
bool observedCastsShadows = true;
float observedIntensity = 0.0f;
float observedRange = 0.0f;
float observedSpotAngle = 0.0f;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIntensity", observedIntensity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRange", observedRange));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedSpotAngle", observedSpotAngle));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastsShadows", observedCastsShadows));
EXPECT_TRUE(lightLookupSucceeded);
EXPECT_FLOAT_EQ(observedIntensity, 1.25f);
EXPECT_FLOAT_EQ(observedRange, 12.0f);
EXPECT_FLOAT_EQ(observedSpotAngle, 33.0f);
EXPECT_FALSE(observedCastsShadows);
EXPECT_FLOAT_EQ(light->GetIntensity(), 2.5f);
EXPECT_FLOAT_EQ(light->GetRange(), 42.0f);
EXPECT_FLOAT_EQ(light->GetSpotAngle(), 55.0f);
EXPECT_TRUE(light->GetCastsShadows());
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteFlagsOnly) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererFlagsProbe");
meshRenderer->SetCastShadows(true);
meshRenderer->SetReceiveShadows(false);
meshRenderer->SetRenderLayer(7);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool meshRendererLookupSucceeded = false;
bool observedCastShadows = false;
bool observedReceiveShadows = true;
int32_t observedRenderLayer = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastShadows", observedCastShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedReceiveShadows", observedReceiveShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer));
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_TRUE(observedCastShadows);
EXPECT_FALSE(observedReceiveShadows);
EXPECT_EQ(observedRenderLayer, 7);
EXPECT_FALSE(meshRenderer->GetCastShadows());
EXPECT_TRUE(meshRenderer->GetReceiveShadows());
EXPECT_EQ(meshRenderer->GetRenderLayer(), 11u);
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteMaterialPathsOnly) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererPathProbe");
meshRenderer->SetMaterialPath(0, "Materials/initial.mat");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool meshRendererLookupSucceeded = false;
int32_t observedInitialMaterialCount = 0;
std::string observedInitialMaterial0Path;
int32_t observedUpdatedMaterialCount = 0;
std::string observedUpdatedMaterial1Path;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterialCount", observedInitialMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterial0Path", observedInitialMaterial0Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterialCount", observedUpdatedMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterial1Path", observedUpdatedMaterial1Path));
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_EQ(observedInitialMaterialCount, 1);
EXPECT_EQ(observedInitialMaterial0Path, "Materials/initial.mat");
EXPECT_EQ(observedUpdatedMaterialCount, 2);
EXPECT_EQ(observedUpdatedMaterial1Path, "Materials/runtime_override.mat");
ASSERT_EQ(meshRenderer->GetMaterialCount(), 2u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/initial.mat");
EXPECT_EQ(meshRenderer->GetMaterialPath(1), "Materials/runtime_override.mat");
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshComponentWrappersReadAndWritePathsAndFlags) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshFilterComponent* meshFilter = host->AddComponent<MeshFilterComponent>();
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshComponentProbe");
meshFilter->SetMeshPath("Meshes/initial.mesh");
meshRenderer->SetMaterialPath(0, "Materials/initial.mat");
meshRenderer->SetCastShadows(true);
meshRenderer->SetReceiveShadows(false);
meshRenderer->SetRenderLayer(7);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasMeshFilter = false;
bool hasMeshRenderer = false;
bool meshFilterLookupSucceeded = false;
bool meshRendererLookupSucceeded = false;
std::string observedInitialMeshPath;
std::string observedUpdatedMeshPath;
int32_t observedInitialMaterialCount = 0;
std::string observedInitialMaterial0Path;
bool observedInitialCastShadows = false;
bool observedInitialReceiveShadows = true;
int32_t observedInitialRenderLayer = 0;
int32_t observedUpdatedMaterialCount = 0;
std::string observedUpdatedMaterial1Path;
bool observedUpdatedCastShadows = true;
bool observedUpdatedReceiveShadows = false;
int32_t observedUpdatedRenderLayer = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasMeshFilter", hasMeshFilter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasMeshRenderer", hasMeshRenderer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshFilterLookupSucceeded", meshFilterLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMeshPath", observedInitialMeshPath));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMeshPath", observedUpdatedMeshPath));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterialCount", observedInitialMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterial0Path", observedInitialMaterial0Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialCastShadows", observedInitialCastShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialReceiveShadows", observedInitialReceiveShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialRenderLayer", observedInitialRenderLayer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterialCount", observedUpdatedMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterial1Path", observedUpdatedMaterial1Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedCastShadows", observedUpdatedCastShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedReceiveShadows", observedUpdatedReceiveShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedRenderLayer", observedUpdatedRenderLayer));
EXPECT_TRUE(hasMeshFilter);
EXPECT_TRUE(hasMeshRenderer);
EXPECT_TRUE(meshFilterLookupSucceeded);
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_EQ(observedInitialMeshPath, "Meshes/initial.mesh");
EXPECT_EQ(observedUpdatedMeshPath, "Meshes/runtime_override.mesh");
EXPECT_EQ(observedInitialMaterialCount, 1);
EXPECT_EQ(observedInitialMaterial0Path, "Materials/initial.mat");
EXPECT_TRUE(observedInitialCastShadows);
EXPECT_FALSE(observedInitialReceiveShadows);
EXPECT_EQ(observedInitialRenderLayer, 7);
EXPECT_EQ(observedUpdatedMaterialCount, 2);
EXPECT_EQ(observedUpdatedMaterial1Path, "Materials/runtime_override.mat");
EXPECT_FALSE(observedUpdatedCastShadows);
EXPECT_TRUE(observedUpdatedReceiveShadows);
EXPECT_EQ(observedUpdatedRenderLayer, 11);
EXPECT_EQ(meshFilter->GetMeshPath(), "Meshes/runtime_override.mesh");
ASSERT_EQ(meshRenderer->GetMaterialCount(), 2u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/initial.mat");
EXPECT_EQ(meshRenderer->GetMaterialPath(1), "Materials/runtime_override.mat");
EXPECT_FALSE(meshRenderer->GetCastShadows());
EXPECT_TRUE(meshRenderer->GetReceiveShadows());
EXPECT_EQ(meshRenderer->GetRenderLayer(), 11u);
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperHandlesClearAndBoundaryCases) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererEdgeCaseProbe");
meshRenderer->SetMaterialPath(0, "Materials/initial0.mat");
meshRenderer->SetMaterialPath(1, "Materials/initial1.mat");
meshRenderer->SetCastShadows(false);
meshRenderer->SetReceiveShadows(true);
meshRenderer->SetRenderLayer(9);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int32_t observedInitialMaterialCount = 0;
std::string observedNegativeIndexPath;
std::string observedOutOfRangePathBeforeClear;
std::string observedMaterial0PathAfterNegativeWrite;
std::string observedMaterial1PathAfterNegativeWrite;
int32_t observedMaterialCountAfterNegativeWrite = 0;
int32_t observedRenderLayerAfterNegativeWrite = -1;
int32_t observedMaterialCountAfterClear = -1;
std::string observedMaterial0PathAfterClear;
std::string observedMaterial3PathAfterClear;
bool observedCastShadowsAfterClear = true;
bool observedReceiveShadowsAfterClear = false;
int32_t observedRenderLayerAfterClear = -1;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterialCount", observedInitialMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedNegativeIndexPath", observedNegativeIndexPath));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedOutOfRangePathBeforeClear", observedOutOfRangePathBeforeClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial0PathAfterNegativeWrite", observedMaterial0PathAfterNegativeWrite));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial1PathAfterNegativeWrite", observedMaterial1PathAfterNegativeWrite));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterialCountAfterNegativeWrite", observedMaterialCountAfterNegativeWrite));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayerAfterNegativeWrite", observedRenderLayerAfterNegativeWrite));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterialCountAfterClear", observedMaterialCountAfterClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial0PathAfterClear", observedMaterial0PathAfterClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial3PathAfterClear", observedMaterial3PathAfterClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastShadowsAfterClear", observedCastShadowsAfterClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedReceiveShadowsAfterClear", observedReceiveShadowsAfterClear));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayerAfterClear", observedRenderLayerAfterClear));
EXPECT_EQ(observedInitialMaterialCount, 2);
EXPECT_EQ(observedNegativeIndexPath, "");
EXPECT_EQ(observedOutOfRangePathBeforeClear, "");
EXPECT_EQ(observedMaterial0PathAfterNegativeWrite, "Materials/initial0.mat");
EXPECT_EQ(observedMaterial1PathAfterNegativeWrite, "Materials/initial1.mat");
EXPECT_EQ(observedMaterialCountAfterNegativeWrite, 2);
EXPECT_EQ(observedRenderLayerAfterNegativeWrite, 0);
EXPECT_EQ(observedMaterialCountAfterClear, 0);
EXPECT_EQ(observedMaterial0PathAfterClear, "");
EXPECT_EQ(observedMaterial3PathAfterClear, "");
EXPECT_FALSE(observedCastShadowsAfterClear);
EXPECT_TRUE(observedReceiveShadowsAfterClear);
EXPECT_EQ(observedRenderLayerAfterClear, 0);
EXPECT_EQ(meshRenderer->GetMaterialCount(), 0u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "");
EXPECT_EQ(meshRenderer->GetMaterialPath(3), "");
EXPECT_FALSE(meshRenderer->GetCastShadows());
EXPECT_TRUE(meshRenderer->GetReceiveShadows());
EXPECT_EQ(meshRenderer->GetRenderLayer(), 0u);
}
TEST_F(MonoScriptRuntimeTest, GameObjectAddComponentApiCreatesBuiltinComponentsAndAvoidsDuplicates) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "AddComponentProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool initialHasCamera = true;
bool initialHasLight = true;
bool initialHasMeshFilter = true;
bool initialHasMeshRenderer = true;
bool addedTransform = false;
bool addedCamera = false;
bool addedLight = false;
bool addedMeshFilter = false;
bool addedMeshRenderer = false;
bool hasCameraAfterAdd = false;
bool hasLightAfterAdd = false;
bool hasMeshFilterAfterAdd = false;
bool hasMeshRendererAfterAdd = false;
bool cameraLookupSucceeded = false;
bool lightLookupSucceeded = false;
bool meshFilterLookupSucceeded = false;
bool meshRendererLookupSucceeded = false;
float observedCameraFieldOfView = 0.0f;
float observedLightIntensity = 0.0f;
std::string observedMeshPath;
int32_t observedMaterialCount = 0;
std::string observedMaterial0Path;
int32_t observedRenderLayer = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasCamera", initialHasCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasLight", initialHasLight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasMeshFilter", initialHasMeshFilter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasMeshRenderer", initialHasMeshRenderer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedTransform", addedTransform));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedCamera", addedCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedLight", addedLight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedMeshFilter", addedMeshFilter));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedMeshRenderer", addedMeshRenderer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasCameraAfterAdd", hasCameraAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasLightAfterAdd", hasLightAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasMeshFilterAfterAdd", hasMeshFilterAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasMeshRendererAfterAdd", hasMeshRendererAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshFilterLookupSucceeded", meshFilterLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCameraFieldOfView", observedCameraFieldOfView));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLightIntensity", observedLightIntensity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMeshPath", observedMeshPath));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterialCount", observedMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial0Path", observedMaterial0Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer));
EXPECT_FALSE(initialHasCamera);
EXPECT_FALSE(initialHasLight);
EXPECT_FALSE(initialHasMeshFilter);
EXPECT_FALSE(initialHasMeshRenderer);
EXPECT_TRUE(addedTransform);
EXPECT_TRUE(addedCamera);
EXPECT_TRUE(addedLight);
EXPECT_TRUE(addedMeshFilter);
EXPECT_TRUE(addedMeshRenderer);
EXPECT_TRUE(hasCameraAfterAdd);
EXPECT_TRUE(hasLightAfterAdd);
EXPECT_TRUE(hasMeshFilterAfterAdd);
EXPECT_TRUE(hasMeshRendererAfterAdd);
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_TRUE(lightLookupSucceeded);
EXPECT_TRUE(meshFilterLookupSucceeded);
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_FLOAT_EQ(observedCameraFieldOfView, 82.0f);
EXPECT_FLOAT_EQ(observedLightIntensity, 4.5f);
EXPECT_EQ(observedMeshPath, "Meshes/added.mesh");
EXPECT_EQ(observedMaterialCount, 1);
EXPECT_EQ(observedMaterial0Path, "Materials/added.mat");
EXPECT_EQ(observedRenderLayer, 6);
ASSERT_NE(host->GetTransform(), nullptr);
EXPECT_EQ(host->GetComponents<CameraComponent>().size(), 1u);
EXPECT_EQ(host->GetComponents<LightComponent>().size(), 1u);
EXPECT_EQ(host->GetComponents<MeshFilterComponent>().size(), 1u);
EXPECT_EQ(host->GetComponents<MeshRendererComponent>().size(), 1u);
CameraComponent* camera = host->GetComponent<CameraComponent>();
LightComponent* light = host->GetComponent<LightComponent>();
MeshFilterComponent* meshFilter = host->GetComponent<MeshFilterComponent>();
MeshRendererComponent* meshRenderer = host->GetComponent<MeshRendererComponent>();
ASSERT_NE(camera, nullptr);
ASSERT_NE(light, nullptr);
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 82.0f);
EXPECT_FLOAT_EQ(light->GetIntensity(), 4.5f);
EXPECT_EQ(meshFilter->GetMeshPath(), "Meshes/added.mesh");
ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/added.mat");
EXPECT_EQ(meshRenderer->GetRenderLayer(), 6u);
}
TEST_F(MonoScriptRuntimeTest, GameObjectComponentApiSupportsManagedScriptTypes) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "ScriptComponentApiProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool initialHasTarget = true;
bool initialLookupReturnedNull = false;
bool addedTarget = false;
bool hasTargetAfterAdd = false;
bool lookupSucceededAfterAdd = false;
bool tryGetSucceededAfterAdd = false;
bool returnedSameInstance = false;
bool targetEnabledAfterAdd = false;
int32_t observedTargetAwakeCount = 0;
int32_t observedTargetStartCount = -1;
int32_t observedTargetHostCallCount = 0;
std::string observedTargetHostName;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasTarget", initialHasTarget));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialLookupReturnedNull", initialLookupReturnedNull));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedTarget", addedTarget));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTargetAfterAdd", hasTargetAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LookupSucceededAfterAdd", lookupSucceededAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TryGetSucceededAfterAdd", tryGetSucceededAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ReturnedSameInstance", returnedSameInstance));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetEnabledAfterAdd", targetEnabledAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetAwakeCount", observedTargetAwakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetStartCount", observedTargetStartCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetHostCallCount", observedTargetHostCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetHostName", observedTargetHostName));
EXPECT_FALSE(initialHasTarget);
EXPECT_TRUE(initialLookupReturnedNull);
EXPECT_TRUE(addedTarget);
EXPECT_TRUE(hasTargetAfterAdd);
EXPECT_TRUE(lookupSucceededAfterAdd);
EXPECT_TRUE(tryGetSucceededAfterAdd);
EXPECT_TRUE(returnedSameInstance);
EXPECT_TRUE(targetEnabledAfterAdd);
EXPECT_EQ(observedTargetAwakeCount, 1);
EXPECT_EQ(observedTargetStartCount, 0);
EXPECT_EQ(observedTargetHostCallCount, 1);
EXPECT_EQ(observedTargetHostName, "Host");
ASSERT_EQ(host->GetComponents<ScriptComponent>().size(), 2u);
ScriptComponent* targetScript = FindScriptComponentByClass(host, "Gameplay", "ScriptComponentTargetProbe");
ASSERT_NE(targetScript, nullptr);
EXPECT_TRUE(runtime->HasManagedInstance(targetScript));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u);
int32_t awakeCount = 0;
int32_t startCount = -1;
int32_t hostCallCount = 0;
std::string hostName;
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "HostCallCount", hostCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "HostName", hostName));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 0);
EXPECT_EQ(hostCallCount, 1);
EXPECT_EQ(hostName, "Host");
engine->OnUpdate(0.016f);
startCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "StartCount", startCount));
EXPECT_EQ(startCount, 1);
}
TEST_F(MonoScriptRuntimeTest, GameObjectRuntimeApiCreatesFindsAndDestroysSceneObjects) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool missingBeforeCreate = false;
bool createdRootSucceeded = false;
bool createdChildSucceeded = false;
bool foundRootSucceeded = false;
bool foundChildSucceeded = false;
std::string observedFoundRootName;
std::string observedFoundChildParentName;
int32_t observedRootChildCountBeforeDestroy = 0;
bool cameraLookupSucceeded = false;
bool meshFilterLookupSucceeded = false;
bool meshRendererLookupSucceeded = false;
float observedCameraFieldOfView = 0.0f;
std::string observedMeshPath;
int32_t observedMaterialCount = 0;
std::string observedMaterial0Path;
int32_t observedRenderLayer = 0;
bool missingChildAfterDestroy = false;
bool foundRootAfterDestroySucceeded = false;
int32_t observedRootChildCountAfterDestroy = -1;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MissingBeforeCreate", missingBeforeCreate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CreatedRootSucceeded", createdRootSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CreatedChildSucceeded", createdChildSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundRootSucceeded", foundRootSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundChildSucceeded", foundChildSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFoundRootName", observedFoundRootName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFoundChildParentName", observedFoundChildParentName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountBeforeDestroy", observedRootChildCountBeforeDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshFilterLookupSucceeded", meshFilterLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCameraFieldOfView", observedCameraFieldOfView));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMeshPath", observedMeshPath));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterialCount", observedMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial0Path", observedMaterial0Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MissingChildAfterDestroy", missingChildAfterDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundRootAfterDestroySucceeded", foundRootAfterDestroySucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountAfterDestroy", observedRootChildCountAfterDestroy));
EXPECT_TRUE(missingBeforeCreate);
EXPECT_TRUE(createdRootSucceeded);
EXPECT_TRUE(createdChildSucceeded);
EXPECT_TRUE(foundRootSucceeded);
EXPECT_TRUE(foundChildSucceeded);
EXPECT_EQ(observedFoundRootName, "RuntimeCreatedRoot");
EXPECT_EQ(observedFoundChildParentName, "RuntimeCreatedRoot");
EXPECT_EQ(observedRootChildCountBeforeDestroy, 1);
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_TRUE(meshFilterLookupSucceeded);
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_FLOAT_EQ(observedCameraFieldOfView, 68.0f);
EXPECT_EQ(observedMeshPath, "Meshes/runtime_created.mesh");
EXPECT_EQ(observedMaterialCount, 1);
EXPECT_EQ(observedMaterial0Path, "Materials/runtime_created.mat");
EXPECT_EQ(observedRenderLayer, 4);
EXPECT_TRUE(missingChildAfterDestroy);
EXPECT_TRUE(foundRootAfterDestroySucceeded);
EXPECT_EQ(observedRootChildCountAfterDestroy, 0);
GameObject* createdRoot = runtimeScene->Find("RuntimeCreatedRoot");
ASSERT_NE(createdRoot, nullptr);
EXPECT_EQ(runtimeScene->Find("RuntimeCreatedChild"), nullptr);
EXPECT_EQ(createdRoot->GetParent(), nullptr);
EXPECT_EQ(createdRoot->GetChildCount(), 0u);
CameraComponent* camera = createdRoot->GetComponent<CameraComponent>();
MeshFilterComponent* meshFilter = createdRoot->GetComponent<MeshFilterComponent>();
MeshRendererComponent* meshRenderer = createdRoot->GetComponent<MeshRendererComponent>();
ASSERT_NE(camera, nullptr);
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 68.0f);
EXPECT_EQ(meshFilter->GetMeshPath(), "Meshes/runtime_created.mesh");
ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/runtime_created.mat");
EXPECT_EQ(meshRenderer->GetRenderLayer(), 4u);
}
TEST_F(MonoScriptRuntimeTest, GameObjectTagAndLayerApiExposeUnityStylePropertiesAndCompareTag) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->SetTag("Enemy");
host->SetLayer(7);
ScriptComponent* component = AddScript(host, "Gameplay", "TagLayerProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
std::string observedInitialTag;
int32_t observedInitialLayer = -1;
bool observedInitialCompareTag = false;
bool observedGameObjectRouteMatches = false;
std::string observedUpdatedTag;
int32_t observedUpdatedLayer = -1;
bool observedUpdatedCompareTag = false;
bool observedOriginalTagRejected = false;
bool observedEmptyTagRejected = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialTag", observedInitialTag));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialLayer", observedInitialLayer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialCompareTag", observedInitialCompareTag));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedGameObjectRouteMatches", observedGameObjectRouteMatches));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedTag", observedUpdatedTag));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedLayer", observedUpdatedLayer));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedCompareTag", observedUpdatedCompareTag));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedOriginalTagRejected", observedOriginalTagRejected));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEmptyTagRejected", observedEmptyTagRejected));
EXPECT_EQ(observedInitialTag, "Enemy");
EXPECT_EQ(observedInitialLayer, 7);
EXPECT_TRUE(observedInitialCompareTag);
EXPECT_TRUE(observedGameObjectRouteMatches);
EXPECT_EQ(observedUpdatedTag, "Player");
EXPECT_EQ(observedUpdatedLayer, 31);
EXPECT_TRUE(observedUpdatedCompareTag);
EXPECT_TRUE(observedOriginalTagRejected);
EXPECT_TRUE(observedEmptyTagRejected);
EXPECT_EQ(host->GetTag(), "Player");
EXPECT_EQ(host->GetLayer(), 31u);
EXPECT_EQ(runtimeScene->FindGameObjectWithTag("Player"), host);
EXPECT_EQ(runtimeScene->FindGameObjectWithTag("Enemy"), nullptr);
}
TEST_F(MonoScriptRuntimeTest, RuntimeCreatedScriptComponentCreatesManagedInstanceAfterClassAssignment) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
engine->OnRuntimeStart(runtimeScene);
GameObject* spawned = runtimeScene->CreateGameObject("RuntimeSpawned");
ScriptComponent* component = spawned->AddComponent<ScriptComponent>();
component->GetFieldStorage().SetFieldValue("Label", "RuntimeConfigured");
component->SetScriptClass("GameScripts", "Gameplay", "LifecycleProbe");
EXPECT_TRUE(runtime->HasManagedInstance(component));
engine->OnUpdate(0.016f);
int32_t awakeCount = 0;
int32_t enableCount = 0;
int32_t startCount = 0;
int32_t updateCount = 0;
std::string label;
std::string observedGameObjectName;
bool wasAwakened = false;
bool observedEnabled = false;
bool observedActiveSelf = false;
bool observedActiveInHierarchy = false;
bool observedIsActiveAndEnabled = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedGameObjectName", observedGameObjectName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "WasAwakened", wasAwakened));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(enableCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(label, "RuntimeConfigured|Awake");
EXPECT_EQ(observedGameObjectName, "RuntimeSpawned_Managed");
EXPECT_TRUE(wasAwakened);
EXPECT_TRUE(observedEnabled);
EXPECT_TRUE(observedActiveSelf);
EXPECT_TRUE(observedActiveInHierarchy);
EXPECT_TRUE(observedIsActiveAndEnabled);
EXPECT_EQ(spawned->GetName(), "RuntimeSpawned_Managed");
}
TEST_F(MonoScriptRuntimeTest, TransformHierarchyApiExposesParentChildAndReparenting) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* root = runtimeScene->CreateGameObject("Root");
GameObject* host = runtimeScene->CreateGameObject("Host", root);
GameObject* child = runtimeScene->CreateGameObject("Child", host);
GameObject* reparentTarget = runtimeScene->CreateGameObject("ReparentTarget");
ScriptComponent* component = AddScript(host, "Gameplay", "HierarchyProbe");
component->GetFieldStorage().SetFieldValue("ReparentTarget", GameObjectReference{reparentTarget->GetUUID()});
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int32_t observedChildCount = 0;
bool parentLookupSucceeded = false;
bool childLookupSucceeded = false;
std::string observedParentName;
std::string observedFirstChildName;
std::string reparentedChildParentName;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedChildCount", observedChildCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ParentLookupSucceeded", parentLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ChildLookupSucceeded", childLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedParentName", observedParentName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFirstChildName", observedFirstChildName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ReparentedChildParentName", reparentedChildParentName));
EXPECT_TRUE(parentLookupSucceeded);
EXPECT_TRUE(childLookupSucceeded);
EXPECT_EQ(observedChildCount, 1);
EXPECT_EQ(observedParentName, "Root");
EXPECT_EQ(observedFirstChildName, "Child");
EXPECT_EQ(reparentedChildParentName, "ReparentTarget");
EXPECT_EQ(host->GetParent(), root);
EXPECT_EQ(host->GetChildCount(), 0u);
EXPECT_EQ(child->GetParent(), reparentTarget);
EXPECT_EQ(reparentTarget->GetChildCount(), 1u);
}
TEST_F(MonoScriptRuntimeTest, UnityObjectApiGetComponentsReturnsDirectComponentsAndReusesManagedInstances) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* child = runtimeScene->CreateGameObject("Child", host);
host->AddComponent<CameraComponent>();
child->AddComponent<CameraComponent>();
child->AddComponent<MeshRendererComponent>();
AddScript(host, "Gameplay", "ObjectApiMarkerProbe");
AddScript(host, "Gameplay", "ObjectApiMarkerProbe");
ScriptComponent* hostProbe = AddScript(host, "Gameplay", "GetComponentsProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
int32_t observedTransformCount = -1;
int32_t observedCameraCount = -1;
int32_t observedCameraCountViaGameObject = -1;
int32_t observedMarkerCount = -1;
int32_t observedMeshRendererCount = -1;
bool observedAllMarkersNonNull = false;
bool observedFirstMarkerMatchesGetComponent = false;
bool observedMarkerInstancesAreDistinct = false;
bool observedTransformBoundToHost = false;
std::string observedFirstMarkerHostName;
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedTransformCount", observedTransformCount));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedCameraCount", observedCameraCount));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedCameraCountViaGameObject", observedCameraCountViaGameObject));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMarkerCount", observedMarkerCount));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMeshRendererCount", observedMeshRendererCount));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedAllMarkersNonNull", observedAllMarkersNonNull));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedFirstMarkerMatchesGetComponent", observedFirstMarkerMatchesGetComponent));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMarkerInstancesAreDistinct", observedMarkerInstancesAreDistinct));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedTransformBoundToHost", observedTransformBoundToHost));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedFirstMarkerHostName", observedFirstMarkerHostName));
EXPECT_EQ(observedTransformCount, 1);
EXPECT_EQ(observedCameraCount, 1);
EXPECT_EQ(observedCameraCountViaGameObject, 1);
EXPECT_EQ(observedMarkerCount, 2);
EXPECT_EQ(observedMeshRendererCount, 0);
EXPECT_TRUE(observedAllMarkersNonNull);
EXPECT_TRUE(observedFirstMarkerMatchesGetComponent);
EXPECT_TRUE(observedMarkerInstancesAreDistinct);
EXPECT_TRUE(observedTransformBoundToHost);
EXPECT_EQ(observedFirstMarkerHostName, "Host");
EXPECT_EQ(host->GetComponents<CameraComponent>().size(), 1u);
EXPECT_EQ(host->GetComponents<ScriptComponent>().size(), 3u);
EXPECT_EQ(child->GetComponents<CameraComponent>().size(), 1u);
EXPECT_EQ(child->GetComponents<MeshRendererComponent>().size(), 1u);
EXPECT_EQ(runtime->GetManagedInstanceCount(), 3u);
}
TEST_F(MonoScriptRuntimeTest, UnityObjectApiSupportsHierarchyLookupAndDestroy) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* root = runtimeScene->CreateGameObject("Root");
GameObject* host = runtimeScene->CreateGameObject("Host", root);
GameObject* child = runtimeScene->CreateGameObject("Child", host);
root->AddComponent<CameraComponent>();
child->AddComponent<CameraComponent>();
child->AddComponent<MeshRendererComponent>();
AddScript(root, "Gameplay", "ObjectApiMarkerProbe");
AddScript(child, "Gameplay", "ObjectApiDestroyTargetProbe");
ScriptComponent* hostProbe = AddScript(host, "Gameplay", "ObjectApiProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool foundSelfScriptViaParentLookup = false;
bool foundSelfScriptViaGameObjectParentLookup = false;
bool foundSelfTransformViaChildrenLookup = false;
bool foundSelfTransformViaGameObjectChildrenLookup = false;
bool foundCameraInParent = false;
bool foundCameraInParentViaGameObject = false;
bool foundMarkerInParent = false;
bool foundMeshRendererInChildren = false;
bool foundMeshRendererInChildrenViaGameObject = false;
bool foundTargetScriptInChildren = false;
bool childCameraMissingAfterDestroy = false;
bool childScriptMissingAfterDestroy = false;
bool childGameObjectMissingAfterDestroy = false;
int32_t observedChildScriptDisableCount = -1;
int32_t observedChildCountAfterDestroy = -1;
std::string observedParentCameraHostName;
std::string observedChildMeshRendererHostName;
std::string observedChildScriptHostName;
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfScriptViaParentLookup", foundSelfScriptViaParentLookup));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfScriptViaGameObjectParentLookup", foundSelfScriptViaGameObjectParentLookup));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfTransformViaChildrenLookup", foundSelfTransformViaChildrenLookup));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfTransformViaGameObjectChildrenLookup", foundSelfTransformViaGameObjectChildrenLookup));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundCameraInParent", foundCameraInParent));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundCameraInParentViaGameObject", foundCameraInParentViaGameObject));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMarkerInParent", foundMarkerInParent));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMeshRendererInChildren", foundMeshRendererInChildren));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMeshRendererInChildrenViaGameObject", foundMeshRendererInChildrenViaGameObject));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundTargetScriptInChildren", foundTargetScriptInChildren));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildCameraMissingAfterDestroy", childCameraMissingAfterDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildScriptMissingAfterDestroy", childScriptMissingAfterDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildGameObjectMissingAfterDestroy", childGameObjectMissingAfterDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildScriptDisableCount", observedChildScriptDisableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildCountAfterDestroy", observedChildCountAfterDestroy));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedParentCameraHostName", observedParentCameraHostName));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildMeshRendererHostName", observedChildMeshRendererHostName));
EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildScriptHostName", observedChildScriptHostName));
EXPECT_TRUE(foundSelfScriptViaParentLookup);
EXPECT_TRUE(foundSelfScriptViaGameObjectParentLookup);
EXPECT_TRUE(foundSelfTransformViaChildrenLookup);
EXPECT_TRUE(foundSelfTransformViaGameObjectChildrenLookup);
EXPECT_TRUE(foundCameraInParent);
EXPECT_TRUE(foundCameraInParentViaGameObject);
EXPECT_TRUE(foundMarkerInParent);
EXPECT_TRUE(foundMeshRendererInChildren);
EXPECT_TRUE(foundMeshRendererInChildrenViaGameObject);
EXPECT_TRUE(foundTargetScriptInChildren);
EXPECT_TRUE(childCameraMissingAfterDestroy);
EXPECT_TRUE(childScriptMissingAfterDestroy);
EXPECT_TRUE(childGameObjectMissingAfterDestroy);
EXPECT_EQ(observedChildScriptDisableCount, 1);
EXPECT_EQ(observedChildCountAfterDestroy, 0);
EXPECT_EQ(observedParentCameraHostName, "Root");
EXPECT_EQ(observedChildMeshRendererHostName, "Child");
EXPECT_EQ(observedChildScriptHostName, "Child");
EXPECT_EQ(runtimeScene->Find("Child"), nullptr);
EXPECT_EQ(host->GetChildCount(), 0u);
EXPECT_EQ(host->GetComponentInChildren<CameraComponent>(), nullptr);
EXPECT_EQ(host->GetComponentInChildren<MeshRendererComponent>(), nullptr);
EXPECT_EQ(host->GetComponents<ScriptComponent>().size(), 1u);
EXPECT_EQ(root->GetComponents<ScriptComponent>().size(), 1u);
EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u);
}
TEST_F(MonoScriptRuntimeTest, TransformSpaceApiReadsAndWritesWorldAndLocalValues) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* parent = runtimeScene->CreateGameObject("Parent");
GameObject* host = runtimeScene->CreateGameObject("Host", parent);
ScriptComponent* component = AddScript(host, "Gameplay", "TransformSpaceProbe");
parent->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
parent->GetTransform()->SetLocalScale(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
host->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
host->GetTransform()->SetLocalScale(XCEngine::Math::Vector3(1.0f, 1.0f, 1.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Math::Vector3 observedInitialWorldPosition;
XCEngine::Math::Vector3 observedInitialWorldScale;
XCEngine::Math::Vector3 observedInitialLocalEulerAngles;
XCEngine::Math::Vector3 observedEulerAfterSet;
XCEngine::Math::Vector3 observedWorldPositionAfterSet;
XCEngine::Math::Vector3 observedWorldScaleAfterSet;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialWorldPosition", observedInitialWorldPosition));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialWorldScale", observedInitialWorldScale));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialLocalEulerAngles", observedInitialLocalEulerAngles));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEulerAfterSet", observedEulerAfterSet));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldPositionAfterSet", observedWorldPositionAfterSet));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldScaleAfterSet", observedWorldScaleAfterSet));
EXPECT_EQ(observedInitialWorldPosition, XCEngine::Math::Vector3(12.0f, 4.0f, 6.0f));
EXPECT_EQ(observedInitialWorldScale, XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
EXPECT_EQ(observedInitialLocalEulerAngles, XCEngine::Math::Vector3(0.0f, 0.0f, 0.0f));
EXPECT_NEAR(observedEulerAfterSet.x, 0.0f, 0.001f);
EXPECT_NEAR(observedEulerAfterSet.y, 45.0f, 0.001f);
EXPECT_NEAR(observedEulerAfterSet.z, 0.0f, 0.001f);
EXPECT_EQ(observedWorldPositionAfterSet, XCEngine::Math::Vector3(20.0f, 6.0f, 10.0f));
EXPECT_EQ(observedWorldScaleAfterSet, XCEngine::Math::Vector3(6.0f, 8.0f, 10.0f));
EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(5.0f, 3.0f, 5.0f));
EXPECT_EQ(host->GetTransform()->GetPosition(), XCEngine::Math::Vector3(20.0f, 6.0f, 10.0f));
EXPECT_EQ(host->GetTransform()->GetLocalScale(), XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f));
EXPECT_EQ(host->GetTransform()->GetScale(), XCEngine::Math::Vector3(6.0f, 8.0f, 10.0f));
const XCEngine::Math::Quaternion& localRotation = host->GetTransform()->GetLocalRotation();
EXPECT_FLOAT_EQ(localRotation.x, 0.0f);
EXPECT_FLOAT_EQ(localRotation.y, 0.5f);
EXPECT_FLOAT_EQ(localRotation.z, 0.0f);
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
}
TEST_F(MonoScriptRuntimeTest, TransformMotionApiExposesDirectionVectorsAndAppliesOperations) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TransformMotionProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Math::Vector3 observedForward;
XCEngine::Math::Vector3 observedRight;
XCEngine::Math::Vector3 observedUp;
XCEngine::Math::Vector3 observedPositionAfterSelfTranslate;
XCEngine::Math::Vector3 observedPositionAfterWorldTranslate;
XCEngine::Math::Vector3 observedForwardAfterWorldRotate;
XCEngine::Math::Vector3 observedForwardAfterSelfRotate;
XCEngine::Math::Vector3 observedForwardAfterLookAt;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForward", observedForward));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRight", observedRight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUp", observedUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPositionAfterSelfTranslate", observedPositionAfterSelfTranslate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPositionAfterWorldTranslate", observedPositionAfterWorldTranslate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterWorldRotate", observedForwardAfterWorldRotate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterSelfRotate", observedForwardAfterSelfRotate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterLookAt", observedForwardAfterLookAt));
ExpectVector3Near(observedForward, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f));
ExpectVector3Near(observedRight, XCEngine::Math::Vector3(0.0f, 0.0f, -1.0f));
ExpectVector3Near(observedUp, XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f));
ExpectVector3Near(observedPositionAfterSelfTranslate, XCEngine::Math::Vector3(2.0f, 0.0f, 0.0f));
ExpectVector3Near(observedPositionAfterWorldTranslate, XCEngine::Math::Vector3(2.0f, 0.0f, 3.0f));
ExpectVector3Near(observedForwardAfterWorldRotate, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f));
ExpectVector3Near(observedForwardAfterSelfRotate, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f));
ExpectVector3Near(observedForwardAfterLookAt, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f));
ExpectVector3Near(host->GetTransform()->GetPosition(), XCEngine::Math::Vector3(2.0f, 0.0f, 3.0f));
ExpectVector3Near(host->GetTransform()->GetForward(), XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f));
}
TEST_F(MonoScriptRuntimeTest, TransformConversionApiTransformsPointsAndDirectionsBetweenSpaces) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TransformConversionProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Math::Vector3 observedWorldPoint;
XCEngine::Math::Vector3 observedLocalPoint;
XCEngine::Math::Vector3 observedWorldDirection;
XCEngine::Math::Vector3 observedLocalDirection;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldPoint", observedWorldPoint));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalPoint", observedLocalPoint));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldDirection", observedWorldDirection));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalDirection", observedLocalDirection));
ExpectVector3Near(observedWorldPoint, XCEngine::Math::Vector3(7.0f, 0.0f, 1.0f));
ExpectVector3Near(observedLocalPoint, XCEngine::Math::Vector3(0.0f, 0.0f, 2.0f));
ExpectVector3Near(observedWorldDirection, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f));
ExpectVector3Near(observedLocalDirection, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f));
ExpectVector3Near(host->GetTransform()->TransformPoint(XCEngine::Math::Vector3(0.0f, 0.0f, 2.0f)), XCEngine::Math::Vector3(7.0f, 0.0f, 1.0f));
ExpectVector3Near(host->GetTransform()->InverseTransformDirection(XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f)), XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f));
}
TEST_F(MonoScriptRuntimeTest, TransformOrientationOverloadsSupportAxisAngleAndCustomUpVector) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TransformOrientationProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
XCEngine::Math::Vector3 observedForwardAfterAxisAngleRotate;
XCEngine::Math::Vector3 observedForwardAfterWorldAxisAngleRotate;
XCEngine::Math::Vector3 observedForwardAfterDefaultLookAt;
XCEngine::Math::Vector3 observedRightAfterDefaultLookAt;
XCEngine::Math::Vector3 observedUpAfterDefaultLookAt;
XCEngine::Math::Vector3 observedForwardAfterLookAtWithUp;
XCEngine::Math::Vector3 observedRightAfterLookAtWithUp;
XCEngine::Math::Vector3 observedUpAfterLookAtWithUp;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterAxisAngleRotate", observedForwardAfterAxisAngleRotate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterWorldAxisAngleRotate", observedForwardAfterWorldAxisAngleRotate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterDefaultLookAt", observedForwardAfterDefaultLookAt));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRightAfterDefaultLookAt", observedRightAfterDefaultLookAt));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpAfterDefaultLookAt", observedUpAfterDefaultLookAt));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterLookAtWithUp", observedForwardAfterLookAtWithUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRightAfterLookAtWithUp", observedRightAfterLookAtWithUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpAfterLookAtWithUp", observedUpAfterLookAtWithUp));
ExpectVector3Near(observedForwardAfterAxisAngleRotate, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f));
ExpectVector3Near(observedForwardAfterWorldAxisAngleRotate, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f));
EXPECT_NEAR(observedForwardAfterDefaultLookAt.Magnitude(), 1.0f, 0.001f);
EXPECT_NEAR(observedRightAfterDefaultLookAt.Magnitude(), 1.0f, 0.001f);
EXPECT_NEAR(observedUpAfterDefaultLookAt.Magnitude(), 1.0f, 0.001f);
EXPECT_NEAR(observedForwardAfterLookAtWithUp.Magnitude(), 1.0f, 0.001f);
EXPECT_NEAR(observedRightAfterLookAtWithUp.Magnitude(), 1.0f, 0.001f);
EXPECT_NEAR(observedUpAfterLookAtWithUp.Magnitude(), 1.0f, 0.001f);
EXPECT_GT((observedRightAfterLookAtWithUp - observedRightAfterDefaultLookAt).Magnitude(), 0.5f);
EXPECT_GT((observedUpAfterLookAtWithUp - observedUpAfterDefaultLookAt).Magnitude(), 0.5f);
ExpectVector3Near(host->GetTransform()->GetForward(), observedForwardAfterLookAtWithUp);
ExpectVector3Near(host->GetTransform()->GetRight(), observedRightAfterLookAtWithUp);
ExpectVector3Near(host->GetTransform()->GetUp(), observedUpAfterLookAtWithUp);
}
TEST_F(MonoScriptRuntimeTest, ManagedBehaviourCanDisableItselfThroughEnabledProperty) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("DisableSelfOnFirstUpdate", true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t updateCount = 0;
int32_t lateUpdateCount = 0;
int32_t disableCount = 0;
bool observedEnabled = false;
bool observedIsActiveAndEnabled = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
EXPECT_FALSE(component->IsEnabled());
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 0);
EXPECT_EQ(disableCount, 1);
EXPECT_TRUE(observedEnabled);
EXPECT_TRUE(observedIsActiveAndEnabled);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_EQ(updateCount, 1);
}
TEST_F(MonoScriptRuntimeTest, ManagedScriptCanDeactivateItsHostGameObject) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("DeactivateGameObjectOnFirstUpdate", true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t updateCount = 0;
int32_t lateUpdateCount = 0;
int32_t disableCount = 0;
bool observedActiveSelf = false;
bool observedActiveInHierarchy = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy));
EXPECT_FALSE(host->IsActive());
EXPECT_FALSE(host->IsActiveInHierarchy());
EXPECT_TRUE(component->IsEnabled());
EXPECT_EQ(updateCount, 1);
EXPECT_EQ(lateUpdateCount, 0);
EXPECT_EQ(disableCount, 1);
EXPECT_TRUE(observedActiveSelf);
EXPECT_TRUE(observedActiveInHierarchy);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_EQ(updateCount, 1);
}
TEST_F(MonoScriptRuntimeTest, DisableEnablePreservesSingleInstanceAndStartOnlyRunsOnce) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
component->SetEnabled(false);
int32_t disableCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount));
EXPECT_EQ(disableCount, 1);
EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u);
component->SetEnabled(true);
engine->OnUpdate(0.016f);
int32_t enableCount = 0;
int32_t startCount = 0;
int32_t updateCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_EQ(enableCount, 2);
EXPECT_EQ(startCount, 1);
EXPECT_EQ(updateCount, 2);
engine->OnRuntimeStop();
EXPECT_FALSE(runtime->HasManagedInstance(component));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 0u);
}
} // namespace