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