From e0e5c1fcaacf1d2d96cb7f5d1b2338e9c11be429 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 16:51:42 +0800 Subject: [PATCH] Add concrete component script field support --- docs/plan/C#脚本模块下一阶段计划.md | 8 + .../Scripting/Mono/MonoScriptRuntime.h | 4 + .../include/XCEngine/Scripting/ScriptField.h | 20 +- .../XCEngine/Scripting/ScriptFieldStorage.h | 5 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 172 ++++++++++++++++ engine/src/Scripting/ScriptField.cpp | 21 ++ managed/CMakeLists.txt | 1 + .../GameScripts/ComponentReferenceProbe.cs | 70 +++++++ tests/scripting/test_mono_script_runtime.cpp | 194 ++++++++++++++++++ tests/scripting/test_script_component.cpp | 4 + tests/scripting/test_script_field_storage.cpp | 12 +- 11 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 managed/GameScripts/ComponentReferenceProbe.cs diff --git a/docs/plan/C#脚本模块下一阶段计划.md b/docs/plan/C#脚本模块下一阶段计划.md index e89cb34c..cc148006 100644 --- a/docs/plan/C#脚本模块下一阶段计划.md +++ b/docs/plan/C#脚本模块下一阶段计划.md @@ -277,3 +277,11 @@ C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0 - `ProjectScriptAssemblyTest.*` 现改为使用独立的测试项目程序集输出目录,不再依赖真实 `project/Library/ScriptAssemblies`,避免与 editor / Mono 持有的文件锁互相干扰。 - 已通过完整 `ScriptFieldStorage_Test.*:MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 验证;输出全绿,`exit code 3` 仍为既有历史现象。 - 字段系统下一步建议切到:组件引用字段支持 +- 已完成字段系统第三小项:具体组件引用字段支持 +- `MonoScriptRuntime` 现已支持把常用具体组件字段纳入脚本字段模型,包括 `Transform`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`,以及具体脚本类字段(`MonoBehaviour` 子类)。 +- 字段存储层已新增 `ScriptFieldType::Component` 与 `ComponentReference`,序列化格式同时保存 `gameObjectUUID` 与 `scriptComponentUUID`,从而能区分内建组件引用与脚本组件引用。 +- 当前刻意暂不支持抽象基类字段:`Component`、`Behaviour`、`MonoBehaviour`;这样先保证字段定位与场景 roundtrip 语义稳定,不把抽象匹配和 editor 复杂度提前拉进主线。 +- 已新增 `ComponentFieldMetadataProbe` / `ComponentFieldProbe` 与对应 C++ 回归测试,覆盖字段发现、默认空引用、存储值应用、运行时写回,以及场景 roundtrip 恢复。 +- 本步定向测试 `ScriptFieldStorage_Test.*:ScriptComponent_Test.*:MonoScriptRuntimeTest.ClassFieldMetadataListsConcreteComponentReferenceFields:MonoScriptRuntimeTest.ClassFieldDefaultValueQueryReturnsNullComponentReferences:MonoScriptRuntimeTest.ComponentReferenceFieldsApplyStoredValuesAndPersistAcrossSceneRoundTrip` 全部通过。 +- 扩大回归 `ScriptFieldStorage_Test.*:MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 日志层面全绿;当前测试进程仍存在历史性的异常退出码现象,需要后续单独跟踪,不是本步引入的问题。 +- 字段系统下一步建议切到:资源引用字段,或等 editor Inspector 重构稳定后,再补组件字段的可视化选择器。 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 255abc2d..8b8bb248 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -111,6 +111,7 @@ private: struct FieldMetadata { ScriptFieldType type = ScriptFieldType::None; MonoClassField* field = nullptr; + MonoClass* componentClass = nullptr; bool isEnum = false; int32_t enumUnderlyingType = 0; }; @@ -154,6 +155,8 @@ private: bool DiscoverScriptClasses(); void DiscoverScriptClassesInImage(const std::string& assemblyName, MonoImage* image); bool IsMonoBehaviourSubclass(MonoClass* monoClass) const; + bool IsSupportedComponentFieldClass(MonoClass* monoClass) const; + bool IsScriptComponentFieldClass(MonoClass* monoClass) const; FieldMetadata BuildFieldMetadata(MonoClassField* field) const; @@ -174,6 +177,7 @@ private: bool ApplyStoredFields(const ScriptRuntimeContext& context, const ClassMetadata& metadata, MonoObject* instance); MonoObject* CreateManagedGameObject(uint64_t gameObjectUUID); bool HasSerializeFieldAttribute(MonoClassField* field) const; + bool TryExtractComponentReference(MonoObject* managedObject, ComponentReference& outReference) const; bool TryExtractGameObjectReference(MonoObject* managedObject, GameObjectReference& outReference) const; bool TrySetFieldValue(MonoObject* instance, const FieldMetadata& fieldMetadata, const ScriptFieldValue& value); bool TryReadFieldValue(MonoObject* instance, const FieldMetadata& fieldMetadata, ScriptFieldValue& outValue) const; diff --git a/engine/include/XCEngine/Scripting/ScriptField.h b/engine/include/XCEngine/Scripting/ScriptField.h index 43cd8f4d..fbd554f3 100644 --- a/engine/include/XCEngine/Scripting/ScriptField.h +++ b/engine/include/XCEngine/Scripting/ScriptField.h @@ -23,7 +23,8 @@ enum class ScriptFieldType { Vector2, Vector3, Vector4, - GameObject + GameObject, + Component }; struct GameObjectReference { @@ -38,6 +39,20 @@ struct GameObjectReference { } }; +struct ComponentReference { + uint64_t gameObjectUUID = 0; + uint64_t scriptComponentUUID = 0; + + bool operator==(const ComponentReference& other) const { + return gameObjectUUID == other.gameObjectUUID + && scriptComponentUUID == other.scriptComponentUUID; + } + + bool operator!=(const ComponentReference& other) const { + return !(*this == other); + } +}; + struct ScriptFieldMetadata { std::string name; ScriptFieldType type = ScriptFieldType::None; @@ -99,7 +114,8 @@ using ScriptFieldValue = std::variant< Math::Vector2, Math::Vector3, Math::Vector4, - GameObjectReference>; + GameObjectReference, + ComponentReference>; struct ScriptFieldDefaultValue { std::string fieldName; diff --git a/engine/include/XCEngine/Scripting/ScriptFieldStorage.h b/engine/include/XCEngine/Scripting/ScriptFieldStorage.h index e7b23589..8639aad0 100644 --- a/engine/include/XCEngine/Scripting/ScriptFieldStorage.h +++ b/engine/include/XCEngine/Scripting/ScriptFieldStorage.h @@ -70,6 +70,11 @@ struct ScriptFieldTypeResolver { static constexpr ScriptFieldType value = ScriptFieldType::GameObject; }; +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Component; +}; + class ScriptFieldStorage { public: template diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index c7c2bdfb..a254b3e3 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -92,6 +92,15 @@ std::string SafeString(const char* value) { return value ? std::string(value) : std::string(); } +bool IsMonoClassOrSubclass(MonoClass* monoClass, MonoClass* potentialBaseClass) { + if (!monoClass || !potentialBaseClass) { + return false; + } + + return monoClass == potentialBaseClass + || mono_class_is_subclass_of(monoClass, potentialBaseClass, 0) != 0; +} + std::string MonoStringToUtf8(MonoString* stringObject) { if (!stringObject) { return std::string(); @@ -2316,6 +2325,40 @@ bool MonoScriptRuntime::IsMonoBehaviourSubclass(MonoClass* monoClass) const { return false; } +bool MonoScriptRuntime::IsSupportedComponentFieldClass(MonoClass* monoClass) const { + if (!IsMonoClassOrSubclass(monoClass, m_componentClass)) { + return false; + } + + if (monoClass == m_componentClass || monoClass == m_behaviourClass || monoClass == m_monoBehaviourClass) { + return false; + } + + if (IsScriptComponentFieldClass(monoClass)) { + return true; + } + + const std::string namespaceName = SafeString(mono_class_get_namespace(monoClass)); + const std::string className = SafeString(mono_class_get_name(monoClass)); + if (namespaceName != m_settings.baseNamespace) { + return false; + } + + return className == "Transform" + || className == "Camera" + || className == "Light" + || className == "MeshFilter" + || className == "MeshRenderer"; +} + +bool MonoScriptRuntime::IsScriptComponentFieldClass(MonoClass* monoClass) const { + if (!monoClass || monoClass == m_monoBehaviourClass) { + return false; + } + + return IsMonoClassOrSubclass(monoClass, m_monoBehaviourClass); +} + bool MonoScriptRuntime::HasSerializeFieldAttribute(MonoClassField* field) const { if (!field || !m_serializeFieldAttributeClass) { return false; @@ -2377,6 +2420,12 @@ MonoScriptRuntime::FieldMetadata MonoScriptRuntime::BuildFieldMetadata(MonoClass const std::string className = SafeString(mono_class_get_name(referenceClass)); if (namespaceName == m_settings.baseNamespace && className == "GameObject") { metadata.type = ScriptFieldType::GameObject; + return metadata; + } + + if (IsSupportedComponentFieldClass(referenceClass)) { + metadata.type = ScriptFieldType::Component; + metadata.componentClass = referenceClass; } return metadata; @@ -2593,6 +2642,40 @@ MonoObject* MonoScriptRuntime::CreateManagedGameObject(uint64_t gameObjectUUID) return managedObject; } +bool MonoScriptRuntime::TryExtractComponentReference( + MonoObject* managedObject, + ComponentReference& outReference) const { + outReference = ComponentReference{}; + if (!managedObject) { + return true; + } + + if (!m_componentClass || !m_gameObjectUUIDField) { + return false; + } + + SetCurrentDomain(); + + MonoClass* monoClass = mono_object_get_class(managedObject); + if (!IsMonoClassOrSubclass(monoClass, m_componentClass)) { + return false; + } + + uint64_t gameObjectUUID = 0; + mono_field_get_value(managedObject, m_gameObjectUUIDField, &gameObjectUUID); + if (gameObjectUUID == 0) { + return false; + } + + uint64_t scriptComponentUUID = 0; + if (m_scriptComponentUUIDField && IsMonoClassOrSubclass(monoClass, m_behaviourClass)) { + mono_field_get_value(managedObject, m_scriptComponentUUIDField, &scriptComponentUUID); + } + + outReference = ComponentReference{gameObjectUUID, scriptComponentUUID}; + return true; +} + bool MonoScriptRuntime::DestroyManagedObject(MonoObject* managedObject) { if (!m_initialized || !managedObject) { return false; @@ -2811,6 +2894,85 @@ bool MonoScriptRuntime::TrySetFieldValue( mono_field_set_value(instance, fieldMetadata.field, managedGameObject); return true; } + case ScriptFieldType::Component: { + const ComponentReference reference = std::get(value); + if (reference.gameObjectUUID == 0 && reference.scriptComponentUUID == 0) { + mono_field_set_value(instance, fieldMetadata.field, nullptr); + return true; + } + + if (!fieldMetadata.componentClass) { + return false; + } + + MonoObject* managedComponent = nullptr; + if (IsScriptComponentFieldClass(fieldMetadata.componentClass)) { + if (reference.gameObjectUUID == 0 || reference.scriptComponentUUID == 0) { + return false; + } + + ScriptComponent* component = FindScriptComponentByUUID(reference.scriptComponentUUID); + if (!component + || !component->GetGameObject() + || component->GetGameObject()->GetUUID() != reference.gameObjectUUID + || component->GetAssemblyName() != m_settings.appAssemblyName + || component->GetNamespaceName() != SafeString(mono_class_get_namespace(fieldMetadata.componentClass)) + || component->GetClassName() != SafeString(mono_class_get_name(fieldMetadata.componentClass))) { + return false; + } + + if (!HasManagedInstance(component)) { + ScriptEngine::Get().OnScriptComponentEnabled(component); + } + + managedComponent = GetManagedInstanceObject(component); + if (!managedComponent) { + return false; + } + } else { + if (reference.gameObjectUUID == 0 || reference.scriptComponentUUID != 0) { + return false; + } + + Components::GameObject* targetObject = FindGameObjectByUUID(reference.gameObjectUUID); + if (!targetObject) { + return false; + } + + const std::string namespaceName = SafeString(mono_class_get_namespace(fieldMetadata.componentClass)); + const std::string className = SafeString(mono_class_get_name(fieldMetadata.componentClass)); + if (namespaceName != m_settings.baseNamespace) { + return false; + } + + bool hasComponent = false; + if (className == "Transform") { + hasComponent = targetObject->GetTransform() != nullptr; + } else if (className == "Camera") { + hasComponent = targetObject->GetComponent() != nullptr; + } else if (className == "Light") { + hasComponent = targetObject->GetComponent() != nullptr; + } else if (className == "MeshFilter") { + hasComponent = targetObject->GetComponent() != nullptr; + } else if (className == "MeshRenderer") { + hasComponent = targetObject->GetComponent() != nullptr; + } else { + return false; + } + + if (!hasComponent) { + return false; + } + + managedComponent = CreateManagedComponentWrapper(fieldMetadata.componentClass, reference.gameObjectUUID); + if (!managedComponent) { + return false; + } + } + + mono_field_set_value(instance, fieldMetadata.field, managedComponent); + return true; + } case ScriptFieldType::None: return false; } @@ -2947,6 +3109,16 @@ bool MonoScriptRuntime::TryReadFieldValue( outValue = reference; return true; } + case ScriptFieldType::Component: { + MonoObject* managedObject = mono_field_get_value_object(m_appDomain, fieldMetadata.field, instance); + ComponentReference reference; + if (!TryExtractComponentReference(managedObject, reference)) { + return false; + } + + outValue = reference; + return true; + } case ScriptFieldType::None: return false; } diff --git a/engine/src/Scripting/ScriptField.cpp b/engine/src/Scripting/ScriptField.cpp index 744a3b5b..6e7dfc9c 100644 --- a/engine/src/Scripting/ScriptField.cpp +++ b/engine/src/Scripting/ScriptField.cpp @@ -75,6 +75,7 @@ std::string ScriptFieldTypeToString(ScriptFieldType type) { case ScriptFieldType::Vector3: return "Vector3"; case ScriptFieldType::Vector4: return "Vector4"; case ScriptFieldType::GameObject: return "GameObject"; + case ScriptFieldType::Component: return "Component"; } return "None"; @@ -103,6 +104,8 @@ bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType) outType = ScriptFieldType::Vector4; } else if (value == "GameObject") { outType = ScriptFieldType::GameObject; + } else if (value == "Component") { + outType = ScriptFieldType::Component; } else { return false; } @@ -123,6 +126,7 @@ bool IsScriptFieldValueCompatible(ScriptFieldType type, const ScriptFieldValue& case ScriptFieldType::Vector3: return std::holds_alternative(value); case ScriptFieldType::Vector4: return std::holds_alternative(value); case ScriptFieldType::GameObject: return std::holds_alternative(value); + case ScriptFieldType::Component: return std::holds_alternative(value); } return false; @@ -141,6 +145,7 @@ ScriptFieldValue CreateDefaultScriptFieldValue(ScriptFieldType type) { case ScriptFieldType::Vector3: return Math::Vector3::Zero(); case ScriptFieldType::Vector4: return Math::Vector4::Zero(); case ScriptFieldType::GameObject: return GameObjectReference{}; + case ScriptFieldType::Component: return ComponentReference{}; } return std::monostate{}; @@ -180,6 +185,10 @@ std::string SerializeScriptFieldValue(ScriptFieldType type, const ScriptFieldVal } case ScriptFieldType::GameObject: return std::to_string(std::get(value).gameObjectUUID); + case ScriptFieldType::Component: { + const ComponentReference reference = std::get(value); + return std::to_string(reference.gameObjectUUID) + "," + std::to_string(reference.scriptComponentUUID); + } } return std::string(); @@ -236,6 +245,18 @@ bool TryDeserializeScriptFieldValue(ScriptFieldType type, const std::string& val case ScriptFieldType::GameObject: outValue = GameObjectReference{static_cast(std::stoull(value))}; return true; + case ScriptFieldType::Component: { + const size_t separator = value.find(','); + if (separator == std::string::npos) { + return false; + } + + outValue = ComponentReference{ + static_cast(std::stoull(value.substr(0, separator))), + static_cast(std::stoull(value.substr(separator + 1))) + }; + return true; + } } } catch (...) { return false; diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 7fffaa59..9061ce96 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -125,6 +125,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/AddComponentProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ComponentReferenceProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/EnumFieldProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/FieldMetadataProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/GetComponentsProbe.cs diff --git a/managed/GameScripts/ComponentReferenceProbe.cs b/managed/GameScripts/ComponentReferenceProbe.cs new file mode 100644 index 00000000..3257b43c --- /dev/null +++ b/managed/GameScripts/ComponentReferenceProbe.cs @@ -0,0 +1,70 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class ComponentFieldMetadataProbe : MonoBehaviour + { + public Transform Pivot; + public Camera SceneCamera; + public ScriptComponentTargetProbe ScriptTarget; + public Component UnsupportedComponent; + public Behaviour UnsupportedBehaviour; + public MonoBehaviour UnsupportedMonoBehaviour; + } + + public sealed class ComponentFieldProbe : MonoBehaviour + { + public Transform Pivot; + public Camera SceneCamera; + public ScriptComponentTargetProbe ScriptTarget; + + public bool ObservedStoredPivotApplied; + public bool ObservedStoredCameraApplied; + public bool ObservedStoredScriptApplied; + public string ObservedPivotName = string.Empty; + public string ObservedCameraName = string.Empty; + public string ObservedScriptName = string.Empty; + public int ObservedScriptAwakeCount = -1; + public int ObservedScriptHostCallCount = -1; + + public bool ObservedUpdatedPivotAssigned; + public bool ObservedUpdatedCameraAssigned; + public bool ObservedUpdatedScriptAssigned; + public string ObservedUpdatedPivotName = string.Empty; + public string ObservedUpdatedCameraName = string.Empty; + public string ObservedUpdatedScriptName = string.Empty; + + public void Start() + { + ObservedStoredPivotApplied = Pivot != null; + ObservedStoredCameraApplied = SceneCamera != null; + ObservedStoredScriptApplied = ScriptTarget != null; + + ObservedPivotName = Pivot != null ? Pivot.gameObject.name : string.Empty; + ObservedCameraName = SceneCamera != null ? SceneCamera.gameObject.name : string.Empty; + ObservedScriptName = ScriptTarget != null ? ScriptTarget.gameObject.name : string.Empty; + + if (ScriptTarget != null) + { + ObservedScriptAwakeCount = ScriptTarget.AwakeCount; + ScriptTarget.IncrementFromHost(); + ObservedScriptHostCallCount = ScriptTarget.HostCallCount; + } + + Pivot = transform; + SceneCamera = GetComponent(); + ScriptTarget = GetComponent(); + if (ScriptTarget == null) + { + ScriptTarget = AddComponent(); + } + + ObservedUpdatedPivotAssigned = Pivot != null; + ObservedUpdatedCameraAssigned = SceneCamera != null; + ObservedUpdatedScriptAssigned = ScriptTarget != null; + ObservedUpdatedPivotName = Pivot != null ? Pivot.gameObject.name : string.Empty; + ObservedUpdatedCameraName = SceneCamera != null ? SceneCamera.gameObject.name : string.Empty; + ObservedUpdatedScriptName = ScriptTarget != null ? ScriptTarget.gameObject.name : string.Empty; + } + } +} diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 495736c7..df68d139 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -248,6 +248,43 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsSerializeFieldPr 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, 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"); @@ -968,6 +1005,163 @@ TEST_F(MonoScriptRuntimeTest, SerializeFieldPrivateFieldsApplyStoredValuesAndPer 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"); diff --git a/tests/scripting/test_script_component.cpp b/tests/scripting/test_script_component.cpp index 61907594..708a9487 100644 --- a/tests/scripting/test_script_component.cpp +++ b/tests/scripting/test_script_component.cpp @@ -41,6 +41,7 @@ TEST(ScriptComponent_Test, SerializeRoundTripPreservesMetadataAndFields) { ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("DisplayName", std::string("Hero=One;Ready|100%"))); ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("MoveSpeed", 6.25f)); ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("Target", GameObjectReference{9988})); + ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("TargetCamera", ComponentReference{9988, 0})); std::ostringstream stream; original.Serialize(stream); @@ -52,6 +53,7 @@ TEST(ScriptComponent_Test, SerializeRoundTripPreservesMetadataAndFields) { std::string displayName; float moveSpeed = 0.0f; GameObjectReference target; + ComponentReference targetCamera; EXPECT_EQ(restored.GetScriptComponentUUID(), original.GetScriptComponentUUID()); EXPECT_EQ(restored.GetAssemblyName(), "GameScripts.Runtime"); @@ -61,9 +63,11 @@ TEST(ScriptComponent_Test, SerializeRoundTripPreservesMetadataAndFields) { EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("DisplayName", displayName)); EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("MoveSpeed", moveSpeed)); EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("Target", target)); + EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("TargetCamera", targetCamera)); EXPECT_EQ(displayName, "Hero=One;Ready|100%"); EXPECT_FLOAT_EQ(moveSpeed, 6.25f); EXPECT_EQ(target, GameObjectReference{9988}); + EXPECT_EQ(targetCamera, (ComponentReference{9988, 0})); } TEST(ScriptComponent_Test, ComponentFactoryRegistryCreatesScriptComponents) { diff --git a/tests/scripting/test_script_field_storage.cpp b/tests/scripting/test_script_field_storage.cpp index ec0b145c..6e472f35 100644 --- a/tests/scripting/test_script_field_storage.cpp +++ b/tests/scripting/test_script_field_storage.cpp @@ -20,6 +20,7 @@ TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) { EXPECT_TRUE(storage.SetFieldValue("Velocity", Vector3(3.0f, 4.0f, 5.0f))); EXPECT_TRUE(storage.SetFieldValue("Tint", Vector4(0.1f, 0.2f, 0.3f, 1.0f))); EXPECT_TRUE(storage.SetFieldValue("Target", GameObjectReference{42})); + EXPECT_TRUE(storage.SetFieldValue("TargetCamera", ComponentReference{42, 0})); float speed = 0.0f; double accuracy = 0.0; @@ -31,8 +32,9 @@ TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) { Vector3 velocity; Vector4 tint; GameObjectReference target; + ComponentReference targetCamera; - EXPECT_EQ(storage.GetFieldCount(), 10u); + EXPECT_EQ(storage.GetFieldCount(), 11u); EXPECT_TRUE(storage.Contains("Velocity")); EXPECT_TRUE(storage.TryGetFieldValue("Speed", speed)); EXPECT_TRUE(storage.TryGetFieldValue("Accuracy", accuracy)); @@ -44,6 +46,7 @@ TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) { EXPECT_TRUE(storage.TryGetFieldValue("Velocity", velocity)); EXPECT_TRUE(storage.TryGetFieldValue("Tint", tint)); EXPECT_TRUE(storage.TryGetFieldValue("Target", target)); + EXPECT_TRUE(storage.TryGetFieldValue("TargetCamera", targetCamera)); EXPECT_FLOAT_EQ(speed, 3.5f); EXPECT_DOUBLE_EQ(accuracy, 0.875); @@ -55,10 +58,11 @@ TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) { EXPECT_EQ(velocity, Vector3(3.0f, 4.0f, 5.0f)); EXPECT_EQ(tint, Vector4(0.1f, 0.2f, 0.3f, 1.0f)); EXPECT_EQ(target, GameObjectReference{42}); + EXPECT_EQ(targetCamera, (ComponentReference{42, 0})); EXPECT_TRUE(storage.Remove("IsAlive")); EXPECT_FALSE(storage.Contains("IsAlive")); - EXPECT_EQ(storage.GetFieldCount(), 9u); + EXPECT_EQ(storage.GetFieldCount(), 10u); } TEST(ScriptFieldStorage_Test, SerializeRoundTripPreservesFieldValues) { @@ -66,6 +70,7 @@ TEST(ScriptFieldStorage_Test, SerializeRoundTripPreservesFieldValues) { ASSERT_TRUE(original.SetFieldValue("Message", std::string("Ready;Set=Go|100%\nLine2"))); ASSERT_TRUE(original.SetFieldValue("Scale", Vector3(2.0f, 3.0f, 4.0f))); ASSERT_TRUE(original.SetFieldValue("Owner", GameObjectReference{778899})); + ASSERT_TRUE(original.SetFieldValue("View", ComponentReference{778899, 123456})); ASSERT_TRUE(original.SetFieldValue("Counter", int32_t(-17))); const std::string serialized = original.SerializeToString(); @@ -76,16 +81,19 @@ TEST(ScriptFieldStorage_Test, SerializeRoundTripPreservesFieldValues) { std::string message; Vector3 scale; GameObjectReference owner; + ComponentReference view; int32_t counter = 0; EXPECT_TRUE(restored.TryGetFieldValue("Message", message)); EXPECT_TRUE(restored.TryGetFieldValue("Scale", scale)); EXPECT_TRUE(restored.TryGetFieldValue("Owner", owner)); + EXPECT_TRUE(restored.TryGetFieldValue("View", view)); EXPECT_TRUE(restored.TryGetFieldValue("Counter", counter)); EXPECT_EQ(message, "Ready;Set=Go|100%\nLine2"); EXPECT_EQ(scale, Vector3(2.0f, 3.0f, 4.0f)); EXPECT_EQ(owner, GameObjectReference{778899}); + EXPECT_EQ(view, (ComponentReference{778899, 123456})); EXPECT_EQ(counter, -17); }