feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

View File

@@ -12,6 +12,12 @@ if(XCENGINE_ENABLE_MONO_SCRIPTING)
list(APPEND SCRIPTING_TEST_SOURCES
test_mono_script_runtime.cpp
)
if(TARGET xcengine_project_managed_assemblies)
list(APPEND SCRIPTING_TEST_SOURCES
test_project_script_assembly.cpp
)
endif()
endif()
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})
@@ -46,6 +52,20 @@ if(TARGET xcengine_managed_assemblies)
)
endif()
if(TARGET xcengine_project_managed_assemblies)
add_dependencies(scripting_tests xcengine_project_managed_assemblies)
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}" XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE)
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_SCRIPT_CORE_DLL}" XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE)
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}" XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE)
target_compile_definitions(scripting_tests PRIVATE
XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR=\"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE}\"
XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL=\"${XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE}\"
XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL=\"${XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE}\"
)
endif()
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
add_custom_command(TARGET scripting_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different

View File

@@ -4,8 +4,13 @@
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Debug/ILogSink.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
@@ -28,6 +33,30 @@ void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Ma
EXPECT_NEAR(actual.z, expected.z, tolerance);
}
class CapturingLogSink final : public XCEngine::Debug::ILogSink {
public:
void Log(const XCEngine::Debug::LogEntry& entry) override {
entries.push_back(entry);
}
void Flush() override {
}
std::vector<std::string> CollectMessagesWithPrefix(const char* prefix) const {
std::vector<std::string> messages;
for (const XCEngine::Debug::LogEntry& entry : entries) {
const std::string message = entry.message.CStr();
if (message.rfind(prefix, 0) == 0) {
messages.push_back(message);
}
}
return messages;
}
std::vector<XCEngine::Debug::LogEntry> entries;
};
ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
if (!gameObject) {
return nullptr;
@@ -60,6 +89,8 @@ protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime);
XCEngine::Input::InputManager::Get().Shutdown();
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
@@ -70,6 +101,7 @@ protected:
void TearDown() override {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
XCEngine::Input::InputManager::Get().Shutdown();
runtime.reset();
scene.reset();
}
@@ -100,6 +132,44 @@ TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasse
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end());
}
TEST_F(MonoScriptRuntimeTest, ScriptClassDescriptorApiReturnsConcreteManagedTypes) {
std::vector<ScriptClassDescriptor> classes;
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classes));
ASSERT_FALSE(classes.empty());
EXPECT_TRUE(std::is_sorted(
classes.begin(),
classes.end(),
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
if (lhs.assemblyName != rhs.assemblyName) {
return lhs.assemblyName < rhs.assemblyName;
}
if (lhs.namespaceName != rhs.namespaceName) {
return lhs.namespaceName < rhs.namespaceName;
}
return lhs.className < rhs.className;
}));
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "LifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "AbstractLifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "UtilityHelper"}),
classes.end());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) {
std::vector<ScriptFieldMetadata> fields;
@@ -184,6 +254,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
bool observedIsActiveAndEnabled = false;
float speed = 0.0f;
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
float observedLateDeltaTime = 0.0f;
std::string label;
@@ -219,6 +291,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", speed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label));
@@ -254,6 +328,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_TRUE(observedIsActiveAndEnabled);
EXPECT_FLOAT_EQ(speed, 6.0f);
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f);
EXPECT_EQ(label, "Configured|Awake");
@@ -276,6 +352,233 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
}
TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossFixedAndVariableUpdates) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->SetRuntimeFixedDeltaTime(0.05f);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.05f);
engine->OnUpdate(0.016f);
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.05f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
}
TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) {
XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get();
inputManager.Initialize(nullptr);
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "InputProbe");
engine->OnRuntimeStart(runtimeScene);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::A, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::Space, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, true, 120, 48);
inputManager.ProcessMouseMove(120, 48, 3, -2);
inputManager.ProcessMouseWheel(1.0f, 120, 48);
engine->OnUpdate(0.016f);
int32_t updateCount = 0;
bool observedKeyA = false;
bool observedKeyADown = false;
bool observedKeyAUp = false;
bool observedKeySpace = false;
bool observedJump = false;
bool observedJumpDown = false;
bool observedJumpUp = false;
bool observedFire1 = false;
bool observedFire1Down = false;
bool observedFire1Up = false;
bool observedAnyKey = false;
bool observedAnyKeyDown = false;
bool observedLeftMouse = false;
bool observedLeftMouseDown = false;
bool observedLeftMouseUp = false;
float observedHorizontal = 0.0f;
float observedHorizontalRaw = 0.0f;
XCEngine::Math::Vector2 observedMouseScrollDelta;
XCEngine::Math::Vector3 observedMousePosition;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMousePosition", observedMousePosition));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 1);
EXPECT_TRUE(observedKeyA);
EXPECT_TRUE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedKeySpace);
EXPECT_TRUE(observedJump);
EXPECT_TRUE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_TRUE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_TRUE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, -1.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f));
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f));
inputManager.Update(0.016f);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 2);
EXPECT_TRUE(observedKeyA);
EXPECT_FALSE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_FALSE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_FALSE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f));
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::A, false, false, false, false);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, false, 120, 48);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_EQ(updateCount, 3);
EXPECT_FALSE(observedKeyA);
EXPECT_TRUE(observedKeyAUp);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpUp);
EXPECT_FALSE(observedFire1);
EXPECT_TRUE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_FALSE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, 0.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f);
inputManager.Update(0.016f);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::Space, false, false, false, false);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_EQ(updateCount, 4);
EXPECT_FALSE(observedKeySpace);
EXPECT_FALSE(observedJump);
EXPECT_TRUE(observedJumpUp);
EXPECT_FALSE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
}
TEST_F(MonoScriptRuntimeTest, ManagedDebugLogBridgeWritesLifecycleTickMessagesToNativeLogger) {
auto sink = std::make_unique<CapturingLogSink>();
CapturingLogSink* sinkPtr = sink.get();
XCEngine::Debug::Logger::Get().AddSink(std::move(sink));
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TickLogProbe");
ASSERT_NE(component, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
const std::vector<std::string> messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]");
const std::vector<std::string> expected = {
"[TickLogProbe] Awake",
"[TickLogProbe] FixedUpdate 1",
"[TickLogProbe] Start",
"[TickLogProbe] Update 1",
"[TickLogProbe] LateUpdate 1",
};
EXPECT_EQ(messages, expected);
XCEngine::Debug::Logger::Get().RemoveSink(sinkPtr);
}
TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) {
Scene originalScene("SerializedMonoScene");
GameObject* hostA = originalScene.CreateGameObject("HostA");

View File

@@ -0,0 +1,87 @@
#include <gtest/gtest.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <algorithm>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Scripting;
namespace {
MonoScriptRuntime::Settings CreateProjectMonoSettings() {
MonoScriptRuntime::Settings settings;
settings.assemblyDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR;
settings.corlibDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR;
settings.coreAssemblyPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL;
settings.appAssemblyPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL;
return settings;
}
class ProjectScriptAssemblyTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL));
ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL));
runtime = std::make_unique<MonoScriptRuntime>(CreateProjectMonoSettings());
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
}
std::unique_ptr<MonoScriptRuntime> runtime;
};
TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) {
EXPECT_TRUE(runtime->IsInitialized());
EXPECT_EQ(runtime->GetSettings().assemblyDirectory, std::filesystem::path(XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR));
EXPECT_EQ(runtime->GetSettings().appAssemblyPath, std::filesystem::path(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL));
}
TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFieldMetadata) {
const std::vector<std::string> classNames = runtime->GetScriptClassNames("GameScripts");
std::vector<ScriptClassDescriptor> classDescriptors;
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classDescriptors));
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "ProjectScripts", "ProjectScriptProbe"));
EXPECT_NE(
std::find(classNames.begin(), classNames.end(), "ProjectScripts.ProjectScriptProbe"),
classNames.end());
EXPECT_NE(
std::find(
classDescriptors.begin(),
classDescriptors.end(),
ScriptClassDescriptor{"GameScripts", "ProjectScripts", "ProjectScriptProbe"}),
classDescriptors.end());
std::vector<ScriptFieldMetadata> fields;
ASSERT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "ProjectScripts", "ProjectScriptProbe", fields));
const std::vector<ScriptFieldMetadata> expectedFields = {
{"EnabledOnBoot", ScriptFieldType::Bool},
{"Label", ScriptFieldType::String},
{"Speed", ScriptFieldType::Float},
};
EXPECT_EQ(fields, expectedFields);
std::vector<ScriptFieldDefaultValue> defaultValues;
ASSERT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "ProjectScripts", "ProjectScriptProbe", defaultValues));
ASSERT_EQ(defaultValues.size(), 3u);
EXPECT_EQ(defaultValues[0].fieldName, "EnabledOnBoot");
EXPECT_EQ(defaultValues[0].type, ScriptFieldType::Bool);
EXPECT_EQ(std::get<bool>(defaultValues[0].value), true);
EXPECT_EQ(defaultValues[1].fieldName, "Label");
EXPECT_EQ(defaultValues[1].type, ScriptFieldType::String);
EXPECT_EQ(std::get<std::string>(defaultValues[1].value), "ProjectScriptProbe");
EXPECT_EQ(defaultValues[2].fieldName, "Speed");
EXPECT_EQ(defaultValues[2].type, ScriptFieldType::Float);
EXPECT_FLOAT_EQ(std::get<float>(defaultValues[2].value), 2.5f);
}
} // namespace

View File

@@ -42,6 +42,12 @@ public:
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,
@@ -117,6 +123,7 @@ public:
}
std::vector<std::string> events;
std::vector<ScriptClassDescriptor> scriptClasses;
std::vector<ScriptFieldMetadata> fieldMetadata;
std::vector<ScriptFieldDefaultValue> fieldDefaultValues;
std::unordered_map<std::string, ScriptFieldValue> managedFieldValues;
@@ -325,6 +332,83 @@ TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndSta
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");