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

@@ -1,6 +1,10 @@
#pragma once
#include <XCEngine/Scripting/ScriptField.h>
#include <cstdint>
#include <string>
#include <vector>
namespace XCEngine {
@@ -39,6 +43,29 @@ public:
virtual void OnRuntimeStart(Components::Scene* scene) = 0;
virtual void OnRuntimeStop(Components::Scene* scene) = 0;
virtual bool TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const = 0;
virtual bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const = 0;
virtual bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) = 0;
virtual bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const = 0;
virtual void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) = 0;
virtual bool CreateScriptInstance(const ScriptRuntimeContext& context) = 0;
virtual void DestroyScriptInstance(const ScriptRuntimeContext& context) = 0;

View File

@@ -50,6 +50,16 @@ public:
const std::string& namespaceName,
const std::string& className) const;
std::vector<std::string> GetScriptClassNames(const std::string& assemblyName = std::string()) const;
bool TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const override;
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override;
bool HasManagedInstance(const ScriptComponent* component) const;
size_t GetManagedInstanceCount() const { return m_instances.size(); }
@@ -79,6 +89,15 @@ public:
void OnRuntimeStart(Components::Scene* scene) override;
void OnRuntimeStop(Components::Scene* scene) override;
bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) override;
bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const override;
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override;
bool CreateScriptInstance(const ScriptRuntimeContext& context) override;
void DestroyScriptInstance(const ScriptRuntimeContext& context) override;
void InvokeMethod(const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) override;

View File

@@ -9,6 +9,25 @@ class NullScriptRuntime : public IScriptRuntime {
public:
void OnRuntimeStart(Components::Scene* scene) override;
void OnRuntimeStop(Components::Scene* scene) override;
bool TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const override;
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override;
bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) override;
bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const override;
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override;
bool CreateScriptInstance(const ScriptRuntimeContext& context) override;
void DestroyScriptInstance(const ScriptRuntimeContext& context) override;

View File

