From b06932724ccac8ebe113d90f645caf597ebbc1c0 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 27 Mar 2026 13:07:39 +0800 Subject: [PATCH] feat(scripting): add mono csharp runtime foundation --- CMakeLists.txt | 8 + engine/CMakeLists.txt | 51 +- engine/include/XCEngine/Scene/SceneRuntime.h | 26 + .../Scripting/Mono/MonoScriptRuntime.h | 185 ++ engine/src/Components/TransformComponent.cpp | 17 +- engine/src/Scene/SceneRuntime.cpp | 64 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 1794 +++++++++++++++++ managed/CMakeLists.txt | 16 + managed/GameScripts/BuiltinComponentProbe.cs | 60 + managed/GameScripts/HierarchyProbe.cs | 45 + managed/GameScripts/LifecycleProbe.cs | 100 +- .../GameScripts/TransformConversionProbe.cs | 23 + managed/GameScripts/TransformMotionProbe.cs | 41 + managed/GameScripts/TransformSpaceProbe.cs | 33 + managed/XCEngine.ScriptCore/Behaviour.cs | 21 + managed/XCEngine.ScriptCore/Camera.cs | 70 + managed/XCEngine.ScriptCore/Component.cs | 70 + managed/XCEngine.ScriptCore/Debug.cs | 20 + managed/XCEngine.ScriptCore/GameObject.cs | 55 + managed/XCEngine.ScriptCore/InternalCalls.cs | 185 ++ managed/XCEngine.ScriptCore/Light.cs | 58 + managed/XCEngine.ScriptCore/MonoBehaviour.cs | 4 +- managed/XCEngine.ScriptCore/Quaternion.cs | 45 + managed/XCEngine.ScriptCore/Space.cs | 8 + managed/XCEngine.ScriptCore/Time.cs | 7 + managed/XCEngine.ScriptCore/Transform.cs | 263 +++ managed/XCEngine.ScriptCore/Vector2.cs | 29 + managed/XCEngine.ScriptCore/Vector3.cs | 37 + managed/XCEngine.ScriptCore/Vector4.cs | 45 + tests/Scene/CMakeLists.txt | 3 +- tests/Scene/test_scene_runtime.cpp | 253 +++ tests/scripting/CMakeLists.txt | 28 + tests/scripting/test_mono_script_runtime.cpp | 581 ++++++ 33 files changed, 4227 insertions(+), 18 deletions(-) create mode 100644 engine/include/XCEngine/Scene/SceneRuntime.h create mode 100644 engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h create mode 100644 engine/src/Scene/SceneRuntime.cpp create mode 100644 engine/src/Scripting/Mono/MonoScriptRuntime.cpp create mode 100644 managed/GameScripts/BuiltinComponentProbe.cs create mode 100644 managed/GameScripts/HierarchyProbe.cs create mode 100644 managed/GameScripts/TransformConversionProbe.cs create mode 100644 managed/GameScripts/TransformMotionProbe.cs create mode 100644 managed/GameScripts/TransformSpaceProbe.cs create mode 100644 managed/XCEngine.ScriptCore/Behaviour.cs create mode 100644 managed/XCEngine.ScriptCore/Camera.cs create mode 100644 managed/XCEngine.ScriptCore/Component.cs create mode 100644 managed/XCEngine.ScriptCore/Debug.cs create mode 100644 managed/XCEngine.ScriptCore/GameObject.cs create mode 100644 managed/XCEngine.ScriptCore/InternalCalls.cs create mode 100644 managed/XCEngine.ScriptCore/Light.cs create mode 100644 managed/XCEngine.ScriptCore/Quaternion.cs create mode 100644 managed/XCEngine.ScriptCore/Space.cs create mode 100644 managed/XCEngine.ScriptCore/Time.cs create mode 100644 managed/XCEngine.ScriptCore/Transform.cs create mode 100644 managed/XCEngine.ScriptCore/Vector2.cs create mode 100644 managed/XCEngine.ScriptCore/Vector3.cs create mode 100644 managed/XCEngine.ScriptCore/Vector4.cs create mode 100644 tests/Scene/test_scene_runtime.cpp create mode 100644 tests/scripting/test_mono_script_runtime.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c0beceb3..3a305405 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,16 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) enable_testing() +option(XCENGINE_ENABLE_MONO_SCRIPTING "Build the Mono-based C# scripting runtime" ON) +set( + XCENGINE_MONO_ROOT_DIR + "${CMAKE_SOURCE_DIR}/参考/Fermion/Fermion/external/mono" + CACHE PATH + "Path to the bundled Mono distribution used by the scripting runtime") + add_subdirectory(engine) add_subdirectory(editor) +add_subdirectory(managed) add_subdirectory(mvs/RenderDoc) add_subdirectory(tests) add_subdirectory(tests/opengl) diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 88c3d59f..540f511d 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.15) project(XCEngineLib) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(XCENGINE_VULKAN_SDK_HINT "$ENV{VULKAN_SDK}") if(NOT EXISTS "${XCENGINE_VULKAN_SDK_HINT}/Lib/vulkan-1.lib") file(GLOB XCENGINE_VULKAN_SDK_DIRS "D:/VulkanSDK/*") @@ -18,8 +20,6 @@ endif() find_package(Vulkan REQUIRED) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - add_library(XCEngine STATIC # Core (Types, RefCounted, SmartPtr, Event) ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Types.h @@ -149,6 +149,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/D3D12/D3D12ResourceView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/D3D12/D3D12QueryHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/D3D12/D3D12RenderPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/D3D12/D3D12Framebuffer.cpp # Vulkan RHI ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanCommon.h @@ -171,7 +172,6 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanSwapChain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanScreenshot.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/D3D12/D3D12Framebuffer.cpp # OpenGL RHI ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/OpenGL/OpenGLBuffer.h @@ -332,8 +332,10 @@ add_library(XCEngine STATIC # Scene ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneRuntime.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp # Platform @@ -413,6 +415,49 @@ target_link_libraries(XCEngine PUBLIC Vulkan::Vulkan ) +if(XCENGINE_ENABLE_MONO_SCRIPTING) + set(XCENGINE_MONO_INCLUDE_DIR "${XCENGINE_MONO_ROOT_DIR}/include") + set(XCENGINE_MONO_STATIC_LIBRARY "${XCENGINE_MONO_ROOT_DIR}/lib/libmono-static-sgen.lib") + set(XCENGINE_MONO_POSIX_HELPER_LIBRARY "${XCENGINE_MONO_ROOT_DIR}/lib/MonoPosixHelper.lib") + + foreach(XCENGINE_MONO_REQUIRED_PATH + "${XCENGINE_MONO_INCLUDE_DIR}" + "${XCENGINE_MONO_STATIC_LIBRARY}" + "${XCENGINE_MONO_POSIX_HELPER_LIBRARY}") + if(NOT EXISTS "${XCENGINE_MONO_REQUIRED_PATH}") + message(FATAL_ERROR "Required Mono dependency is missing: ${XCENGINE_MONO_REQUIRED_PATH}") + endif() + endforeach() + + add_library(XCEMono STATIC IMPORTED) + set_target_properties(XCEMono PROPERTIES + IMPORTED_LOCATION "${XCENGINE_MONO_STATIC_LIBRARY}" + ) + + target_sources(XCEngine PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/Mono/MonoScriptRuntime.cpp + ) + + target_include_directories(XCEngine PRIVATE + ${XCENGINE_MONO_INCLUDE_DIR} + ) + + target_link_libraries(XCEngine PUBLIC + XCEMono + ${XCENGINE_MONO_POSIX_HELPER_LIBRARY} + ws2_32 + bcrypt + version + iphlpapi + winmm + ) + + target_compile_definitions(XCEngine PRIVATE + XCENGINE_ENABLE_MONO_SCRIPTING + ) +endif() + if(MSVC) target_compile_options(XCEngine PRIVATE /W3 /FS) else() diff --git a/engine/include/XCEngine/Scene/SceneRuntime.h b/engine/include/XCEngine/Scene/SceneRuntime.h new file mode 100644 index 00000000..d96aa745 --- /dev/null +++ b/engine/include/XCEngine/Scene/SceneRuntime.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Components { + +class SceneRuntime { +public: + void Start(Scene* scene); + void Stop(); + + void FixedUpdate(float fixedDeltaTime); + void Update(float deltaTime); + void LateUpdate(float deltaTime); + + bool IsRunning() const { return m_running; } + Scene* GetScene() const { return m_scene; } + +private: + Scene* m_scene = nullptr; + bool m_running = false; +}; + +} // namespace Components +} // namespace XCEngine diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h new file mode 100644 index 00000000..a7b0733b --- /dev/null +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -0,0 +1,185 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +typedef struct _MonoDomain MonoDomain; +typedef struct _MonoAssembly MonoAssembly; +typedef struct _MonoImage MonoImage; +typedef struct _MonoClass MonoClass; +typedef struct _MonoObject MonoObject; +typedef struct _MonoMethod MonoMethod; +typedef struct _MonoClassField MonoClassField; + +namespace XCEngine { +namespace Scripting { + +class ScriptComponent; + +class MonoScriptRuntime : public IScriptRuntime { +public: + struct Settings { + std::filesystem::path assemblyDirectory; + std::filesystem::path corlibDirectory; + std::filesystem::path coreAssemblyPath; + std::filesystem::path appAssemblyPath; + std::string coreAssemblyName = "XCEngine.ScriptCore"; + std::string appAssemblyName = "GameScripts"; + std::string baseNamespace = "XCEngine"; + std::string baseClassName = "MonoBehaviour"; + }; + + explicit MonoScriptRuntime(Settings settings = {}); + ~MonoScriptRuntime() override; + + bool Initialize(); + void Shutdown(); + + bool IsInitialized() const { return m_initialized; } + const Settings& GetSettings() const { return m_settings; } + const std::string& GetLastError() const { return m_lastError; } + + bool IsClassAvailable( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className) const; + std::vector GetScriptClassNames(const std::string& assemblyName = std::string()) const; + + bool HasManagedInstance(const ScriptComponent* component) const; + size_t GetManagedInstanceCount() const { return m_instances.size(); } + + bool TryGetFieldValue( + const ScriptComponent* component, + const std::string& fieldName, + ScriptFieldValue& outValue) const; + + template + bool TryGetFieldValue(const ScriptComponent* component, const std::string& fieldName, T& outValue) const { + ScriptFieldValue value; + if (!TryGetFieldValue(component, fieldName, value)) { + return false; + } + + const auto* typedValue = std::get_if(&value); + if (!typedValue) { + return false; + } + + outValue = *typedValue; + return true; + } + + void OnRuntimeStart(Components::Scene* scene) override; + void OnRuntimeStop(Components::Scene* scene) override; + bool CreateScriptInstance(const ScriptRuntimeContext& context) override; + void DestroyScriptInstance(const ScriptRuntimeContext& context) override; + void InvokeMethod(const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) override; + +private: + static constexpr size_t LifecycleMethodCount = 8; + + struct FieldMetadata { + ScriptFieldType type = ScriptFieldType::None; + MonoClassField* field = nullptr; + }; + + struct ClassMetadata { + std::string assemblyName; + std::string namespaceName; + std::string className; + std::string fullName; + MonoClass* monoClass = nullptr; + std::array lifecycleMethods{}; + std::unordered_map fields; + }; + + struct InstanceKey { + uint64_t gameObjectUUID = 0; + uint64_t scriptComponentUUID = 0; + + bool operator==(const InstanceKey& other) const { + return gameObjectUUID == other.gameObjectUUID + && scriptComponentUUID == other.scriptComponentUUID; + } + }; + + struct InstanceKeyHasher { + size_t operator()(const InstanceKey& key) const; + }; + + struct InstanceData { + const ClassMetadata* classMetadata = nullptr; + uint32_t gcHandle = 0; + }; + + void ResolveSettings(); + bool InitializeRootDomain(); + bool CreateAppDomain(); + void DestroyAppDomain(); + void SetCurrentDomain() const; + + bool LoadAssemblies(); + bool DiscoverScriptClasses(); + void DiscoverScriptClassesInImage(const std::string& assemblyName, MonoImage* image); + bool IsMonoBehaviourSubclass(MonoClass* monoClass) const; + + ScriptFieldType MapMonoFieldType(MonoClassField* field) const; + + static const char* ToLifecycleMethodName(ScriptLifecycleMethod method); + + const ClassMetadata* FindClassMetadata( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className) const; + + InstanceData* FindInstance(const ScriptRuntimeContext& context); + const InstanceData* FindInstance(const ScriptRuntimeContext& context) const; + const InstanceData* FindInstance(const ScriptComponent* component) const; + + MonoObject* GetManagedObject(const InstanceData& instanceData) const; + + bool ApplyContextFields(const ScriptRuntimeContext& context, MonoObject* instance); + bool ApplyStoredFields(const ScriptRuntimeContext& context, const ClassMetadata& metadata, MonoObject* instance); + MonoObject* CreateManagedGameObject(uint64_t gameObjectUUID); + bool TryExtractGameObjectReference(MonoObject* managedObject, GameObjectReference& outReference) const; + bool TrySetFieldValue(MonoObject* instance, const FieldMetadata& fieldMetadata, const ScriptFieldValue& value); + bool TryReadFieldValue(MonoObject* instance, const FieldMetadata& fieldMetadata, ScriptFieldValue& outValue) const; + + void ClearManagedInstances(); + void ClearClassCache(); + bool InvokeManagedMethod(MonoObject* instance, MonoMethod* method); + void RecordException(MonoObject* exception); + void SetError(const std::string& error); + + Settings m_settings; + Components::Scene* m_activeScene = nullptr; + + bool m_initialized = false; + std::string m_lastError; + + MonoDomain* m_appDomain = nullptr; + MonoAssembly* m_coreAssembly = nullptr; + MonoAssembly* m_appAssembly = nullptr; + MonoImage* m_coreImage = nullptr; + MonoImage* m_appImage = nullptr; + MonoClass* m_componentClass = nullptr; + MonoClass* m_behaviourClass = nullptr; + MonoClass* m_gameObjectClass = nullptr; + MonoClass* m_monoBehaviourClass = nullptr; + MonoMethod* m_gameObjectConstructor = nullptr; + MonoClassField* m_managedGameObjectUUIDField = nullptr; + MonoClassField* m_gameObjectUUIDField = nullptr; + MonoClassField* m_scriptComponentUUIDField = nullptr; + + std::unordered_map m_classes; + std::unordered_map m_instances; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Components/TransformComponent.cpp b/engine/src/Components/TransformComponent.cpp index 24a0c6df..cf4c3467 100644 --- a/engine/src/Components/TransformComponent.cpp +++ b/engine/src/Components/TransformComponent.cpp @@ -228,30 +228,27 @@ void TransformComponent::LookAt(const Math::Vector3& target, const Math::Vector3 void TransformComponent::Rotate(const Math::Vector3& eulers, Space relativeTo) { Math::Quaternion rotation = Math::Quaternion::FromEulerAngles(eulers * Math::DEG_TO_RAD); if (relativeTo == Space::Self) { - m_localRotation = m_localRotation * rotation; + SetRotation(GetRotation() * rotation); } else { - m_localRotation = rotation * m_localRotation; + SetRotation(rotation * GetRotation()); } - SetDirty(); } void TransformComponent::Rotate(const Math::Vector3& axis, float angle, Space relativeTo) { Math::Quaternion rotation = Math::Quaternion::FromAxisAngle(axis, angle * Math::DEG_TO_RAD); if (relativeTo == Space::Self) { - m_localRotation = m_localRotation * rotation; + SetRotation(GetRotation() * rotation); } else { - m_localRotation = rotation * m_localRotation; + SetRotation(rotation * GetRotation()); } - SetDirty(); } void TransformComponent::Translate(const Math::Vector3& translation, Space relativeTo) { if (relativeTo == Space::Self) { - m_localPosition = m_localPosition + translation; + SetPosition(GetPosition() + TransformDirection(translation)); } else { - m_localPosition = m_localPosition + translation; + SetPosition(GetPosition() + translation); } - SetDirty(); } Math::Vector3 TransformComponent::TransformPoint(const Math::Vector3& point) const { @@ -317,4 +314,4 @@ void TransformComponent::Deserialize(std::istream& is) { } } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/src/Scene/SceneRuntime.cpp b/engine/src/Scene/SceneRuntime.cpp new file mode 100644 index 00000000..cb26c731 --- /dev/null +++ b/engine/src/Scene/SceneRuntime.cpp @@ -0,0 +1,64 @@ +#include "Scene/SceneRuntime.h" + +#include "Scripting/ScriptEngine.h" + +namespace XCEngine { +namespace Components { + +void SceneRuntime::Start(Scene* scene) { + if (m_running && m_scene == scene) { + return; + } + + Stop(); + + if (!scene) { + return; + } + + m_scene = scene; + m_running = true; + Scripting::ScriptEngine::Get().OnRuntimeStart(scene); +} + +void SceneRuntime::Stop() { + if (!m_running) { + m_scene = nullptr; + return; + } + + Scripting::ScriptEngine::Get().OnRuntimeStop(); + m_running = false; + m_scene = nullptr; +} + +void SceneRuntime::FixedUpdate(float fixedDeltaTime) { + if (!m_running || !m_scene || !m_scene->IsActive()) { + return; + } + + // Scripts run first so their state changes are visible to native components in the same frame. + Scripting::ScriptEngine::Get().OnFixedUpdate(fixedDeltaTime); + m_scene->FixedUpdate(fixedDeltaTime); +} + +void SceneRuntime::Update(float deltaTime) { + if (!m_running || !m_scene || !m_scene->IsActive()) { + return; + } + + Scripting::ScriptEngine::Get().OnUpdate(deltaTime); + m_scene->Update(deltaTime); +} + +void SceneRuntime::LateUpdate(float deltaTime) { + if (!m_running || !m_scene || !m_scene->IsActive()) { + return; + } + + Scripting::ScriptEngine::Get().OnLateUpdate(deltaTime); + m_scene->LateUpdate(deltaTime); +} + +} // namespace Components +} // namespace XCEngine diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp new file mode 100644 index 00000000..912dff45 --- /dev/null +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -0,0 +1,1794 @@ +#include "Scripting/Mono/MonoScriptRuntime.h" + +#include "Components/CameraComponent.h" +#include "Components/GameObject.h" +#include "Components/LightComponent.h" +#include "Components/TransformComponent.h" +#include "Debug/Logger.h" +#include "Scene/Scene.h" +#include "Scripting/ScriptComponent.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace Scripting { + +namespace { + +struct MonoRootState { + MonoDomain* rootDomain = nullptr; + bool initialized = false; +}; + +enum class ManagedComponentKind { + Unknown, + Transform, + Camera, + Light, +}; + +MonoRootState& GetMonoRootState() { + static MonoRootState state; + return state; +} + +Components::Scene*& GetInternalCallScene() { + static Components::Scene* scene = nullptr; + return scene; +} + +float& GetInternalCallDeltaTime() { + static float deltaTime = 0.0f; + return deltaTime; +} + +bool& GetInternalCallRegistrationState() { + static bool registered = false; + return registered; +} + +std::string BuildFullClassName(const std::string& namespaceName, const std::string& className) { + return namespaceName.empty() ? className : namespaceName + "." + className; +} + +std::string BuildClassKey( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className) { + return assemblyName + "|" + BuildFullClassName(namespaceName, className); +} + +std::string SafeString(const char* value) { + return value ? std::string(value) : std::string(); +} + +std::string MonoStringToUtf8(MonoString* stringObject) { + if (!stringObject) { + return std::string(); + } + + char* utf8 = mono_string_to_utf8(stringObject); + std::string result = utf8 ? std::string(utf8) : std::string(); + if (utf8) { + mono_free(utf8); + } + + return result; +} + +ManagedComponentKind ResolveManagedComponentKind(MonoReflectionType* reflectionType) { + if (!reflectionType) { + return ManagedComponentKind::Unknown; + } + + MonoType* monoType = mono_reflection_type_get_type(reflectionType); + if (!monoType) { + return ManagedComponentKind::Unknown; + } + + MonoClass* monoClass = mono_class_from_mono_type(monoType); + if (!monoClass) { + return ManagedComponentKind::Unknown; + } + + const std::string namespaceName = SafeString(mono_class_get_namespace(monoClass)); + const std::string className = SafeString(mono_class_get_name(monoClass)); + if (namespaceName == "XCEngine" && className == "Transform") { + return ManagedComponentKind::Transform; + } + if (namespaceName == "XCEngine" && className == "Camera") { + return ManagedComponentKind::Camera; + } + if (namespaceName == "XCEngine" && className == "Light") { + return ManagedComponentKind::Light; + } + + return ManagedComponentKind::Unknown; +} + +Components::GameObject* FindGameObjectByUUIDRecursive(Components::GameObject* gameObject, uint64_t uuid) { + if (!gameObject) { + return nullptr; + } + + if (gameObject->GetUUID() == uuid) { + return gameObject; + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + if (Components::GameObject* found = FindGameObjectByUUIDRecursive(child, uuid)) { + return found; + } + } + + return nullptr; +} + +ScriptComponent* FindScriptComponentByUUIDRecursive(Components::GameObject* gameObject, uint64_t scriptComponentUUID) { + if (!gameObject || scriptComponentUUID == 0) { + return nullptr; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + if (component && component->GetScriptComponentUUID() == scriptComponentUUID) { + return component; + } + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + if (ScriptComponent* found = FindScriptComponentByUUIDRecursive(child, scriptComponentUUID)) { + return found; + } + } + + return nullptr; +} + +Components::GameObject* FindGameObjectByUUID(uint64_t uuid) { + Components::Scene* scene = GetInternalCallScene(); + if (!scene || uuid == 0) { + return nullptr; + } + + for (Components::GameObject* root : scene->GetRootGameObjects()) { + if (Components::GameObject* found = FindGameObjectByUUIDRecursive(root, uuid)) { + return found; + } + } + + return nullptr; +} + +ScriptComponent* FindScriptComponentByUUID(uint64_t scriptComponentUUID) { + Components::Scene* scene = GetInternalCallScene(); + if (!scene || scriptComponentUUID == 0) { + return nullptr; + } + + for (Components::GameObject* root : scene->GetRootGameObjects()) { + if (ScriptComponent* found = FindScriptComponentByUUIDRecursive(root, scriptComponentUUID)) { + return found; + } + } + + return nullptr; +} + +bool HasNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { + if (!gameObject) { + return false; + } + + switch (componentKind) { + case ManagedComponentKind::Transform: + return gameObject->GetTransform() != nullptr; + case ManagedComponentKind::Camera: + return gameObject->GetComponent() != nullptr; + case ManagedComponentKind::Light: + return gameObject->GetComponent() != nullptr; + case ManagedComponentKind::Unknown: + return false; + } + + return false; +} + +Components::CameraComponent* FindCameraComponent(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return gameObject ? gameObject->GetComponent() : nullptr; +} + +Components::LightComponent* FindLightComponent(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return gameObject ? gameObject->GetComponent() : nullptr; +} + +Components::Space ResolveManagedSpace(int32_t value) { + return value == static_cast(Components::Space::World) + ? Components::Space::World + : Components::Space::Self; +} + +void InternalCall_Debug_Log(MonoString* message) { + XCEngine::Debug::Logger::Get().Info( + XCEngine::Debug::LogCategory::Scripting, + XCEngine::Containers::String(MonoStringToUtf8(message).c_str())); +} + +void InternalCall_Debug_LogWarning(MonoString* message) { + XCEngine::Debug::Logger::Get().Warning( + XCEngine::Debug::LogCategory::Scripting, + XCEngine::Containers::String(MonoStringToUtf8(message).c_str())); +} + +void InternalCall_Debug_LogError(MonoString* message) { + XCEngine::Debug::Logger::Get().Error( + XCEngine::Debug::LogCategory::Scripting, + XCEngine::Containers::String(MonoStringToUtf8(message).c_str())); +} + +float InternalCall_Time_GetDeltaTime() { + return GetInternalCallDeltaTime(); +} + +MonoString* InternalCall_GameObject_GetName(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return mono_string_new( + mono_domain_get(), + gameObject ? gameObject->GetName().c_str() : ""); +} + +void InternalCall_GameObject_SetName(uint64_t gameObjectUUID, MonoString* name) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject) { + return; + } + + gameObject->SetName(MonoStringToUtf8(name)); +} + +mono_bool InternalCall_GameObject_GetActiveSelf(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return (gameObject && gameObject->IsActive()) ? 1 : 0; +} + +mono_bool InternalCall_GameObject_GetActiveInHierarchy(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return (gameObject && gameObject->IsActiveInHierarchy()) ? 1 : 0; +} + +void InternalCall_GameObject_SetActive(uint64_t gameObjectUUID, mono_bool active) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject) { + return; + } + + gameObject->SetActive(active != 0); +} + +mono_bool InternalCall_GameObject_HasComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return HasNativeComponent(gameObject, ResolveManagedComponentKind(componentType)) ? 1 : 0; +} + +uint64_t InternalCall_GameObject_GetComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!HasNativeComponent(gameObject, ResolveManagedComponentKind(componentType))) { + return 0; + } + + return gameObjectUUID; +} + +mono_bool InternalCall_Behaviour_GetEnabled(uint64_t scriptComponentUUID) { + ScriptComponent* component = FindScriptComponentByUUID(scriptComponentUUID); + return (component && component->IsEnabled()) ? 1 : 0; +} + +void InternalCall_Behaviour_SetEnabled(uint64_t scriptComponentUUID, mono_bool enabled) { + ScriptComponent* component = FindScriptComponentByUUID(scriptComponentUUID); + if (!component) { + return; + } + + component->SetEnabled(enabled != 0); +} + +void InternalCall_Transform_GetLocalPosition(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outPosition) { + if (!outPosition) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outPosition = XCEngine::Math::Vector3::Zero(); + return; + } + + *outPosition = gameObject->GetTransform()->GetLocalPosition(); +} + +void InternalCall_Transform_SetLocalPosition(uint64_t gameObjectUUID, XCEngine::Math::Vector3* position) { + if (!position) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetLocalPosition(*position); +} + +void InternalCall_Transform_GetLocalRotation(uint64_t gameObjectUUID, XCEngine::Math::Quaternion* outRotation) { + if (!outRotation) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outRotation = XCEngine::Math::Quaternion::Identity(); + return; + } + + *outRotation = gameObject->GetTransform()->GetLocalRotation(); +} + +void InternalCall_Transform_SetLocalRotation(uint64_t gameObjectUUID, XCEngine::Math::Quaternion* rotation) { + if (!rotation) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetLocalRotation(*rotation); +} + +void InternalCall_Transform_GetLocalScale(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outScale) { + if (!outScale) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outScale = XCEngine::Math::Vector3::One(); + return; + } + + *outScale = gameObject->GetTransform()->GetLocalScale(); +} + +void InternalCall_Transform_SetLocalScale(uint64_t gameObjectUUID, XCEngine::Math::Vector3* scale) { + if (!scale) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetLocalScale(*scale); +} + +void InternalCall_Transform_GetLocalEulerAngles(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outEulerAngles) { + if (!outEulerAngles) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outEulerAngles = XCEngine::Math::Vector3::Zero(); + return; + } + + *outEulerAngles = gameObject->GetTransform()->GetLocalEulerAngles(); +} + +void InternalCall_Transform_SetLocalEulerAngles(uint64_t gameObjectUUID, XCEngine::Math::Vector3* eulerAngles) { + if (!eulerAngles) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetLocalEulerAngles(*eulerAngles); +} + +void InternalCall_Transform_GetPosition(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outPosition) { + if (!outPosition) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outPosition = XCEngine::Math::Vector3::Zero(); + return; + } + + *outPosition = gameObject->GetTransform()->GetPosition(); +} + +void InternalCall_Transform_SetPosition(uint64_t gameObjectUUID, XCEngine::Math::Vector3* position) { + if (!position) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetPosition(*position); +} + +void InternalCall_Transform_GetRotation(uint64_t gameObjectUUID, XCEngine::Math::Quaternion* outRotation) { + if (!outRotation) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outRotation = XCEngine::Math::Quaternion::Identity(); + return; + } + + *outRotation = gameObject->GetTransform()->GetRotation(); +} + +void InternalCall_Transform_SetRotation(uint64_t gameObjectUUID, XCEngine::Math::Quaternion* rotation) { + if (!rotation) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetRotation(*rotation); +} + +void InternalCall_Transform_GetScale(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outScale) { + if (!outScale) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outScale = XCEngine::Math::Vector3::One(); + return; + } + + *outScale = gameObject->GetTransform()->GetScale(); +} + +void InternalCall_Transform_SetScale(uint64_t gameObjectUUID, XCEngine::Math::Vector3* scale) { + if (!scale) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->SetScale(*scale); +} + +void InternalCall_Transform_GetForward(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outForward) { + if (!outForward) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outForward = XCEngine::Math::Vector3::Forward(); + return; + } + + *outForward = gameObject->GetTransform()->GetForward(); +} + +void InternalCall_Transform_GetRight(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outRight) { + if (!outRight) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outRight = XCEngine::Math::Vector3::Right(); + return; + } + + *outRight = gameObject->GetTransform()->GetRight(); +} + +void InternalCall_Transform_GetUp(uint64_t gameObjectUUID, XCEngine::Math::Vector3* outUp) { + if (!outUp) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outUp = XCEngine::Math::Vector3::Up(); + return; + } + + *outUp = gameObject->GetTransform()->GetUp(); +} + +void InternalCall_Transform_Translate(uint64_t gameObjectUUID, XCEngine::Math::Vector3* translation, int32_t relativeTo) { + if (!translation) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->Translate(*translation, ResolveManagedSpace(relativeTo)); +} + +void InternalCall_Transform_Rotate(uint64_t gameObjectUUID, XCEngine::Math::Vector3* eulers, int32_t relativeTo) { + if (!eulers) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->Rotate(*eulers, ResolveManagedSpace(relativeTo)); +} + +void InternalCall_Transform_LookAt(uint64_t gameObjectUUID, XCEngine::Math::Vector3* target) { + if (!target) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + return; + } + + gameObject->GetTransform()->LookAt(*target); +} + +void InternalCall_Transform_TransformPoint(uint64_t gameObjectUUID, XCEngine::Math::Vector3* point, XCEngine::Math::Vector3* outPoint) { + if (!point || !outPoint) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outPoint = *point; + return; + } + + *outPoint = gameObject->GetTransform()->TransformPoint(*point); +} + +void InternalCall_Transform_InverseTransformPoint(uint64_t gameObjectUUID, XCEngine::Math::Vector3* point, XCEngine::Math::Vector3* outPoint) { + if (!point || !outPoint) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outPoint = *point; + return; + } + + *outPoint = gameObject->GetTransform()->InverseTransformPoint(*point); +} + +void InternalCall_Transform_TransformDirection(uint64_t gameObjectUUID, XCEngine::Math::Vector3* direction, XCEngine::Math::Vector3* outDirection) { + if (!direction || !outDirection) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outDirection = *direction; + return; + } + + *outDirection = gameObject->GetTransform()->TransformDirection(*direction); +} + +void InternalCall_Transform_InverseTransformDirection(uint64_t gameObjectUUID, XCEngine::Math::Vector3* direction, XCEngine::Math::Vector3* outDirection) { + if (!direction || !outDirection) { + return; + } + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetTransform()) { + *outDirection = *direction; + return; + } + + *outDirection = gameObject->GetTransform()->InverseTransformDirection(*direction); +} + +uint64_t InternalCall_Transform_GetParent(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || !gameObject->GetParent()) { + return 0; + } + + return gameObject->GetParent()->GetUUID(); +} + +void InternalCall_Transform_SetParent(uint64_t gameObjectUUID, uint64_t parentGameObjectUUID, mono_bool worldPositionStays) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject) { + return; + } + + Components::GameObject* parent = parentGameObjectUUID != 0 ? FindGameObjectByUUID(parentGameObjectUUID) : nullptr; + gameObject->SetParent(parent, worldPositionStays != 0); +} + +int32_t InternalCall_Transform_GetChildCount(uint64_t gameObjectUUID) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + return gameObject ? static_cast(gameObject->GetChildCount()) : 0; +} + +uint64_t InternalCall_Transform_GetChild(uint64_t gameObjectUUID, int32_t index) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject || index < 0) { + return 0; + } + + Components::GameObject* child = gameObject->GetChild(static_cast(index)); + return child ? child->GetUUID() : 0; +} + +float InternalCall_Camera_GetFieldOfView(uint64_t gameObjectUUID) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + return component ? component->GetFieldOfView() : 0.0f; +} + +void InternalCall_Camera_SetFieldOfView(uint64_t gameObjectUUID, float value) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetFieldOfView(value); +} + +float InternalCall_Camera_GetNearClipPlane(uint64_t gameObjectUUID) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + return component ? component->GetNearClipPlane() : 0.0f; +} + +void InternalCall_Camera_SetNearClipPlane(uint64_t gameObjectUUID, float value) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetNearClipPlane(value); +} + +float InternalCall_Camera_GetFarClipPlane(uint64_t gameObjectUUID) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + return component ? component->GetFarClipPlane() : 0.0f; +} + +void InternalCall_Camera_SetFarClipPlane(uint64_t gameObjectUUID, float value) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetFarClipPlane(value); +} + +float InternalCall_Camera_GetDepth(uint64_t gameObjectUUID) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + return component ? component->GetDepth() : 0.0f; +} + +void InternalCall_Camera_SetDepth(uint64_t gameObjectUUID, float value) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetDepth(value); +} + +mono_bool InternalCall_Camera_GetPrimary(uint64_t gameObjectUUID) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + return (component && component->IsPrimary()) ? 1 : 0; +} + +void InternalCall_Camera_SetPrimary(uint64_t gameObjectUUID, mono_bool value) { + Components::CameraComponent* component = FindCameraComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetPrimary(value != 0); +} + +float InternalCall_Light_GetIntensity(uint64_t gameObjectUUID) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + return component ? component->GetIntensity() : 0.0f; +} + +void InternalCall_Light_SetIntensity(uint64_t gameObjectUUID, float value) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetIntensity(value); +} + +float InternalCall_Light_GetRange(uint64_t gameObjectUUID) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + return component ? component->GetRange() : 0.0f; +} + +void InternalCall_Light_SetRange(uint64_t gameObjectUUID, float value) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetRange(value); +} + +float InternalCall_Light_GetSpotAngle(uint64_t gameObjectUUID) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + return component ? component->GetSpotAngle() : 0.0f; +} + +void InternalCall_Light_SetSpotAngle(uint64_t gameObjectUUID, float value) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetSpotAngle(value); +} + +mono_bool InternalCall_Light_GetCastsShadows(uint64_t gameObjectUUID) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + return (component && component->GetCastsShadows()) ? 1 : 0; +} + +void InternalCall_Light_SetCastsShadows(uint64_t gameObjectUUID, mono_bool value) { + Components::LightComponent* component = FindLightComponent(gameObjectUUID); + if (!component) { + return; + } + + component->SetCastsShadows(value != 0); +} + +void RegisterInternalCalls() { + if (GetInternalCallRegistrationState()) { + return; + } + + mono_add_internal_call("XCEngine.InternalCalls::Debug_Log", reinterpret_cast(&InternalCall_Debug_Log)); + mono_add_internal_call("XCEngine.InternalCalls::Debug_LogWarning", reinterpret_cast(&InternalCall_Debug_LogWarning)); + mono_add_internal_call("XCEngine.InternalCalls::Debug_LogError", reinterpret_cast(&InternalCall_Debug_LogError)); + mono_add_internal_call("XCEngine.InternalCalls::Time_GetDeltaTime", reinterpret_cast(&InternalCall_Time_GetDeltaTime)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetName", reinterpret_cast(&InternalCall_GameObject_GetName)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_SetName", reinterpret_cast(&InternalCall_GameObject_SetName)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetActiveSelf", reinterpret_cast(&InternalCall_GameObject_GetActiveSelf)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetActiveInHierarchy", reinterpret_cast(&InternalCall_GameObject_GetActiveInHierarchy)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_SetActive", reinterpret_cast(&InternalCall_GameObject_SetActive)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_HasComponent", reinterpret_cast(&InternalCall_GameObject_HasComponent)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponent", reinterpret_cast(&InternalCall_GameObject_GetComponent)); + mono_add_internal_call("XCEngine.InternalCalls::Behaviour_GetEnabled", reinterpret_cast(&InternalCall_Behaviour_GetEnabled)); + mono_add_internal_call("XCEngine.InternalCalls::Behaviour_SetEnabled", reinterpret_cast(&InternalCall_Behaviour_SetEnabled)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalPosition", reinterpret_cast(&InternalCall_Transform_GetLocalPosition)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetLocalPosition", reinterpret_cast(&InternalCall_Transform_SetLocalPosition)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalRotation", reinterpret_cast(&InternalCall_Transform_GetLocalRotation)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetLocalRotation", reinterpret_cast(&InternalCall_Transform_SetLocalRotation)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalScale", reinterpret_cast(&InternalCall_Transform_GetLocalScale)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetLocalScale", reinterpret_cast(&InternalCall_Transform_SetLocalScale)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalEulerAngles", reinterpret_cast(&InternalCall_Transform_GetLocalEulerAngles)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetLocalEulerAngles", reinterpret_cast(&InternalCall_Transform_SetLocalEulerAngles)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetPosition", reinterpret_cast(&InternalCall_Transform_GetPosition)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetPosition", reinterpret_cast(&InternalCall_Transform_SetPosition)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetRotation", reinterpret_cast(&InternalCall_Transform_GetRotation)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetRotation", reinterpret_cast(&InternalCall_Transform_SetRotation)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetScale", reinterpret_cast(&InternalCall_Transform_GetScale)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetScale", reinterpret_cast(&InternalCall_Transform_SetScale)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetForward", reinterpret_cast(&InternalCall_Transform_GetForward)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetRight", reinterpret_cast(&InternalCall_Transform_GetRight)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetUp", reinterpret_cast(&InternalCall_Transform_GetUp)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_Translate", reinterpret_cast(&InternalCall_Transform_Translate)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_Rotate", reinterpret_cast(&InternalCall_Transform_Rotate)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_LookAt", reinterpret_cast(&InternalCall_Transform_LookAt)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_TransformPoint", reinterpret_cast(&InternalCall_Transform_TransformPoint)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_InverseTransformPoint", reinterpret_cast(&InternalCall_Transform_InverseTransformPoint)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_TransformDirection", reinterpret_cast(&InternalCall_Transform_TransformDirection)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_InverseTransformDirection", reinterpret_cast(&InternalCall_Transform_InverseTransformDirection)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetParent", reinterpret_cast(&InternalCall_Transform_GetParent)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_SetParent", reinterpret_cast(&InternalCall_Transform_SetParent)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetChildCount", reinterpret_cast(&InternalCall_Transform_GetChildCount)); + mono_add_internal_call("XCEngine.InternalCalls::Transform_GetChild", reinterpret_cast(&InternalCall_Transform_GetChild)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_GetFieldOfView", reinterpret_cast(&InternalCall_Camera_GetFieldOfView)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_SetFieldOfView", reinterpret_cast(&InternalCall_Camera_SetFieldOfView)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_GetNearClipPlane", reinterpret_cast(&InternalCall_Camera_GetNearClipPlane)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_SetNearClipPlane", reinterpret_cast(&InternalCall_Camera_SetNearClipPlane)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_GetFarClipPlane", reinterpret_cast(&InternalCall_Camera_GetFarClipPlane)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_SetFarClipPlane", reinterpret_cast(&InternalCall_Camera_SetFarClipPlane)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_GetDepth", reinterpret_cast(&InternalCall_Camera_GetDepth)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_SetDepth", reinterpret_cast(&InternalCall_Camera_SetDepth)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_GetPrimary", reinterpret_cast(&InternalCall_Camera_GetPrimary)); + mono_add_internal_call("XCEngine.InternalCalls::Camera_SetPrimary", reinterpret_cast(&InternalCall_Camera_SetPrimary)); + mono_add_internal_call("XCEngine.InternalCalls::Light_GetIntensity", reinterpret_cast(&InternalCall_Light_GetIntensity)); + mono_add_internal_call("XCEngine.InternalCalls::Light_SetIntensity", reinterpret_cast(&InternalCall_Light_SetIntensity)); + mono_add_internal_call("XCEngine.InternalCalls::Light_GetRange", reinterpret_cast(&InternalCall_Light_GetRange)); + mono_add_internal_call("XCEngine.InternalCalls::Light_SetRange", reinterpret_cast(&InternalCall_Light_SetRange)); + mono_add_internal_call("XCEngine.InternalCalls::Light_GetSpotAngle", reinterpret_cast(&InternalCall_Light_GetSpotAngle)); + mono_add_internal_call("XCEngine.InternalCalls::Light_SetSpotAngle", reinterpret_cast(&InternalCall_Light_SetSpotAngle)); + mono_add_internal_call("XCEngine.InternalCalls::Light_GetCastsShadows", reinterpret_cast(&InternalCall_Light_GetCastsShadows)); + mono_add_internal_call("XCEngine.InternalCalls::Light_SetCastsShadows", reinterpret_cast(&InternalCall_Light_SetCastsShadows)); + + GetInternalCallRegistrationState() = true; +} + +} // namespace + +MonoScriptRuntime::MonoScriptRuntime(Settings settings) + : m_settings(std::move(settings)) { + ResolveSettings(); +} + +MonoScriptRuntime::~MonoScriptRuntime() { + Shutdown(); +} + +bool MonoScriptRuntime::Initialize() { + ResolveSettings(); + m_lastError.clear(); + + if (m_initialized) { + return true; + } + + if (!InitializeRootDomain()) { + return false; + } + + if (!CreateAppDomain()) { + return false; + } + + if (!LoadAssemblies()) { + DestroyAppDomain(); + return false; + } + + if (!DiscoverScriptClasses()) { + DestroyAppDomain(); + return false; + } + + m_initialized = true; + return true; +} + +void MonoScriptRuntime::Shutdown() { + ClearManagedInstances(); + ClearClassCache(); + + m_coreAssembly = nullptr; + m_appAssembly = nullptr; + m_coreImage = nullptr; + m_appImage = nullptr; + m_componentClass = nullptr; + m_behaviourClass = nullptr; + m_gameObjectClass = nullptr; + m_monoBehaviourClass = nullptr; + m_gameObjectConstructor = nullptr; + m_managedGameObjectUUIDField = nullptr; + m_gameObjectUUIDField = nullptr; + m_scriptComponentUUIDField = nullptr; + + DestroyAppDomain(); + + m_activeScene = nullptr; + GetInternalCallScene() = nullptr; + GetInternalCallDeltaTime() = 0.0f; + m_initialized = false; +} + +bool MonoScriptRuntime::IsClassAvailable( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className) const { + return FindClassMetadata(assemblyName, namespaceName, className) != nullptr; +} + +std::vector MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const { + std::vector classNames; + classNames.reserve(m_classes.size()); + + for (const auto& [key, metadata] : m_classes) { + (void)key; + if (!assemblyName.empty() && metadata.assemblyName != assemblyName) { + continue; + } + + classNames.push_back(metadata.fullName); + } + + std::sort(classNames.begin(), classNames.end()); + return classNames; +} + +bool MonoScriptRuntime::HasManagedInstance(const ScriptComponent* component) const { + const InstanceData* instanceData = FindInstance(component); + return instanceData != nullptr && GetManagedObject(*instanceData) != nullptr; +} + +bool MonoScriptRuntime::TryGetFieldValue( + const ScriptComponent* component, + const std::string& fieldName, + ScriptFieldValue& outValue) const { + const InstanceData* instanceData = FindInstance(component); + if (!instanceData || !instanceData->classMetadata) { + return false; + } + + const auto fieldIt = instanceData->classMetadata->fields.find(fieldName); + if (fieldIt == instanceData->classMetadata->fields.end()) { + return false; + } + + MonoObject* instance = GetManagedObject(*instanceData); + if (!instance) { + return false; + } + + return TryReadFieldValue(instance, fieldIt->second, outValue); +} + +void MonoScriptRuntime::OnRuntimeStart(Components::Scene* scene) { + m_activeScene = nullptr; + GetInternalCallDeltaTime() = 0.0f; + if (Initialize()) { + m_activeScene = scene; + GetInternalCallScene() = scene; + } +} + +void MonoScriptRuntime::OnRuntimeStop(Components::Scene* scene) { + (void)scene; + ClearManagedInstances(); + m_activeScene = nullptr; + GetInternalCallScene() = nullptr; + GetInternalCallDeltaTime() = 0.0f; +} + +bool MonoScriptRuntime::CreateScriptInstance(const ScriptRuntimeContext& context) { + if (!context.component) { + SetError("Cannot create a managed script instance without a ScriptComponent."); + return false; + } + + if (FindInstance(context)) { + return true; + } + + if (!Initialize()) { + return false; + } + + const std::string assemblyName = context.component->GetAssemblyName().empty() + ? m_settings.appAssemblyName + : context.component->GetAssemblyName(); + + const ClassMetadata* classMetadata = FindClassMetadata( + assemblyName, + context.component->GetNamespaceName(), + context.component->GetClassName()); + if (!classMetadata) { + SetError("Managed script class was not found: " + assemblyName + "|" + context.component->GetFullClassName()); + return false; + } + + SetCurrentDomain(); + + MonoObject* instance = mono_object_new(m_appDomain, classMetadata->monoClass); + if (!instance) { + SetError("Mono failed to allocate a managed object for " + classMetadata->fullName + "."); + return false; + } + + mono_runtime_object_init(instance); + + if (!ApplyContextFields(context, instance) || !ApplyStoredFields(context, *classMetadata, instance)) { + return false; + } + + const uint32_t gcHandle = mono_gchandle_new(instance, false); + const InstanceKey key{context.gameObjectUUID, context.scriptComponentUUID}; + m_instances[key] = InstanceData{classMetadata, gcHandle}; + return true; +} + +void MonoScriptRuntime::DestroyScriptInstance(const ScriptRuntimeContext& context) { + InstanceData* instanceData = FindInstance(context); + if (!instanceData) { + return; + } + + if (instanceData->gcHandle != 0) { + mono_gchandle_free(instanceData->gcHandle); + } + + m_instances.erase(InstanceKey{context.gameObjectUUID, context.scriptComponentUUID}); +} + +void MonoScriptRuntime::InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) { + const InstanceData* instanceData = FindInstance(context); + if (!instanceData || !instanceData->classMetadata) { + return; + } + + MonoMethod* managedMethod = instanceData->classMetadata->lifecycleMethods[static_cast(method)]; + if (!managedMethod) { + return; + } + + MonoObject* instance = GetManagedObject(*instanceData); + if (!instance) { + return; + } + + const float previousDeltaTime = GetInternalCallDeltaTime(); + GetInternalCallDeltaTime() = deltaTime; + InvokeManagedMethod(instance, managedMethod); + GetInternalCallDeltaTime() = previousDeltaTime; +} + +size_t MonoScriptRuntime::InstanceKeyHasher::operator()(const InstanceKey& key) const { + const size_t h1 = std::hash{}(key.gameObjectUUID); + const size_t h2 = std::hash{}(key.scriptComponentUUID); + return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2)); +} + +void MonoScriptRuntime::ResolveSettings() { + if (!m_settings.coreAssemblyPath.empty() && m_settings.assemblyDirectory.empty()) { + m_settings.assemblyDirectory = m_settings.coreAssemblyPath.parent_path(); + } + + if (!m_settings.appAssemblyPath.empty() && m_settings.assemblyDirectory.empty()) { + m_settings.assemblyDirectory = m_settings.appAssemblyPath.parent_path(); + } + + if (m_settings.coreAssemblyPath.empty() && !m_settings.assemblyDirectory.empty()) { + m_settings.coreAssemblyPath = m_settings.assemblyDirectory / (m_settings.coreAssemblyName + ".dll"); + } + + if (m_settings.appAssemblyPath.empty() && !m_settings.assemblyDirectory.empty()) { + m_settings.appAssemblyPath = m_settings.assemblyDirectory / (m_settings.appAssemblyName + ".dll"); + } + + if (m_settings.corlibDirectory.empty()) { + if (!m_settings.assemblyDirectory.empty()) { + m_settings.corlibDirectory = m_settings.assemblyDirectory; + } else if (!m_settings.coreAssemblyPath.empty()) { + m_settings.corlibDirectory = m_settings.coreAssemblyPath.parent_path(); + } + } +} + +bool MonoScriptRuntime::InitializeRootDomain() { + MonoRootState& rootState = GetMonoRootState(); + if (rootState.initialized) { + return true; + } + + if (!m_settings.corlibDirectory.empty()) { + const std::string corlibDirectory = m_settings.corlibDirectory.string(); + mono_set_assemblies_path(corlibDirectory.c_str()); + } + + mono_config_parse(nullptr); + + rootState.rootDomain = mono_jit_init_version("XCEngineRootDomain", "v4.0.30319"); + if (!rootState.rootDomain) { + SetError("Failed to initialize the Mono root domain."); + return false; + } + + mono_domain_set(rootState.rootDomain, true); + RegisterInternalCalls(); + rootState.initialized = true; + return true; +} + +bool MonoScriptRuntime::CreateAppDomain() { + if (m_appDomain) { + return true; + } + + MonoRootState& rootState = GetMonoRootState(); + if (!rootState.rootDomain) { + SetError("Mono root domain is not initialized."); + return false; + } + + mono_domain_set(rootState.rootDomain, true); + m_appDomain = mono_domain_create_appdomain(const_cast("XCEngineScriptDomain"), nullptr); + if (!m_appDomain) { + SetError("Failed to create the Mono app domain."); + return false; + } + + mono_domain_set(m_appDomain, true); + return true; +} + +void MonoScriptRuntime::DestroyAppDomain() { + if (!m_appDomain) { + return; + } + + MonoRootState& rootState = GetMonoRootState(); + if (rootState.rootDomain) { + mono_domain_set(rootState.rootDomain, true); + } + + mono_domain_unload(m_appDomain); + m_appDomain = nullptr; + + if (rootState.rootDomain) { + mono_domain_set(rootState.rootDomain, true); + } +} + +void MonoScriptRuntime::SetCurrentDomain() const { + if (m_appDomain) { + mono_domain_set(m_appDomain, true); + } +} + +bool MonoScriptRuntime::LoadAssemblies() { + if (m_settings.coreAssemblyPath.empty() || m_settings.appAssemblyPath.empty()) { + SetError("Managed assembly paths are not configured."); + return false; + } + + if (!std::filesystem::exists(m_settings.coreAssemblyPath)) { + SetError("Script core assembly does not exist: " + m_settings.coreAssemblyPath.string()); + return false; + } + + if (!std::filesystem::exists(m_settings.appAssemblyPath)) { + SetError("Game scripts assembly does not exist: " + m_settings.appAssemblyPath.string()); + return false; + } + + SetCurrentDomain(); + + m_coreAssembly = mono_domain_assembly_open(m_appDomain, m_settings.coreAssemblyPath.string().c_str()); + if (!m_coreAssembly) { + SetError("Failed to load script core assembly: " + m_settings.coreAssemblyPath.string()); + return false; + } + + m_coreImage = mono_assembly_get_image(m_coreAssembly); + if (!m_coreImage) { + SetError("Failed to access the script core image."); + return false; + } + + m_appAssembly = mono_domain_assembly_open(m_appDomain, m_settings.appAssemblyPath.string().c_str()); + if (!m_appAssembly) { + SetError("Failed to load game scripts assembly: " + m_settings.appAssemblyPath.string()); + return false; + } + + m_appImage = mono_assembly_get_image(m_appAssembly); + if (!m_appImage) { + SetError("Failed to access the game scripts image."); + return false; + } + + return true; +} + +bool MonoScriptRuntime::DiscoverScriptClasses() { + ClearClassCache(); + + m_componentClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "Component"); + if (!m_componentClass) { + SetError("Failed to locate the managed Component base type."); + return false; + } + + m_behaviourClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "Behaviour"); + if (!m_behaviourClass) { + SetError("Failed to locate the managed Behaviour base type."); + return false; + } + + m_gameObjectClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "GameObject"); + if (!m_gameObjectClass) { + SetError("Failed to locate the managed GameObject wrapper type."); + return false; + } + + m_gameObjectConstructor = mono_class_get_method_from_name(m_gameObjectClass, ".ctor", 1); + m_managedGameObjectUUIDField = mono_class_get_field_from_name(m_gameObjectClass, "m_uuid"); + if (!m_gameObjectConstructor || !m_managedGameObjectUUIDField) { + SetError("Failed to locate the managed GameObject constructor or UUID field."); + return false; + } + + m_monoBehaviourClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + m_settings.baseClassName.c_str()); + if (!m_monoBehaviourClass) { + SetError("Failed to locate the managed base type " + BuildFullClassName(m_settings.baseNamespace, m_settings.baseClassName) + "."); + return false; + } + + m_gameObjectUUIDField = mono_class_get_field_from_name(m_componentClass, "m_gameObjectUUID"); + m_scriptComponentUUIDField = mono_class_get_field_from_name(m_behaviourClass, "m_scriptComponentUUID"); + if (!m_gameObjectUUIDField || !m_scriptComponentUUIDField) { + SetError("Failed to locate the managed context fields for Component/Behaviour."); + return false; + } + + DiscoverScriptClassesInImage(m_settings.appAssemblyName, m_appImage); + return true; +} + +void MonoScriptRuntime::DiscoverScriptClassesInImage(const std::string& assemblyName, MonoImage* image) { + if (!image) { + return; + } + + const int typeCount = mono_image_get_table_rows(image, MONO_TABLE_TYPEDEF); + for (int index = 1; index <= typeCount; ++index) { + const uint32_t typeToken = mono_metadata_make_token(MONO_TABLE_TYPEDEF, index); + MonoClass* monoClass = mono_class_get(image, typeToken); + if (!monoClass || !IsMonoBehaviourSubclass(monoClass)) { + continue; + } + + if ((mono_class_get_flags(monoClass) & MONO_TYPE_ATTR_ABSTRACT) != 0) { + continue; + } + + ClassMetadata metadata; + metadata.assemblyName = assemblyName; + metadata.namespaceName = SafeString(mono_class_get_namespace(monoClass)); + metadata.className = SafeString(mono_class_get_name(monoClass)); + metadata.fullName = BuildFullClassName(metadata.namespaceName, metadata.className); + metadata.monoClass = monoClass; + + for (size_t methodIndex = 0; methodIndex < LifecycleMethodCount; ++methodIndex) { + metadata.lifecycleMethods[methodIndex] = mono_class_get_method_from_name( + monoClass, + ToLifecycleMethodName(static_cast(methodIndex)), + 0); + } + + void* fieldIterator = nullptr; + while (MonoClassField* field = mono_class_get_fields(monoClass, &fieldIterator)) { + const uint32_t fieldFlags = mono_field_get_flags(field); + if ((fieldFlags & MONO_FIELD_ATTR_PUBLIC) == 0 || (fieldFlags & MONO_FIELD_ATTR_STATIC) != 0) { + continue; + } + + const ScriptFieldType fieldType = MapMonoFieldType(field); + if (fieldType == ScriptFieldType::None) { + continue; + } + + metadata.fields.emplace( + mono_field_get_name(field), + FieldMetadata{fieldType, field}); + } + + m_classes.emplace( + BuildClassKey(metadata.assemblyName, metadata.namespaceName, metadata.className), + std::move(metadata)); + } +} + +bool MonoScriptRuntime::IsMonoBehaviourSubclass(MonoClass* monoClass) const { + if (!monoClass || !m_monoBehaviourClass || monoClass == m_monoBehaviourClass) { + return false; + } + + MonoClass* current = monoClass; + while (current) { + if (current == m_monoBehaviourClass) { + return true; + } + + current = mono_class_get_parent(current); + } + + return false; +} + +ScriptFieldType MonoScriptRuntime::MapMonoFieldType(MonoClassField* field) const { + if (!field) { + return ScriptFieldType::None; + } + + MonoType* monoType = mono_field_get_type(field); + if (!monoType) { + return ScriptFieldType::None; + } + + switch (mono_type_get_type(monoType)) { + case MONO_TYPE_R4: + return ScriptFieldType::Float; + case MONO_TYPE_R8: + return ScriptFieldType::Double; + case MONO_TYPE_BOOLEAN: + return ScriptFieldType::Bool; + case MONO_TYPE_I4: + return ScriptFieldType::Int32; + case MONO_TYPE_U8: + return ScriptFieldType::UInt64; + case MONO_TYPE_STRING: + return ScriptFieldType::String; + case MONO_TYPE_CLASS: { + MonoClass* referenceClass = mono_class_from_mono_type(monoType); + if (!referenceClass) { + return ScriptFieldType::None; + } + + const std::string namespaceName = SafeString(mono_class_get_namespace(referenceClass)); + const std::string className = SafeString(mono_class_get_name(referenceClass)); + if (namespaceName == m_settings.baseNamespace && className == "GameObject") { + return ScriptFieldType::GameObject; + } + + return ScriptFieldType::None; + } + case MONO_TYPE_VALUETYPE: { + MonoClass* valueTypeClass = mono_class_from_mono_type(monoType); + if (!valueTypeClass) { + return ScriptFieldType::None; + } + + const std::string namespaceName = SafeString(mono_class_get_namespace(valueTypeClass)); + const std::string className = SafeString(mono_class_get_name(valueTypeClass)); + if (namespaceName != m_settings.baseNamespace) { + return ScriptFieldType::None; + } + + if (className == "Vector2") { + return ScriptFieldType::Vector2; + } + if (className == "Vector3") { + return ScriptFieldType::Vector3; + } + if (className == "Vector4") { + return ScriptFieldType::Vector4; + } + return ScriptFieldType::None; + } + default: + return ScriptFieldType::None; + } +} + +const char* MonoScriptRuntime::ToLifecycleMethodName(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 ""; +} + +const MonoScriptRuntime::ClassMetadata* MonoScriptRuntime::FindClassMetadata( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className) const { + const auto it = m_classes.find(BuildClassKey(assemblyName, namespaceName, className)); + return it != m_classes.end() ? &it->second : nullptr; +} + +MonoScriptRuntime::InstanceData* MonoScriptRuntime::FindInstance(const ScriptRuntimeContext& context) { + const auto it = m_instances.find(InstanceKey{context.gameObjectUUID, context.scriptComponentUUID}); + return it != m_instances.end() ? &it->second : nullptr; +} + +const MonoScriptRuntime::InstanceData* MonoScriptRuntime::FindInstance(const ScriptRuntimeContext& context) const { + const auto it = m_instances.find(InstanceKey{context.gameObjectUUID, context.scriptComponentUUID}); + return it != m_instances.end() ? &it->second : nullptr; +} + +const MonoScriptRuntime::InstanceData* MonoScriptRuntime::FindInstance(const ScriptComponent* component) const { + if (!component || !component->GetGameObject()) { + return nullptr; + } + + const auto it = m_instances.find(InstanceKey{ + component->GetGameObject()->GetUUID(), + component->GetScriptComponentUUID() + }); + return it != m_instances.end() ? &it->second : nullptr; +} + +MonoObject* MonoScriptRuntime::GetManagedObject(const InstanceData& instanceData) const { + if (instanceData.gcHandle == 0) { + return nullptr; + } + + SetCurrentDomain(); + return mono_gchandle_get_target(instanceData.gcHandle); +} + +bool MonoScriptRuntime::ApplyContextFields(const ScriptRuntimeContext& context, MonoObject* instance) { + if (!instance) { + return false; + } + + SetCurrentDomain(); + + if (m_gameObjectUUIDField) { + uint64_t gameObjectUUID = context.gameObjectUUID; + mono_field_set_value(instance, m_gameObjectUUIDField, &gameObjectUUID); + } + + if (m_scriptComponentUUIDField) { + uint64_t scriptComponentUUID = context.scriptComponentUUID; + mono_field_set_value(instance, m_scriptComponentUUIDField, &scriptComponentUUID); + } + + return true; +} + +bool MonoScriptRuntime::ApplyStoredFields( + const ScriptRuntimeContext& context, + const ClassMetadata& metadata, + MonoObject* instance) { + if (!context.component || !instance) { + return false; + } + + const ScriptFieldStorage& fieldStorage = context.component->GetFieldStorage(); + for (const std::string& fieldName : fieldStorage.GetFieldNames()) { + const StoredScriptField* storedField = fieldStorage.FindField(fieldName); + if (!storedField) { + continue; + } + + const auto metadataIt = metadata.fields.find(fieldName); + if (metadataIt == metadata.fields.end() || storedField->type != metadataIt->second.type) { + continue; + } + + if (!TrySetFieldValue(instance, metadataIt->second, storedField->value)) { + return false; + } + } + + return true; +} + +MonoObject* MonoScriptRuntime::CreateManagedGameObject(uint64_t gameObjectUUID) { + if (gameObjectUUID == 0 || !m_gameObjectClass || !m_gameObjectConstructor) { + return nullptr; + } + + SetCurrentDomain(); + + MonoObject* managedObject = mono_object_new(m_appDomain, m_gameObjectClass); + if (!managedObject) { + return nullptr; + } + + void* args[1]; + uint64_t uuidArgument = gameObjectUUID; + args[0] = &uuidArgument; + + MonoObject* exception = nullptr; + mono_runtime_invoke(m_gameObjectConstructor, managedObject, args, &exception); + if (exception) { + RecordException(exception); + return nullptr; + } + + return managedObject; +} + +bool MonoScriptRuntime::TryExtractGameObjectReference( + MonoObject* managedObject, + GameObjectReference& outReference) const { + outReference = GameObjectReference{}; + if (!managedObject) { + return true; + } + + if (!m_gameObjectClass || !m_managedGameObjectUUIDField) { + return false; + } + + if (mono_object_get_class(managedObject) != m_gameObjectClass) { + return false; + } + + uint64_t gameObjectUUID = 0; + mono_field_get_value(managedObject, m_managedGameObjectUUIDField, &gameObjectUUID); + outReference = GameObjectReference{gameObjectUUID}; + return true; +} + +bool MonoScriptRuntime::TrySetFieldValue( + MonoObject* instance, + const FieldMetadata& fieldMetadata, + const ScriptFieldValue& value) { + if (!instance || !fieldMetadata.field) { + return false; + } + + SetCurrentDomain(); + + switch (fieldMetadata.type) { + case ScriptFieldType::Float: { + float nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::Double: { + double nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::Bool: { + mono_bool nativeValue = std::get(value) ? 1 : 0; + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::Int32: { + int32_t nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::UInt64: { + uint64_t nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::String: { + MonoString* managedString = mono_string_new(m_appDomain, std::get(value).c_str()); + mono_field_set_value(instance, fieldMetadata.field, managedString); + return true; + } + case ScriptFieldType::Vector2: { + Math::Vector2 nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::Vector3: { + Math::Vector3 nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::Vector4: { + Math::Vector4 nativeValue = std::get(value); + mono_field_set_value(instance, fieldMetadata.field, &nativeValue); + return true; + } + case ScriptFieldType::GameObject: { + const GameObjectReference reference = std::get(value); + MonoObject* managedGameObject = CreateManagedGameObject(reference.gameObjectUUID); + if (reference.gameObjectUUID != 0 && !managedGameObject) { + return false; + } + mono_field_set_value(instance, fieldMetadata.field, managedGameObject); + return true; + } + case ScriptFieldType::None: + return false; + } + + return false; +} + +bool MonoScriptRuntime::TryReadFieldValue( + MonoObject* instance, + const FieldMetadata& fieldMetadata, + ScriptFieldValue& outValue) const { + if (!instance || !fieldMetadata.field) { + return false; + } + + switch (fieldMetadata.type) { + case ScriptFieldType::Float: { + float nativeValue = 0.0f; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::Double: { + double nativeValue = 0.0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::Bool: { + mono_bool nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = (nativeValue != 0); + return true; + } + case ScriptFieldType::Int32: { + int32_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::UInt64: { + uint64_t nativeValue = 0; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::String: { + MonoObject* managedObject = mono_field_get_value_object(m_appDomain, fieldMetadata.field, instance); + if (!managedObject) { + outValue = std::string(); + return true; + } + + MonoString* managedString = reinterpret_cast(managedObject); + char* utf8 = mono_string_to_utf8(managedString); + outValue = utf8 ? std::string(utf8) : std::string(); + if (utf8) { + mono_free(utf8); + } + return true; + } + case ScriptFieldType::Vector2: { + Math::Vector2 nativeValue; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::Vector3: { + Math::Vector3 nativeValue; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::Vector4: { + Math::Vector4 nativeValue; + mono_field_get_value(instance, fieldMetadata.field, &nativeValue); + outValue = nativeValue; + return true; + } + case ScriptFieldType::GameObject: { + MonoObject* managedObject = mono_field_get_value_object(m_appDomain, fieldMetadata.field, instance); + GameObjectReference reference; + if (!TryExtractGameObjectReference(managedObject, reference)) { + return false; + } + + outValue = reference; + return true; + } + case ScriptFieldType::None: + return false; + } + + return false; +} + +void MonoScriptRuntime::ClearManagedInstances() { + for (auto& [key, instanceData] : m_instances) { + (void)key; + if (instanceData.gcHandle != 0) { + mono_gchandle_free(instanceData.gcHandle); + } + } + + m_instances.clear(); +} + +void MonoScriptRuntime::ClearClassCache() { + m_classes.clear(); +} + +bool MonoScriptRuntime::InvokeManagedMethod(MonoObject* instance, MonoMethod* method) { + if (!instance || !method) { + return false; + } + + SetCurrentDomain(); + + MonoObject* exception = nullptr; + mono_runtime_invoke(method, instance, nullptr, &exception); + if (exception) { + RecordException(exception); + return false; + } + + return true; +} + +void MonoScriptRuntime::RecordException(MonoObject* exception) { + m_lastError = "Managed exception"; + if (!exception) { + return; + } + + MonoObject* secondaryException = nullptr; + MonoString* exceptionString = mono_object_to_string(exception, &secondaryException); + if (!exceptionString || secondaryException) { + return; + } + + char* utf8 = mono_string_to_utf8(exceptionString); + if (!utf8) { + return; + } + + m_lastError = utf8; + mono_free(utf8); +} + +void MonoScriptRuntime::SetError(const std::string& error) { + m_lastError = error; +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index c698a19a..8dac6dab 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -50,14 +50,30 @@ foreach(XCENGINE_REQUIRED_PATH endforeach() set(XCENGINE_SCRIPT_CORE_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Camera.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Behaviour.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Component.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Debug.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/GameObject.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/InternalCalls.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Light.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MonoBehaviour.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Quaternion.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Space.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Time.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Transform.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Vector2.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Vector3.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Vector4.cs ) set(XCENGINE_GAME_SCRIPT_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformConversionProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformSpaceProbe.cs ) set(XCENGINE_MANAGED_FRAMEWORK_REFERENCES diff --git a/managed/GameScripts/BuiltinComponentProbe.cs b/managed/GameScripts/BuiltinComponentProbe.cs new file mode 100644 index 00000000..ffeda5e6 --- /dev/null +++ b/managed/GameScripts/BuiltinComponentProbe.cs @@ -0,0 +1,60 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class BuiltinComponentProbe : MonoBehaviour + { + public bool HasCamera; + public bool HasLight; + public bool CameraLookupSucceeded; + public bool LightLookupSucceeded; + + public float ObservedFieldOfView; + public float ObservedNearClipPlane; + public float ObservedFarClipPlane; + public float ObservedDepth; + public bool ObservedPrimary; + + public float ObservedIntensity; + public float ObservedRange; + public float ObservedSpotAngle; + public bool ObservedCastsShadows; + + public void Start() + { + HasCamera = HasComponent(); + HasLight = HasComponent(); + + CameraLookupSucceeded = TryGetComponent(out Camera camera); + LightLookupSucceeded = TryGetComponent(out Light light); + + if (camera != null) + { + 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; + } + + if (light != null) + { + 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/HierarchyProbe.cs b/managed/GameScripts/HierarchyProbe.cs new file mode 100644 index 00000000..7b88e50a --- /dev/null +++ b/managed/GameScripts/HierarchyProbe.cs @@ -0,0 +1,45 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class HierarchyProbe : MonoBehaviour + { + public GameObject ReparentTarget; + public string ObservedParentName = string.Empty; + public string ObservedFirstChildName = string.Empty; + public string ReparentedChildParentName = string.Empty; + public bool ParentLookupSucceeded; + public bool ChildLookupSucceeded; + public int ObservedChildCount; + + public void Start() + { + Transform currentParent = transform.parent; + ParentLookupSucceeded = currentParent != null; + if (currentParent != null) + { + ObservedParentName = currentParent.gameObject.name; + } + + ObservedChildCount = transform.childCount; + + Transform firstChild = transform.GetChild(0); + ChildLookupSucceeded = firstChild != null; + if (firstChild != null) + { + ObservedFirstChildName = firstChild.gameObject.name; + + if (ReparentTarget != null) + { + firstChild.SetParent(ReparentTarget.transform, true); + + Transform newParent = firstChild.parent; + if (newParent != null) + { + ReparentedChildParentName = newParent.gameObject.name; + } + } + } + } + } +} diff --git a/managed/GameScripts/LifecycleProbe.cs b/managed/GameScripts/LifecycleProbe.cs index 986973d4..0f0a59f1 100644 --- a/managed/GameScripts/LifecycleProbe.cs +++ b/managed/GameScripts/LifecycleProbe.cs @@ -6,6 +6,14 @@ namespace Gameplay { } + public sealed class UnsupportedManagedComponent : Component + { + private UnsupportedManagedComponent(ulong gameObjectUUID) + : base(gameObjectUUID) + { + } + } + public sealed class LifecycleProbe : MonoBehaviour { public int AwakeCount; @@ -18,14 +26,47 @@ namespace Gameplay public int DestroyCount; public float Speed; + public float ObservedFixedDeltaTime; + public float ObservedUpdateDeltaTime; + public float ObservedLateDeltaTime; public string Label = string.Empty; + public string ObservedGameObjectName = string.Empty; + public string ObservedTargetName = string.Empty; public bool WasAwakened; + public bool WarningLogged; + public bool ErrorLogged; + public bool HasTransform; + public bool TransformLookupSucceeded; + public bool HasUnsupportedComponent; + public bool UnsupportedComponentLookupReturnedNull; + public bool TargetResolved; + public bool RotationAccessed; + public bool ScaleAccessed; + public bool TransformAccessed; + public bool ObservedEnabled; + public bool ObservedActiveSelf; + public bool ObservedActiveInHierarchy; + public bool ObservedIsActiveAndEnabled; + public bool DisableSelfOnFirstUpdate; + public bool DeactivateGameObjectOnFirstUpdate; + public GameObject Target; + public GameObject SelfReference; + public Vector4 ObservedLocalRotation; + public Vector3 ObservedLocalPosition; + public Vector3 ObservedLocalScale; public Vector3 SpawnPoint; public void Awake() { AwakeCount += 1; WasAwakened = true; + gameObject.name = gameObject.name + "_Managed"; + ObservedGameObjectName = gameObject.name; + Debug.Log(ObservedGameObjectName); + Debug.LogWarning(ObservedGameObjectName); + WarningLogged = true; + Debug.LogError(ObservedGameObjectName); + ErrorLogged = true; Label = Label + "|Awake"; } @@ -37,23 +78,80 @@ namespace Gameplay public void Start() { StartCount += 1; + HasTransform = HasComponent(); + HasUnsupportedComponent = HasComponent(); + + SelfReference = gameObject; + TargetResolved = Target != null; + if (Target != null) + { + ObservedTargetName = Target.name; + } + + TransformLookupSucceeded = TryGetComponent(out Transform resolvedTransform); + UnsupportedComponentLookupReturnedNull = !gameObject.TryGetComponent(out UnsupportedManagedComponent unsupportedComponent); + + if (resolvedTransform != null) + { + resolvedTransform.localPosition = new Vector3(7.0f, 8.0f, 9.0f); + resolvedTransform.localRotation = new Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f); + resolvedTransform.localScale = new Vector3(2.0f, 3.0f, 4.0f); + } } public void FixedUpdate() { FixedUpdateCount += 1; + ObservedFixedDeltaTime = Time.deltaTime; } public void Update() { UpdateCount += 1; Speed += 1.0f; + ObservedUpdateDeltaTime = Time.deltaTime; + ObservedLocalPosition = transform.localPosition; + Quaternion rotation = transform.localRotation; + ObservedLocalRotation = new Vector4(rotation.x, rotation.y, rotation.z, rotation.w); + ObservedLocalScale = transform.localScale; + ObservedEnabled = enabled; + ObservedActiveSelf = gameObject.activeSelf; + ObservedActiveInHierarchy = gameObject.activeInHierarchy; + ObservedIsActiveAndEnabled = isActiveAndEnabled; + RotationAccessed = true; + ScaleAccessed = true; + TransformAccessed = true; + + if (UpdateCount == 1) + { + if (DisableSelfOnFirstUpdate) + { + enabled = false; + } + + if (DeactivateGameObjectOnFirstUpdate) + { + gameObject.SetActive(false); + } + } } public void LateUpdate() { LateUpdateCount += 1; - SpawnPoint = new Vector3(SpawnPoint.X + 1.0f, SpawnPoint.Y, SpawnPoint.Z); + ObservedLateDeltaTime = Time.deltaTime; + + Vector3 position = transform.localPosition; + position.x = position.x + 1.0f; + transform.localPosition = position; + ObservedLocalPosition = transform.localPosition; + + Vector3 scale = transform.localScale; + scale.x = scale.x + 1.0f; + transform.localScale = scale; + ObservedLocalScale = transform.localScale; + + SpawnPoint.x = SpawnPoint.x + 1.0f; } public void OnDisable() diff --git a/managed/GameScripts/TransformConversionProbe.cs b/managed/GameScripts/TransformConversionProbe.cs new file mode 100644 index 00000000..7b3599a1 --- /dev/null +++ b/managed/GameScripts/TransformConversionProbe.cs @@ -0,0 +1,23 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class TransformConversionProbe : MonoBehaviour + { + public Vector3 ObservedWorldPoint; + public Vector3 ObservedLocalPoint; + public Vector3 ObservedWorldDirection; + public Vector3 ObservedLocalDirection; + + public void Start() + { + transform.localPosition = new Vector3(5.0f, 0.0f, 1.0f); + transform.localEulerAngles = new Vector3(0.0f, 90.0f, 0.0f); + + ObservedWorldPoint = transform.TransformPoint(new Vector3(0.0f, 0.0f, 2.0f)); + ObservedLocalPoint = transform.InverseTransformPoint(new Vector3(7.0f, 0.0f, 1.0f)); + ObservedWorldDirection = transform.TransformDirection(new Vector3(0.0f, 0.0f, 1.0f)); + ObservedLocalDirection = transform.InverseTransformDirection(new Vector3(1.0f, 0.0f, 0.0f)); + } + } +} diff --git a/managed/GameScripts/TransformMotionProbe.cs b/managed/GameScripts/TransformMotionProbe.cs new file mode 100644 index 00000000..6bc547ad --- /dev/null +++ b/managed/GameScripts/TransformMotionProbe.cs @@ -0,0 +1,41 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class TransformMotionProbe : MonoBehaviour + { + public Vector3 ObservedForward; + public Vector3 ObservedRight; + public Vector3 ObservedUp; + public Vector3 ObservedPositionAfterSelfTranslate; + public Vector3 ObservedPositionAfterWorldTranslate; + public Vector3 ObservedForwardAfterWorldRotate; + public Vector3 ObservedForwardAfterSelfRotate; + public Vector3 ObservedForwardAfterLookAt; + + public void Start() + { + transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); + transform.localEulerAngles = new Vector3(0.0f, 90.0f, 0.0f); + + ObservedForward = transform.forward; + ObservedRight = transform.right; + ObservedUp = transform.up; + + transform.Translate(new Vector3(0.0f, 0.0f, 2.0f)); + ObservedPositionAfterSelfTranslate = transform.position; + + transform.Translate(new Vector3(0.0f, 0.0f, 3.0f), Space.World); + ObservedPositionAfterWorldTranslate = transform.position; + + transform.Rotate(new Vector3(0.0f, -90.0f, 0.0f), Space.World); + ObservedForwardAfterWorldRotate = transform.forward; + + transform.Rotate(new Vector3(0.0f, 90.0f, 0.0f)); + ObservedForwardAfterSelfRotate = transform.forward; + + transform.LookAt(new Vector3(2.0f, 0.0f, 8.0f)); + ObservedForwardAfterLookAt = transform.forward; + } + } +} diff --git a/managed/GameScripts/TransformSpaceProbe.cs b/managed/GameScripts/TransformSpaceProbe.cs new file mode 100644 index 00000000..5260378d --- /dev/null +++ b/managed/GameScripts/TransformSpaceProbe.cs @@ -0,0 +1,33 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class TransformSpaceProbe : MonoBehaviour + { + public Vector3 ObservedInitialWorldPosition; + public Vector3 ObservedInitialWorldScale; + public Vector3 ObservedInitialLocalEulerAngles; + public Vector3 ObservedEulerAfterSet; + public Vector3 ObservedWorldPositionAfterSet; + public Vector3 ObservedWorldScaleAfterSet; + + public void Start() + { + ObservedInitialWorldPosition = transform.position; + ObservedInitialWorldScale = transform.scale; + ObservedInitialLocalEulerAngles = transform.localEulerAngles; + + transform.localEulerAngles = new Vector3(0.0f, 45.0f, 0.0f); + ObservedEulerAfterSet = transform.localEulerAngles; + + transform.position = new Vector3(20.0f, 6.0f, 10.0f); + ObservedWorldPositionAfterSet = transform.position; + + Quaternion targetRotation = new Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f); + transform.rotation = targetRotation; + + transform.scale = new Vector3(6.0f, 8.0f, 10.0f); + ObservedWorldScaleAfterSet = transform.scale; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Behaviour.cs b/managed/XCEngine.ScriptCore/Behaviour.cs new file mode 100644 index 00000000..7299a996 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Behaviour.cs @@ -0,0 +1,21 @@ +namespace XCEngine +{ + public class Behaviour : Component + { + internal ulong m_scriptComponentUUID = 0; + + protected Behaviour() + { + } + + public ulong ScriptComponentUUID => m_scriptComponentUUID; + + public bool enabled + { + get => InternalCalls.Behaviour_GetEnabled(m_scriptComponentUUID); + set => InternalCalls.Behaviour_SetEnabled(m_scriptComponentUUID, value); + } + + public bool isActiveAndEnabled => enabled && GameObject.activeInHierarchy; + } +} diff --git a/managed/XCEngine.ScriptCore/Camera.cs b/managed/XCEngine.ScriptCore/Camera.cs new file mode 100644 index 00000000..f1418357 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Camera.cs @@ -0,0 +1,70 @@ +namespace XCEngine +{ + public sealed class Camera : Component + { + internal Camera(ulong gameObjectUUID) + : base(gameObjectUUID) + { + } + + public float FieldOfView + { + get => InternalCalls.Camera_GetFieldOfView(GameObjectUUID); + set => InternalCalls.Camera_SetFieldOfView(GameObjectUUID, value); + } + + public float fieldOfView + { + get => FieldOfView; + set => FieldOfView = value; + } + + public float NearClipPlane + { + get => InternalCalls.Camera_GetNearClipPlane(GameObjectUUID); + set => InternalCalls.Camera_SetNearClipPlane(GameObjectUUID, value); + } + + public float nearClipPlane + { + get => NearClipPlane; + set => NearClipPlane = value; + } + + public float FarClipPlane + { + get => InternalCalls.Camera_GetFarClipPlane(GameObjectUUID); + set => InternalCalls.Camera_SetFarClipPlane(GameObjectUUID, value); + } + + public float farClipPlane + { + get => FarClipPlane; + set => FarClipPlane = value; + } + + public float Depth + { + get => InternalCalls.Camera_GetDepth(GameObjectUUID); + set => InternalCalls.Camera_SetDepth(GameObjectUUID, value); + } + + public float depth + { + get => Depth; + set => Depth = value; + } + + public bool Primary + { + get => InternalCalls.Camera_GetPrimary(GameObjectUUID); + set => InternalCalls.Camera_SetPrimary(GameObjectUUID, value); + } + + public bool primary + { + get => Primary; + set => Primary = value; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Component.cs b/managed/XCEngine.ScriptCore/Component.cs new file mode 100644 index 00000000..57f262ed --- /dev/null +++ b/managed/XCEngine.ScriptCore/Component.cs @@ -0,0 +1,70 @@ +using System; +using System.Reflection; + +namespace XCEngine +{ + public abstract class Component + { + internal ulong m_gameObjectUUID; + + protected Component() + { + } + + protected Component(ulong gameObjectUUID) + { + m_gameObjectUUID = gameObjectUUID; + } + + public ulong GameObjectUUID => m_gameObjectUUID; + + public GameObject GameObject => new GameObject(m_gameObjectUUID); + public GameObject gameObject => GameObject; + + public Transform Transform => GameObject.Transform; + public Transform transform => Transform; + + public bool HasComponent() where T : Component + { + return GameObject.HasComponent(); + } + + public T GetComponent() where T : Component + { + return GameObject.GetComponent(); + } + + public bool TryGetComponent(out T component) where T : Component + { + component = GetComponent(); + return component != null; + } + + internal static T Create(ulong gameObjectUUID) where T : Component + { + return Create(typeof(T), gameObjectUUID) as T; + } + + internal static Component Create(Type componentType, ulong gameObjectUUID) + { + if (componentType == null || gameObjectUUID == 0 || !typeof(Component).IsAssignableFrom(componentType)) + { + return null; + } + + try + { + return Activator.CreateInstance( + componentType, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + args: new object[] { gameObjectUUID }, + culture: null) as Component; + } + catch + { + return null; + } + } + } +} diff --git a/managed/XCEngine.ScriptCore/Debug.cs b/managed/XCEngine.ScriptCore/Debug.cs new file mode 100644 index 00000000..731578ac --- /dev/null +++ b/managed/XCEngine.ScriptCore/Debug.cs @@ -0,0 +1,20 @@ +namespace XCEngine +{ + public static class Debug + { + public static void Log(string message) + { + InternalCalls.Debug_Log(message ?? string.Empty); + } + + public static void LogWarning(string message) + { + InternalCalls.Debug_LogWarning(message ?? string.Empty); + } + + public static void LogError(string message) + { + InternalCalls.Debug_LogError(message ?? string.Empty); + } + } +} diff --git a/managed/XCEngine.ScriptCore/GameObject.cs b/managed/XCEngine.ScriptCore/GameObject.cs new file mode 100644 index 00000000..2b726fa1 --- /dev/null +++ b/managed/XCEngine.ScriptCore/GameObject.cs @@ -0,0 +1,55 @@ +namespace XCEngine +{ + public sealed class GameObject + { + private readonly ulong m_uuid; + + internal GameObject(ulong uuid) + { + m_uuid = uuid; + } + + public ulong UUID => m_uuid; + + public string Name + { + get => InternalCalls.GameObject_GetName(UUID) ?? string.Empty; + set => InternalCalls.GameObject_SetName(UUID, value ?? string.Empty); + } + + public string name + { + get => Name; + set => Name = value; + } + + public bool activeSelf => InternalCalls.GameObject_GetActiveSelf(UUID); + + public bool activeInHierarchy => InternalCalls.GameObject_GetActiveInHierarchy(UUID); + + public void SetActive(bool value) + { + InternalCalls.GameObject_SetActive(UUID, value); + } + + public Transform Transform => GetComponent(); + public Transform transform => Transform; + + public bool HasComponent() where T : Component + { + return InternalCalls.GameObject_HasComponent(UUID, typeof(T)); + } + + public T GetComponent() where T : Component + { + ulong componentOwnerUUID = InternalCalls.GameObject_GetComponent(UUID, typeof(T)); + return Component.Create(componentOwnerUUID); + } + + public bool TryGetComponent(out T component) where T : Component + { + component = GetComponent(); + return component != null; + } + } +} diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs new file mode 100644 index 00000000..eed59f4a --- /dev/null +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -0,0 +1,185 @@ +using System; +using System.Runtime.CompilerServices; + +namespace XCEngine +{ + internal static class InternalCalls + { + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Debug_Log(string message); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Debug_LogWarning(string message); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Debug_LogError(string message); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Time_GetDeltaTime(); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern string GameObject_GetName(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void GameObject_SetName(ulong gameObjectUUID, string name); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool GameObject_GetActiveSelf(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool GameObject_GetActiveInHierarchy(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void GameObject_SetActive(ulong gameObjectUUID, bool active); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool GameObject_HasComponent(ulong gameObjectUUID, Type componentType); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong GameObject_GetComponent(ulong gameObjectUUID, Type componentType); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Behaviour_GetEnabled(ulong scriptComponentUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Behaviour_SetEnabled(ulong scriptComponentUUID, bool enabled); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetLocalPosition(ulong gameObjectUUID, out Vector3 position); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetLocalPosition(ulong gameObjectUUID, ref Vector3 position); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetLocalRotation(ulong gameObjectUUID, out Quaternion rotation); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetLocalRotation(ulong gameObjectUUID, ref Quaternion rotation); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetLocalScale(ulong gameObjectUUID, out Vector3 scale); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetLocalScale(ulong gameObjectUUID, ref Vector3 scale); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetLocalEulerAngles(ulong gameObjectUUID, out Vector3 eulerAngles); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetLocalEulerAngles(ulong gameObjectUUID, ref Vector3 eulerAngles); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetPosition(ulong gameObjectUUID, out Vector3 position); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetPosition(ulong gameObjectUUID, ref Vector3 position); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetRotation(ulong gameObjectUUID, out Quaternion rotation); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetRotation(ulong gameObjectUUID, ref Quaternion rotation); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetScale(ulong gameObjectUUID, out Vector3 scale); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetScale(ulong gameObjectUUID, ref Vector3 scale); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetForward(ulong gameObjectUUID, out Vector3 forward); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetRight(ulong gameObjectUUID, out Vector3 right); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_GetUp(ulong gameObjectUUID, out Vector3 up); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_Translate(ulong gameObjectUUID, ref Vector3 translation, int relativeTo); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_Rotate(ulong gameObjectUUID, ref Vector3 eulers, int relativeTo); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_LookAt(ulong gameObjectUUID, ref Vector3 target); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_TransformPoint(ulong gameObjectUUID, ref Vector3 point, out Vector3 transformedPoint); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_InverseTransformPoint(ulong gameObjectUUID, ref Vector3 point, out Vector3 transformedPoint); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_TransformDirection(ulong gameObjectUUID, ref Vector3 direction, out Vector3 transformedDirection); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_InverseTransformDirection(ulong gameObjectUUID, ref Vector3 direction, out Vector3 transformedDirection); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong Transform_GetParent(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Transform_SetParent(ulong gameObjectUUID, ulong parentGameObjectUUID, bool worldPositionStays); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern int Transform_GetChildCount(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong Transform_GetChild(ulong gameObjectUUID, int index); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Camera_GetFieldOfView(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Camera_SetFieldOfView(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Camera_GetNearClipPlane(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Camera_SetNearClipPlane(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Camera_GetFarClipPlane(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Camera_SetFarClipPlane(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Camera_GetDepth(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Camera_SetDepth(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Camera_GetPrimary(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Camera_SetPrimary(ulong gameObjectUUID, bool value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Light_GetIntensity(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Light_SetIntensity(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Light_GetRange(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Light_SetRange(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Light_GetSpotAngle(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Light_SetSpotAngle(ulong gameObjectUUID, float value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Light_GetCastsShadows(ulong gameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Light_SetCastsShadows(ulong gameObjectUUID, bool value); + } +} diff --git a/managed/XCEngine.ScriptCore/Light.cs b/managed/XCEngine.ScriptCore/Light.cs new file mode 100644 index 00000000..ed161eb8 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Light.cs @@ -0,0 +1,58 @@ +namespace XCEngine +{ + public sealed class Light : Component + { + internal Light(ulong gameObjectUUID) + : base(gameObjectUUID) + { + } + + public float Intensity + { + get => InternalCalls.Light_GetIntensity(GameObjectUUID); + set => InternalCalls.Light_SetIntensity(GameObjectUUID, value); + } + + public float intensity + { + get => Intensity; + set => Intensity = value; + } + + public float Range + { + get => InternalCalls.Light_GetRange(GameObjectUUID); + set => InternalCalls.Light_SetRange(GameObjectUUID, value); + } + + public float range + { + get => Range; + set => Range = value; + } + + public float SpotAngle + { + get => InternalCalls.Light_GetSpotAngle(GameObjectUUID); + set => InternalCalls.Light_SetSpotAngle(GameObjectUUID, value); + } + + public float spotAngle + { + get => SpotAngle; + set => SpotAngle = value; + } + + public bool CastsShadows + { + get => InternalCalls.Light_GetCastsShadows(GameObjectUUID); + set => InternalCalls.Light_SetCastsShadows(GameObjectUUID, value); + } + + public bool castsShadows + { + get => CastsShadows; + set => CastsShadows = value; + } + } +} diff --git a/managed/XCEngine.ScriptCore/MonoBehaviour.cs b/managed/XCEngine.ScriptCore/MonoBehaviour.cs index 6868e8aa..ab2d7edd 100644 --- a/managed/XCEngine.ScriptCore/MonoBehaviour.cs +++ b/managed/XCEngine.ScriptCore/MonoBehaviour.cs @@ -1,8 +1,6 @@ namespace XCEngine { - public class MonoBehaviour + public class MonoBehaviour : Behaviour { - public ulong GameObjectUUID; - public ulong ScriptComponentUUID; } } diff --git a/managed/XCEngine.ScriptCore/Quaternion.cs b/managed/XCEngine.ScriptCore/Quaternion.cs new file mode 100644 index 00000000..b2697f1e --- /dev/null +++ b/managed/XCEngine.ScriptCore/Quaternion.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace XCEngine +{ + [StructLayout(LayoutKind.Sequential)] + public struct Quaternion + { + public float X; + public float Y; + public float Z; + public float W; + + public float x + { + get => X; + set => X = value; + } + + public float y + { + get => Y; + set => Y = value; + } + + public float z + { + get => Z; + set => Z = value; + } + + public float w + { + get => W; + set => W = value; + } + + public Quaternion(float x, float y, float z, float w) + { + X = x; + Y = y; + Z = z; + W = w; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Space.cs b/managed/XCEngine.ScriptCore/Space.cs new file mode 100644 index 00000000..77223751 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Space.cs @@ -0,0 +1,8 @@ +namespace XCEngine +{ + public enum Space + { + Self = 0, + World = 1, + } +} diff --git a/managed/XCEngine.ScriptCore/Time.cs b/managed/XCEngine.ScriptCore/Time.cs new file mode 100644 index 00000000..bca31a47 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Time.cs @@ -0,0 +1,7 @@ +namespace XCEngine +{ + public static class Time + { + public static float deltaTime => InternalCalls.Time_GetDeltaTime(); + } +} diff --git a/managed/XCEngine.ScriptCore/Transform.cs b/managed/XCEngine.ScriptCore/Transform.cs new file mode 100644 index 00000000..4cbceaa4 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Transform.cs @@ -0,0 +1,263 @@ +namespace XCEngine +{ + public sealed class Transform : Component + { + internal Transform(ulong gameObjectUUID) + : base(gameObjectUUID) + { + } + + public Vector3 LocalPosition + { + get + { + InternalCalls.Transform_GetLocalPosition(GameObjectUUID, out Vector3 position); + return position; + } + set + { + InternalCalls.Transform_SetLocalPosition(GameObjectUUID, ref value); + } + } + + public Vector3 localPosition + { + get => LocalPosition; + set => LocalPosition = value; + } + + public Quaternion LocalRotation + { + get + { + InternalCalls.Transform_GetLocalRotation(GameObjectUUID, out Quaternion rotation); + return rotation; + } + set + { + InternalCalls.Transform_SetLocalRotation(GameObjectUUID, ref value); + } + } + + public Quaternion localRotation + { + get => LocalRotation; + set => LocalRotation = value; + } + + public Vector3 LocalScale + { + get + { + InternalCalls.Transform_GetLocalScale(GameObjectUUID, out Vector3 scale); + return scale; + } + set + { + InternalCalls.Transform_SetLocalScale(GameObjectUUID, ref value); + } + } + + public Vector3 localScale + { + get => LocalScale; + set => LocalScale = value; + } + + public Vector3 LocalEulerAngles + { + get + { + InternalCalls.Transform_GetLocalEulerAngles(GameObjectUUID, out Vector3 eulerAngles); + return eulerAngles; + } + set + { + InternalCalls.Transform_SetLocalEulerAngles(GameObjectUUID, ref value); + } + } + + public Vector3 localEulerAngles + { + get => LocalEulerAngles; + set => LocalEulerAngles = value; + } + + public Vector3 Position + { + get + { + InternalCalls.Transform_GetPosition(GameObjectUUID, out Vector3 position); + return position; + } + set + { + InternalCalls.Transform_SetPosition(GameObjectUUID, ref value); + } + } + + public Vector3 position + { + get => Position; + set => Position = value; + } + + public Quaternion Rotation + { + get + { + InternalCalls.Transform_GetRotation(GameObjectUUID, out Quaternion rotation); + return rotation; + } + set + { + InternalCalls.Transform_SetRotation(GameObjectUUID, ref value); + } + } + + public Quaternion rotation + { + get => Rotation; + set => Rotation = value; + } + + public Vector3 Scale + { + get + { + InternalCalls.Transform_GetScale(GameObjectUUID, out Vector3 scale); + return scale; + } + set + { + InternalCalls.Transform_SetScale(GameObjectUUID, ref value); + } + } + + public Vector3 scale + { + get => Scale; + set => Scale = value; + } + + public Vector3 Forward + { + get + { + InternalCalls.Transform_GetForward(GameObjectUUID, out Vector3 forward); + return forward; + } + } + + public Vector3 forward => Forward; + + public Vector3 Right + { + get + { + InternalCalls.Transform_GetRight(GameObjectUUID, out Vector3 right); + return right; + } + } + + public Vector3 right => Right; + + public Vector3 Up + { + get + { + InternalCalls.Transform_GetUp(GameObjectUUID, out Vector3 up); + return up; + } + } + + public Vector3 up => Up; + + public Transform Parent + { + get + { + ulong parentUUID = InternalCalls.Transform_GetParent(GameObjectUUID); + return Create(parentUUID); + } + set + { + InternalCalls.Transform_SetParent(GameObjectUUID, value?.GameObjectUUID ?? 0, true); + } + } + + public Transform parent + { + get => Parent; + set => Parent = value; + } + + public int ChildCount => InternalCalls.Transform_GetChildCount(GameObjectUUID); + public int childCount => ChildCount; + + public Transform GetChild(int index) + { + ulong childUUID = InternalCalls.Transform_GetChild(GameObjectUUID, index); + return Create(childUUID); + } + + public void SetParent(Transform parent) + { + SetParent(parent, true); + } + + public void SetParent(Transform parent, bool worldPositionStays) + { + InternalCalls.Transform_SetParent(GameObjectUUID, parent?.GameObjectUUID ?? 0, worldPositionStays); + } + + public void Translate(Vector3 translation) + { + Translate(translation, Space.Self); + } + + public void Translate(Vector3 translation, Space relativeTo) + { + InternalCalls.Transform_Translate(GameObjectUUID, ref translation, (int)relativeTo); + } + + public void Rotate(Vector3 eulers) + { + Rotate(eulers, Space.Self); + } + + public void Rotate(Vector3 eulers, Space relativeTo) + { + InternalCalls.Transform_Rotate(GameObjectUUID, ref eulers, (int)relativeTo); + } + + public void LookAt(Vector3 worldPosition) + { + InternalCalls.Transform_LookAt(GameObjectUUID, ref worldPosition); + } + + public Vector3 TransformPoint(Vector3 point) + { + InternalCalls.Transform_TransformPoint(GameObjectUUID, ref point, out Vector3 transformedPoint); + return transformedPoint; + } + + public Vector3 InverseTransformPoint(Vector3 point) + { + InternalCalls.Transform_InverseTransformPoint(GameObjectUUID, ref point, out Vector3 transformedPoint); + return transformedPoint; + } + + public Vector3 TransformDirection(Vector3 direction) + { + InternalCalls.Transform_TransformDirection(GameObjectUUID, ref direction, out Vector3 transformedDirection); + return transformedDirection; + } + + public Vector3 InverseTransformDirection(Vector3 direction) + { + InternalCalls.Transform_InverseTransformDirection(GameObjectUUID, ref direction, out Vector3 transformedDirection); + return transformedDirection; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Vector2.cs b/managed/XCEngine.ScriptCore/Vector2.cs new file mode 100644 index 00000000..6504052b --- /dev/null +++ b/managed/XCEngine.ScriptCore/Vector2.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; + +namespace XCEngine +{ + [StructLayout(LayoutKind.Sequential)] + public struct Vector2 + { + public float X; + public float Y; + + public float x + { + get => X; + set => X = value; + } + + public float y + { + get => Y; + set => Y = value; + } + + public Vector2(float x, float y) + { + X = x; + Y = y; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Vector3.cs b/managed/XCEngine.ScriptCore/Vector3.cs new file mode 100644 index 00000000..4947c411 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Vector3.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +namespace XCEngine +{ + [StructLayout(LayoutKind.Sequential)] + public struct Vector3 + { + public float X; + public float Y; + public float Z; + + public float x + { + get => X; + set => X = value; + } + + public float y + { + get => Y; + set => Y = value; + } + + public float z + { + get => Z; + set => Z = value; + } + + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Vector4.cs b/managed/XCEngine.ScriptCore/Vector4.cs new file mode 100644 index 00000000..eb739297 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Vector4.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace XCEngine +{ + [StructLayout(LayoutKind.Sequential)] + public struct Vector4 + { + public float X; + public float Y; + public float Z; + public float W; + + public float x + { + get => X; + set => X = value; + } + + public float y + { + get => Y; + set => Y = value; + } + + public float z + { + get => Z; + set => Z = value; + } + + public float w + { + get => W; + set => W = value; + } + + public Vector4(float x, float y, float z, float w) + { + X = x; + Y = y; + Z = z; + W = w; + } + } +} diff --git a/tests/Scene/CMakeLists.txt b/tests/Scene/CMakeLists.txt index 092e95af..679eadaa 100644 --- a/tests/Scene/CMakeLists.txt +++ b/tests/Scene/CMakeLists.txt @@ -4,6 +4,7 @@ project(XCEngine_SceneTests) set(SCENE_TEST_SOURCES test_scene.cpp + test_scene_runtime.cpp test_scene_manager.cpp ) @@ -26,4 +27,4 @@ target_include_directories(scene_tests PRIVATE ) include(GoogleTest) -gtest_discover_tests(scene_tests) \ No newline at end of file +gtest_discover_tests(scene_tests) diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp new file mode 100644 index 00000000..2f3d6bb4 --- /dev/null +++ b/tests/Scene/test_scene_runtime.cpp @@ -0,0 +1,253 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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 OrderedObserverComponent : public Component { +public: + explicit OrderedObserverComponent(std::vector* events) + : m_events(events) { + } + + std::string GetName() const override { return "OrderedObserver"; } + + void Start() override { + if (m_events) { + m_events->push_back("NativeStart:" + GetGameObject()->GetName()); + } + } + + void FixedUpdate() override { + if (m_events) { + m_events->push_back("NativeFixedUpdate:" + GetGameObject()->GetName()); + } + } + + void Update(float deltaTime) override { + (void)deltaTime; + if (m_events) { + m_events->push_back("NativeUpdate:" + GetGameObject()->GetName()); + } + } + + void LateUpdate(float deltaTime) override { + (void)deltaTime; + if (m_events) { + m_events->push_back("NativeLateUpdate:" + GetGameObject()->GetName()); + } + } + +private: + std::vector* m_events = nullptr; +}; + +class RecordingScriptRuntime : public IScriptRuntime { +public: + explicit RecordingScriptRuntime(std::vector* events) + : m_events(events) { + } + + void OnRuntimeStart(Scene* scene) override { + if (m_events) { + m_events->push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null"))); + } + } + + void OnRuntimeStop(Scene* scene) override { + if (m_events) { + m_events->push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null"))); + } + } + + bool CreateScriptInstance(const ScriptRuntimeContext& context) override { + if (m_events) { + m_events->push_back("Create:" + Describe(context)); + } + return true; + } + + void DestroyScriptInstance(const ScriptRuntimeContext& context) override { + if (m_events) { + m_events->push_back("Destroy:" + Describe(context)); + } + } + + void InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) override { + (void)deltaTime; + if (m_events) { + m_events->push_back(LifecycleMethodToString(method) + ":" + Describe(context)); + } + } + +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; + } + + std::vector* m_events = nullptr; +}; + +class SceneRuntimeTest : public ::testing::Test { +protected: + void SetUp() override { + scriptEngine = &ScriptEngine::Get(); + scriptEngine->OnRuntimeStop(); + runtimeImpl = std::make_unique(&events); + scriptEngine->SetRuntime(runtimeImpl.get()); + } + + void TearDown() override { + runtime.Stop(); + scriptEngine->OnRuntimeStop(); + scriptEngine->SetRuntime(nullptr); + runtimeImpl.reset(); + scene.reset(); + } + + Scene* CreateScene(const std::string& sceneName) { + scene = std::make_unique(sceneName); + return scene.get(); + } + + ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", namespaceName, className); + return component; + } + + std::vector events; + std::unique_ptr scene; + std::unique_ptr runtimeImpl; + ScriptEngine* scriptEngine = nullptr; + SceneRuntime runtime; +}; + +TEST_F(SceneRuntimeTest, StartAndStopForwardToScriptEngine) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + AddScript(host, "Gameplay", "Bootstrap"); + + runtime.Start(runtimeScene); + runtime.Stop(); + + const std::vector expected = { + "RuntimeStart:RuntimeScene", + "Create:Host:Gameplay.Bootstrap", + "Awake:Host:Gameplay.Bootstrap", + "OnEnable:Host:Gameplay.Bootstrap", + "OnDisable:Host:Gameplay.Bootstrap", + "OnDestroy:Host:Gameplay.Bootstrap", + "Destroy:Host:Gameplay.Bootstrap", + "RuntimeStop:RuntimeScene" + }; + EXPECT_EQ(events, expected); + EXPECT_FALSE(runtime.IsRunning()); + EXPECT_EQ(runtime.GetScene(), nullptr); +} + +TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + host->AddComponent(&events); + AddScript(host, "Gameplay", "Mover"); + + runtime.Start(runtimeScene); + events.clear(); + + runtime.FixedUpdate(0.02f); + runtime.Update(0.016f); + runtime.LateUpdate(0.016f); + + const std::vector expected = { + "FixedUpdate:Host:Gameplay.Mover", + "NativeFixedUpdate:Host", + "Start:Host:Gameplay.Mover", + "Update:Host:Gameplay.Mover", + "NativeStart:Host", + "NativeUpdate:Host", + "LateUpdate:Host:Gameplay.Mover", + "NativeLateUpdate:Host" + }; + EXPECT_EQ(events, expected); +} + +TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + host->AddComponent(&events); + AddScript(host, "Gameplay", "PausedScript"); + runtimeScene->SetActive(false); + + runtime.Start(runtimeScene); + events.clear(); + + runtime.FixedUpdate(0.02f); + runtime.Update(0.016f); + runtime.LateUpdate(0.016f); + + EXPECT_TRUE(events.empty()); +} + +TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) { + Scene* firstScene = CreateScene("FirstScene"); + GameObject* firstHost = firstScene->CreateGameObject("FirstHost"); + AddScript(firstHost, "Gameplay", "FirstScript"); + + runtime.Start(firstScene); + + std::unique_ptr secondScene = std::make_unique("SecondScene"); + GameObject* secondHost = secondScene->CreateGameObject("SecondHost"); + ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript"); + + events.clear(); + runtime.Start(secondScene.get()); + + const std::vector expected = { + "OnDisable:FirstHost:Gameplay.FirstScript", + "OnDestroy:FirstHost:Gameplay.FirstScript", + "Destroy:FirstHost:Gameplay.FirstScript", + "RuntimeStop:FirstScene", + "RuntimeStart:SecondScene", + "Create:SecondHost:Gameplay.SecondScript", + "Awake:SecondHost:Gameplay.SecondScript", + "OnEnable:SecondHost:Gameplay.SecondScript" + }; + EXPECT_EQ(events, expected); + EXPECT_EQ(runtime.GetScene(), secondScene.get()); + EXPECT_TRUE(scriptEngine->HasRuntimeInstance(secondScript)); + + runtime.Stop(); +} + +} // namespace diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt index c3d9ba6a..75f8dcde 100644 --- a/tests/scripting/CMakeLists.txt +++ b/tests/scripting/CMakeLists.txt @@ -8,6 +8,12 @@ set(SCRIPTING_TEST_SOURCES test_script_engine.cpp ) +if(XCENGINE_ENABLE_MONO_SCRIPTING) + list(APPEND SCRIPTING_TEST_SOURCES + test_mono_script_runtime.cpp + ) +endif() + add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES}) if(MSVC) @@ -26,5 +32,27 @@ target_include_directories(scripting_tests PRIVATE ${CMAKE_SOURCE_DIR}/engine/include ) +if(TARGET xcengine_managed_assemblies) + add_dependencies(scripting_tests xcengine_managed_assemblies) + + file(TO_CMAKE_PATH "${XCENGINE_MANAGED_OUTPUT_DIR}" XCENGINE_MANAGED_OUTPUT_DIR_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_SCRIPT_CORE_DLL}" XCENGINE_SCRIPT_CORE_DLL_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_GAME_SCRIPTS_DLL}" XCENGINE_GAME_SCRIPTS_DLL_CMAKE) + + target_compile_definitions(scripting_tests PRIVATE + XCENGINE_TEST_MANAGED_OUTPUT_DIR=\"${XCENGINE_MANAGED_OUTPUT_DIR_CMAKE}\" + XCENGINE_TEST_SCRIPT_CORE_DLL=\"${XCENGINE_SCRIPT_CORE_DLL_CMAKE}\" + XCENGINE_TEST_GAME_SCRIPTS_DLL=\"${XCENGINE_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 + ${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll + $/assimp-vc143-mt.dll + ) +endif() + include(GoogleTest) gtest_discover_tests(scripting_tests) diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp new file mode 100644 index 00000000..3993d5fc --- /dev/null +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -0,0 +1,581 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Scripting; + +namespace { + +void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Math::Vector3& expected, float tolerance = 0.001f) { + EXPECT_NEAR(actual.x, expected.x, tolerance); + EXPECT_NEAR(actual.y, expected.y, tolerance); + EXPECT_NEAR(actual.z, expected.z, tolerance); +} + +MonoScriptRuntime::Settings CreateMonoSettings() { + MonoScriptRuntime::Settings settings; + settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; + settings.corlibDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; + settings.coreAssemblyPath = XCENGINE_TEST_SCRIPT_CORE_DLL; + settings.appAssemblyPath = XCENGINE_TEST_GAME_SCRIPTS_DLL; + return settings; +} + +class MonoScriptRuntimeTest : public ::testing::Test { +protected: + void SetUp() override { + engine = &ScriptEngine::Get(); + engine->OnRuntimeStop(); + + runtime = std::make_unique(CreateMonoSettings()); + ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + + engine->SetRuntime(runtime.get()); + } + + void TearDown() override { + engine->OnRuntimeStop(); + engine->SetRuntime(nullptr); + runtime.reset(); + scene.reset(); + } + + Scene* CreateScene(const std::string& sceneName) { + scene = std::make_unique(sceneName); + return scene.get(); + } + + ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", namespaceName, className); + return component; + } + + ScriptEngine* engine = nullptr; + std::unique_ptr runtime; + std::unique_ptr scene; +}; + +TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasses) { + const std::vector classNames = runtime->GetScriptClassNames("GameScripts"); + + EXPECT_TRUE(runtime->IsInitialized()); + EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "Gameplay", "LifecycleProbe")); + EXPECT_NE(std::find(classNames.begin(), classNames.end(), "Gameplay.LifecycleProbe"), classNames.end()); + EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.AbstractLifecycleProbe"), classNames.end()); + EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end()); +} + +TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycleMethods) { + 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); + + EXPECT_TRUE(runtime->HasManagedInstance(component)); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u); + + int32_t awakeCount = 0; + int32_t enableCount = 0; + int32_t startCount = 0; + int32_t fixedUpdateCount = 0; + int32_t updateCount = 0; + int32_t lateUpdateCount = 0; + bool wasAwakened = false; + bool warningLogged = false; + bool errorLogged = false; + bool hasTransform = false; + bool transformLookupSucceeded = false; + bool hasUnsupportedComponent = true; + bool unsupportedComponentLookupReturnedNull = false; + bool targetResolved = false; + bool rotationAccessed = false; + bool scaleAccessed = false; + bool transformAccessed = false; + bool observedEnabled = false; + bool observedActiveSelf = false; + bool observedActiveInHierarchy = false; + bool observedIsActiveAndEnabled = false; + float speed = 0.0f; + float observedFixedDeltaTime = 0.0f; + float observedUpdateDeltaTime = 0.0f; + float observedLateDeltaTime = 0.0f; + std::string label; + std::string observedGameObjectName; + std::string observedTargetName; + GameObjectReference targetReference; + GameObjectReference selfReference; + XCEngine::Math::Vector4 observedLocalRotation; + XCEngine::Math::Vector3 observedLocalPosition; + XCEngine::Math::Vector3 observedLocalScale; + XCEngine::Math::Vector3 spawnPoint; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "FixedUpdateCount", fixedUpdateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "WasAwakened", wasAwakened)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "WarningLogged", warningLogged)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ErrorLogged", errorLogged)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTransform", hasTransform)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformLookupSucceeded", transformLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasUnsupportedComponent", hasUnsupportedComponent)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UnsupportedComponentLookupReturnedNull", unsupportedComponentLookupReturnedNull)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetResolved", targetResolved)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "RotationAccessed", rotationAccessed)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ScaleAccessed", scaleAccessed)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformAccessed", transformAccessed)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy)); + 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, "ObservedUpdateDeltaTime", observedUpdateDeltaTime)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedGameObjectName", observedGameObjectName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetName", observedTargetName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "Target", targetReference)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "SelfReference", selfReference)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalRotation", observedLocalRotation)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalPosition", observedLocalPosition)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalScale", observedLocalScale)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "SpawnPoint", spawnPoint)); + + EXPECT_EQ(awakeCount, 1); + EXPECT_EQ(enableCount, 1); + EXPECT_EQ(startCount, 1); + EXPECT_EQ(fixedUpdateCount, 1); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 1); + EXPECT_TRUE(wasAwakened); + EXPECT_TRUE(warningLogged); + EXPECT_TRUE(errorLogged); + EXPECT_TRUE(hasTransform); + EXPECT_TRUE(transformLookupSucceeded); + EXPECT_FALSE(hasUnsupportedComponent); + EXPECT_TRUE(unsupportedComponentLookupReturnedNull); + EXPECT_TRUE(targetResolved); + EXPECT_TRUE(rotationAccessed); + EXPECT_TRUE(scaleAccessed); + EXPECT_TRUE(transformAccessed); + EXPECT_TRUE(observedEnabled); + EXPECT_TRUE(observedActiveSelf); + EXPECT_TRUE(observedActiveInHierarchy); + EXPECT_TRUE(observedIsActiveAndEnabled); + EXPECT_FLOAT_EQ(speed, 6.0f); + EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f); + EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f); + EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f); + EXPECT_EQ(label, "Configured|Awake"); + EXPECT_EQ(observedGameObjectName, "Host_Managed"); + EXPECT_EQ(observedTargetName, "Target"); + EXPECT_EQ(targetReference, GameObjectReference{target->GetUUID()}); + EXPECT_EQ(selfReference, GameObjectReference{host->GetUUID()}); + EXPECT_EQ(observedLocalRotation, XCEngine::Math::Vector4(0.0f, 0.5f, 0.0f, 0.8660254f)); + EXPECT_EQ(observedLocalPosition, XCEngine::Math::Vector3(8.0f, 8.0f, 9.0f)); + EXPECT_EQ(observedLocalScale, XCEngine::Math::Vector3(3.0f, 3.0f, 4.0f)); + EXPECT_EQ(spawnPoint, XCEngine::Math::Vector3(3.0f, 4.0f, 6.0f)); + EXPECT_EQ(host->GetName(), "Host_Managed"); + EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(8.0f, 8.0f, 9.0f)); + EXPECT_EQ(host->GetTransform()->GetLocalScale(), XCEngine::Math::Vector3(3.0f, 3.0f, 4.0f)); + + const XCEngine::Math::Quaternion& localRotation = host->GetTransform()->GetLocalRotation(); + EXPECT_FLOAT_EQ(localRotation.x, 0.0f); + EXPECT_FLOAT_EQ(localRotation.y, 0.5f); + EXPECT_FLOAT_EQ(localRotation.z, 0.0f); + EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f); +} + +TEST_F(MonoScriptRuntimeTest, GameObjectComponentApiResolvesTransformAndRejectsUnsupportedManagedTypes) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool hasTransform = false; + bool transformLookupSucceeded = false; + bool hasUnsupportedComponent = true; + bool unsupportedComponentLookupReturnedNull = false; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTransform", hasTransform)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TransformLookupSucceeded", transformLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasUnsupportedComponent", hasUnsupportedComponent)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UnsupportedComponentLookupReturnedNull", unsupportedComponentLookupReturnedNull)); + + EXPECT_TRUE(hasTransform); + EXPECT_TRUE(transformLookupSucceeded); + EXPECT_FALSE(hasUnsupportedComponent); + EXPECT_TRUE(unsupportedComponentLookupReturnedNull); + EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f)); +} + +TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentWrappersReadAndWriteCameraAndLight) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + CameraComponent* camera = host->AddComponent(); + LightComponent* light = host->AddComponent(); + ScriptComponent* component = AddScript(host, "Gameplay", "BuiltinComponentProbe"); + + camera->SetFieldOfView(52.0f); + camera->SetNearClipPlane(0.2f); + camera->SetFarClipPlane(300.0f); + camera->SetDepth(1.5f); + camera->SetPrimary(true); + + light->SetIntensity(1.25f); + light->SetRange(12.0f); + light->SetSpotAngle(33.0f); + light->SetCastsShadows(false); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool hasCamera = false; + bool hasLight = false; + bool cameraLookupSucceeded = false; + bool lightLookupSucceeded = false; + bool observedPrimary = false; + bool observedCastsShadows = true; + float observedFieldOfView = 0.0f; + float observedNearClipPlane = 0.0f; + float observedFarClipPlane = 0.0f; + float observedDepth = 0.0f; + float observedIntensity = 0.0f; + float observedRange = 0.0f; + float observedSpotAngle = 0.0f; + + 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(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(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(hasCamera); + EXPECT_TRUE(hasLight); + EXPECT_TRUE(cameraLookupSucceeded); + EXPECT_TRUE(lightLookupSucceeded); + + 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(observedIntensity, 1.25f); + EXPECT_FLOAT_EQ(observedRange, 12.0f); + EXPECT_FLOAT_EQ(observedSpotAngle, 33.0f); + EXPECT_FALSE(observedCastsShadows); + + 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()); + + 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, TransformHierarchyApiExposesParentChildAndReparenting) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* root = runtimeScene->CreateGameObject("Root"); + GameObject* host = runtimeScene->CreateGameObject("Host", root); + GameObject* child = runtimeScene->CreateGameObject("Child", host); + GameObject* reparentTarget = runtimeScene->CreateGameObject("ReparentTarget"); + ScriptComponent* component = AddScript(host, "Gameplay", "HierarchyProbe"); + + component->GetFieldStorage().SetFieldValue("ReparentTarget", GameObjectReference{reparentTarget->GetUUID()}); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + int32_t observedChildCount = 0; + bool parentLookupSucceeded = false; + bool childLookupSucceeded = false; + std::string observedParentName; + std::string observedFirstChildName; + std::string reparentedChildParentName; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedChildCount", observedChildCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ParentLookupSucceeded", parentLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ChildLookupSucceeded", childLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedParentName", observedParentName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFirstChildName", observedFirstChildName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ReparentedChildParentName", reparentedChildParentName)); + + EXPECT_TRUE(parentLookupSucceeded); + EXPECT_TRUE(childLookupSucceeded); + EXPECT_EQ(observedChildCount, 1); + EXPECT_EQ(observedParentName, "Root"); + EXPECT_EQ(observedFirstChildName, "Child"); + EXPECT_EQ(reparentedChildParentName, "ReparentTarget"); + + EXPECT_EQ(host->GetParent(), root); + EXPECT_EQ(host->GetChildCount(), 0u); + EXPECT_EQ(child->GetParent(), reparentTarget); + EXPECT_EQ(reparentTarget->GetChildCount(), 1u); +} + +TEST_F(MonoScriptRuntimeTest, TransformSpaceApiReadsAndWritesWorldAndLocalValues) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* parent = runtimeScene->CreateGameObject("Parent"); + GameObject* host = runtimeScene->CreateGameObject("Host", parent); + ScriptComponent* component = AddScript(host, "Gameplay", "TransformSpaceProbe"); + + parent->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f)); + parent->GetTransform()->SetLocalScale(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f)); + host->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + host->GetTransform()->SetLocalScale(XCEngine::Math::Vector3(1.0f, 1.0f, 1.0f)); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + XCEngine::Math::Vector3 observedInitialWorldPosition; + XCEngine::Math::Vector3 observedInitialWorldScale; + XCEngine::Math::Vector3 observedInitialLocalEulerAngles; + XCEngine::Math::Vector3 observedEulerAfterSet; + XCEngine::Math::Vector3 observedWorldPositionAfterSet; + XCEngine::Math::Vector3 observedWorldScaleAfterSet; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialWorldPosition", observedInitialWorldPosition)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialWorldScale", observedInitialWorldScale)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialLocalEulerAngles", observedInitialLocalEulerAngles)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEulerAfterSet", observedEulerAfterSet)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldPositionAfterSet", observedWorldPositionAfterSet)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldScaleAfterSet", observedWorldScaleAfterSet)); + + EXPECT_EQ(observedInitialWorldPosition, XCEngine::Math::Vector3(12.0f, 4.0f, 6.0f)); + EXPECT_EQ(observedInitialWorldScale, XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f)); + EXPECT_EQ(observedInitialLocalEulerAngles, XCEngine::Math::Vector3(0.0f, 0.0f, 0.0f)); + EXPECT_NEAR(observedEulerAfterSet.x, 0.0f, 0.001f); + EXPECT_NEAR(observedEulerAfterSet.y, 45.0f, 0.001f); + EXPECT_NEAR(observedEulerAfterSet.z, 0.0f, 0.001f); + EXPECT_EQ(observedWorldPositionAfterSet, XCEngine::Math::Vector3(20.0f, 6.0f, 10.0f)); + EXPECT_EQ(observedWorldScaleAfterSet, XCEngine::Math::Vector3(6.0f, 8.0f, 10.0f)); + + EXPECT_EQ(host->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(5.0f, 3.0f, 5.0f)); + EXPECT_EQ(host->GetTransform()->GetPosition(), XCEngine::Math::Vector3(20.0f, 6.0f, 10.0f)); + EXPECT_EQ(host->GetTransform()->GetLocalScale(), XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f)); + EXPECT_EQ(host->GetTransform()->GetScale(), XCEngine::Math::Vector3(6.0f, 8.0f, 10.0f)); + + const XCEngine::Math::Quaternion& localRotation = host->GetTransform()->GetLocalRotation(); + EXPECT_FLOAT_EQ(localRotation.x, 0.0f); + EXPECT_FLOAT_EQ(localRotation.y, 0.5f); + EXPECT_FLOAT_EQ(localRotation.z, 0.0f); + EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f); +} + +TEST_F(MonoScriptRuntimeTest, TransformMotionApiExposesDirectionVectorsAndAppliesOperations) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "TransformMotionProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + XCEngine::Math::Vector3 observedForward; + XCEngine::Math::Vector3 observedRight; + XCEngine::Math::Vector3 observedUp; + XCEngine::Math::Vector3 observedPositionAfterSelfTranslate; + XCEngine::Math::Vector3 observedPositionAfterWorldTranslate; + XCEngine::Math::Vector3 observedForwardAfterWorldRotate; + XCEngine::Math::Vector3 observedForwardAfterSelfRotate; + XCEngine::Math::Vector3 observedForwardAfterLookAt; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForward", observedForward)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRight", observedRight)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUp", observedUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPositionAfterSelfTranslate", observedPositionAfterSelfTranslate)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPositionAfterWorldTranslate", observedPositionAfterWorldTranslate)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterWorldRotate", observedForwardAfterWorldRotate)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterSelfRotate", observedForwardAfterSelfRotate)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedForwardAfterLookAt", observedForwardAfterLookAt)); + + ExpectVector3Near(observedForward, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f)); + ExpectVector3Near(observedRight, XCEngine::Math::Vector3(0.0f, 0.0f, -1.0f)); + ExpectVector3Near(observedUp, XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f)); + ExpectVector3Near(observedPositionAfterSelfTranslate, XCEngine::Math::Vector3(2.0f, 0.0f, 0.0f)); + ExpectVector3Near(observedPositionAfterWorldTranslate, XCEngine::Math::Vector3(2.0f, 0.0f, 3.0f)); + ExpectVector3Near(observedForwardAfterWorldRotate, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f)); + ExpectVector3Near(observedForwardAfterSelfRotate, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f)); + ExpectVector3Near(observedForwardAfterLookAt, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f)); + + ExpectVector3Near(host->GetTransform()->GetPosition(), XCEngine::Math::Vector3(2.0f, 0.0f, 3.0f)); + ExpectVector3Near(host->GetTransform()->GetForward(), XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f)); +} + +TEST_F(MonoScriptRuntimeTest, TransformConversionApiTransformsPointsAndDirectionsBetweenSpaces) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "TransformConversionProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + XCEngine::Math::Vector3 observedWorldPoint; + XCEngine::Math::Vector3 observedLocalPoint; + XCEngine::Math::Vector3 observedWorldDirection; + XCEngine::Math::Vector3 observedLocalDirection; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldPoint", observedWorldPoint)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalPoint", observedLocalPoint)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedWorldDirection", observedWorldDirection)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLocalDirection", observedLocalDirection)); + + ExpectVector3Near(observedWorldPoint, XCEngine::Math::Vector3(7.0f, 0.0f, 1.0f)); + ExpectVector3Near(observedLocalPoint, XCEngine::Math::Vector3(0.0f, 0.0f, 2.0f)); + ExpectVector3Near(observedWorldDirection, XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f)); + ExpectVector3Near(observedLocalDirection, XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f)); + + ExpectVector3Near(host->GetTransform()->TransformPoint(XCEngine::Math::Vector3(0.0f, 0.0f, 2.0f)), XCEngine::Math::Vector3(7.0f, 0.0f, 1.0f)); + ExpectVector3Near(host->GetTransform()->InverseTransformDirection(XCEngine::Math::Vector3(1.0f, 0.0f, 0.0f)), XCEngine::Math::Vector3(0.0f, 0.0f, 1.0f)); +} + +TEST_F(MonoScriptRuntimeTest, ManagedBehaviourCanDisableItselfThroughEnabledProperty) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe"); + + component->GetFieldStorage().SetFieldValue("DisableSelfOnFirstUpdate", true); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + engine->OnLateUpdate(0.016f); + + int32_t updateCount = 0; + int32_t lateUpdateCount = 0; + int32_t disableCount = 0; + bool observedEnabled = false; + bool observedIsActiveAndEnabled = false; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled)); + + EXPECT_FALSE(component->IsEnabled()); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 0); + EXPECT_EQ(disableCount, 1); + EXPECT_TRUE(observedEnabled); + EXPECT_TRUE(observedIsActiveAndEnabled); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_EQ(updateCount, 1); +} + +TEST_F(MonoScriptRuntimeTest, ManagedScriptCanDeactivateItsHostGameObject) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe"); + + component->GetFieldStorage().SetFieldValue("DeactivateGameObjectOnFirstUpdate", true); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + engine->OnLateUpdate(0.016f); + + int32_t updateCount = 0; + int32_t lateUpdateCount = 0; + int32_t disableCount = 0; + bool observedActiveSelf = false; + bool observedActiveInHierarchy = false; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LateUpdateCount", lateUpdateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy)); + + EXPECT_FALSE(host->IsActive()); + EXPECT_FALSE(host->IsActiveInHierarchy()); + EXPECT_TRUE(component->IsEnabled()); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 0); + EXPECT_EQ(disableCount, 1); + EXPECT_TRUE(observedActiveSelf); + EXPECT_TRUE(observedActiveInHierarchy); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_EQ(updateCount, 1); +} + +TEST_F(MonoScriptRuntimeTest, DisableEnablePreservesSingleInstanceAndStartOnlyRunsOnce) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + component->SetEnabled(false); + + int32_t disableCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue(component, "DisableCount", disableCount)); + EXPECT_EQ(disableCount, 1); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u); + + component->SetEnabled(true); + engine->OnUpdate(0.016f); + + int32_t enableCount = 0; + int32_t startCount = 0; + int32_t updateCount = 0; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + + EXPECT_EQ(enableCount, 2); + EXPECT_EQ(startCount, 1); + EXPECT_EQ(updateCount, 2); + + engine->OnRuntimeStop(); + + EXPECT_FALSE(runtime->HasManagedInstance(component)); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 0u); +} + +} // namespace