Map: fix T key teleport in overlay and popout; pass M through when module disabled

Overlay: our IsMouseOverGameWindow patch was blocking StrategyCameraController.TeleportToMouse()
as a side effect. Added native export RRPOPOUT_GetOverlayMouseMapPos (stores imgPos atomically
each render frame, returns normalized map-image cursor coords) and handle the T key directly in
UiHost.Update(), invoking MapDrag.OnTeleport with the correct viewport position. MapDrag.Update()
stays gated behind _pointerOver which never fires on our collapsed canvas, so there is no
double-teleport.

Popout: T key does not fire through Unity Input System when the game window lacks focus. Added
GetAsyncKeyState(VK_T) rising-edge detection in DetachedPanel.Update(), using the last MouseMove
event position (tracked before the drag-only guard) as the viewport coordinate.

Module disabled: all three MapWindow_Toggle/Show/ShowPos Harmony prefixes now pass through to the
game's native map when PopoutModule.Settings.enabled is false. Previously UiService.Install()
always applied the intercepts regardless of module state.

Also removed F10 as a shortcut to switch to the popout - F9 already does this and F10 conflicts
with the Shift+F10 UMM hotkey.
This commit is contained in:
Seton Carmichael 2026-06-25 18:16:49 -04:00
parent c5e75ad54a
commit fcfdc6fba0
6 changed files with 63 additions and 4 deletions

View file

@ -1457,6 +1457,20 @@ void Overlay_GetMapView(float* outW, float* outH) {
if (outH) *outH = g_ovViewH.load(); 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<float> 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_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_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))); } 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 // normalized [0,1] image space (top-left origin) so C# can forward
// them to the map camera with the same math the popout uses. // them to the map camera with the same math the popout uses.
ImVec2 imgPos = ImGui::GetCursorScreenPos(); ImVec2 imgPos = ImGui::GetCursorScreenPos();
g_ovMapImgX.store(imgPos.x);
g_ovMapImgY.store(imgPos.y);
ImGui::InvisibleButton("##mapHit", sz, ImGuiButtonFlags_MouseButtonLeft); ImGui::InvisibleButton("##mapHit", sz, ImGuiButtonFlags_MouseButtonLeft);
bool hov = ImGui::IsItemHovered(); bool hov = ImGui::IsItemHovered();
ImVec2 nrm = { (io.MousePos.x - imgPos.x) / sz.x, ImVec2 nrm = { (io.MousePos.x - imgPos.x) / sz.x,

View file

@ -43,6 +43,10 @@ int Overlay_PollInput(InputEvent* out, int maxEvents);
// aspect to the window shape (map fills the window, no letterbox bars). // aspect to the window shape (map fills the window, no letterbox bars).
void Overlay_GetMapView(float* outW, float* outH); 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. // Show/hide the overlay UI.
void Overlay_SetVisible(bool visible); void Overlay_SetVisible(bool visible);

View file

@ -130,6 +130,13 @@ void RRPOPOUT_GetOverlayMapView(float* outW, float* outH) {
Overlay_GetMapView(outW, 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, // 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 // loco/location lists, rotation, follow flags, etc. to this handle with the same
// RRPOPOUT_Set* / RRPOPOUT_PollInputEvents functions the popout uses. // RRPOPOUT_Set* / RRPOPOUT_PollInputEvents functions the popout uses.

View file

@ -85,12 +85,14 @@ internal static class GameInput_IsMouseOverGameWindow_Patch
// Intercept the map hotkey (GameInput calls Toggle() when M / the bound key is pressed). // Intercept the map hotkey (GameInput calls Toggle() when M / the bound key is pressed).
// Open our overlay instead of the stock map panel. // 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))] [HarmonyPatch(typeof(MapWindow), nameof(MapWindow.Toggle))]
internal static class MapWindow_Toggle_Patch internal static class MapWindow_Toggle_Patch
{ {
private static bool Prefix() private static bool Prefix()
{ {
if (UiService.MapBypass) return true; if (UiService.MapBypass) return true;
if (!PopoutModule.Settings.enabled) return true;
UiService.ToggleOverlay(); UiService.ToggleOverlay();
return false; return false;
} }
@ -103,6 +105,7 @@ internal static class MapWindow_Show_Patch
private static bool Prefix() private static bool Prefix()
{ {
if (UiService.MapBypass) return true; if (UiService.MapBypass) return true;
if (!PopoutModule.Settings.enabled) return true;
UiService.OpenOverlay(); UiService.OpenOverlay();
return false; return false;
} }
@ -116,6 +119,7 @@ internal static class MapWindow_ShowPos_Patch
private static bool Prefix(Vector3 gamePosition) private static bool Prefix(Vector3 gamePosition)
{ {
if (UiService.MapBypass) return true; if (UiService.MapBypass) return true;
if (!PopoutModule.Settings.enabled) return true;
UiService.OpenOverlay(); UiService.OpenOverlay();
MapBuilder.Shared?.SetMapCenter(gamePosition); MapBuilder.Shared?.SetMapCenter(gamePosition);
return false; return false;
@ -255,10 +259,16 @@ internal sealed class UiHost : MonoBehaviour
// window, so the rest of the screen stays interactive while the map floats. // window, so the rest of the screen stays interactive while the map floats.
MouseOverOverlay = _visible && Native.RRPOPOUT_OverlayWantsMouse() == 1; MouseOverOverlay = _visible && Native.RRPOPOUT_OverlayWantsMouse() == 1;
// F10: switch the in-game map to the OS popout window. Only fires while the // T key ("Jump to Mouse"): our IsMouseOverGameWindow patch blocks the game's
// in-game overlay is active (no-op if neither map surface is open). // StrategyCameraController.TeleportToMouse() while the overlay is up, so we
if (_visible && Input.GetKeyDown(KeyCode.F10)) // handle it ourselves using the normalised cursor position within the map image.
PopoutModule.ScheduleExternalLaunch(); 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() private void LateUpdate()

View file

@ -36,8 +36,14 @@ namespace S3.Modules.Popout {
[DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey);
private bool _prevHotkeyDown; private bool _prevHotkeyDown;
private bool _prevTeleportDown;
public bool CloseRequested { get; private set; } 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. // Status bar: only push to native when the string changes.
private string _lastStatusText = ""; private string _lastStatusText = "";
@ -169,6 +175,15 @@ namespace S3.Modules.Popout {
bool hotkeyNow = IsHotkeyDown(); bool hotkeyNow = IsHotkeyDown();
if (hotkeyNow && !_prevHotkeyDown) CloseRequested = true; if (hotkeyNow && !_prevHotkeyDown) CloseRequested = true;
_prevHotkeyDown = hotkeyNow; _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; break;
case InputEventType.MouseMove: case InputEventType.MouseMove:
_lastMouseX = e.x;
_lastMouseY = e.y;
if (!_isDragging) break; if (!_isDragging) break;
if (!_didDrag && if (!_didDrag &&
(Mathf.Abs(e.x - _dragStartX) > kClickThreshold || (Mathf.Abs(e.x - _dragStartX) > kClickThreshold ||

View file

@ -223,6 +223,11 @@ namespace S3.Modules.Popout {
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_GetOverlayMapView(out float outW, out float outH); 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 // 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 // (status/loco/location/rotation/follow) and poll it for toolbar commands
// with the same handle-based functions the popout uses. // with the same handle-based functions the popout uses.