@@ -2,9 +2,12 @@
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/NullScriptRuntime.h>
#include <XCEngine/Scripting/ScriptFieldStorage.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>
@@ -37,6 +40,60 @@ public:
bool HasTrackedScriptComponent(const ScriptComponent* component) const;
bool HasRuntimeInstance(const ScriptComponent* component) const;
size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); }
bool TrySetScriptFieldValue(
ScriptComponent* component,
const std::string& fieldName,
ScriptFieldType type,
const ScriptFieldValue& value);
bool TryGetScriptFieldValue(
const ScriptComponent* component,
const std::string& fieldName,
ScriptFieldValue& outValue) const;
bool ApplyScriptFieldWrites(
ScriptComponent* component,
const std::vector<ScriptFieldWriteRequest>& requests,
std::vector<ScriptFieldWriteResult>& outResults);
bool ClearScriptFieldOverrides(
ScriptComponent* component,
const std::vector<ScriptFieldClearRequest>& requests,
std::vector<ScriptFieldClearResult>& outResults);
bool TryGetScriptFieldModel(
const ScriptComponent* component,
ScriptFieldModel& outModel) const;
bool TryGetScriptFieldSnapshots(
const ScriptComponent* component,
std::vector<ScriptFieldSnapshot>& outFields) const;
template<typename T>
bool TrySetScriptFieldValue(ScriptComponent* component, const std::string& fieldName, const T& value) {
using ValueType = std::decay_t<T>;
return TrySetScriptFieldValue(
component,
fieldName,
ScriptFieldTypeResolver<ValueType>::value,
ScriptFieldValue(ValueType(value)));
}
bool TrySetScriptFieldValue(ScriptComponent* component, const std::string& fieldName, const char* value) {
return TrySetScriptFieldValue(component, fieldName, std::string(value ? value : ""));
}
template<typename T>
bool TryGetScriptFieldValue(const ScriptComponent* component, const std::string& fieldName, T& outValue) const {
ScriptFieldValue value;
if (!TryGetScriptFieldValue(component, fieldName, value)) {
return false;
}
using ValueType = std::decay_t<T>;
const ValueType* typedValue = std::get_if<ValueType>(&value);
if (!typedValue) {
return false;
}
outValue = *typedValue;
return true;
}
private:
struct ScriptInstanceKey {

View File

@@ -7,6 +7,7 @@
#include <cstdint>
#include <string>
#include <variant>
#include <vector>
namespace XCEngine {
namespace Scripting {
@@ -37,6 +38,56 @@ struct GameObjectReference {
}
};
struct ScriptFieldMetadata {
std::string name;
ScriptFieldType type = ScriptFieldType::None;
bool operator==(const ScriptFieldMetadata& other) const {
return name == other.name && type == other.type;
}
bool operator!=(const ScriptFieldMetadata& other) const {
return !(*this == other);
}
};
enum class ScriptFieldClassStatus {
Unassigned = 0,
Available,
Missing
};
enum class ScriptFieldValueSource {
None = 0,
DefaultValue,
StoredValue,
ManagedValue
};
enum class ScriptFieldIssue {
None = 0,
StoredOnly,
TypeMismatch
};
enum class ScriptFieldWriteStatus {
Applied = 0,
EmptyFieldName,
UnknownField,
InvalidValue,
TypeMismatch,
StoredOnlyField,
ApplyFailed
};
enum class ScriptFieldClearStatus {
Applied = 0,
EmptyFieldName,
UnknownField,
NoValueToClear,
ApplyFailed
};
using ScriptFieldValue = std::variant<
std::monostate,
float,
@@ -50,6 +101,80 @@ using ScriptFieldValue = std::variant<
Math::Vector4,
GameObjectReference>;
struct ScriptFieldDefaultValue {
std::string fieldName;
ScriptFieldType type = ScriptFieldType::None;
ScriptFieldValue value = std::monostate{};
bool operator==(const ScriptFieldDefaultValue& other) const {
return fieldName == other.fieldName
&& type == other.type
&& value == other.value;
}
bool operator!=(const ScriptFieldDefaultValue& other) const {
return !(*this == other);
}
};
struct ScriptFieldSnapshot {
ScriptFieldMetadata metadata;
bool declaredInClass = false;
bool hasDefaultValue = false;
ScriptFieldValue defaultValue = std::monostate{};
bool hasValue = false;
ScriptFieldValue value = std::monostate{};
ScriptFieldValueSource valueSource = ScriptFieldValueSource::None;
ScriptFieldIssue issue = ScriptFieldIssue::None;
bool hasStoredValue = false;
ScriptFieldType storedType = ScriptFieldType::None;
ScriptFieldValue storedValue = std::monostate{};
bool operator==(const ScriptFieldSnapshot& other) const {
return metadata == other.metadata
&& declaredInClass == other.declaredInClass
&& hasDefaultValue == other.hasDefaultValue
&& defaultValue == other.defaultValue
&& hasValue == other.hasValue
&& value == other.value
&& valueSource == other.valueSource
&& issue == other.issue
&& hasStoredValue == other.hasStoredValue
&& storedType == other.storedType
&& storedValue == other.storedValue;
}
bool operator!=(const ScriptFieldSnapshot& other) const {
return !(*this == other);
}
};
struct ScriptFieldModel {
ScriptFieldClassStatus classStatus = ScriptFieldClassStatus::Unassigned;
std::vector<ScriptFieldSnapshot> fields;
};
struct ScriptFieldWriteRequest {
std::string fieldName;
ScriptFieldType type = ScriptFieldType::None;
ScriptFieldValue value = std::monostate{};
};
struct ScriptFieldWriteResult {
std::string fieldName;
ScriptFieldType type = ScriptFieldType::None;
ScriptFieldWriteStatus status = ScriptFieldWriteStatus::ApplyFailed;
};
struct ScriptFieldClearRequest {
std::string fieldName;
};
struct ScriptFieldClearResult {
std::string fieldName;
ScriptFieldClearStatus status = ScriptFieldClearStatus::ApplyFailed;
};
std::string ScriptFieldTypeToString(ScriptFieldType type);
bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType);

View File

@@ -1298,6 +1298,73 @@ std::vector<std::string> MonoScriptRuntime::GetScriptClassNames(const std::strin
return classNames;
}
bool MonoScriptRuntime::TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const {
outFields.clear();
const ClassMetadata* metadata = FindClassMetadata(assemblyName, namespaceName, className);
if (!metadata) {
return false;
}
outFields.reserve(metadata->fields.size());
for (const auto& [fieldName, fieldMetadata] : metadata->fields) {
outFields.push_back(ScriptFieldMetadata{fieldName, fieldMetadata.type});
}
std::sort(
outFields.begin(),
outFields.end(),
[](const ScriptFieldMetadata& lhs, const ScriptFieldMetadata& rhs) {
return lhs.name < rhs.name;
});
return true;
}
bool MonoScriptRuntime::TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const {
outFields.clear();
const ClassMetadata* metadata = FindClassMetadata(assemblyName, namespaceName, className);
if (!metadata) {
return false;
}
SetCurrentDomain();
MonoObject* instance = mono_object_new(m_appDomain, metadata->monoClass);
if (!instance) {
return false;
}
mono_runtime_object_init(instance);
outFields.reserve(metadata->fields.size());
for (const auto& [fieldName, fieldMetadata] : metadata->fields) {
ScriptFieldValue value;
if (!TryReadFieldValue(instance, fieldMetadata, value)) {
outFields.clear();
return false;
}
outFields.push_back(ScriptFieldDefaultValue{fieldName, fieldMetadata.type, value});
}
std::sort(
outFields.begin(),
outFields.end(),
[](const ScriptFieldDefaultValue& lhs, const ScriptFieldDefaultValue& rhs) {
return lhs.fieldName < rhs.fieldName;
});
return true;
}
bool MonoScriptRuntime::HasManagedInstance(const ScriptComponent* component) const {
const InstanceData* instanceData = FindInstance(component);
return instanceData != nullptr && GetManagedObject(*instanceData) != nullptr;
@@ -1347,6 +1414,91 @@ void MonoScriptRuntime::OnRuntimeStop(Components::Scene* scene) {
GetInternalCallDeltaTime() = 0.0f;
}
bool MonoScriptRuntime::TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) {
const InstanceData* instanceData = FindInstance(context);
if (!instanceData || !instanceData->classMetadata) {
return false;
}
const auto metadataIt = instanceData->classMetadata->fields.find(fieldName);
if (metadataIt == instanceData->classMetadata->fields.end()) {
return false;
}
if (!IsScriptFieldValueCompatible(metadataIt->second.type, value)) {
return false;
}
MonoObject* instance = GetManagedObject(*instanceData);
if (!instance) {
return false;
}
return TrySetFieldValue(instance, metadataIt->second, value);
}
bool MonoScriptRuntime::TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const {
const InstanceData* instanceData = FindInstance(context);
if (!instanceData || !instanceData->classMetadata) {
return false;
}
const auto metadataIt = instanceData->classMetadata->fields.find(fieldName);
if (metadataIt == instanceData->classMetadata->fields.end()) {
return false;
}
MonoObject* instance = GetManagedObject(*instanceData);
if (!instance) {
return false;
}
return TryReadFieldValue(instance, metadataIt->second, outValue);
}
void MonoScriptRuntime::SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) {
if (!context.component) {
return;
}
const InstanceData* instanceData = FindInstance(context);
if (!instanceData || !instanceData->classMetadata) {
return;
}
MonoObject* instance = GetManagedObject(*instanceData);
if (!instance) {
return;
}
ScriptFieldStorage& fieldStorage = context.component->GetFieldStorage();
const std::vector<std::string> fieldNames = fieldStorage.GetFieldNames();
for (const std::string& fieldName : fieldNames) {
StoredScriptField* storedField = fieldStorage.FindField(fieldName);
if (!storedField) {
continue;
}
const auto metadataIt = instanceData->classMetadata->fields.find(fieldName);
if (metadataIt == instanceData->classMetadata->fields.end() || storedField->type != metadataIt->second.type) {
continue;
}
ScriptFieldValue value;
if (!TryReadFieldValue(instance, metadataIt->second, value)) {
continue;
}
fieldStorage.SetFieldValue(fieldName, storedField->type, value);
}
}
bool MonoScriptRuntime::CreateScriptInstance(const ScriptRuntimeContext& context) {
if (!context.component) {
SetError("Cannot create a managed script instance without a ScriptComponent.");

View File

@@ -11,6 +11,54 @@ void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) {
(void)scene;
}
bool NullScriptRuntime::TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields.clear();
return false;
}
bool NullScriptRuntime::TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields.clear();
return false;
}
bool NullScriptRuntime::TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) {
(void)context;
(void)fieldName;
(void)value;
return true;
}
bool NullScriptRuntime::TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const {
(void)context;
(void)fieldName;
(void)outValue;
return false;
}
void NullScriptRuntime::SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) {
(void)context;
}
bool NullScriptRuntime::CreateScriptInstance(const ScriptRuntimeContext& context) {
return context.component != nullptr;
}

