From 2b19b4bece510a731dc5d41be3a36ac886a80e4e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 16:19:56 +0800 Subject: [PATCH] Add SerializeField private field support --- docs/plan/C#脚本模块下一阶段计划.md | 7 +- .../Scripting/Mono/MonoScriptRuntime.h | 2 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 39 ++++++- managed/CMakeLists.txt | 70 ++++++++++++ managed/GameScripts/FieldMetadataProbe.cs | 4 +- managed/GameScripts/SerializeFieldProbe.cs | 32 ++++++ managed/XCEngine.ScriptCore/SerializeField.cs | 9 ++ tests/scripting/CMakeLists.txt | 21 +++- tests/scripting/test_mono_script_runtime.cpp | 104 ++++++++++++++++++ .../test_project_script_assembly.cpp | 62 +++++++++-- 10 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 managed/GameScripts/SerializeFieldProbe.cs create mode 100644 managed/XCEngine.ScriptCore/SerializeField.cs diff --git a/docs/plan/C#脚本模块下一阶段计划.md b/docs/plan/C#脚本模块下一阶段计划.md index 47cedbff..e89cb34c 100644 --- a/docs/plan/C#脚本模块下一阶段计划.md +++ b/docs/plan/C#脚本模块下一阶段计划.md @@ -271,4 +271,9 @@ C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0 - `MonoScriptRuntime` 现已支持把常见整数底层的 C# `enum` 字段纳入脚本字段模型,并按 `Int32` 进入默认值、存储值、运行时值三层同步链路。 - 已新增 `FieldMetadataProbeState` / `EnumFieldProbeState` 探针,覆盖枚举字段发现、默认值提取、运行时写回与场景 roundtrip。 - 已通过新增定向测试,以及完整 `ScriptFieldStorage_Test.*:MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 整组验证;输出全绿,`exit code 3` 仍为既有历史现象。 -- 字段系统下一步建议切到:`[SerializeField] private` 字段支持 +- 已完成字段系统第二小项:`[SerializeField] private` 字段支持 +- 新增 `XCEngine.SerializeField` attribute,命名与 Unity 对齐;runtime 字段发现现已支持“public 字段”与“带 `[SerializeField]` 的 private 字段”双通路。 +- 字段筛选已同步排除 `static` / `const` / `readonly`,并新增 `SerializeFieldProbe`、`FieldMetadataProbe` 扩展与对应 C++ 回归测试,覆盖默认值提取、存储覆盖、运行时写回与场景 roundtrip。 +- `ProjectScriptAssemblyTest.*` 现改为使用独立的测试项目程序集输出目录,不再依赖真实 `project/Library/ScriptAssemblies`,避免与 editor / Mono 持有的文件锁互相干扰。 +- 已通过完整 `ScriptFieldStorage_Test.*:MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 验证;输出全绿,`exit code 3` 仍为既有历史现象。 +- 字段系统下一步建议切到:组件引用字段支持 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 84769f5b..255abc2d 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -173,6 +173,7 @@ private: bool ApplyContextFields(const ScriptRuntimeContext& context, MonoObject* instance); bool ApplyStoredFields(const ScriptRuntimeContext& context, const ClassMetadata& metadata, MonoObject* instance); MonoObject* CreateManagedGameObject(uint64_t gameObjectUUID); + bool HasSerializeFieldAttribute(MonoClassField* field) 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; @@ -198,6 +199,7 @@ private: MonoClass* m_behaviourClass = nullptr; MonoClass* m_gameObjectClass = nullptr; MonoClass* m_monoBehaviourClass = nullptr; + MonoClass* m_serializeFieldAttributeClass = nullptr; MonoMethod* m_gameObjectConstructor = nullptr; MonoClassField* m_managedGameObjectUUIDField = nullptr; MonoClassField* m_gameObjectUUIDField = nullptr; diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index c2e44b13..c7c2bdfb 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -1665,6 +1665,7 @@ void MonoScriptRuntime::Shutdown() { m_behaviourClass = nullptr; m_gameObjectClass = nullptr; m_monoBehaviourClass = nullptr; + m_serializeFieldAttributeClass = nullptr; m_gameObjectConstructor = nullptr; m_managedGameObjectUUIDField = nullptr; m_gameObjectUUIDField = nullptr; @@ -2219,6 +2220,15 @@ bool MonoScriptRuntime::DiscoverScriptClasses() { return false; } + m_serializeFieldAttributeClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "SerializeField"); + if (!m_serializeFieldAttributeClass) { + SetError("Failed to locate the managed SerializeField attribute type."); + return false; + } + m_gameObjectUUIDField = mono_class_get_field_from_name(m_componentClass, "m_gameObjectUUID"); m_scriptComponentUUIDField = mono_class_get_field_from_name(m_behaviourClass, "m_scriptComponentUUID"); if (!m_gameObjectUUIDField || !m_scriptComponentUUIDField) { @@ -2264,7 +2274,14 @@ void MonoScriptRuntime::DiscoverScriptClassesInImage(const std::string& assembly void* fieldIterator = nullptr; while (MonoClassField* field = mono_class_get_fields(monoClass, &fieldIterator)) { const uint32_t fieldFlags = mono_field_get_flags(field); - if ((fieldFlags & MONO_FIELD_ATTR_PUBLIC) == 0 || (fieldFlags & MONO_FIELD_ATTR_STATIC) != 0) { + if ((fieldFlags & MONO_FIELD_ATTR_STATIC) != 0 + || (fieldFlags & MONO_FIELD_ATTR_LITERAL) != 0 + || (fieldFlags & MONO_FIELD_ATTR_INIT_ONLY) != 0) { + continue; + } + + const bool isPublicField = (fieldFlags & MONO_FIELD_ATTR_PUBLIC) != 0; + if (!isPublicField && !HasSerializeFieldAttribute(field)) { continue; } @@ -2299,6 +2316,26 @@ bool MonoScriptRuntime::IsMonoBehaviourSubclass(MonoClass* monoClass) const { return false; } +bool MonoScriptRuntime::HasSerializeFieldAttribute(MonoClassField* field) const { + if (!field || !m_serializeFieldAttributeClass) { + return false; + } + + MonoClass* ownerClass = mono_field_get_parent(field); + if (!ownerClass) { + return false; + } + + MonoCustomAttrInfo* attributes = mono_custom_attrs_from_field(ownerClass, field); + if (!attributes) { + return false; + } + + const mono_bool hasAttribute = mono_custom_attrs_has_attr(attributes, m_serializeFieldAttributeClass); + mono_custom_attrs_free(attributes); + return hasAttribute != 0; +} + MonoScriptRuntime::FieldMetadata MonoScriptRuntime::BuildFieldMetadata(MonoClassField* field) const { FieldMetadata metadata; metadata.field = field; diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index e4c5867b..7fffaa59 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -62,6 +62,30 @@ set( "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/mscorlib.dll" CACHE FILEPATH "Mono corlib copied into the project script assembly directory") +set( + XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR + "${CMAKE_BINARY_DIR}/managed/ProjectScriptAssemblies" + CACHE PATH + "Output directory for scripting tests that validate project asset assemblies" +) +set( + XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL + "${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR}/XCEngine.ScriptCore.dll" + CACHE FILEPATH + "ScriptCore assembly used by scripting tests for project asset assembly validation" +) +set( + XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL + "${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR}/GameScripts.dll" + CACHE FILEPATH + "GameScripts assembly used by scripting tests for project asset assembly validation" +) +set( + XCENGINE_SCRIPTING_TEST_PROJECT_MONO_MSCORLIB_PATH + "${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR}/mscorlib.dll" + CACHE FILEPATH + "Mono corlib copied into the scripting test project assembly directory" +) foreach(XCENGINE_REQUIRED_PATH "${XCENGINE_CSC_DLL}" @@ -89,6 +113,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MonoBehaviour.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Object.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Quaternion.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/SerializeField.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Space.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Time.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Transform.cs @@ -111,6 +136,7 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ObjectApiProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/SerializeFieldProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TagLayerProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TickLogProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformConversionProbe.cs @@ -190,6 +216,50 @@ add_custom_target( ${XCENGINE_MANAGED_OUTPUT_DIR}/mscorlib.dll ) +add_custom_command( + OUTPUT ${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${XCENGINE_SCRIPT_CORE_DLL} + ${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL} + DEPENDS ${XCENGINE_SCRIPT_CORE_DLL} + VERBATIM + COMMENT "Copying XCEngine.ScriptCore.dll into the scripting test project assembly directory") + +add_custom_command( + OUTPUT ${XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${XCENGINE_DOTNET_EXECUTABLE} ${XCENGINE_CSC_DLL} + /nologo + /target:library + /langversion:latest + /nostdlib+ + /out:${XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL} + ${XCENGINE_MANAGED_FRAMEWORK_REFERENCES} + /reference:${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL} + ${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES} + DEPENDS ${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES} ${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL} + VERBATIM + COMMENT "Building scripting test GameScripts.dll from project asset scripts") + +add_custom_command( + OUTPUT ${XCENGINE_SCRIPTING_TEST_PROJECT_MONO_MSCORLIB_PATH} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${XCENGINE_MONO_MSCORLIB_PATH} + ${XCENGINE_SCRIPTING_TEST_PROJECT_MONO_MSCORLIB_PATH} + DEPENDS ${XCENGINE_MONO_MSCORLIB_PATH} + VERBATIM + COMMENT "Copying mscorlib.dll into the scripting test project assembly directory") + +add_custom_target( + xcengine_test_project_managed_assemblies + DEPENDS + ${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL} + ${XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL} + ${XCENGINE_SCRIPTING_TEST_PROJECT_MONO_MSCORLIB_PATH} +) + add_custom_command( OUTPUT ${XCENGINE_PROJECT_SCRIPT_CORE_DLL} COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR} diff --git a/managed/GameScripts/FieldMetadataProbe.cs b/managed/GameScripts/FieldMetadataProbe.cs index 5f0b0c8f..6cb8d723 100644 --- a/managed/GameScripts/FieldMetadataProbe.cs +++ b/managed/GameScripts/FieldMetadataProbe.cs @@ -15,10 +15,12 @@ namespace Gameplay public string Label = string.Empty; public Vector3 SpawnPoint; public FieldMetadataProbeState State = FieldMetadataProbeState.Running; + [SerializeField] private bool HiddenFlag = true; public GameObject Target; public Quaternion UnsupportedRotation; public static int SharedCounter; - private bool HiddenFlag = true; + private int IgnoredPrivateCounter = 7; public bool HiddenFlagMirror => HiddenFlag; + public int IgnoredPrivateCounterMirror => IgnoredPrivateCounter; } } diff --git a/managed/GameScripts/SerializeFieldProbe.cs b/managed/GameScripts/SerializeFieldProbe.cs new file mode 100644 index 00000000..95a43cb3 --- /dev/null +++ b/managed/GameScripts/SerializeFieldProbe.cs @@ -0,0 +1,32 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class SerializeFieldProbe : MonoBehaviour + { + [SerializeField] private int HiddenCounter = 7; + [SerializeField] private bool HiddenEnabled = true; + private int IgnoredPrivateCounter = 99; + + public int ObservedInitialHiddenCounter = -1; + public bool ObservedInitialHiddenEnabled; + public bool ObservedStoredValuesApplied; + public int ObservedUpdatedHiddenCounter = -1; + public bool ObservedUpdatedHiddenEnabled; + public bool ObservedIgnoredPrivateCounterUntouched; + + public void Start() + { + ObservedInitialHiddenCounter = HiddenCounter; + ObservedInitialHiddenEnabled = HiddenEnabled; + ObservedStoredValuesApplied = HiddenCounter == 42 && !HiddenEnabled; + ObservedIgnoredPrivateCounterUntouched = IgnoredPrivateCounter == 99; + + HiddenCounter += 1; + HiddenEnabled = !HiddenEnabled; + + ObservedUpdatedHiddenCounter = HiddenCounter; + ObservedUpdatedHiddenEnabled = HiddenEnabled; + } + } +} diff --git a/managed/XCEngine.ScriptCore/SerializeField.cs b/managed/XCEngine.ScriptCore/SerializeField.cs new file mode 100644 index 00000000..22aa8fe5 --- /dev/null +++ b/managed/XCEngine.ScriptCore/SerializeField.cs @@ -0,0 +1,9 @@ +using System; + +namespace XCEngine +{ + [AttributeUsage(AttributeTargets.Field, Inherited = true)] + public sealed class SerializeField : Attribute + { + } +} diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt index a519147a..19686082 100644 --- a/tests/scripting/CMakeLists.txt +++ b/tests/scripting/CMakeLists.txt @@ -13,7 +13,7 @@ if(XCENGINE_ENABLE_MONO_SCRIPTING) test_mono_script_runtime.cpp ) - if(TARGET xcengine_project_managed_assemblies) + if(TARGET xcengine_test_project_managed_assemblies) list(APPEND SCRIPTING_TEST_SOURCES test_project_script_assembly.cpp ) @@ -52,12 +52,21 @@ if(TARGET xcengine_managed_assemblies) ) endif() -if(TARGET xcengine_project_managed_assemblies) - add_dependencies(scripting_tests xcengine_project_managed_assemblies) +if(TARGET xcengine_test_project_managed_assemblies) + add_dependencies(scripting_tests xcengine_test_project_managed_assemblies) - file(TO_CMAKE_PATH "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}" XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE) - file(TO_CMAKE_PATH "${XCENGINE_PROJECT_SCRIPT_CORE_DLL}" XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE) - file(TO_CMAKE_PATH "${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}" XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE) + file( + TO_CMAKE_PATH + "${XCENGINE_SCRIPTING_TEST_PROJECT_MANAGED_OUTPUT_DIR}" + XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE) + file( + TO_CMAKE_PATH + "${XCENGINE_SCRIPTING_TEST_PROJECT_SCRIPT_CORE_DLL}" + XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE) + file( + TO_CMAKE_PATH + "${XCENGINE_SCRIPTING_TEST_PROJECT_GAME_SCRIPTS_DLL}" + XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE) target_compile_definitions(scripting_tests PRIVATE XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR=\"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE}\" diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 974e591f..495736c7 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -177,6 +177,7 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFiel const std::vector expected = { {"Health", ScriptFieldType::Int32}, + {"HiddenFlag", ScriptFieldType::Bool}, {"Label", ScriptFieldType::String}, {"SpawnPoint", ScriptFieldType::Vector3}, {"Speed", ScriptFieldType::Float}, @@ -230,6 +231,23 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsEnumInitializers 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, ScriptEngineAppliesStoredFieldsAndInvokesLifecycleMethods) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); @@ -864,6 +882,92 @@ TEST_F(MonoScriptRuntimeTest, EnumScriptFieldsApplyStoredValuesAndPersistAcrossS 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, ScriptEngineFieldApiUpdatesLiveManagedInstanceAndStoredCache) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); diff --git a/tests/scripting/test_project_script_assembly.cpp b/tests/scripting/test_project_script_assembly.cpp index 3a1a6233..82c3f8b6 100644 --- a/tests/scripting/test_project_script_assembly.cpp +++ b/tests/scripting/test_project_script_assembly.cpp @@ -8,24 +8,70 @@ #include #include +#ifdef _WIN32 +#include +#endif + using namespace XCEngine::Scripting; namespace { +std::filesystem::path GetExecutableDirectory() { +#ifdef _WIN32 + std::wstring buffer(MAX_PATH, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length == 0 || length >= buffer.size()) { + return std::filesystem::current_path(); + } + + buffer.resize(length); + return std::filesystem::path(buffer).parent_path(); +#else + return std::filesystem::current_path(); +#endif +} + +std::filesystem::path ResolveProjectManagedOutputDirectory() { + constexpr const char* configuredDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR; + if (configuredDirectory[0] != '\0') { + return std::filesystem::path(configuredDirectory); + } + + return (GetExecutableDirectory() / ".." / ".." / "managed" / "ProjectScriptAssemblies").lexically_normal(); +} + +std::filesystem::path ResolveProjectScriptCoreDllPath() { + constexpr const char* configuredPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL; + if (configuredPath[0] != '\0') { + return std::filesystem::path(configuredPath); + } + + return ResolveProjectManagedOutputDirectory() / "XCEngine.ScriptCore.dll"; +} + +std::filesystem::path ResolveProjectGameScriptsDllPath() { + constexpr const char* configuredPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL; + if (configuredPath[0] != '\0') { + return std::filesystem::path(configuredPath); + } + + return ResolveProjectManagedOutputDirectory() / "GameScripts.dll"; +} + MonoScriptRuntime::Settings CreateProjectMonoSettings() { MonoScriptRuntime::Settings settings; - settings.assemblyDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR; - settings.corlibDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR; - settings.coreAssemblyPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL; - settings.appAssemblyPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL; + settings.assemblyDirectory = ResolveProjectManagedOutputDirectory(); + settings.corlibDirectory = settings.assemblyDirectory; + settings.coreAssemblyPath = ResolveProjectScriptCoreDllPath(); + settings.appAssemblyPath = ResolveProjectGameScriptsDllPath(); return settings; } class ProjectScriptAssemblyTest : public ::testing::Test { protected: void SetUp() override { - ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL)); - ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL)); + ASSERT_TRUE(std::filesystem::exists(ResolveProjectScriptCoreDllPath())); + ASSERT_TRUE(std::filesystem::exists(ResolveProjectGameScriptsDllPath())); runtime = std::make_unique(CreateProjectMonoSettings()); ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); @@ -36,8 +82,8 @@ protected: TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) { EXPECT_TRUE(runtime->IsInitialized()); - EXPECT_EQ(runtime->GetSettings().assemblyDirectory, std::filesystem::path(XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR)); - EXPECT_EQ(runtime->GetSettings().appAssemblyPath, std::filesystem::path(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL)); + EXPECT_EQ(runtime->GetSettings().assemblyDirectory, ResolveProjectManagedOutputDirectory()); + EXPECT_EQ(runtime->GetSettings().appAssemblyPath, ResolveProjectGameScriptsDllPath()); } TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFieldMetadata) {