798 lines
31 KiB
C++
798 lines
31 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <XCEngine/Scene/Scene.h>
|
|
#include <XCEngine/Scripting/IScriptRuntime.h>
|
|
#include <XCEngine/Scripting/ScriptComponent.h>
|
|
#include <XCEngine/Scripting/ScriptEngine.h>
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace XCEngine::Components;
|
|
using namespace XCEngine::Scripting;
|
|
|
|
namespace {
|
|
|
|
std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
|
|
switch (method) {
|
|
case ScriptLifecycleMethod::Awake: return "Awake";
|
|
case ScriptLifecycleMethod::OnEnable: return "OnEnable";
|
|
case ScriptLifecycleMethod::Start: return "Start";
|
|
case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate";
|
|
case ScriptLifecycleMethod::Update: return "Update";
|
|
case ScriptLifecycleMethod::LateUpdate: return "LateUpdate";
|
|
case ScriptLifecycleMethod::OnDisable: return "OnDisable";
|
|
case ScriptLifecycleMethod::OnDestroy: return "OnDestroy";
|
|
}
|
|
|
|
return "Unknown";
|
|
}
|
|
|
|
class FakeScriptRuntime : public IScriptRuntime {
|
|
public:
|
|
void OnRuntimeStart(Scene* scene) override {
|
|
events.push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
|
|
}
|
|
|
|
void OnRuntimeStop(Scene* scene) override {
|
|
events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
|
|
}
|
|
|
|
bool TryGetAvailableScriptClasses(
|
|
std::vector<ScriptClassDescriptor>& outClasses) const override {
|
|
outClasses = scriptClasses;
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
|
|
events.push_back("Destroy:" + Describe(context));
|
|
}
|
|
|
|
void InvokeMethod(
|
|
const ScriptRuntimeContext& context,
|
|
ScriptLifecycleMethod method,
|
|
float deltaTime) override {
|
|
(void)deltaTime;
|
|
events.push_back(LifecycleMethodToString(method) + ":" + Describe(context));
|
|
}
|
|
|
|
void Clear() {
|
|
events.clear();
|
|
}
|
|
|
|
std::vector<std::string> events;
|
|
std::vector<ScriptClassDescriptor> scriptClasses;
|
|
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) {
|
|
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
|
|
const std::string className = context.component ? context.component->GetFullClassName() : "null";
|
|
return gameObjectName + ":" + className;
|
|
}
|
|
};
|
|
|
|
class ScriptEngineTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
engine = &ScriptEngine::Get();
|
|
engine->OnRuntimeStop();
|
|
engine->SetRuntime(&runtime);
|
|
}
|
|
|
|
void TearDown() override {
|
|
engine->OnRuntimeStop();
|
|
engine->SetRuntime(nullptr);
|
|
scene.reset();
|
|
}
|
|
|
|
ScriptComponent* AddScriptComponent(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
|
|
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
|
|
component->SetScriptClass("GameScripts", namespaceName, className);
|
|
return component;
|
|
}
|
|
|
|
Scene* CreateScene(const std::string& sceneName) {
|
|
scene = std::make_unique<Scene>(sceneName);
|
|
return scene.get();
|
|
}
|
|
|
|
ScriptEngine* engine = nullptr;
|
|
FakeScriptRuntime runtime;
|
|
std::unique_ptr<Scene> scene;
|
|
};
|
|
|
|
TEST_F(ScriptEngineTest, RuntimeStartTracksActiveScriptsAndInvokesAwakeThenOnEnable) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "PlayerController");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
|
|
EXPECT_TRUE(engine->IsRuntimeRunning());
|
|
EXPECT_EQ(engine->GetRuntimeScene(), runtimeScene);
|
|
EXPECT_EQ(engine->GetTrackedScriptCount(), 1u);
|
|
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
|
|
EXPECT_TRUE(engine->HasRuntimeInstance(component));
|
|
|
|
const std::vector<std::string> expected = {
|
|
"RuntimeStart:RuntimeScene",
|
|
"Create:Host:Gameplay.PlayerController",
|
|
"Awake:Host:Gameplay.PlayerController",
|
|
"OnEnable:Host:Gameplay.PlayerController"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, UpdateInvokesStartOnlyOnceAndRunsPerFrameMethods) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
AddScriptComponent(host, "Gameplay", "Mover");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
engine->OnUpdate(0.016f);
|
|
engine->OnFixedUpdate(0.02f);
|
|
engine->OnUpdate(0.016f);
|
|
engine->OnLateUpdate(0.016f);
|
|
|
|
const std::vector<std::string> expected = {
|
|
"Start:Host:Gameplay.Mover",
|
|
"Update:Host:Gameplay.Mover",
|
|
"FixedUpdate:Host:Gameplay.Mover",
|
|
"Update:Host:Gameplay.Mover",
|
|
"LateUpdate:Host:Gameplay.Mover"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, InactiveObjectDefersCreationUntilActivated) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "SpawnedLater");
|
|
host->SetActive(false);
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
|
|
ASSERT_EQ(engine->GetTrackedScriptCount(), 1u);
|
|
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
|
|
EXPECT_FALSE(engine->HasRuntimeInstance(component));
|
|
ASSERT_EQ(runtime.events.size(), 1u);
|
|
EXPECT_EQ(runtime.events[0], "RuntimeStart:RuntimeScene");
|
|
|
|
runtime.Clear();
|
|
host->SetActive(true);
|
|
engine->OnUpdate(0.016f);
|
|
|
|
const std::vector<std::string> expected = {
|
|
"Create:Host:Gameplay.SpawnedLater",
|
|
"Awake:Host:Gameplay.SpawnedLater",
|
|
"OnEnable:Host:Gameplay.SpawnedLater",
|
|
"Start:Host:Gameplay.SpawnedLater",
|
|
"Update:Host:Gameplay.SpawnedLater"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, RuntimeStopInvokesDisableDestroyAndRuntimeStop) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
AddScriptComponent(host, "Gameplay", "ShutdownWatcher");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
engine->OnRuntimeStop();
|
|
|
|
const std::vector<std::string> expected = {
|
|
"OnDisable:Host:Gameplay.ShutdownWatcher",
|
|
"OnDestroy:Host:Gameplay.ShutdownWatcher",
|
|
"Destroy:Host:Gameplay.ShutdownWatcher",
|
|
"RuntimeStop:RuntimeScene"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
EXPECT_FALSE(engine->IsRuntimeRunning());
|
|
EXPECT_EQ(engine->GetRuntimeScene(), nullptr);
|
|
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, ReEnablingScriptDoesNotInvokeStartTwice) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "ToggleWatcher");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
engine->OnUpdate(0.016f);
|
|
runtime.Clear();
|
|
|
|
component->SetEnabled(false);
|
|
component->SetEnabled(true);
|
|
engine->OnUpdate(0.016f);
|
|
|
|
const std::vector<std::string> expected = {
|
|
"OnDisable:Host:Gameplay.ToggleWatcher",
|
|
"OnEnable:Host:Gameplay.ToggleWatcher",
|
|
"Update:Host:Gameplay.ToggleWatcher"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, DestroyingGameObjectWhileRuntimeRunningDestroysTrackedScript) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
AddScriptComponent(host, "Gameplay", "DestroyWatcher");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
runtimeScene->DestroyGameObject(host);
|
|
|
|
const std::vector<std::string> expected = {
|
|
"OnDisable:Host:Gameplay.DestroyWatcher",
|
|
"OnDestroy:Host:Gameplay.DestroyWatcher",
|
|
"Destroy:Host:Gameplay.DestroyWatcher"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndStartsOnNextUpdate) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
GameObject* spawned = runtimeScene->CreateGameObject("Spawned");
|
|
ScriptComponent* component = AddScriptComponent(spawned, "Gameplay", "RuntimeSpawned");
|
|
|
|
EXPECT_EQ(engine->GetTrackedScriptCount(), 1u);
|
|
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
|
|
EXPECT_TRUE(engine->HasRuntimeInstance(component));
|
|
|
|
const std::vector<std::string> expectedBeforeUpdate = {
|
|
"Create:Spawned:Gameplay.RuntimeSpawned",
|
|
"Awake:Spawned:Gameplay.RuntimeSpawned",
|
|
"OnEnable:Spawned:Gameplay.RuntimeSpawned"
|
|
};
|
|
EXPECT_EQ(runtime.events, expectedBeforeUpdate);
|
|
|
|
runtime.Clear();
|
|
engine->OnUpdate(0.016f);
|
|
|
|
const std::vector<std::string> expectedAfterUpdate = {
|
|
"Start:Spawned:Gameplay.RuntimeSpawned",
|
|
"Update:Spawned:Gameplay.RuntimeSpawned"
|
|
};
|
|
EXPECT_EQ(runtime.events, expectedAfterUpdate);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, ScriptClassDiscoveryApiReturnsSortedDescriptorsAndSupportsAssemblyFilter) {
|
|
runtime.scriptClasses = {
|
|
{"Tools", "", "UtilityProbe"},
|
|
{"GameScripts", "Gameplay", "Zombie"},
|
|
{"GameScripts", "", "Bootstrap"},
|
|
{"GameScripts", "Gameplay", "Actor"},
|
|
{"Broken", "", ""}
|
|
};
|
|
|
|
std::vector<ScriptClassDescriptor> allClasses;
|
|
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(allClasses));
|
|
|
|
const std::vector<ScriptClassDescriptor> expectedAllClasses = {
|
|
{"GameScripts", "", "Bootstrap"},
|
|
{"GameScripts", "Gameplay", "Actor"},
|
|
{"GameScripts", "Gameplay", "Zombie"},
|
|
{"Tools", "", "UtilityProbe"}
|
|
};
|
|
EXPECT_EQ(allClasses, expectedAllClasses);
|
|
EXPECT_EQ(allClasses[0].GetFullName(), "Bootstrap");
|
|
EXPECT_EQ(allClasses[1].GetFullName(), "Gameplay.Actor");
|
|
|
|
std::vector<ScriptClassDescriptor> gameScriptClasses;
|
|
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(gameScriptClasses, "GameScripts"));
|
|
|
|
const std::vector<ScriptClassDescriptor> expectedGameScriptClasses = {
|
|
{"GameScripts", "", "Bootstrap"},
|
|
{"GameScripts", "Gameplay", "Actor"},
|
|
{"GameScripts", "Gameplay", "Zombie"}
|
|
};
|
|
EXPECT_EQ(gameScriptClasses, expectedGameScriptClasses);
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, ChangingScriptClassWhileRuntimeRunningRecreatesTrackedInstance) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
component->SetScriptClass("GameScripts", "Gameplay", "NewClass");
|
|
|
|
const std::vector<std::string> expected = {
|
|
"OnDisable:Host:Gameplay.NewClass",
|
|
"OnDestroy:Host:Gameplay.NewClass",
|
|
"Destroy:Host:Gameplay.NewClass",
|
|
"Create:Host:Gameplay.NewClass",
|
|
"Awake:Host:Gameplay.NewClass",
|
|
"OnEnable:Host:Gameplay.NewClass"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
|
|
EXPECT_TRUE(engine->HasRuntimeInstance(component));
|
|
}
|
|
|
|
TEST_F(ScriptEngineTest, ClearingScriptClassWhileRuntimeRunningDestroysTrackedInstance) {
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
|
|
|
|
engine->OnRuntimeStart(runtimeScene);
|
|
runtime.Clear();
|
|
|
|
component->ClearScriptClass();
|
|
|
|
const std::vector<std::string> expected = {
|
|
"OnDisable:Host:",
|
|
"OnDestroy:Host:",
|
|
"Destroy:Host:"
|
|
};
|
|
EXPECT_EQ(runtime.events, expected);
|
|
EXPECT_FALSE(engine->HasTrackedScriptComponent(component));
|
|
EXPECT_FALSE(engine->HasRuntimeInstance(component));
|
|
EXPECT_FALSE(component->HasScriptClass());
|
|
}
|
|
|
|
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
|