#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 CollectMessagesWithPrefix(const char* prefix) const { std::vector 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 entries; }; ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { if (!gameObject) { return nullptr; } for (ScriptComponent* component : gameObject->GetComponents()) { if (!component) { continue; } if (component->GetNamespaceName() == namespaceName && component->GetClassName() == className) { return component; } } return nullptr; } MonoScriptRuntime::Settings CreateMonoSettings() { MonoScriptRuntime::Settings settings; settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; settings.corlibDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; settings.coreAssemblyPath = XCENGINE_TEST_SCRIPT_CORE_DLL; settings.appAssemblyPath = XCENGINE_TEST_GAME_SCRIPTS_DLL; return settings; } class MonoScriptRuntimeTest : public ::testing::Test { protected: void SetUp() override { engine = &ScriptEngine::Get(); engine->OnRuntimeStop(); engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime); XCEngine::Input::InputManager::Get().Shutdown(); XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor(); runtime = std::make_unique(CreateMonoSettings()); ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); engine->SetRuntime(runtime.get()); } void TearDown() override { engine->OnRuntimeStop(); engine->SetRuntime(nullptr); XCEngine::Input::InputManager::Get().Shutdown(); XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor(); runtime.reset(); scene.reset(); } Scene* CreateScene(const std::string& sceneName) { scene = std::make_unique(sceneName); return scene.get(); } ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { ScriptComponent* component = gameObject->AddComponent(); component->SetScriptClass("GameScripts", namespaceName, className); return component; } ScriptEngine* engine = nullptr; std::unique_ptr runtime; std::unique_ptr scene; }; TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasses) { const std::vector 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 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 fields; EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "FieldMetadataProbe", fields)); const std::vector 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 fields = { {"Sentinel", ScriptFieldType::Bool}, }; EXPECT_FALSE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "MissingProbe", fields)); EXPECT_TRUE(fields.empty()); } TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsManagedInitializers) { std::vector 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(fieldIt->value), -1); } TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsEnumInitializersAsInt32) { std::vector 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(fieldIt->value), 2); } TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers) { std::vector 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(fieldIt->value)); } TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsConcreteComponentReferenceFields) { std::vector fields; EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "ComponentFieldMetadataProbe", fields)); const std::vector expected = { {"Pivot", ScriptFieldType::Component}, {"SceneCamera", ScriptFieldType::Component}, {"ScriptTarget", ScriptFieldType::Component}, }; EXPECT_EQ(fields, expected); } TEST_F(MonoScriptRuntimeTest, ManagedGraphicsSettingsRoundTripsRenderPipelineAssetSelection) { Scene* runtimeScene = CreateScene("ManagedRenderPipelineSelectionScene"); GameObject* scriptObject = runtimeScene->CreateGameObject("RenderPipelineProbe"); ScriptComponent* script = AddScript(scriptObject, "Gameplay", "RenderPipelineApiProbe"); ASSERT_NE(script, nullptr); engine->OnRuntimeStart(runtimeScene); engine->OnUpdate(0.016f); const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = XCEngine::Rendering::Pipelines::GetManagedRenderPipelineAssetDescriptor(); EXPECT_EQ(descriptor.assemblyName, "GameScripts"); EXPECT_EQ(descriptor.namespaceName, "Gameplay"); EXPECT_EQ(descriptor.className, "RenderPipelineApiProbeAsset"); bool selectionRoundTripSucceeded = false; std::string selectedPipelineTypeName; EXPECT_TRUE(runtime->TryGetFieldValue( script, "SelectionRoundTripSucceeded", selectionRoundTripSucceeded)); EXPECT_TRUE(selectionRoundTripSucceeded); EXPECT_TRUE(runtime->TryGetFieldValue( script, "SelectedPipelineTypeName", selectedPipelineTypeName)); EXPECT_EQ( selectedPipelineTypeName, "Gameplay.RenderPipelineApiProbeAsset"); } TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsNullComponentReferences) { std::vector 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(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* 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 messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]"); const std::vector 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("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("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("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("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(); GameObject* pivotTarget = runtimeScene->CreateGameObject("PivotTarget"); GameObject* cameraTarget = runtimeScene->CreateGameObject("CameraTarget"); cameraTarget->AddComponent(); 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("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 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 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(fieldIt->defaultValue), -1); EXPECT_EQ(std::get(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 requests = { {"ObservedRootChildCountAfterDestroy"}, {"LegacyOnly"}, {"DoesNotExist"}, {""} }; std::vector 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& 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(awakeSnapshot->defaultValue), 0); EXPECT_EQ(std::get(awakeSnapshot->value), 0); EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); EXPECT_EQ(std::get(labelSnapshot->value), "Stored"); EXPECT_EQ(std::get(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(speedSnapshot->defaultValue), 0.0f); EXPECT_FLOAT_EQ(std::get(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(legacySnapshot->value), 99u); EXPECT_EQ(std::get(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(awakeSnapshot->defaultValue), 0); EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); EXPECT_EQ(std::get(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(speedSnapshot->defaultValue), 0.0f); EXPECT_FLOAT_EQ(std::get(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(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(); LightComponent* light = host->AddComponent(); 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(); MeshRendererComponent* meshRenderer = host->AddComponent(); 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(); 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().size(), 1u); EXPECT_EQ(host->GetComponents().size(), 1u); EXPECT_EQ(host->GetComponents().size(), 1u); EXPECT_EQ(host->GetComponents().size(), 1u); CameraComponent* camera = host->GetComponent(); LightComponent* light = host->GetComponent(); MeshFilterComponent* meshFilter = host->GetComponent(); MeshRendererComponent* meshRenderer = host->GetComponent(); 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().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(); MeshFilterComponent* meshFilter = createdRoot->GetComponent(); MeshRendererComponent* meshRenderer = createdRoot->GetComponent(); 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(); 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(); child->AddComponent(); child->AddComponent(); 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().size(), 1u); EXPECT_EQ(host->GetComponents().size(), 3u); EXPECT_EQ(child->GetComponents().size(), 1u); EXPECT_EQ(child->GetComponents().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(); child->AddComponent(); child->AddComponent(); 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(), nullptr); EXPECT_EQ(host->GetComponentInChildren(), nullptr); EXPECT_EQ(host->GetComponents().size(), 1u); EXPECT_EQ(root->GetComponents().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