diff --git a/native/src/d3d11_renderer.cpp b/native/src/d3d11_renderer.cpp index 6cf6ff8..99419c7 100644 --- a/native/src/d3d11_renderer.cpp +++ b/native/src/d3d11_renderer.cpp @@ -1457,6 +1457,20 @@ void Overlay_GetMapView(float* outW, float* outH) { if (outH) *outH = g_ovViewH.load(); } +// Screen-space top-left corner (pixels, top-left origin) of the map image area, stored +// each frame so C# can compute a normalized in-image mouse position for teleport. +// -1 when the map is not currently drawn (no save loaded, or wrong frame). +static std::atomic g_ovMapImgX {-1.f}, g_ovMapImgY {-1.f}; +void Overlay_GetMouseMapPos(float* outX, float* outY) { + float imgX = g_ovMapImgX.load(), imgY = g_ovMapImgY.load(); + float vw = g_ovViewW.load(), vh = g_ovViewH.load(); + if (imgX < 0.f || vw <= 0.f || vh <= 0.f) { + if (outX) *outX = -1.f; if (outY) *outY = -1.f; return; + } + if (outX) *outX = (g_ovMouseX.load() - imgX) / vw; + if (outY) *outY = (g_ovMouseY.load() - imgY) / vh; +} + void Overlay_SetAlpha(float alpha) { g_ovAlpha.store(std::max(0.1f, std::min(1.0f, alpha))); } void Overlay_SetMapAlpha(float alpha) { g_mapAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); } void Overlay_SetMapBgAlpha(float alpha) { g_mapBgAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); } @@ -1596,6 +1610,8 @@ void Renderer_PresentOverlay() { // normalized [0,1] image space (top-left origin) so C# can forward // them to the map camera with the same math the popout uses. ImVec2 imgPos = ImGui::GetCursorScreenPos(); + g_ovMapImgX.store(imgPos.x); + g_ovMapImgY.store(imgPos.y); ImGui::InvisibleButton("##mapHit", sz, ImGuiButtonFlags_MouseButtonLeft); bool hov = ImGui::IsItemHovered(); ImVec2 nrm = { (io.MousePos.x - imgPos.x) / sz.x, diff --git a/native/src/d3d11_renderer.h b/native/src/d3d11_renderer.h index c40725d..5bbddde 100644 --- a/native/src/d3d11_renderer.h +++ b/native/src/d3d11_renderer.h @@ -43,6 +43,10 @@ int Overlay_PollInput(InputEvent* out, int maxEvents); // aspect to the window shape (map fills the window, no letterbox bars). void Overlay_GetMapView(float* outW, float* outH); +// Current mouse position normalised within the map image (top-left origin, [0,1]). +// Returns (-1, -1) when the map is not currently rendered (no save or map inactive). +void Overlay_GetMouseMapPos(float* outX, float* outY); + // Show/hide the overlay UI. void Overlay_SetVisible(bool visible); diff --git a/native/src/exports.cpp b/native/src/exports.cpp index 8baeb4f..f60fb51 100644 --- a/native/src/exports.cpp +++ b/native/src/exports.cpp @@ -130,6 +130,13 @@ void RRPOPOUT_GetOverlayMapView(float* outW, float* outH) { Overlay_GetMapView(outW, outH); } +// Current mouse position normalised within the map image (top-left origin, [0,1]). +// Returns (-1, -1) when the map image is not visible this frame. +extern "C" __declspec(dllexport) +void RRPOPOUT_GetOverlayMouseMapPos(float* outX, float* outY) { + Overlay_GetMouseMapPos(outX, outY); +} + // Handle of the in-game overlay's shared UI-state holder. C# pushes status text, // loco/location lists, rotation, follow flags, etc. to this handle with the same // RRPOPOUT_Set* / RRPOPOUT_PollInputEvents functions the popout uses. diff --git a/src/Core/Ui/UiService.cs b/src/Core/Ui/UiService.cs index bf6db56..5f9283a 100644 --- a/src/Core/Ui/UiService.cs +++ b/src/Core/Ui/UiService.cs @@ -85,12 +85,14 @@ internal static class GameInput_IsMouseOverGameWindow_Patch // Intercept the map hotkey (GameInput calls Toggle() when M / the bound key is pressed). // Open our overlay instead of the stock map panel. +// Pass through when the Map Module is disabled so the game's native map opens normally. [HarmonyPatch(typeof(MapWindow), nameof(MapWindow.Toggle))] internal static class MapWindow_Toggle_Patch { private static bool Prefix() { if (UiService.MapBypass) return true; + if (!PopoutModule.Settings.enabled) return true; UiService.ToggleOverlay(); return false; } @@ -103,6 +105,7 @@ internal static class MapWindow_Show_Patch private static bool Prefix() { if (UiService.MapBypass) return true; + if (!PopoutModule.Settings.enabled) return true; UiService.OpenOverlay(); return false; } @@ -116,6 +119,7 @@ internal static class MapWindow_ShowPos_Patch private static bool Prefix(Vector3 gamePosition) { if (UiService.MapBypass) return true; + if (!PopoutModule.Settings.enabled) return true; UiService.OpenOverlay(); MapBuilder.Shared?.SetMapCenter(gamePosition); return false; @@ -255,10 +259,16 @@ internal sealed class UiHost : MonoBehaviour // window, so the rest of the screen stays interactive while the map floats. MouseOverOverlay = _visible && Native.RRPOPOUT_OverlayWantsMouse() == 1; - // F10: switch the in-game map to the OS popout window. Only fires while the - // in-game overlay is active (no-op if neither map surface is open). - if (_visible && Input.GetKeyDown(KeyCode.F10)) - PopoutModule.ScheduleExternalLaunch(); + // T key ("Jump to Mouse"): our IsMouseOverGameWindow patch blocks the game's + // StrategyCameraController.TeleportToMouse() while the overlay is up, so we + // handle it ourselves using the normalised cursor position within the map image. + if (_visible && _mapActive && GameInput.shared.Teleport) + { + Native.RRPOPOUT_GetOverlayMouseMapPos(out float mx, out float my); + // mx/my are top-left origin [0,1]; Unity viewport is bottom-left origin. + if (mx >= 0f && mx <= 1f && my >= 0f && my <= 1f) + PanelFinder.GetMapDrag()?.OnTeleport?.Invoke(new Vector2(mx, 1f - my)); + } } private void LateUpdate() diff --git a/src/Modules/Popout/DetachedPanel.cs b/src/Modules/Popout/DetachedPanel.cs index d2aaeca..97a5307 100644 --- a/src/Modules/Popout/DetachedPanel.cs +++ b/src/Modules/Popout/DetachedPanel.cs @@ -36,8 +36,14 @@ namespace S3.Modules.Popout { [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); private bool _prevHotkeyDown; + private bool _prevTeleportDown; public bool CloseRequested { get; private set; } + // Last mouse position (normalised [0,1], top-left origin) received via MouseMove. + // Updated by ForwardInputEvent; used for popout teleport key handling. + private float _lastMouseX = -1f; + private float _lastMouseY = -1f; + // Status bar: only push to native when the string changes. private string _lastStatusText = ""; @@ -169,6 +175,15 @@ namespace S3.Modules.Popout { bool hotkeyNow = IsHotkeyDown(); if (hotkeyNow && !_prevHotkeyDown) CloseRequested = true; _prevHotkeyDown = hotkeyNow; + + // T key ("Jump to Mouse"): teleport the player to the last-known cursor position + // on the map. We use GetAsyncKeyState so this fires even when the game window + // doesn't have focus. Unity's Input System won't fire Teleport from the popout + // window, so we poll here instead. VK_T = 0x54 (matches the game's default binding). + bool teleportNow = IsDown(0x54); + if (teleportNow && !_prevTeleportDown && _lastMouseX >= 0f) + PanelFinder.GetMapDrag()?.OnTeleport?.Invoke(new Vector2(_lastMouseX, 1f - _lastMouseY)); + _prevTeleportDown = teleportNow; } // --------------------------------------------------------------------------- @@ -321,6 +336,8 @@ namespace S3.Modules.Popout { break; case InputEventType.MouseMove: + _lastMouseX = e.x; + _lastMouseY = e.y; if (!_isDragging) break; if (!_didDrag && (Mathf.Abs(e.x - _dragStartX) > kClickThreshold || diff --git a/src/Modules/Popout/NativeInterop.cs b/src/Modules/Popout/NativeInterop.cs index 80ed903..c35733e 100644 --- a/src/Modules/Popout/NativeInterop.cs +++ b/src/Modules/Popout/NativeInterop.cs @@ -223,6 +223,11 @@ namespace S3.Modules.Popout { [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void RRPOPOUT_GetOverlayMapView(out float outW, out float outH); + // Current mouse position normalised within the map image (top-left origin, [0,1]). + // Returns (-1, -1) when the map is not visible this frame. + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] + public static extern void RRPOPOUT_GetOverlayMouseMapPos(out float outX, out float outY); + // Handle of the in-game overlay's shared UI-state holder. Push state to it // (status/loco/location/rotation/follow) and poll it for toolbar commands // with the same handle-based functions the popout uses.