Add SerializeField private field support

This commit is contained in:
2026-04-03 16:19:56 +08:00
parent d4d825e1b1
commit 2b19b4bece
10 changed files with 333 additions and 17 deletions

View File

@@ -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}\"

View File

@@ -177,6 +177,7 @@ TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFiel
const std::vector<ScriptFieldMetadata> 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<int32_t>(fieldIt->value), 2);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsSerializeFieldPrivateInitializers) {
std::vector<ScriptFieldDefaultValue> 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<bool>(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<Scene>("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");

View File

@@ -8,24 +8,70 @@
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#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<DWORD>(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<MonoScriptRuntime>(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) {