railroader-setons-special-s.../src/Modules/Popout/NativeInterop.cs
seton e618bbf601 feat(map): S3 v0.2.1 - in-game overlay and popout handoff
Replace the bare-bones stock map with a full Dear ImGui in-game overlay
(M key). The overlay shares the same toolbar and compass as the OS popout
window: Follow, Pop Out, Gear, rotation compass.

Pop Out button is now in the bottom toolbar where it is actually visible.
Pressing it (or F10) closes the overlay, waits 0.5 s for the camera to
release, then opens the OS popout window. Closing the popout restores the
overlay if it was running when the launch was triggered. Pressing M while
the popout is open closes the popout and reopens the overlay.

Harmony patches intercept MapWindow.Toggle and Show so base-game "Show on
map" links and the map hotkey both route through the S3 overlay. MapBypass
lets internal S3 calls through without triggering the patch recursively.

Camera ownership is enforced in LateUpdate so no base-game script can reset
targetTexture before the camera renders. Object.Destroy replaces Release()
on both RT teardown paths so the D3D address stays live until end-of-frame
and cannot be recycled into a new RT mid-frame.
2026-06-17 22:17:30 -04:00

173 lines
9.3 KiB
C#

using System;
using System.Runtime.InteropServices;
namespace S3.Modules.Popout {
// Must match InputEvent in native/include/shared_types.h exactly.
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct InputEvent {
public int type; // InputEventType (0=Move,1=LDown,2=LUp,3=RDown,4=RUp,5=Wheel)
public float x; // normalized [0,1] left=0
public float y; // normalized [0,1] top=0
public float delta; // wheel delta; positive = scroll up
}
public enum InputEventType {
MouseMove = 0,
LButtonDown = 1,
LButtonUp = 2,
RButtonDown = 3,
RButtonUp = 4,
MouseWheel = 5,
UICommand = 6, // toolbar button; x = UICmd value cast to float
}
public enum UICmd {
Recenter = 1,
FollowMode = 2, // toggle MapEnhancer follow on/off
FollowPlayerCam = 3, // follow Camera.main continuously
FollowSelectedLoco = 4, // follow TrainController.Shared.SelectedCar
FollowLoco = 5, // follow specific loco; InputEvent.y = 0-based index
JumpToLocation = 6, // jump to named location; InputEvent.y = 0-based index
SetRotation = 7, // set map rotation; InputEvent.y = degrees [0,360)
RotateReset = 8, // reset map rotation to 0
RotateSyncPlayer = 9, // toggle sync-rotation-to-player-camera mode
ToggleFollowPlayer = 10, // toggle continuous follow of player world position
PopOut = 11, // (in-game only) close the overlay and open the OS popout
}
internal static class Native {
// S3Native.dll lives in the mod folder (Mods/S3) and is loaded by full path
// via NativeLoader before any of these are called; once loaded, [DllImport]
// binds to it by name. (Pure-UMM install — no game-root copy, no winhttp proxy.)
// Named S3Native (not RRPopout) so it can never collide with a leftover copy
// of the old standalone PopOut's RRPopout.dll still injected in someone's game.
private const string Dll = "S3Native";
// Create a floating window. Returns a handle (> 0) or 0 on failure.
// Native loads saved position/size from its state file; width/height are the first-run defaults.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int RRPOPOUT_CreateWindow(string title, int width, int height);
// Set the source texture and the UV sub-rect to blit next frame.
// Call this on the main thread immediately before IssuePluginEvent.
// u0,v0 = top-left UV in D3D convention (V=0 at top); u1,v1 = bottom-right.
// Pass (0, 0, 1, 1) for full texture.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetFrameTexture(
int windowHandle, IntPtr texturePtr,
float u0, float v0, float u1, float v1);
// Returns the UnityRenderingEvent callback pointer for GL.IssuePluginEvent.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr RRPOPOUT_GetRenderEventFunc();
// Fills outWidth / outHeight with the current client area size.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_GetWindowSize(int windowHandle, out int outWidth, out int outHeight);
// Drains queued input events from the OS window into outEvents.
// Returns the number of events written (≤ maxEvents).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int RRPOPOUT_PollInputEvents(
int windowHandle,
[Out] InputEvent[] outEvents,
int maxEvents);
// Close and free the window.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_DestroyWindow(int windowHandle);
// Update the status bar text shown at the bottom of the popout.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern void RRPOPOUT_SetStatusText(int windowHandle, string text);
// Set the locomotive list shown in the Follow submenu.
// names: loco display names joined by '\n'.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern void RRPOPOUT_SetLocoList(int windowHandle, string names);
// Set the location list shown in the Jump to location submenu.
// names: location names joined by '\n'.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern void RRPOPOUT_SetLocationList(int windowHandle, string names);
// Tell the native UI whether MapEnhancer is installed (controls menu layout).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed);
// Update the compass display to reflect the current map rotation (degrees).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapRotation(int windowHandle, float degrees);
// Highlight (or un-highlight) the sync button in the toolbar.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapSyncPlayer(int windowHandle, bool active);
// Highlight (or un-highlight) the follow-player toolbar button.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetFollowPlayer(int windowHandle, bool active);
// Push current map zoom so native can persist it on window close.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapZoom(int windowHandle, float zoom);
// Read back the C#-side state that native loaded from its save file.
// followPlayer / syncRotation return 1 (true) or 0 (false).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_GetPersistedState(int windowHandle,
out int followPlayer, out int syncRotation,
out float mapRotation, out float mapZoom);
// -------------------------------------------------------------------
// In-game overlay. Shares the native engine's shared ImGui context
// and D3D11 device; renders into Unity's bound backbuffer after frame.
// -------------------------------------------------------------------
// Event id to pass to GL.IssuePluginEvent to drive the overlay render.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int RRPOPOUT_GetOverlayEventId();
// One-time: hand native a texture so it can acquire Unity's D3D device.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayDeviceTexture(IntPtr texturePtr);
// Per-frame input snapshot (top-left origin, pixels; wheel in WHEEL_DELTA units).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayInput(
float displayW, float displayH,
float mouseX, float mouseY,
int lButton, int rButton, int wheel);
// Show or hide the overlay.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayVisible(int visible);
// Map render texture to display inside the in-game overlay window, plus the
// UV sub-rect (V flipped for a Unity RT: v0=1, v1=0). IntPtr.Zero clears it.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayMapTexture(
IntPtr texturePtr, float u0, float v0, float u1, float v1);
// 1 when ImGui wants the mouse this frame (hovering a widget).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int RRPOPOUT_OverlayWantsMouse();
// Drains queued in-game map input (drag/zoom over the map image). Events are
// in normalized [0,1] image space (top-left origin); forward to the map camera.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int RRPOPOUT_PollOverlayInput(
[Out] InputEvent[] outEvents, int maxEvents);
// Reads the map image region size (px) so C# can set camera.aspect to match,
// filling the window with no letterbox bars.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_GetOverlayMapView(out float outW, out float outH);
// 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.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int RRPOPOUT_GetOverlayWindowHandle();
}
}