Files
XCEngine/tests/scripting/test_mono_script_runtime.cpp

2753 lines
138 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.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/Input/InputManager.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
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);
}
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();
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
engine->SetRuntime(runtime.get());
}
void TearDown() override {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
XCEngine::Input::InputManager::Get().Shutdown();
runtime.reset();
scene.reset();
}
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, 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, 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, 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, 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