View File

@@ -9,6 +9,51 @@
namespace XCEngine {
namespace Scripting {
namespace {
std::unordered_map<std::string, ScriptFieldValue> CollectClassDefaultValues(
const IScriptRuntime* runtime,
const ScriptComponent* component,
ScriptFieldClassStatus classStatus) {
std::unordered_map<std::string, ScriptFieldValue> defaultValues;
if (!runtime || !component || classStatus != ScriptFieldClassStatus::Available || !component->HasScriptClass()) {
return defaultValues;
}
std::vector<ScriptFieldDefaultValue> fields;
if (!runtime->TryGetClassFieldDefaultValues(
component->GetAssemblyName(),
component->GetNamespaceName(),
component->GetClassName(),
fields)) {
return defaultValues;
}
defaultValues.reserve(fields.size());
for (const ScriptFieldDefaultValue& field : fields) {
if (field.fieldName.empty() || !IsScriptFieldValueCompatible(field.type, field.value)) {
continue;
}
defaultValues.emplace(field.fieldName, field.value);
}
return defaultValues;
}
ScriptFieldValue ResolveScriptFieldDefaultValue(
const ScriptFieldMetadata& metadata,
const std::unordered_map<std::string, ScriptFieldValue>& defaultValues) {
const auto it = defaultValues.find(metadata.name);
if (it != defaultValues.end() && IsScriptFieldValueCompatible(metadata.type, it->second)) {
return it->second;
}
return CreateDefaultScriptFieldValue(metadata.type);
}
} // namespace
ScriptEngine& ScriptEngine::Get() {
static ScriptEngine instance;
return instance;
@@ -203,6 +248,346 @@ bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const {
return state && state->instanceCreated;
}
bool ScriptEngine::TrySetScriptFieldValue(
ScriptComponent* component,
const std::string& fieldName,
ScriptFieldType type,
const ScriptFieldValue& value) {
if (!component || fieldName.empty() || !IsScriptFieldValueCompatible(type, value)) {
return false;
}
if (component->HasScriptClass()) {
std::vector<ScriptFieldMetadata> fields;
if (m_runtime->TryGetClassFieldMetadata(
component->GetAssemblyName(),
component->GetNamespaceName(),
component->GetClassName(),
fields)) {
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[&fieldName](const ScriptFieldMetadata& metadata) {
return metadata.name == fieldName;
});
if (fieldIt == fields.end() || fieldIt->type != type) {
return false;
}
}
}
ScriptInstanceState* state = m_runtimeRunning ? FindState(component) : nullptr;
if (state && state->instanceCreated && !m_runtime->TrySetManagedFieldValue(state->context, fieldName, value)) {
return false;
}
return component->GetFieldStorage().SetFieldValue(fieldName, type, value);
}
bool ScriptEngine::TryGetScriptFieldValue(
const ScriptComponent* component,
const std::string& fieldName,
ScriptFieldValue& outValue) const {
if (!component || fieldName.empty()) {
return false;
}
const ScriptInstanceState* state = m_runtimeRunning ? FindState(component) : nullptr;
if (state && state->instanceCreated && m_runtime->TryGetManagedFieldValue(state->context, fieldName, outValue)) {
return true;
}
const StoredScriptField* storedField = component->GetFieldStorage().FindField(fieldName);
if (!storedField) {
return false;
}
outValue = storedField->value;
return true;
}
bool ScriptEngine::ApplyScriptFieldWrites(
ScriptComponent* component,
const std::vector<ScriptFieldWriteRequest>& requests,
std::vector<ScriptFieldWriteResult>& outResults) {
outResults.clear();
if (!component) {
return false;
}
ScriptFieldModel model;
if (!TryGetScriptFieldModel(component, model)) {
return false;
}
const std::unordered_map<std::string, ScriptFieldValue> defaultValues =
CollectClassDefaultValues(m_runtime, component, model.classStatus);
std::unordered_map<std::string, const ScriptFieldSnapshot*> fieldByName;
fieldByName.reserve(model.fields.size());
for (const ScriptFieldSnapshot& field : model.fields) {
fieldByName.emplace(field.metadata.name, &field);
}
outResults.reserve(requests.size());
bool allApplied = true;
for (const ScriptFieldWriteRequest& request : requests) {
ScriptFieldWriteResult result;
result.fieldName = request.fieldName;
result.type = request.type;
if (request.fieldName.empty()) {
result.status = ScriptFieldWriteStatus::EmptyFieldName;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
if (!IsScriptFieldValueCompatible(request.type, request.value)) {
result.status = ScriptFieldWriteStatus::InvalidValue;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
const auto fieldIt = fieldByName.find(request.fieldName);
if (fieldIt == fieldByName.end()) {
result.status = ScriptFieldWriteStatus::UnknownField;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
const ScriptFieldSnapshot& field = *fieldIt->second;
if (field.metadata.type != request.type) {
result.status = ScriptFieldWriteStatus::TypeMismatch;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
if (model.classStatus == ScriptFieldClassStatus::Available && !field.declaredInClass) {
result.status = ScriptFieldWriteStatus::StoredOnlyField;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
const bool applied = field.declaredInClass
? TrySetScriptFieldValue(component, request.fieldName, request.type, request.value)
: component->GetFieldStorage().SetFieldValue(request.fieldName, request.type, request.value);
if (!applied) {
result.status = ScriptFieldWriteStatus::ApplyFailed;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
result.status = ScriptFieldWriteStatus::Applied;
outResults.push_back(std::move(result));
}
return allApplied;
}
bool ScriptEngine::ClearScriptFieldOverrides(
ScriptComponent* component,
const std::vector<ScriptFieldClearRequest>& requests,
std::vector<ScriptFieldClearResult>& outResults) {
outResults.clear();
if (!component) {
return false;
}
ScriptFieldModel model;
if (!TryGetScriptFieldModel(component, model)) {
return false;
}
const std::unordered_map<std::string, ScriptFieldValue> defaultValues =
CollectClassDefaultValues(m_runtime, component, model.classStatus);
std::unordered_map<std::string, const ScriptFieldSnapshot*> fieldByName;
fieldByName.reserve(model.fields.size());
for (const ScriptFieldSnapshot& field : model.fields) {
fieldByName.emplace(field.metadata.name, &field);
}
ScriptInstanceState* state = m_runtimeRunning ? FindState(component) : nullptr;
const bool hasLiveInstance = state && state->instanceCreated;
outResults.reserve(requests.size());
bool allApplied = true;
for (const ScriptFieldClearRequest& request : requests) {
ScriptFieldClearResult result;
result.fieldName = request.fieldName;
if (request.fieldName.empty()) {
result.status = ScriptFieldClearStatus::EmptyFieldName;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
const auto fieldIt = fieldByName.find(request.fieldName);
if (fieldIt == fieldByName.end()) {
result.status = ScriptFieldClearStatus::UnknownField;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
const ScriptFieldSnapshot& field = *fieldIt->second;
bool resetManagedValue = false;
if (field.declaredInClass && hasLiveInstance) {
if (!m_runtime->TrySetManagedFieldValue(
state->context,
field.metadata.name,
ResolveScriptFieldDefaultValue(field.metadata, defaultValues))) {
result.status = ScriptFieldClearStatus::ApplyFailed;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
resetManagedValue = true;
}
bool removedStoredValue = false;
if (field.hasStoredValue) {
removedStoredValue = component->GetFieldStorage().Remove(field.metadata.name);
if (!removedStoredValue) {
result.status = ScriptFieldClearStatus::ApplyFailed;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
}
if (!removedStoredValue && !resetManagedValue) {
result.status = ScriptFieldClearStatus::NoValueToClear;
allApplied = false;
outResults.push_back(std::move(result));
continue;
}
result.status = ScriptFieldClearStatus::Applied;
outResults.push_back(std::move(result));
}
return allApplied;
}
bool ScriptEngine::TryGetScriptFieldModel(
const ScriptComponent* component,
ScriptFieldModel& outModel) const {
outModel = ScriptFieldModel{};
if (!component) {
return false;
}
std::vector<ScriptFieldMetadata> metadataFields;
if (!component->HasScriptClass()) {
outModel.classStatus = ScriptFieldClassStatus::Unassigned;
} else if (m_runtime->TryGetClassFieldMetadata(
component->GetAssemblyName(),
component->GetNamespaceName(),
component->GetClassName(),
metadataFields)) {
outModel.classStatus = ScriptFieldClassStatus::Available;
} else {
outModel.classStatus = ScriptFieldClassStatus::Missing;
}
const std::unordered_map<std::string, ScriptFieldValue> defaultValues =
CollectClassDefaultValues(m_runtime, component, outModel.classStatus);
const ScriptInstanceState* state = m_runtimeRunning ? FindState(component) : nullptr;
std::unordered_map<std::string, size_t> fieldIndexByName;
fieldIndexByName.reserve(metadataFields.size());
for (const ScriptFieldMetadata& metadata : metadataFields) {
ScriptFieldSnapshot snapshot;
snapshot.metadata = metadata;
snapshot.declaredInClass = true;
snapshot.hasDefaultValue = true;
snapshot.defaultValue = ResolveScriptFieldDefaultValue(metadata, defaultValues);
snapshot.value = snapshot.defaultValue;
snapshot.valueSource = ScriptFieldValueSource::DefaultValue;
const StoredScriptField* storedField = component->GetFieldStorage().FindField(metadata.name);
if (storedField) {
snapshot.hasStoredValue = true;
snapshot.storedType = storedField->type;
snapshot.storedValue = storedField->value;
if (storedField->type != metadata.type) {
snapshot.issue = ScriptFieldIssue::TypeMismatch;
}
}
if (state && state->instanceCreated && m_runtime->TryGetManagedFieldValue(state->context, metadata.name, snapshot.value)) {
snapshot.hasValue = true;
snapshot.valueSource = ScriptFieldValueSource::ManagedValue;
} else if (storedField && storedField->type == metadata.type) {
snapshot.hasValue = true;
snapshot.value = storedField->value;
snapshot.valueSource = ScriptFieldValueSource::StoredValue;
}
fieldIndexByName.emplace(metadata.name, outModel.fields.size());
outModel.fields.push_back(std::move(snapshot));
}
for (const std::string& fieldName : component->GetFieldStorage().GetFieldNames()) {
const StoredScriptField* storedField = component->GetFieldStorage().FindField(fieldName);
if (!storedField) {
continue;
}
const auto fieldIndexIt = fieldIndexByName.find(fieldName);
if (fieldIndexIt != fieldIndexByName.end()) {
continue;
}
ScriptFieldSnapshot snapshot;
snapshot.metadata = ScriptFieldMetadata{fieldName, storedField->type};
snapshot.hasValue = true;
snapshot.value = storedField->value;
snapshot.valueSource = ScriptFieldValueSource::StoredValue;
snapshot.issue = ScriptFieldIssue::StoredOnly;
snapshot.hasStoredValue = true;
snapshot.storedType = storedField->type;
snapshot.storedValue = storedField->value;
outModel.fields.push_back(std::move(snapshot));
}
if (outModel.classStatus != ScriptFieldClassStatus::Available) {
std::sort(
outModel.fields.begin(),
outModel.fields.end(),
[](const ScriptFieldSnapshot& lhs, const ScriptFieldSnapshot& rhs) {
return lhs.metadata.name < rhs.metadata.name;
});
}
return true;
}
bool ScriptEngine::TryGetScriptFieldSnapshots(
const ScriptComponent* component,
std::vector<ScriptFieldSnapshot>& outFields) const {
ScriptFieldModel model;
if (!TryGetScriptFieldModel(component, model)) {
outFields.clear();
return false;
}
outFields = std::move(model.fields);
return !outFields.empty();
}
size_t ScriptEngine::ScriptInstanceKeyHasher::operator()(const ScriptInstanceKey& key) const {
const size_t h1 = std::hash<uint64_t>{}(key.gameObjectUUID);
const size_t h2 = std::hash<uint64_t>{}(key.scriptComponentUUID);
@@ -372,6 +757,7 @@ bool ScriptEngine::EnsureScriptReady(ScriptInstanceState& state, bool invokeEnab
void ScriptEngine::InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime) {
m_runtime->InvokeMethod(state.context, method, deltaTime);
m_runtime->SyncManagedFieldsToStorage(state.context);
}
void ScriptEngine::StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping) {

View File

@@ -72,6 +72,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/FieldMetadataProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ScriptComponentApiProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs

View File

@@ -0,0 +1,17 @@
using XCEngine;
namespace Gameplay
{
public sealed class FieldMetadataProbe : MonoBehaviour
{
public int Health;
public float Speed;
public string Label = string.Empty;
public Vector3 SpawnPoint;
public GameObject Target;
public Quaternion UnsupportedRotation;
public static int SharedCounter;
private bool HiddenFlag = true;
public bool HiddenFlagMirror => HiddenFlag;
}
}

View File

@@ -85,6 +85,42 @@ public:
}
}
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.clear();
return false;
}
bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) override {
(void)context;
(void)fieldName;
(void)value;
return true;
}
bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const override {
(void)context;
(void)fieldName;
(void)outValue;
return false;
}
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override {
(void)context;
}
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
if (m_events) {
m_events->push_back("Create:" + Describe(context));

View File

@@ -100,6 +100,48 @@ TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasse
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) {
std::vector<ScriptFieldMetadata> fields;
EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "FieldMetadataProbe", fields));
const std::vector<ScriptFieldMetadata> expected = {
{"Health", ScriptFieldType::Int32},
{"Label", ScriptFieldType::String},
{"SpawnPoint", ScriptFieldType::Vector3},
{"Speed", ScriptFieldType::Float},
{"Target", ScriptFieldType::GameObject},
};
EXPECT_EQ(fields, expected);
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataQueryFailsForUnknownClass) {
std::vector<ScriptFieldMetadata> fields = {
{"Sentinel", ScriptFieldType::Bool},
};
EXPECT_FALSE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "MissingProbe", fields));
EXPECT_TRUE(fields.empty());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsManagedInitializers) {
std::vector<ScriptFieldDefaultValue> fields;
EXPECT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "Gameplay", "RuntimeGameObjectProbe", fields));
const auto fieldIt = std::find_if(
fields.begin(),
fields.end(),
[](const ScriptFieldDefaultValue& field) {
return field.fieldName == "ObservedRootChildCountAfterDestroy";
});
ASSERT_NE(fieldIt, fields.end());
EXPECT_EQ(fieldIt->type, ScriptFieldType::Int32);
EXPECT_EQ(std::get<int32_t>(fieldIt->value), -1);
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycleMethods) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
@@ -234,6 +276,746 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
}
TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) {
Scene originalScene("SerializedMonoScene");
GameObject* hostA = originalScene.CreateGameObject("HostA");
GameObject* hostB = originalScene.CreateGameObject("HostB");
GameObject* targetA = originalScene.CreateGameObject("TargetA");
GameObject* targetB = originalScene.CreateGameObject("TargetB");
ScriptComponent* originalComponentA = AddScript(hostA, "Gameplay", "LifecycleProbe");
ScriptComponent* originalComponentB = AddScript(hostB, "Gameplay", "LifecycleProbe");
originalComponentA->GetFieldStorage().SetFieldValue("Speed", 2.5f);
originalComponentA->GetFieldStorage().SetFieldValue("Label", "Alpha");
originalComponentA->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetA->GetUUID()});
originalComponentA->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
originalComponentB->GetFieldStorage().SetFieldValue("Speed", 9.0f);
originalComponentB->GetFieldStorage().SetFieldValue("Label", "Beta");
originalComponentB->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetB->GetUUID()});
originalComponentB->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f));
const uint64_t originalUUIDA = originalComponentA->GetScriptComponentUUID();
const uint64_t originalUUIDB = originalComponentB->GetScriptComponentUUID();
const std::string serializedScene = originalScene.SerializeToString();
scene = std::make_unique<Scene>("LoadedMonoScene");
scene->DeserializeFromString(serializedScene);
Scene* runtimeScene = scene.get();
GameObject* loadedHostA = runtimeScene->Find("HostA");
GameObject* loadedHostB = runtimeScene->Find("HostB");
GameObject* loadedTargetA = runtimeScene->Find("TargetA");
GameObject* loadedTargetB = runtimeScene->Find("TargetB");
ASSERT_NE(loadedHostA, nullptr);
ASSERT_NE(loadedHostB, nullptr);
ASSERT_NE(loadedTargetA, nullptr);
ASSERT_NE(loadedTargetB, nullptr);
ScriptComponent* loadedComponentA = FindScriptComponentByClass(loadedHostA, "Gameplay", "LifecycleProbe");
ScriptComponent* loadedComponentB = FindScriptComponentByClass(loadedHostB, "Gameplay", "LifecycleProbe");
ASSERT_NE(loadedComponentA, nullptr);
ASSERT_NE(loadedComponentB, nullptr);
EXPECT_EQ(loadedComponentA->GetScriptComponentUUID(), originalUUIDA);
EXPECT_EQ(loadedComponentB->GetScriptComponentUUID(), originalUUIDB);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
EXPECT_TRUE(runtime->HasManagedInstance(loadedComponentA));
EXPECT_TRUE(runtime->HasManagedInstance(loadedComponentB));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u);
int32_t awakeCountA = 0;
int32_t startCountA = 0;
int32_t awakeCountB = 0;
int32_t startCountB = 0;
bool targetResolvedA = false;
bool targetResolvedB = false;
float speedA = 0.0f;
float speedB = 0.0f;
std::string labelA;
std::string labelB;
std::string observedTargetNameA;
std::string observedTargetNameB;
GameObjectReference targetReferenceA;
GameObjectReference targetReferenceB;
GameObjectReference selfReferenceA;
GameObjectReference selfReferenceB;
XCEngine::Math::Vector3 spawnPointA;
XCEngine::Math::Vector3 spawnPointB;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "AwakeCount", awakeCountA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "StartCount", startCountA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "TargetResolved", targetResolvedA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Speed", speedA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Label", labelA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "ObservedTargetName", observedTargetNameA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "Target", targetReferenceA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "SelfReference", selfReferenceA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentA, "SpawnPoint", spawnPointA));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "AwakeCount", awakeCountB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "StartCount", startCountB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "TargetResolved", targetResolvedB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Speed", speedB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Label", labelB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "ObservedTargetName", observedTargetNameB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "Target", targetReferenceB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "SelfReference", selfReferenceB));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponentB, "SpawnPoint", spawnPointB));
EXPECT_EQ(awakeCountA, 1);
EXPECT_EQ(startCountA, 1);
EXPECT_TRUE(targetResolvedA);
EXPECT_FLOAT_EQ(speedA, 3.5f);
EXPECT_EQ(labelA, "Alpha|Awake");
EXPECT_EQ(observedTargetNameA, "TargetA");
EXPECT_EQ(targetReferenceA, GameObjectReference{loadedTargetA->GetUUID()});
EXPECT_EQ(selfReferenceA, GameObjectReference{loadedHostA->GetUUID()});
EXPECT_EQ(spawnPointA, XCEngine::Math::Vector3(2.0f, 2.0f, 3.0f));
EXPECT_EQ(awakeCountB, 1);
EXPECT_EQ(startCountB, 1);
EXPECT_TRUE(targetResolvedB);
EXPECT_FLOAT_EQ(speedB, 10.0f);
EXPECT_EQ(labelB, "Beta|Awake");
EXPECT_EQ(observedTargetNameB, "TargetB");
EXPECT_EQ(targetReferenceB, GameObjectReference{loadedTargetB->GetUUID()});
EXPECT_EQ(selfReferenceB, GameObjectReference{loadedHostB->GetUUID()});
EXPECT_EQ(spawnPointB, XCEngine::Math::Vector3(5.0f, 5.0f, 6.0f));
EXPECT_EQ(loadedHostA->GetName(), "HostA_Managed");
EXPECT_EQ(loadedHostB->GetName(), "HostB_Managed");
}
TEST_F(MonoScriptRuntimeTest, ManagedFieldChangesWriteBackToStoredCacheAndPersistAcrossSceneRoundTrip) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("Speed", 5.0f);
component->GetFieldStorage().SetFieldValue("Label", "Configured");
component->GetFieldStorage().SetFieldValue("Target", GameObjectReference{target->GetUUID()});
component->GetFieldStorage().SetFieldValue("SpawnPoint", XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
float storedSpeed = 0.0f;
std::string storedLabel;
GameObjectReference storedTarget;
XCEngine::Math::Vector3 storedSpawnPoint;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeed));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabel));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTarget));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPoint));
EXPECT_FLOAT_EQ(storedSpeed, 6.0f);
EXPECT_EQ(storedLabel, "Configured|Awake");
EXPECT_EQ(storedTarget, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f));
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
const std::string persistedHostName = host->GetName();
const std::string serializedScene = runtimeScene->SerializeToString();
engine->OnRuntimeStop();
scene = std::make_unique<Scene>("ReloadedMonoScene");
scene->DeserializeFromString(serializedScene);
Scene* reloadedScene = scene.get();
GameObject* loadedHost = reloadedScene->Find(persistedHostName);
GameObject* loadedTarget = reloadedScene->Find("Target");
ASSERT_NE(loadedHost, nullptr);
ASSERT_NE(loadedTarget, nullptr);
ScriptComponent* loadedComponent = FindScriptComponentByClass(loadedHost, "Gameplay", "LifecycleProbe");
ASSERT_NE(loadedComponent, nullptr);
float loadedStoredSpeed = 0.0f;
std::string loadedStoredLabel;
GameObjectReference loadedStoredTarget;
XCEngine::Math::Vector3 loadedStoredSpawnPoint;
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Speed", loadedStoredSpeed));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Label", loadedStoredLabel));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("Target", loadedStoredTarget));
EXPECT_TRUE(loadedComponent->GetFieldStorage().TryGetFieldValue("SpawnPoint", loadedStoredSpawnPoint));
EXPECT_FLOAT_EQ(loadedStoredSpeed, 6.0f);
EXPECT_EQ(loadedStoredLabel, "Configured|Awake");
EXPECT_EQ(loadedStoredTarget, GameObjectReference{loadedTarget->GetUUID()});
EXPECT_EQ(loadedStoredSpawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f));
EXPECT_FALSE(loadedComponent->GetFieldStorage().Contains("AwakeCount"));
engine->OnRuntimeStart(reloadedScene);
engine->OnUpdate(0.016f);
int32_t awakeCount = 0;
int32_t startCount = 0;
int32_t updateCount = 0;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(loadedComponent, "ObservedTargetName", observedTargetName));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_EQ(updateCount, 1);
EXPECT_FLOAT_EQ(runtimeSpeed, 7.0f);
EXPECT_EQ(runtimeLabel, "Configured|Awake|Awake");
EXPECT_EQ(observedTargetName, "Target");
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiUpdatesLiveManagedInstanceAndStoredCache) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 41.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "Edited"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f)));
EXPECT_FALSE(engine->TrySetScriptFieldValue(component, "DoesNotExist", int32_t(1)));
EXPECT_FALSE(engine->TrySetScriptFieldValue(component, "Speed", std::string("wrong")));
float storedSpeedBeforeUpdate = 0.0f;
std::string storedLabelBeforeUpdate;
GameObjectReference storedTargetBeforeUpdate;
XCEngine::Math::Vector3 storedSpawnPointBeforeUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeUpdate));
EXPECT_FLOAT_EQ(storedSpeedBeforeUpdate, 41.0f);
EXPECT_EQ(storedLabelBeforeUpdate, "Edited");
EXPECT_EQ(storedTargetBeforeUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeUpdate, XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f));
EXPECT_FALSE(component->GetFieldStorage().Contains("DoesNotExist"));
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 42.0f);
EXPECT_EQ(runtimeLabel, "Edited");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 42.0f);
EXPECT_EQ(storedLabelAfterUpdate, "Edited");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiCachesValuesBeforeRuntimeStartAndAppliesThemOnFirstInstance) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
EXPECT_FALSE(runtime->HasManagedInstance(component));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 9.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "PreStart"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f)));
float storedSpeedBeforeRuntime = 0.0f;
std::string storedLabelBeforeRuntime;
GameObjectReference storedTargetBeforeRuntime;
XCEngine::Math::Vector3 storedSpawnPointBeforeRuntime;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeRuntime));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeRuntime));
EXPECT_FLOAT_EQ(storedSpeedBeforeRuntime, 9.0f);
EXPECT_EQ(storedLabelBeforeRuntime, "PreStart");
EXPECT_EQ(storedTargetBeforeRuntime, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeRuntime, XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f));
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(runtime->HasManagedInstance(component));
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t awakeCount = 0;
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 10.0f);
EXPECT_EQ(runtimeLabel, "PreStart|Awake");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(4.0f, 4.0f, 5.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 10.0f);
EXPECT_EQ(storedLabelAfterUpdate, "PreStart|Awake");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(4.0f, 4.0f, 5.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldApiCachesValuesForInactiveRuntimeScriptUntilActivation) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
host->SetActive(false);
engine->OnRuntimeStart(runtimeScene);
EXPECT_FALSE(runtime->HasManagedInstance(component));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Speed", 12.0f));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Label", "Dormant"));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "Target", GameObjectReference{target->GetUUID()}));
EXPECT_TRUE(engine->TrySetScriptFieldValue(component, "SpawnPoint", XCEngine::Math::Vector3(5.0f, 6.0f, 7.0f)));
float storedSpeedBeforeActivation = 0.0f;
std::string storedLabelBeforeActivation;
GameObjectReference storedTargetBeforeActivation;
XCEngine::Math::Vector3 storedSpawnPointBeforeActivation;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetBeforeActivation));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointBeforeActivation));
EXPECT_FLOAT_EQ(storedSpeedBeforeActivation, 12.0f);
EXPECT_EQ(storedLabelBeforeActivation, "Dormant");
EXPECT_EQ(storedTargetBeforeActivation, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointBeforeActivation, XCEngine::Math::Vector3(5.0f, 6.0f, 7.0f));
host->SetActive(true);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
EXPECT_TRUE(runtime->HasManagedInstance(component));
int32_t awakeCount = 0;
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 13.0f);
EXPECT_EQ(runtimeLabel, "Dormant|Awake");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(6.0f, 6.0f, 7.0f));
float storedSpeedAfterUpdate = 0.0f;
std::string storedLabelAfterUpdate;
GameObjectReference storedTargetAfterUpdate;
XCEngine::Math::Vector3 storedSpawnPointAfterUpdate;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeedAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabelAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTargetAfterUpdate));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPointAfterUpdate));
EXPECT_FLOAT_EQ(storedSpeedAfterUpdate, 13.0f);
EXPECT_EQ(storedLabelAfterUpdate, "Dormant|Awake");
EXPECT_EQ(storedTargetAfterUpdate, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPointAfterUpdate, XCEngine::Math::Vector3(6.0f, 6.0f, 7.0f));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldBatchApiUpdatesLiveManagedInstanceAndReportsPerFieldStatus) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
GameObject* target = runtimeScene->CreateGameObject("Target");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldWriteRequest> requests = {
{"Speed", ScriptFieldType::Float, ScriptFieldValue(41.0f)},
{"Label", ScriptFieldType::String, ScriptFieldValue(std::string("BatchEdited"))},
{"Target", ScriptFieldType::GameObject, ScriptFieldValue(GameObjectReference{target->GetUUID()})},
{"SpawnPoint", ScriptFieldType::Vector3, ScriptFieldValue(XCEngine::Math::Vector3(10.0f, 20.0f, 30.0f))},
{"LegacyOnly", ScriptFieldType::UInt64, ScriptFieldValue(uint64_t(100))},
{"DoesNotExist", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))},
{"Speed", ScriptFieldType::String, ScriptFieldValue(std::string("wrong"))},
{"Label", ScriptFieldType::String, ScriptFieldValue(int32_t(5))}
};
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::Applied);
EXPECT_EQ(results[3].status, ScriptFieldWriteStatus::Applied);
EXPECT_EQ(results[4].status, ScriptFieldWriteStatus::StoredOnlyField);
EXPECT_EQ(results[5].status, ScriptFieldWriteStatus::UnknownField);
EXPECT_EQ(results[6].status, ScriptFieldWriteStatus::TypeMismatch);
EXPECT_EQ(results[7].status, ScriptFieldWriteStatus::InvalidValue);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
int32_t startCount = 0;
bool targetResolved = false;
float runtimeSpeed = 0.0f;
std::string runtimeLabel;
std::string observedTargetName;
XCEngine::Math::Vector3 runtimeSpawnPoint;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", runtimeSpeed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", runtimeLabel));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", runtimeSpawnPoint));
EXPECT_EQ(startCount, 1);
EXPECT_TRUE(targetResolved);
EXPECT_FLOAT_EQ(runtimeSpeed, 42.0f);
EXPECT_EQ(runtimeLabel, "BatchEdited");
EXPECT_EQ(observedTargetName, "Target");
EXPECT_EQ(runtimeSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
float storedSpeed = 0.0f;
std::string storedLabel;
GameObjectReference storedTarget;
XCEngine::Math::Vector3 storedSpawnPoint;
uint64_t storedLegacyOnly = 0;
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Speed", storedSpeed));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Label", storedLabel));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("Target", storedTarget));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("SpawnPoint", storedSpawnPoint));
EXPECT_TRUE(component->GetFieldStorage().TryGetFieldValue("LegacyOnly", storedLegacyOnly));
EXPECT_FLOAT_EQ(storedSpeed, 42.0f);
EXPECT_EQ(storedLabel, "BatchEdited");
EXPECT_EQ(storedTarget, GameObjectReference{target->GetUUID()});
EXPECT_EQ(storedSpawnPoint, XCEngine::Math::Vector3(11.0f, 20.0f, 30.0f));
EXPECT_EQ(storedLegacyOnly, 99u);
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldModelUsesManagedClassDefaultValuesBeforeRuntimeStart) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe");
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
const auto fieldIt = std::find_if(
model.fields.begin(),
model.fields.end(),
[](const ScriptFieldSnapshot& field) {
return field.metadata.name == "ObservedRootChildCountAfterDestroy";
});
ASSERT_NE(fieldIt, model.fields.end());
EXPECT_TRUE(fieldIt->declaredInClass);
EXPECT_TRUE(fieldIt->hasDefaultValue);
EXPECT_FALSE(fieldIt->hasValue);
EXPECT_EQ(fieldIt->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(std::get<int32_t>(fieldIt->defaultValue), -1);
EXPECT_EQ(std::get<int32_t>(fieldIt->value), -1);
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldClearApiRestoresManagedClassDefaultValuesAndRemovesStoredOverrides) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe");
component->GetFieldStorage().SetFieldValue("ObservedRootChildCountAfterDestroy", int32_t(7));
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(99));
engine->OnRuntimeStart(runtimeScene);
const std::vector<ScriptFieldClearRequest> requests = {
{"ObservedRootChildCountAfterDestroy"},
{"LegacyOnly"},
{"DoesNotExist"},
{""}
};
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::UnknownField);
EXPECT_EQ(results[3].status, ScriptFieldClearStatus::EmptyFieldName);
EXPECT_FALSE(component->GetFieldStorage().Contains("ObservedRootChildCountAfterDestroy"));
EXPECT_FALSE(component->GetFieldStorage().Contains("LegacyOnly"));
int32_t runtimeObservedCountAfterDestroy = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountAfterDestroy", runtimeObservedCountAfterDestroy));
EXPECT_EQ(runtimeObservedCountAfterDestroy, -1);
EXPECT_FALSE(component->GetFieldStorage().Contains("ObservedRootChildCountAfterDestroy"));
EXPECT_FALSE(component->GetFieldStorage().Contains("LegacyOnly"));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldReadApiReturnsManagedOnlyRuntimeValues) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
float speed = 0.0f;
int32_t awakeCount = 0;
EXPECT_FALSE(component->GetFieldStorage().Contains("Speed"));
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
EXPECT_FALSE(engine->TryGetScriptFieldValue(component, "Speed", speed));
EXPECT_FALSE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "AwakeCount", awakeCount));
EXPECT_EQ(awakeCount, 1);
EXPECT_FALSE(component->GetFieldStorage().Contains("AwakeCount"));
engine->OnUpdate(0.016f);
EXPECT_TRUE(engine->TryGetScriptFieldValue(component, "Speed", speed));
EXPECT_FLOAT_EQ(speed, 1.0f);
EXPECT_FALSE(component->GetFieldStorage().Contains("Speed"));
}
TEST_F(MonoScriptRuntimeTest, ScriptEngineFieldSnapshotApiReportsMetadataAndCurrentValues) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
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;
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* speedSnapshot = findSnapshot("Speed");
const ScriptFieldSnapshot* legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(speedSnapshot, 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(speedSnapshot->metadata.type, ScriptFieldType::Float);
EXPECT_TRUE(speedSnapshot->declaredInClass);
EXPECT_TRUE(speedSnapshot->hasDefaultValue);
EXPECT_FALSE(speedSnapshot->hasValue);
EXPECT_EQ(speedSnapshot->valueSource, ScriptFieldValueSource::DefaultValue);
EXPECT_EQ(speedSnapshot->issue, ScriptFieldIssue::None);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->defaultValue), 0.0f);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->value), 0.0f);
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);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Available);
awakeSnapshot = findSnapshot("AwakeCount");
labelSnapshot = findSnapshot("Label");
speedSnapshot = findSnapshot("Speed");
legacySnapshot = findSnapshot("LegacyOnly");
ASSERT_NE(awakeSnapshot, nullptr);
ASSERT_NE(labelSnapshot, nullptr);
ASSERT_NE(speedSnapshot, 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), "Stored|Awake");
EXPECT_TRUE(speedSnapshot->hasValue);
EXPECT_TRUE(speedSnapshot->hasDefaultValue);
EXPECT_EQ(speedSnapshot->valueSource, ScriptFieldValueSource::ManagedValue);
EXPECT_EQ(speedSnapshot->issue, ScriptFieldIssue::None);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->defaultValue), 0.0f);
EXPECT_FLOAT_EQ(std::get<float>(speedSnapshot->value), 1.0f);
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(MonoScriptRuntimeTest, ScriptEngineFieldModelReportsMissingScriptClassAndStoredFields) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "MissingLifecycleProbe");
component->GetFieldStorage().SetFieldValue("Label", "Stored");
component->GetFieldStorage().SetFieldValue("LegacyOnly", uint64_t(9));
ScriptFieldModel model;
ASSERT_TRUE(engine->TryGetScriptFieldModel(component, model));
EXPECT_EQ(model.classStatus, ScriptFieldClassStatus::Missing);
ASSERT_EQ(model.fields.size(), 2u);
EXPECT_EQ(model.fields[0].metadata.name, "Label");
EXPECT_EQ(model.fields[1].metadata.name, "LegacyOnly");
for (const ScriptFieldSnapshot& field : model.fields) {
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.metadata.type, field.storedType);
EXPECT_EQ(field.value, field.storedValue);
}
}
TEST_F(MonoScriptRuntimeTest, GameObjectComponentApiResolvesTransformAndRejectsUnsupportedManagedTypes) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");

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