feat(scripting): add field model editing and defaults support

This commit is contained in:
2026-03-28 15:09:42 +08:00
parent 4717b595c4
commit 14c7fd69ec
13 changed files with 2113 additions and 0 deletions

View File

@@ -5,8 +5,10 @@
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -40,6 +42,59 @@ public:
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<ScriptFieldMetadata>& 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<ScriptFieldDefaultValue>& 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;
@@ -62,6 +117,10 @@ public:
}
std::vector<std::string> events;
std::vector<ScriptFieldMetadata> fieldMetadata;
std::vector<ScriptFieldDefaultValue> fieldDefaultValues;
std::unordered_map<std::string, ScriptFieldValue> managedFieldValues;
std::vector<std::string> managedSetFieldNames;
private:
static std::string Describe(const ScriptRuntimeContext& context) {
@@ -266,4 +325,389 @@ TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndSta
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<ScriptFieldSnapshot>& 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<int32_t>(awakeSnapshot->defaultValue), 0);
EXPECT_EQ(std::get<int32_t>(awakeSnapshot->value), 0);
EXPECT_EQ(std::get<uint64_t>(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<std::string>(labelSnapshot->defaultValue), "");
EXPECT_EQ(std::get<std::string>(labelSnapshot->value), "Stored");
EXPECT_EQ(std::get<std::string>(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<uint64_t>(legacySnapshot->value), 99u);
EXPECT_EQ(std::get<uint64_t>(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<int32_t>(awakeSnapshot->defaultValue), 0);
EXPECT_EQ(std::get<int32_t>(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<std::string>(labelSnapshot->defaultValue), "");
EXPECT_EQ(std::get<std::string>(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<uint64_t>(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<std::string>(field.value), "Stored");
EXPECT_EQ(std::get<std::string>(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<int32_t>(healthSnapshot->defaultValue), 17);
EXPECT_EQ(std::get<int32_t>(healthSnapshot->value), 17);
EXPECT_TRUE(labelSnapshot->hasDefaultValue);
EXPECT_FALSE(labelSnapshot->hasValue);
EXPECT_EQ(labelSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<std::string>(labelSnapshot->defaultValue), "Seeded");
EXPECT_EQ(std::get<std::string>(labelSnapshot->value), "Seeded");
EXPECT_TRUE(spawnPointSnapshot->hasDefaultValue);
EXPECT_FALSE(spawnPointSnapshot->hasValue);
EXPECT_EQ(spawnPointSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<XCEngine::Math::Vector3>(spawnPointSnapshot->defaultValue), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
EXPECT_EQ(std::get<XCEngine::Math::Vector3>(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<ScriptFieldWriteRequest> 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<ScriptFieldWriteResult> 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<float>(runtime.managedFieldValues["Speed"]), 5.0f);
EXPECT_EQ(std::get<std::string>(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<ScriptFieldWriteRequest> requests = {
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Retained"))},
{"Missing", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))}
};
std::vector<ScriptFieldWriteResult> 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<ScriptFieldClearRequest> requests = {
{"Speed"},
{"Label"},
{"LegacyOnly"},
{"Missing"},
{""}
};
std::vector<ScriptFieldClearResult> 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<float>(runtime.managedFieldValues["Speed"]), 6.5f);
EXPECT_EQ(std::get<std::string>(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<ScriptFieldClearRequest> requests = {
{"Speed"}
};
std::vector<ScriptFieldClearResult> 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<ScriptFieldClearRequest> requests = {
{"Label"},
{"Missing"}
};
std::vector<ScriptFieldClearResult> 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