diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 1fb190cf..c6e0356a 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -40,6 +41,7 @@ namespace { struct MonoRootState { MonoDomain* rootDomain = nullptr; bool initialized = false; + bool cleanupRegistered = false; }; enum class ManagedComponentKind { @@ -81,6 +83,19 @@ bool& GetInternalCallRegistrationState() { return registered; } +void CleanupMonoRootDomainAtExit() { + MonoRootState& rootState = GetMonoRootState(); + if (!rootState.rootDomain) { + return; + } + + mono_domain_set(rootState.rootDomain, true); + mono_jit_cleanup(rootState.rootDomain); + rootState.rootDomain = nullptr; + rootState.initialized = false; + GetInternalCallRegistrationState() = false; +} + std::string BuildFullClassName(const std::string& namespaceName, const std::string& className) { return namespaceName.empty() ? className : namespaceName + "." + className; } @@ -2455,6 +2470,10 @@ bool MonoScriptRuntime::InitializeRootDomain() { mono_domain_set(rootState.rootDomain, true); RegisterInternalCalls(); rootState.initialized = true; + if (!rootState.cleanupRegistered) { + std::atexit(&CleanupMonoRootDomainAtExit); + rootState.cleanupRegistered = true; + } return true; } diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 7aef231f..71977bd4 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -131,6 +131,8 @@ set(XCENGINE_SCRIPT_CORE_SOURCES set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/CameraLightLookupProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/CameraPropertyProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/AddComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ComponentReferenceProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/EnumFieldProbe.cs @@ -141,8 +143,11 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/InputProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LightPropertyProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererFlagsProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererPathProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ObjectApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/PhysicsApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/PhysicsEventProbe.cs diff --git a/managed/GameScripts/CameraLightLookupProbe.cs b/managed/GameScripts/CameraLightLookupProbe.cs new file mode 100644 index 00000000..bbda2c57 --- /dev/null +++ b/managed/GameScripts/CameraLightLookupProbe.cs @@ -0,0 +1,22 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class CameraLightLookupProbe : MonoBehaviour + { + public bool HasCamera; + public bool HasLight; + public bool CameraLookupSucceeded; + public bool LightLookupSucceeded; + + public void Start() + { + HasCamera = HasComponent(); + HasLight = HasComponent(); + CameraLookupSucceeded = TryGetComponent(out Camera camera); + LightLookupSucceeded = TryGetComponent(out Light light); + CameraLookupSucceeded = CameraLookupSucceeded && camera != null; + LightLookupSucceeded = LightLookupSucceeded && light != null; + } + } +} diff --git a/managed/GameScripts/CameraPropertyProbe.cs b/managed/GameScripts/CameraPropertyProbe.cs new file mode 100644 index 00000000..68fcfb90 --- /dev/null +++ b/managed/GameScripts/CameraPropertyProbe.cs @@ -0,0 +1,36 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class CameraPropertyProbe : MonoBehaviour + { + public bool CameraLookupSucceeded; + public float ObservedFieldOfView; + public float ObservedNearClipPlane; + public float ObservedFarClipPlane; + public float ObservedDepth; + public bool ObservedPrimary; + + public void Start() + { + CameraLookupSucceeded = TryGetComponent(out Camera camera); + if (camera == null) + { + CameraLookupSucceeded = false; + return; + } + + ObservedFieldOfView = camera.fieldOfView; + ObservedNearClipPlane = camera.nearClipPlane; + ObservedFarClipPlane = camera.farClipPlane; + ObservedDepth = camera.depth; + ObservedPrimary = camera.primary; + + camera.fieldOfView = 75.0f; + camera.nearClipPlane = 0.3f; + camera.farClipPlane = 500.0f; + camera.depth = 3.0f; + camera.primary = false; + } + } +} diff --git a/managed/GameScripts/LightPropertyProbe.cs b/managed/GameScripts/LightPropertyProbe.cs new file mode 100644 index 00000000..534ef091 --- /dev/null +++ b/managed/GameScripts/LightPropertyProbe.cs @@ -0,0 +1,33 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class LightPropertyProbe : MonoBehaviour + { + public bool LightLookupSucceeded; + public float ObservedIntensity; + public float ObservedRange; + public float ObservedSpotAngle; + public bool ObservedCastsShadows; + + public void Start() + { + LightLookupSucceeded = TryGetComponent(out Light light); + if (light == null) + { + LightLookupSucceeded = false; + return; + } + + ObservedIntensity = light.intensity; + ObservedRange = light.range; + ObservedSpotAngle = light.spotAngle; + ObservedCastsShadows = light.castsShadows; + + light.intensity = 2.5f; + light.range = 42.0f; + light.spotAngle = 55.0f; + light.castsShadows = true; + } + } +} diff --git a/managed/GameScripts/MeshRendererFlagsProbe.cs b/managed/GameScripts/MeshRendererFlagsProbe.cs new file mode 100644 index 00000000..08dd0155 --- /dev/null +++ b/managed/GameScripts/MeshRendererFlagsProbe.cs @@ -0,0 +1,30 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class MeshRendererFlagsProbe : MonoBehaviour + { + public bool MeshRendererLookupSucceeded; + public bool ObservedCastShadows; + public bool ObservedReceiveShadows; + public int ObservedRenderLayer; + + public void Start() + { + MeshRendererLookupSucceeded = TryGetComponent(out MeshRenderer meshRenderer); + if (meshRenderer == null) + { + MeshRendererLookupSucceeded = false; + return; + } + + ObservedCastShadows = meshRenderer.castShadows; + ObservedReceiveShadows = meshRenderer.receiveShadows; + ObservedRenderLayer = meshRenderer.renderLayer; + + meshRenderer.castShadows = false; + meshRenderer.receiveShadows = true; + meshRenderer.renderLayer = 11; + } + } +} diff --git a/managed/GameScripts/MeshRendererPathProbe.cs b/managed/GameScripts/MeshRendererPathProbe.cs new file mode 100644 index 00000000..3a0907d5 --- /dev/null +++ b/managed/GameScripts/MeshRendererPathProbe.cs @@ -0,0 +1,29 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class MeshRendererPathProbe : MonoBehaviour + { + public bool MeshRendererLookupSucceeded; + public int ObservedInitialMaterialCount; + public string ObservedInitialMaterial0Path = string.Empty; + public int ObservedUpdatedMaterialCount; + public string ObservedUpdatedMaterial1Path = string.Empty; + + public void Start() + { + MeshRendererLookupSucceeded = TryGetComponent(out MeshRenderer meshRenderer); + if (meshRenderer == null) + { + MeshRendererLookupSucceeded = false; + return; + } + + ObservedInitialMaterialCount = meshRenderer.materialCount; + ObservedInitialMaterial0Path = meshRenderer.GetMaterialPath(0); + meshRenderer.SetMaterialPath(1, "Materials/runtime_override.mat"); + ObservedUpdatedMaterialCount = meshRenderer.materialCount; + ObservedUpdatedMaterial1Path = meshRenderer.GetMaterialPath(1); + } + } +} diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 7e3c5dd3..4b8029f0 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -97,6 +98,7 @@ protected: engine->OnRuntimeStop(); engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime); XCEngine::Input::InputManager::Get().Shutdown(); + XCEngine::Resources::ResourceManager::Get().Shutdown(); XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor(); runtime = std::make_unique(CreateMonoSettings()); @@ -112,6 +114,7 @@ protected: XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor(); runtime.reset(); scene.reset(); + XCEngine::Resources::ResourceManager::Get().Shutdown(); } Scene* CreateScene(const std::string& sceneName) { @@ -2036,6 +2039,180 @@ TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentWrappersReadAndWriteCameraA EXPECT_TRUE(light->GetCastsShadows()); } +TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentLookupOnlyCameraAndLight) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + host->AddComponent(); + host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "CameraLightLookupProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool hasCamera = false; + bool hasLight = false; + bool cameraLookupSucceeded = false; + bool lightLookupSucceeded = false; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasCamera", hasCamera)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasLight", hasLight)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded)); + + EXPECT_TRUE(hasCamera); + EXPECT_TRUE(hasLight); + EXPECT_TRUE(cameraLookupSucceeded); + EXPECT_TRUE(lightLookupSucceeded); +} + +TEST_F(MonoScriptRuntimeTest, ManagedCameraWrapperReadAndWriteProperties) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + CameraComponent* camera = host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "CameraPropertyProbe"); + + camera->SetFieldOfView(52.0f); + camera->SetNearClipPlane(0.2f); + camera->SetFarClipPlane(300.0f); + camera->SetDepth(1.5f); + camera->SetPrimary(true); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool cameraLookupSucceeded = false; + bool observedPrimary = false; + float observedFieldOfView = 0.0f; + float observedNearClipPlane = 0.0f; + float observedFarClipPlane = 0.0f; + float observedDepth = 0.0f; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFieldOfView", observedFieldOfView)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedNearClipPlane", observedNearClipPlane)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFarClipPlane", observedFarClipPlane)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedDepth", observedDepth)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPrimary", observedPrimary)); + + EXPECT_TRUE(cameraLookupSucceeded); + EXPECT_FLOAT_EQ(observedFieldOfView, 52.0f); + EXPECT_FLOAT_EQ(observedNearClipPlane, 0.2f); + EXPECT_FLOAT_EQ(observedFarClipPlane, 300.0f); + EXPECT_FLOAT_EQ(observedDepth, 1.5f); + EXPECT_TRUE(observedPrimary); + + EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 75.0f); + EXPECT_FLOAT_EQ(camera->GetNearClipPlane(), 0.3f); + EXPECT_FLOAT_EQ(camera->GetFarClipPlane(), 500.0f); + EXPECT_FLOAT_EQ(camera->GetDepth(), 3.0f); + EXPECT_FALSE(camera->IsPrimary()); +} + +TEST_F(MonoScriptRuntimeTest, ManagedLightWrapperReadAndWriteProperties) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + LightComponent* light = host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "LightPropertyProbe"); + + light->SetIntensity(1.25f); + light->SetRange(12.0f); + light->SetSpotAngle(33.0f); + light->SetCastsShadows(false); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool lightLookupSucceeded = false; + bool observedCastsShadows = true; + float observedIntensity = 0.0f; + float observedRange = 0.0f; + float observedSpotAngle = 0.0f; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIntensity", observedIntensity)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRange", observedRange)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedSpotAngle", observedSpotAngle)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastsShadows", observedCastsShadows)); + + EXPECT_TRUE(lightLookupSucceeded); + EXPECT_FLOAT_EQ(observedIntensity, 1.25f); + EXPECT_FLOAT_EQ(observedRange, 12.0f); + EXPECT_FLOAT_EQ(observedSpotAngle, 33.0f); + EXPECT_FALSE(observedCastsShadows); + + EXPECT_FLOAT_EQ(light->GetIntensity(), 2.5f); + EXPECT_FLOAT_EQ(light->GetRange(), 42.0f); + EXPECT_FLOAT_EQ(light->GetSpotAngle(), 55.0f); + EXPECT_TRUE(light->GetCastsShadows()); +} + +TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteFlagsOnly) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + MeshRendererComponent* meshRenderer = host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererFlagsProbe"); + + meshRenderer->SetCastShadows(true); + meshRenderer->SetReceiveShadows(false); + meshRenderer->SetRenderLayer(7); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool meshRendererLookupSucceeded = false; + bool observedCastShadows = false; + bool observedReceiveShadows = true; + int32_t observedRenderLayer = 0; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastShadows", observedCastShadows)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedReceiveShadows", observedReceiveShadows)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer)); + + EXPECT_TRUE(meshRendererLookupSucceeded); + EXPECT_TRUE(observedCastShadows); + EXPECT_FALSE(observedReceiveShadows); + EXPECT_EQ(observedRenderLayer, 7); + + EXPECT_FALSE(meshRenderer->GetCastShadows()); + EXPECT_TRUE(meshRenderer->GetReceiveShadows()); + EXPECT_EQ(meshRenderer->GetRenderLayer(), 11u); +} + +TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteMaterialPathsOnly) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + MeshRendererComponent* meshRenderer = host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererPathProbe"); + + meshRenderer->SetMaterialPath(0, "Materials/initial.mat"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool meshRendererLookupSucceeded = false; + int32_t observedInitialMaterialCount = 0; + std::string observedInitialMaterial0Path; + int32_t observedUpdatedMaterialCount = 0; + std::string observedUpdatedMaterial1Path; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterialCount", observedInitialMaterialCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterial0Path", observedInitialMaterial0Path)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterialCount", observedUpdatedMaterialCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterial1Path", observedUpdatedMaterial1Path)); + + EXPECT_TRUE(meshRendererLookupSucceeded); + EXPECT_EQ(observedInitialMaterialCount, 1); + EXPECT_EQ(observedInitialMaterial0Path, "Materials/initial.mat"); + EXPECT_EQ(observedUpdatedMaterialCount, 2); + EXPECT_EQ(observedUpdatedMaterial1Path, "Materials/runtime_override.mat"); + + ASSERT_EQ(meshRenderer->GetMaterialCount(), 2u); + EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/initial.mat"); + EXPECT_EQ(meshRenderer->GetMaterialPath(1), "Materials/runtime_override.mat"); +} + TEST_F(MonoScriptRuntimeTest, ManagedMeshComponentWrappersReadAndWritePathsAndFlags) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host");