railroader-setons-special-s.../src/Modules/Popout/NativeInterop.cs
seton c5e75ad54a track and industry labels on map; Mesh LOD module skeleton
Map: per-track industry labels rendered via Dear ImGui on both the popout
and in-game overlay. Three zoom stages - per-track labels, merged same-name
clusters, and one-per-industry labels at wider zoom. Settings: font size
(min/max with zoom scaling), leader lines, parallel rotation, collision
spread, utility track filters (repair/diesel/loader/interchange), zoom
thresholds per category. C# runs a world-space AABB solver at rebuild so
offsets are rotation-invariant; native runs a per-frame screen-space
fine-tuning pass and draws leader lines.

Mesh LOD: new module skeleton with 4-level LOD group injected on
HandleModelsLoaded - three progressive renderer cull tiers (sorted by
bounds volume) plus a 12-tri proxy box at max distance. lodBias-corrected
distance-to-screenHeight conversion. Debounced live refresh on settings
change. Disabled by default.
2026-06-25 12:33:37 -04:00

310 lines
18 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
SetTheme = 12, // (in-game only) apply theme preset; InputEvent.y = preset index
SetAlpha = 13, // (in-game only) set chrome alpha; InputEvent.y = [0.1, 1.0]
Close = 14, // (in-game only) user clicked [X] — hide the overlay
SetMapAlpha = 15, // (in-game only) set map image alpha; InputEvent.y = [0.0, 1.0]
// MapEnhancer settings
SetMEBool = 16, // toggle ME bool setting; y = MEBoolBit index, delta = 0|1
SetMEFloat = 17, // set ME float setting; y = MEFloatIdx index, delta = value
MEResetSwitchesNormal = 18, // bulk reset all switches to Normal
MEResetSwitchesThrown = 19, // bulk reset all switches to Thrown
TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode
ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player
SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0]
// Icon culling + EOTD (gear menu Icons submenu)
ToggleIconCulling = 23,
SetCullThreshold = 24, // y = [0.3, 1.0]
ToggleHideMuLocos = 25,
SetLocoBoostedScale = 26, // y = [1.0, 3.0]
ToggleEotd = 27,
ToggleEotdOnlyWhenCulled = 28,
SetEotdSizeScale = 29, // y = [1.0, 10.0]
ToggleTrackLabels = 30, // toggle industry track name labels on the map
TrackLabelSetFontSize = 31, // y = font size px [8, 24]
TrackLabelSetLineThick = 32, // y = leader line thickness [1, 4]
TrackLabelSetZoomLimit = 33, // y = orthographicSize zoom-out limit [200, 8000]
ToggleLeaderLines = 34, // toggle leader lines from label to track anchor
ToggleCollision = 35, // toggle collision-avoidance label spread
ToggleParallelLabels = 36, // rotate labels to run parallel to their track
ToggleMergeLabels = 37, // merge same-name nearby spans into one label
TrackLabelSetMergeZoom = 38, // y = orthographicSize threshold for auto-merge
TrackLabelSetIndustryZoom = 39, // y = orthographicSize threshold for Stage 3 industry labels
ToggleUtilityRepairLabels = 40, // toggle repair-track labels
ToggleUtilityDieselLabels = 41, // toggle diesel-stand labels
ToggleUtilityLoaderLabels = 42, // toggle coal-loader labels
ToggleUtilityInterchangeLabels = 43, // toggle interchange labels
TrackLabelSetUtilityZoom = 44, // y = orthographicSize beyond which utility labels hide
TrackLabelSetAllZoom = 45, // y = orthographicSize beyond which ALL labels hide
ToggleAvoidTrackLabels = 46, // push labels off their own track line
TrackLabelSetFontSizeMin = 47, // y = minimum font size px [4, max]; labels auto-scale with zoom
}
// Must match MapThemeData in native/include/shared_types.h exactly (36 floats = 144 bytes).
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct MapThemeData {
public float wBgR, wBgG, wBgB, wBgA; // window / toolbar background
public float accR, accG, accB, accA; // accent (TitleBgActive, buttons)
public float txtR, txtG, txtB, txtA; // status text
public float popR, popG, popB, popA; // settings popup background
public float mapR, mapG, mapB, mapA; // map image tint (1,1,1,1 = no tint)
public float cmpR, cmpG, cmpB, cmpA; // compass background disk
public float nNR, nNG, nNB, nNA; // compass needle N half
public float nSR, nSG, nSB, nSA; // compass needle S half
public float mapBgR, mapBgG, mapBgB, mapBgA; // map camera clear colour
}
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);
// Push ME capability flags to native.
// hasTurntable — ShowTurntableMarkers exists (v1.6.0 or community); gates Turntable Markers.
// community — full community build (crossing/spawn/switch-reset); gates community items.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapEnhancerCaps(int windowHandle, bool hasTurntable, bool community);
// 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();
// Apply a theme to the shared ImGui context. Affects both the in-game overlay
// and the popout (they share one context). presetIndex is stored for the gear
// menu checkmarks. Thread-safe: stored on main thread, applied on render thread.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetTheme(ref MapThemeData data, int presetIndex);
// Set the in-game overlay's chrome alpha [0.1, 1.0]. Thread-safe via atomic.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayAlpha(float alpha);
// Set the map image alpha [0.0, 1.0]. Independent of chrome alpha.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayMapAlpha(float alpha);
// Tell native whether panning should cancel follow mode (for the gear menu checkmark).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active);
// Tell native whether right-clicking the map recenters on the player (gear menu checkmark).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active);
// Seed all icon-culling and EOTD state for the gear menu Icons submenu.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetIconCullingState(int windowHandle,
bool cullEnabled, float cullThresh, bool hideMU, float locoBoost,
bool eotdEnabled, bool eotdOnlyWhenCulled, float eotdSize);
// Seed the map camera clear-colour opacity slider (shared global, overlay + popout).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayMapBgAlpha(float alpha);
// Push MapEnhancer settings state to a window's render-thread atomics.
// flags: bit-packed booleans (see MEBoolBit in shared_types.h).
// Scale values seed slider positions; sliders update them live during drag
// and push UICmd::SetMEFloat only on release (IsItemDeactivatedAfterEdit).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMEState(
int windowHandle, uint flags,
float flareScale, float junctionScale,
float trackThickness, float crossingScale);
// Push industry track name labels for the map.
// namesBlob: track names joined by '\n'.
// us/vs: centroid UV per label (C# projects TrackSpan centroid through the map camera).
// anchorUs/anchorVs: flat UV arrays for all anchor span positions across all labels.
// anchorStarts/anchorCounts: per-label start index and count into the anchor arrays.
// count=0 clears everything (call when zoomed out or map inactive).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern void RRPOPOUT_SetTrackLabels(
int windowHandle, string namesBlob,
[In] float[] us, [In] float[] vs,
[In] float[] anchorUs, [In] float[] anchorVs,
[In] int[] anchorStarts, [In] int[] anchorCounts,
[In] float[] angles, // screen-space angle per label (degrees; 0=right, +ve=CW in screen space)
[In] float[] scales, // font size multiplier per label (1.0=track label, 1.5=industry label)
int count);
// Enable or disable track label rendering for this window. Default: enabled.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetTrackLabelsEnabled(int windowHandle, bool enabled);
// Seed all track-label style settings at once. Called on map activate and
// whenever any of the settings change via the gear menu.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetTrackLabelStyle(
int windowHandle,
float fontSize, float lineThickness, float zoomLimit,
bool leaderLinesEnabled, bool collisionEnabled, bool parallelEnabled,
bool mergeEnabled, float mergeZoom,
float industryZoom,
bool utilityRepairEnabled, bool utilityDieselEnabled,
bool utilityLoaderEnabled, bool utilityInterchangeEnabled,
float utilityZoom,
float allLabelsZoom,
bool avoidTrack,
float fontSizeMin);
}
}