#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" #include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" #include "MetaCorePlatform/MetaCoreInput.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreComponents.h" #include #include #include #include #include #include #include namespace { void Expect(bool value, const char* message) { if (!value) { std::cerr << message << '\n'; std::exit(1); } } } int main() { MetaCore::MetaCoreRuntimeInput runtime; runtime.SetInputMap(MetaCore::MetaCoreBuildDefaultInputMap()); MetaCore::MetaCoreInput input; input.BeginFrame(); input.SetControlValue("/W", 1.0F); runtime.Update(input); Expect(std::abs(runtime.GetAxis("MoveY") - 1.0F) < 0.001F, "W should drive MoveY"); input.BeginFrame(); input.SetControlValue("/leftButton", 1.0F); runtime.Update(input); Expect(runtime.GetAction("Interact").Pressed && runtime.GetAction("Interact").Held, "action should start"); input.BeginFrame(); runtime.Update(input); Expect(!runtime.GetAction("Interact").Pressed && runtime.GetAction("Interact").Held, "action should remain held"); input.BeginFrame(); input.SetControlValue("/leftButton", 0.0F); runtime.Update(input); Expect(runtime.GetAction("Interact").Released, "action should cancel"); Expect(runtime.Rebind("game.move.forward", "/Up"), "rebind should succeed"); input.BeginFrame(); input.SetControlValue("/W", 0.0F); input.SetControlValue("/Up", 1.0F); runtime.Update(input); Expect(runtime.GetAxis("MoveY") > 0.9F, "override should be evaluated"); runtime.CancelAll(); Expect(!runtime.GetAction("Interact").Held && runtime.GetAxis("MoveY") == 0.0F, "cancel should clear state"); MetaCore::MetaCoreTypeRegistry registry; MetaCore::MetaCoreRegisterRuntimeInputGeneratedTypes(registry); const auto mapPath = std::filesystem::temp_directory_path() / "MetaCoreInputMap.test"; Expect(MetaCore::MetaCoreWriteInputMap(mapPath, MetaCore::MetaCoreBuildDefaultInputMap(), registry), "input map should serialize"); const auto loaded = MetaCore::MetaCoreReadInputMap(mapPath, registry); Expect(loaded && loaded->Bindings.size() == 10 && loaded->Contexts.size() == 1, "input map should round trip"); const auto overridePath = std::filesystem::temp_directory_path() / "MetaCoreInputOverrides.test.json"; Expect(runtime.SaveOverrides(overridePath), "overrides should save atomically"); MetaCore::MetaCoreRuntimeInput loadedRuntime; loadedRuntime.SetInputMap(MetaCore::MetaCoreBuildDefaultInputMap()); Expect(loadedRuntime.LoadOverrides(overridePath), "overrides should load"); auto invalidMap = MetaCore::MetaCoreBuildDefaultInputMap(); invalidMap.Bindings.push_back(invalidMap.Bindings.front()); invalidMap.Bindings.back().ControlPath = "invalid"; const auto invalidIssues = MetaCore::MetaCoreValidateInputMap(invalidMap); Expect(std::count_if(invalidIssues.begin(), invalidIssues.end(), [](const auto& issue){ return issue.Error; }) >= 2, "duplicate binding IDs and invalid paths should fail validation"); { std::ofstream corrupted(overridePath, std::ios::trunc); corrupted << "{broken"; } Expect(!loadedRuntime.LoadOverrides(overridePath) && !loadedRuntime.GetWarnings().empty(), "corrupt overrides should fall back with diagnostics"); MetaCore::MetaCoreInputMapDocument priorityMap; priorityMap.Actions = {{"High", "High"}, {"Low", "Low"}}; priorityMap.Bindings = {{"high", "High", MetaCore::MetaCoreInputBindingTarget::Action, "/Space", 1.0F, {}}, {"low", "Low", MetaCore::MetaCoreInputBindingTarget::Action, "/Space", 1.0F, {}}}; priorityMap.Contexts = {{"HighContext", 10, true, true, {"high"}}, {"LowContext", 0, true, false, {"low"}}}; MetaCore::MetaCoreRuntimeInput priorityRuntime; priorityRuntime.SetInputMap(priorityMap); MetaCore::MetaCoreInput priorityInput; priorityInput.BeginFrame(); priorityInput.SetControlValue("/Space", 1.0F); priorityRuntime.Update(priorityInput); Expect(priorityRuntime.GetAction("High").Held && !priorityRuntime.GetAction("Low").Held, "higher context should consume the physical control"); priorityInput.SetWindowFocused(false); priorityRuntime.Update(priorityInput); Expect(priorityRuntime.GetAction("High").Released, "focus loss should cancel held actions"); MetaCore::MetaCoreScene scene; auto interactiveObject = scene.CreateGameObject("Interactive"); interactiveObject.AddComponent(); interactiveObject.AddComponent(); MetaCore::MetaCoreRuntimeInteraction interaction; std::vector pointerEvents; std::vector pickCallbacks; interaction.SetEventCallback([&](const auto& event) { pointerEvents.push_back(event); }); const MetaCore::MetaCoreRuntimeInteraction::PickRequest pickBackend = [&](std::uint32_t, std::uint32_t, auto callback) { pickCallbacks.push_back(std::move(callback)); return true; }; const MetaCore::MetaCoreViewportRect viewport{0.0F, 0.0F, 100.0F, 100.0F}; MetaCore::MetaCoreInput pointerInput; pointerInput.BeginFrame(); pointerInput.SetCursorPosition({20.0F, 20.0F}); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(pickCallbacks.size() == 1, "hover should submit one pick"); auto firstHover = std::move(pickCallbacks.back()); pickCallbacks.clear(); pointerInput.BeginFrame(); pointerInput.SetCursorPosition({21.0F, 20.0F}); interaction.Update(scene, pickBackend, pointerInput, viewport, false); auto latestHover = std::move(pickCallbacks.back()); pickCallbacks.clear(); latestHover({interactiveObject.GetId(), {1.0F, 2.0F, 3.0F}}); firstHover({0, {}}); pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(!pointerEvents.empty() && pointerEvents.back().Type == MetaCore::MetaCorePointerEventType::Enter, "out-of-order hover should accept only the latest result"); pickCallbacks.clear(); pointerEvents.clear(); pointerInput.BeginFrame(); pointerInput.SetMouseButtonState(MetaCore::MetaCoreMouseButton::Left, true); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(pickCallbacks.size() == 2, "press frame should preserve hover and down picks"); auto down = std::move(pickCallbacks[1]); pickCallbacks.clear(); down({interactiveObject.GetId(), {}}); pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(std::any_of(pointerEvents.begin(), pointerEvents.end(), [](const auto& e){ return e.Type == MetaCore::MetaCorePointerEventType::Down; }), "press result should capture target"); pickCallbacks.clear(); pointerInput.BeginFrame(); pointerInput.SetMouseButtonState(MetaCore::MetaCoreMouseButton::Left, false); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(pickCallbacks.size() == 2, "release frame should preserve hover and up picks"); auto up = std::move(pickCallbacks[1]); pickCallbacks.clear(); up({interactiveObject.GetId(), {}}); pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); Expect(std::any_of(pointerEvents.begin(), pointerEvents.end(), [](const auto& e){ return e.Type == MetaCore::MetaCorePointerEventType::Click; }), "same-target release should click"); pickCallbacks.clear(); pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, true); Expect(pickCallbacks.empty(), "UI-consumed mouse should not submit picking"); std::filesystem::remove(mapPath); std::filesystem::remove(overridePath); std::cout << "MetaCoreRuntimeInputTests passed\n"; return 0; }