diff --git a/engine/include/XCEngine/Scripting/IScriptRuntime.h b/engine/include/XCEngine/Scripting/IScriptRuntime.h index e27462dc..7c103c45 100644 --- a/engine/include/XCEngine/Scripting/IScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/IScriptRuntime.h @@ -1,6 +1,10 @@ #pragma once +#include + #include +#include +#include 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& outFields) const = 0; + virtual bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& 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; diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 8266c900..3e25614c 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -50,6 +50,16 @@ public: const std::string& namespaceName, const std::string& className) const; std::vector GetScriptClassNames(const std::string& assemblyName = std::string()) const; + bool TryGetClassFieldMetadata( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& outFields) const override; + bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& 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; diff --git a/engine/include/XCEngine/Scripting/NullScriptRuntime.h b/engine/include/XCEngine/Scripting/NullScriptRuntime.h index 07c47ad0..5c5f411a 100644 --- a/engine/include/XCEngine/Scripting/NullScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/NullScriptRuntime.h @@ -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& outFields) const override; + bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& 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; diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h index 45df1254..a63fb7d8 100644 --- a/engine/include/XCEngine/Scripting/ScriptEngine.h +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include +#include #include #include @@ -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& requests, + std::vector& outResults); + bool ClearScriptFieldOverrides( + ScriptComponent* component, + const std::vector& requests, + std::vector& outResults); + bool TryGetScriptFieldModel( + const ScriptComponent* component, + ScriptFieldModel& outModel) const; + bool TryGetScriptFieldSnapshots( + const ScriptComponent* component, + std::vector& outFields) const; + + template + bool TrySetScriptFieldValue(ScriptComponent* component, const std::string& fieldName, const T& value) { + using ValueType = std::decay_t; + return TrySetScriptFieldValue( + component, + fieldName, + ScriptFieldTypeResolver::value, + ScriptFieldValue(ValueType(value))); + } + + bool TrySetScriptFieldValue(ScriptComponent* component, const std::string& fieldName, const char* value) { + return TrySetScriptFieldValue(component, fieldName, std::string(value ? value : "")); + } + + template + 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; + const ValueType* typedValue = std::get_if(&value); + if (!typedValue) { + return false; + } + + outValue = *typedValue; + return true; + } private: struct ScriptInstanceKey { diff --git a/engine/include/XCEngine/Scripting/ScriptField.h b/engine/include/XCEngine/Scripting/ScriptField.h index a1b7050f..43cd8f4d 100644 --- a/engine/include/XCEngine/Scripting/ScriptField.h +++ b/engine/include/XCEngine/Scripting/ScriptField.h @@ -7,6 +7,7 @@ #include #include #include +#include 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 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); diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 8e9582a4..3814fc11 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -1298,6 +1298,73 @@ std::vector MonoScriptRuntime::GetScriptClassNames(const std::strin return classNames; } +bool MonoScriptRuntime::TryGetClassFieldMetadata( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& 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& 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 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."); diff --git a/engine/src/Scripting/NullScriptRuntime.cpp b/engine/src/Scripting/NullScriptRuntime.cpp index f946eed5..e8266b9f 100644 --- a/engine/src/Scripting/NullScriptRuntime.cpp +++ b/engine/src/Scripting/NullScriptRuntime.cpp @@ -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& 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& 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; } diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp index eca772bb..3df0c8d4 100644 --- a/engine/src/Scripting/ScriptEngine.cpp +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -9,6 +9,51 @@ namespace XCEngine { namespace Scripting { +namespace { + +std::unordered_map CollectClassDefaultValues( + const IScriptRuntime* runtime, + const ScriptComponent* component, + ScriptFieldClassStatus classStatus) { + std::unordered_map defaultValues; + if (!runtime || !component || classStatus != ScriptFieldClassStatus::Available || !component->HasScriptClass()) { + return defaultValues; + } + + std::vector 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& 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 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& requests, + std::vector& outResults) { + outResults.clear(); + if (!component) { + return false; + } + + ScriptFieldModel model; + if (!TryGetScriptFieldModel(component, model)) { + return false; + } + + const std::unordered_map defaultValues = + CollectClassDefaultValues(m_runtime, component, model.classStatus); + + std::unordered_map 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& requests, + std::vector& outResults) { + outResults.clear(); + if (!component) { + return false; + } + + ScriptFieldModel model; + if (!TryGetScriptFieldModel(component, model)) { + return false; + } + + const std::unordered_map defaultValues = + CollectClassDefaultValues(m_runtime, component, model.classStatus); + + std::unordered_map 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 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 defaultValues = + CollectClassDefaultValues(m_runtime, component, outModel.classStatus); + + const ScriptInstanceState* state = m_runtimeRunning ? FindState(component) : nullptr; + std::unordered_map 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& 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{}(key.gameObjectUUID); const size_t h2 = std::hash{}(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) { diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 9f52125e..d9b4ed88 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -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 diff --git a/managed/GameScripts/FieldMetadataProbe.cs b/managed/GameScripts/FieldMetadataProbe.cs new file mode 100644 index 00000000..410e1276 --- /dev/null +++ b/managed/GameScripts/FieldMetadataProbe.cs @@ -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; + } +} diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp index 2f3d6bb4..7fbdf8d2 100644 --- a/tests/Scene/test_scene_runtime.cpp +++ b/tests/Scene/test_scene_runtime.cpp @@ -85,6 +85,42 @@ public: } } + bool TryGetClassFieldMetadata( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& 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)); diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index af631a3b..3fc21797 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -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 fields; + + EXPECT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "FieldMetadataProbe", fields)); + + const std::vector 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 fields = { + {"Sentinel", ScriptFieldType::Bool}, + }; + + EXPECT_FALSE(runtime->TryGetClassFieldMetadata("GameScripts", "Gameplay", "MissingProbe", fields)); + EXPECT_TRUE(fields.empty()); +} + +TEST_F(MonoScriptRuntimeTest, ClassFieldDefaultValueQueryReturnsManagedInitializers) { + std::vector 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(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("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("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 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 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(fieldIt->defaultValue), -1); + EXPECT_EQ(std::get(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 requests = { + {"ObservedRootChildCountAfterDestroy"}, + {"LegacyOnly"}, + {"DoesNotExist"}, + {""} + }; + + std::vector 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& 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(awakeSnapshot->defaultValue), 0); + EXPECT_EQ(std::get(awakeSnapshot->value), 0); + EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); + EXPECT_EQ(std::get(labelSnapshot->value), "Stored"); + EXPECT_EQ(std::get(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(speedSnapshot->defaultValue), 0.0f); + EXPECT_FLOAT_EQ(std::get(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(legacySnapshot->value), 99u); + EXPECT_EQ(std::get(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(awakeSnapshot->defaultValue), 0); + EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); + EXPECT_EQ(std::get(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(speedSnapshot->defaultValue), 0.0f); + EXPECT_FLOAT_EQ(std::get(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(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"); diff --git a/tests/scripting/test_script_engine.cpp b/tests/scripting/test_script_engine.cpp index 58cdd524..012ceb27 100644 --- a/tests/scripting/test_script_engine.cpp +++ b/tests/scripting/test_script_engine.cpp @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include #include @@ -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& 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& 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 events; + std::vector fieldMetadata; + std::vector fieldDefaultValues; + std::unordered_map managedFieldValues; + std::vector 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& 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(awakeSnapshot->defaultValue), 0); + EXPECT_EQ(std::get(awakeSnapshot->value), 0); + EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); + EXPECT_EQ(std::get(labelSnapshot->value), "Stored"); + EXPECT_EQ(std::get(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(legacySnapshot->value), 99u); + EXPECT_EQ(std::get(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(awakeSnapshot->defaultValue), 0); + EXPECT_EQ(std::get(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(labelSnapshot->defaultValue), ""); + EXPECT_EQ(std::get(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(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(field.value), "Stored"); + EXPECT_EQ(std::get(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(healthSnapshot->defaultValue), 17); + EXPECT_EQ(std::get(healthSnapshot->value), 17); + + EXPECT_TRUE(labelSnapshot->hasDefaultValue); + EXPECT_FALSE(labelSnapshot->hasValue); + EXPECT_EQ(labelSnapshot->valueSource, ScriptFieldValueSource::DefaultValue); + EXPECT_EQ(std::get(labelSnapshot->defaultValue), "Seeded"); + EXPECT_EQ(std::get(labelSnapshot->value), "Seeded"); + + EXPECT_TRUE(spawnPointSnapshot->hasDefaultValue); + EXPECT_FALSE(spawnPointSnapshot->hasValue); + EXPECT_EQ(spawnPointSnapshot->valueSource, ScriptFieldValueSource::DefaultValue); + EXPECT_EQ(std::get(spawnPointSnapshot->defaultValue), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + EXPECT_EQ(std::get(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 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 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(runtime.managedFieldValues["Speed"]), 5.0f); + EXPECT_EQ(std::get(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 requests = { + {"Label", ScriptFieldType::String, ScriptFieldValue(std::string("Retained"))}, + {"Missing", ScriptFieldType::Int32, ScriptFieldValue(int32_t(1))} + }; + + std::vector 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 requests = { + {"Speed"}, + {"Label"}, + {"LegacyOnly"}, + {"Missing"}, + {""} + }; + + std::vector 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(runtime.managedFieldValues["Speed"]), 6.5f); + EXPECT_EQ(std::get(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 requests = { + {"Speed"} + }; + + std::vector 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 requests = { + {"Label"}, + {"Missing"} + }; + + std::vector 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