diff --git a/docs/plan/C#脚本模块下一阶段计划.md b/docs/plan/C#脚本模块下一阶段计划.md index 2dd6d746..47cedbff 100644 --- a/docs/plan/C#脚本模块下一阶段计划.md +++ b/docs/plan/C#脚本模块下一阶段计划.md @@ -266,3 +266,9 @@ C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0 - 已新增 `TagLayerProbe` 与对应 C++ / Mono 回归测试,覆盖默认 tag、layer 裁剪、脚本侧读写与场景 roundtrip。 - 相关定向测试全部通过;完整 `MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 输出全绿,当前仍存在历史性的 `exit code 3` 异常,需后续单独跟踪。 - 下一步建议继续做第三小项:`Object.Instantiate` + +- 已完成字段系统第一小项:`enum` 字段支持 +- `MonoScriptRuntime` 现已支持把常见整数底层的 C# `enum` 字段纳入脚本字段模型,并按 `Int32` 进入默认值、存储值、运行时值三层同步链路。 +- 已新增 `FieldMetadataProbeState` / `EnumFieldProbeState` 探针,覆盖枚举字段发现、默认值提取、运行时写回与场景 roundtrip。 +- 已通过新增定向测试,以及完整 `ScriptFieldStorage_Test.*:MonoScriptRuntimeTest.*:ProjectScriptAssemblyTest.*` 整组验证;输出全绿,`exit code 3` 仍为既有历史现象。 +- 字段系统下一步建议切到:`[SerializeField] private` 字段支持 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 0678fb43..84769f5b 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -111,6 +111,8 @@ private: struct FieldMetadata { ScriptFieldType type = ScriptFieldType::None; MonoClassField* field = nullptr; + bool isEnum = false; + int32_t enumUnderlyingType = 0; }; struct ClassMetadata { @@ -153,7 +155,7 @@ private: void DiscoverScriptClassesInImage(const std::string& assemblyName, MonoImage* image); bool IsMonoBehaviourSubclass(MonoClass* monoClass) const; - ScriptFieldType MapMonoFieldType(MonoClassField* field) const; + FieldMetadata BuildFieldMetadata(MonoClassField* field) const; static const char* ToLifecycleMethodName(ScriptLifecycleMethod method); diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index dd5d15bf..c2e44b13 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -26,6 +26,7 @@ #include #include +#include #include namespace XCEngine { @@ -2267,14 +2268,12 @@ void MonoScriptRuntime::DiscoverScriptClassesInImage(const std::string& assembly continue; } - const ScriptFieldType fieldType = MapMonoFieldType(field); - if (fieldType == ScriptFieldType::None) { + FieldMetadata fieldMetadata = BuildFieldMetadata(field); + if (fieldMetadata.type == ScriptFieldType::None) { continue; } - metadata.fields.emplace( - mono_field_get_name(field), - FieldMetadata{fieldType, field}); + metadata.fields.emplace(mono_field_get_name(field), std::move(fieldMetadata)); } m_classes.emplace( @@ -2300,68 +2299,101 @@ bool MonoScriptRuntime::IsMonoBehaviourSubclass(MonoClass* monoClass) const { return false; } -ScriptFieldType MonoScriptRuntime::MapMonoFieldType(MonoClassField* field) const { +MonoScriptRuntime::FieldMetadata MonoScriptRuntime::BuildFieldMetadata(MonoClassField* field) const { + FieldMetadata metadata; + metadata.field = field; if (!field) { - return ScriptFieldType::None; + return metadata; } MonoType* monoType = mono_field_get_type(field); if (!monoType) { - return ScriptFieldType::None; + return metadata; } switch (mono_type_get_type(monoType)) { case MONO_TYPE_R4: - return ScriptFieldType::Float; + metadata.type = ScriptFieldType::Float; + return metadata; case MONO_TYPE_R8: - return ScriptFieldType::Double; + metadata.type = ScriptFieldType::Double; + return metadata; case MONO_TYPE_BOOLEAN: - return ScriptFieldType::Bool; + metadata.type = ScriptFieldType::Bool; + return metadata; case MONO_TYPE_I4: - return ScriptFieldType::Int32; + metadata.type = ScriptFieldType::Int32; + return metadata; case MONO_TYPE_U8: - return ScriptFieldType::UInt64; + metadata.type = ScriptFieldType::UInt64; + return metadata; case MONO_TYPE_STRING: - return ScriptFieldType::String; + metadata.type = ScriptFieldType::String; + return metadata; case MONO_TYPE_CLASS: { MonoClass* referenceClass = mono_class_from_mono_type(monoType); if (!referenceClass) { - return ScriptFieldType::None; + return metadata; } const std::string namespaceName = SafeString(mono_class_get_namespace(referenceClass)); const std::string className = SafeString(mono_class_get_name(referenceClass)); if (namespaceName == m_settings.baseNamespace && className == "GameObject") { - return ScriptFieldType::GameObject; + metadata.type = ScriptFieldType::GameObject; } - return ScriptFieldType::None; + return metadata; } case MONO_TYPE_VALUETYPE: { MonoClass* valueTypeClass = mono_class_from_mono_type(monoType); if (!valueTypeClass) { - return ScriptFieldType::None; + return metadata; + } + + if (mono_class_is_enum(valueTypeClass) != 0) { + MonoType* underlyingType = mono_class_enum_basetype(valueTypeClass); + if (!underlyingType) { + return metadata; + } + + switch (mono_type_get_type(underlyingType)) { + case MONO_TYPE_I1: + case MONO_TYPE_U1: + case MONO_TYPE_I2: + case MONO_TYPE_U2: + case MONO_TYPE_I4: + case MONO_TYPE_U4: + metadata.type = ScriptFieldType::Int32; + metadata.isEnum = true; + metadata.enumUnderlyingType = static_cast(mono_type_get_type(underlyingType)); + return metadata; + default: + return metadata; + } } const std::string namespaceName = SafeString(mono_class_get_namespace(valueTypeClass)); const std::string className = SafeString(mono_class_get_name(valueTypeClass)); if (namespaceName != m_settings.baseNamespace) { - return ScriptFieldType::None; + return metadata; } if (className == "Vector2") { - return ScriptFieldType::Vector2; + metadata.type = ScriptFieldType::Vector2; + return metadata; } if (className == "Vector3") { - return ScriptFieldType::Vector3; + metadata.type = ScriptFieldType::Vector3; + return metadata; } if (className == "Vector4") { - return ScriptFieldType::Vector4; + metadata.type = ScriptFieldType::Vector4; + return metadata; } - return ScriptFieldType::None; + return metadata; } default: - return ScriptFieldType::None; + return metadata; } } @@ -2623,6 +2655,70 @@ bool MonoScriptRuntime::TrySetFieldValue( SetCurrentDomain(); + if (fieldMetadata.isEnum) { + if (!std::holds_alternative(value)) { + return false; + } + + const int32_t storedValue = std::get(value); + switch (fieldMetadata.enumUnderlyingType) { + case MONO_TYPE_I1: { + if (storedValue < std::numeric_limits::min() + || storedValue > std::numeric_limits::max()) { + return false; + } + + int8_t nativeValue = static_cast(storedValue); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case MONO_TYPE_U1: { + if (storedValue < 0 || storedValue > std::numeric_limits::max()) { + return false; + } + + uint8_t nativeValue = static_cast(storedValue); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case MONO_TYPE_I2: { + if (storedValue < std::numeric_limits::min() + || storedValue > std::numeric_limits::max()) { + return false; + } + + int16_t nativeValue = static_cast(storedValue); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case MONO_TYPE_U2: { + if (storedValue < 0 || storedValue > std::numeric_limits::max()) { + return false; + } + + uint16_t nativeValue = static_cast(storedValue); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case MONO_TYPE_I4: { + int32_t nativeValue = storedValue; + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case MONO_TYPE_U4: { + if (storedValue < 0) { + return false; + } + + uint32_t nativeValue = static_cast(storedValue); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + default: + return false; + } + } + switch (fieldMetadata.type) { case ScriptFieldType::Float: { float nativeValue = std::get(value); @@ -2693,6 +2789,53 @@ bool MonoScriptRuntime::TryReadFieldValue( return false; } + if (fieldMetadata.isEnum) { + switch (fieldMetadata.enumUnderlyingType) { + case MONO_TYPE_I1: { + int8_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = static_cast(nativeValue); + return true; + } + case MONO_TYPE_U1: { + uint8_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = static_cast(nativeValue); + return true; + } + case MONO_TYPE_I2: { + int16_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = static_cast(nativeValue); + return true; + } + case MONO_TYPE_U2: { + uint16_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = static_cast(nativeValue); + return true; + } + case MONO_TYPE_I4: { + int32_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case MONO_TYPE_U4: { + uint32_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + if (nativeValue > static_cast(std::numeric_limits::max())) { + return false; + } + + outValue = static_cast(nativeValue); + return true; + } + default: + return false; + } + } + switch (fieldMetadata.type) { case ScriptFieldType::Float: { float nativeValue = 0.0f; diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 2b0dda74..e4c5867b 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -100,6 +100,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/EnumFieldProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/FieldMetadataProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/GetComponentsProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ScriptComponentApiProbe.cs diff --git a/managed/GameScripts/EnumFieldProbe.cs b/managed/GameScripts/EnumFieldProbe.cs new file mode 100644 index 00000000..20e2445f --- /dev/null +++ b/managed/GameScripts/EnumFieldProbe.cs @@ -0,0 +1,28 @@ +using XCEngine; + +namespace Gameplay +{ + public enum EnumFieldProbeState + { + Default = 1, + StoredConfigured = 5, + RuntimeUpdated = 9, + } + + public sealed class EnumFieldProbe : MonoBehaviour + { + public EnumFieldProbeState State = EnumFieldProbeState.Default; + public int ObservedInitialState = -1; + public bool ObservedStoredStateApplied; + public int ObservedUpdatedState = -1; + + public void Start() + { + ObservedInitialState = (int)State; + ObservedStoredStateApplied = State == EnumFieldProbeState.StoredConfigured; + + State = EnumFieldProbeState.RuntimeUpdated; + ObservedUpdatedState = (int)State; + } + } +} diff --git a/managed/GameScripts/FieldMetadataProbe.cs b/managed/GameScripts/FieldMetadataProbe.cs index 410e1276..5f0b0c8f 100644 --- a/managed/GameScripts/FieldMetadataProbe.cs +++ b/managed/GameScripts/FieldMetadataProbe.cs @@ -2,12 +2,19 @@ using XCEngine; namespace Gameplay { + public enum FieldMetadataProbeState + { + Idle = 0, + Running = 2, + } + public sealed class FieldMetadataProbe : MonoBehaviour { public int Health; public float Speed; public string Label = string.Empty; public Vector3 SpawnPoint; + public FieldMetadataProbeState State = FieldMetadataProbeState.Running; public GameObject Target; public Quaternion UnsupportedRotation; public static int SharedCounter; diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 28ff863a..974e591f 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -180,6 +180,7 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFiel {"Label", ScriptFieldType::String}, {"SpawnPoint", ScriptFieldType::Vector3}, {"Speed", ScriptFieldType::Float}, + {"State", ScriptFieldType::Int32}, {"Target", ScriptFieldType::GameObject}, }; @@ -212,6 +213,23 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsManagedInitializ 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, ScriptEngineAppliesStoredFieldsAndInvokesLifecycleMethods) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); @@ -786,6 +804,66 @@ TEST_F(MonoScriptRuntimeTest, ManagedFieldChangesWriteBackToStoredCacheAndPersis 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, ScriptEngineFieldApiUpdatesLiveManagedInstanceAndStoredCache) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host");