railroader-setons-special-s.../native/src/exports.cpp
seton fcfdc6fba0 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.
2026-06-25 18:16:49 -04:00

457 lines
19 KiB
C++

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <d3d11.h>
#include <cstring>
#include <mutex>
#include "../external/IUnityInterface.h"
#include "../external/IUnityGraphics.h"
#include "../external/IUnityGraphicsD3D11.h"
#include "../include/shared_types.h"
#include "d3d11_renderer.h"
#include "popout_windows.h"
#include <cstdint>
// ---------------------------------------------------------------------------
// Unity plugin lifecycle
// ---------------------------------------------------------------------------
static IUnityInterfaces* g_unityInterfaces = nullptr;
static IUnityGraphics* g_unityGraphics = nullptr;
static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType) {
if (eventType == kUnityGfxDeviceEventInitialize) {
auto* d3d11 = g_unityInterfaces->Get<IUnityGraphicsD3D11>();
if (d3d11) Renderer_OnDeviceInit(d3d11->GetDevice());
} else if (eventType == kUnityGfxDeviceEventShutdown) {
Renderer_OnDeviceShutdown();
}
}
extern "C" UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API
UnityPluginLoad(IUnityInterfaces* interfaces) {
g_unityInterfaces = interfaces;
g_unityGraphics = interfaces->Get<IUnityGraphics>();
g_unityGraphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}
extern "C" UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API
UnityPluginUnload() {
if (g_unityGraphics)
g_unityGraphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent);
}
// ---------------------------------------------------------------------------
// Render event callback — called by Unity on the render thread
// ---------------------------------------------------------------------------
// Sentinel event id for the in-game overlay. Window handles start at 1 and grow
// by one (see CreatePopoutWindow), so this high value can never collide.
static const int kOverlayEventId = 0x05E70001;
static void UNITY_INTERFACE_API OnRenderEvent(int eventId) {
if (eventId == kOverlayEventId) { Renderer_PresentOverlay(); return; }
PopoutWindow* win = GetPopoutWindow(eventId);
if (win) Renderer_Present(win);
}
// ---------------------------------------------------------------------------
// Exported functions (called from C# via [DllImport])
// ---------------------------------------------------------------------------
extern "C" __declspec(dllexport)
int RRPOPOUT_CreateWindow(const wchar_t* title, int width, int height) {
return CreatePopoutWindow(title, width, height);
}
extern "C" __declspec(dllexport)
void RRPOPOUT_SetFrameTexture(int windowHandle, void* texturePtr,
float u0, float v0, float u1, float v1) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->srcU0.store(u0); win->srcV0.store(v0);
win->srcU1.store(u1); win->srcV1.store(v1);
win->pendingTexture.store(texturePtr);
}
extern "C" __declspec(dllexport)
UnityRenderingEvent RRPOPOUT_GetRenderEventFunc() {
return OnRenderEvent;
}
extern "C" __declspec(dllexport)
void RRPOPOUT_GetWindowSize(int windowHandle, int* outWidth, int* outHeight) {
GetPopoutWindowSize(windowHandle, outWidth, outHeight);
}
// ---------------------------------------------------------------------------
// In-game overlay (spike) — exports
// ---------------------------------------------------------------------------
// Returns the event id to pass to GL.IssuePluginEvent for the overlay render.
extern "C" __declspec(dllexport)
int RRPOPOUT_GetOverlayEventId() { return kOverlayEventId; }
// One-time: hand native a texture so it can lazily acquire Unity's D3D device.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayDeviceTexture(void* texturePtr) {
Overlay_SetDeviceTexture(texturePtr);
}
// Per-frame input snapshot (top-left origin, pixels).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayInput(float displayW, float displayH,
float mouseX, float mouseY,
int lButton, int rButton, int wheel) {
Overlay_SetInput(displayW, displayH, mouseX, mouseY,
lButton != 0, rButton != 0, wheel);
}
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayVisible(int visible) { Overlay_SetVisible(visible != 0); }
// Map render texture to show in the in-game window (+ UV rect; V flipped for a
// Unity RT). Call each frame before issuing the overlay render event.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayMapTexture(void* texturePtr,
float u0, float v0, float u1, float v1) {
Overlay_SetMapTexture(texturePtr, u0, v0, u1, v1);
}
extern "C" __declspec(dllexport)
int RRPOPOUT_OverlayWantsMouse() { return Overlay_WantsMouse() ? 1 : 0; }
// Drain queued in-game map input (drag/zoom over the map image) for C# to apply.
extern "C" __declspec(dllexport)
int RRPOPOUT_PollOverlayInput(InputEvent* outEvents, int maxEvents) {
return Overlay_PollInput(outEvents, maxEvents);
}
// Read back the map image region size so C# can match the camera aspect to it.
extern "C" __declspec(dllexport)
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.
extern "C" __declspec(dllexport)
int RRPOPOUT_GetOverlayWindowHandle() {
return GetOrCreateOverlayStateHandle();
}
// Poll input events — mouse-type events are suppressed while ImGui has capture
// (e.g. cursor is over the toolbar) to prevent accidental map pan/zoom.
extern "C" __declspec(dllexport)
int RRPOPOUT_PollInputEvents(int windowHandle, InputEvent* outEvents, int maxEvents) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return 0;
int n = win->inputQueue.drain(outEvents, maxEvents);
if (win->imWantMouse.load()) {
// Keep only non-mouse events (UICommand etc.); drop pointer motion.
int kept = 0;
for (int i = 0; i < n; ++i) {
if (outEvents[i].type == static_cast<int32_t>(UICommand))
outEvents[kept++] = outEvents[i];
}
return kept;
}
return n;
}
extern "C" __declspec(dllexport)
void RRPOPOUT_DestroyWindow(int windowHandle) {
DestroyPopoutWindow(windowHandle);
}
// Helper: parse a newline-separated UTF-16 string into the window's ImMenuItem list.
static void SetNamedList(int windowHandle, const wchar_t* names,
std::vector<PopoutWindow::ImMenuItem>& list, std::mutex& mtx) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win || !names) return;
std::vector<PopoutWindow::ImMenuItem> tmp;
const wchar_t* p = names;
while (*p) {
const wchar_t* end = p;
while (*end && *end != L'\n') ++end;
PopoutWindow::ImMenuItem item{};
WideCharToMultiByte(CP_UTF8, 0, p, (int)(end - p),
item.label, (int)sizeof(item.label) - 1, nullptr, nullptr);
tmp.push_back(item);
p = (*end == L'\n') ? end + 1 : end;
}
std::lock_guard<std::mutex> lk(mtx);
list = std::move(tmp);
}
// Set the locomotive list shown in the Follow submenu.
// names: UTF-16 loco display names joined by '\n'.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetLocoList(int windowHandle, const wchar_t* names) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) SetNamedList(windowHandle, names, win->imLocoList, win->imLocoMutex);
}
// Set the location list shown in the Jump to location submenu.
// names: UTF-16 location names joined by '\n'.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetLocationList(int windowHandle, const wchar_t* names) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) SetNamedList(windowHandle, names, win->imLocationList, win->imLocationMutex);
}
// Notify native side whether MapEnhancer is installed, so the ImGui menu can
// show or hide ME-specific items without polling C# each frame.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imMapEnhancerInstalled.store(installed);
}
// Set ME capability flags.
// hasTurntable — ShowTurntableMarkers field exists (v1.6.0 or community); gates Turntable Markers toggle.
// community — full community build (crossing/spawn/switch-reset); gates all other community items.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapEnhancerCaps(int windowHandle, bool hasTurntable, bool community) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->imMEHasTurntable.store(hasTurntable);
win->imMECommunity.store(community);
}
// Tell native whether panning the map should cancel follow mode (for the gear menu checkmark).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imPanDisablesFollow.store(active);
}
// Tell native whether right-clicking the map recenters on the player (for the gear menu checkmark).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imRightClickRecenter.store(active);
}
// Seed all icon-culling and EOTD settings for the gear menu Icons submenu.
// Called on window/overlay init and whenever C# settings change.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetIconCullingState(int windowHandle,
bool cullEnabled, float cullThresh, bool hideMU, float locoBoost,
bool eotdEnabled, bool eotdOnlyWhenCulled, float eotdSize)
{
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->imIconCullingEnabled.store(cullEnabled);
win->imCullThreshold.store(cullThresh);
win->imHideMuLocos.store(hideMU);
win->imLocoBoostedScale.store(locoBoost);
win->imEotdEnabled.store(eotdEnabled);
win->imEotdOnlyWhenCulled.store(eotdOnlyWhenCulled);
win->imEotdSizeScale.store(eotdSize);
}
// Seed the map camera clear-colour opacity slider (shared global, affects both surfaces).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayMapBgAlpha(float alpha) {
Overlay_SetMapBgAlpha(alpha);
}
// Sync the compass display to the current map rotation (degrees, [0,360)).
// Called by C# after it applies a rotation or while in sync-to-player mode.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapRotation(int windowHandle, float degrees) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imMapRotationDeg.store(degrees);
}
// Tell the sync button whether sync-to-player mode is active (highlights it).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapSyncPlayer(int windowHandle, bool active) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imMapSyncPlayer.store(active);
}
// Highlight (or un-highlight) the follow-player toolbar button.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetFollowPlayer(int windowHandle, bool active) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imFollowPlayer.store(active);
}
// Push current map zoom level so native can persist it on close.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapZoom(int windowHandle, float zoom) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imMapZoom.store(zoom);
}
// Read back the C#-side persisted state that native loaded from the state file.
// Call this once after RRPOPOUT_CreateWindow to restore the previous session.
extern "C" __declspec(dllexport)
void RRPOPOUT_GetPersistedState(int windowHandle,
int32_t* followPlayer, int32_t* syncRotation,
float* mapRotation, float* mapZoom)
{
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) {
*followPlayer = 0; *syncRotation = 0;
*mapRotation = 0.f; *mapZoom = 500.f;
return;
}
*followPlayer = win->imFollowPlayer.load() ? 1 : 0;
*syncRotation = win->imMapSyncPlayer.load() ? 1 : 0;
*mapRotation = win->imMapRotationDeg.load();
*mapZoom = win->imMapZoom.load();
}
// Update the ImGui toolbar status label.
// text is UTF-16; converted to UTF-8 for ImGui here on the calling thread.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetStatusText(int windowHandle, const wchar_t* text) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win || !text) return;
char buf[256] = {};
WideCharToMultiByte(CP_UTF8, 0, text, -1, buf, (int)sizeof(buf) - 1, nullptr, nullptr);
std::lock_guard<std::mutex> lk(win->imStatusMutex);
strncpy_s(win->imStatusText, buf, _TRUNCATE);
}
// Apply a theme preset. data is a pointer to a MapThemeData struct; presetIndex is
// stored for checkmark display in the gear menu. Thread-safe: stores via mutex + atomic.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetTheme(const MapThemeData* data, int presetIndex) {
Renderer_SetTheme(data, presetIndex);
}
// Push MapEnhancer settings state to a window's render-thread atomics.
// flags: bit-packed bool settings (see MEBoolBit in shared_types.h).
// The scale values are used to initialise / update the slider positions;
// the render thread writes them back during drag and only pushes UICmd::SetMEFloat
// when the slider is released (IsItemDeactivatedAfterEdit).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMEState(int windowHandle, uint32_t flags,
float flareScale, float junctionScale,
float trackThickness, float crossingScale) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->imMEFlags.store(flags);
win->imMEFlareScale.store(flareScale);
win->imMEJunctionSc.store(junctionScale);
win->imMETrackThick.store(trackThickness);
win->imMECrossingSc.store(crossingScale);
}
// Set the in-game overlay's chrome (window + toolbar + compass) alpha [0.1, 1.0].
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayAlpha(float alpha) {
Overlay_SetAlpha(alpha);
}
// Set the map image alpha [0.0, 1.0]. Independent of chrome alpha.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayMapAlpha(float alpha) {
Overlay_SetMapAlpha(alpha);
}
// Push industry track name labels for the map.
// namesBlobW: UTF-16 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 span anchor positions.
// anchorStarts/anchorCounts: per-label index + count into the anchor arrays.
// count=0 clears everything (zoomed out or map inactive).
extern "C" __declspec(dllexport)
void RRPOPOUT_SetTrackLabels(int windowHandle, const wchar_t* namesBlobW,
const float* us, const float* vs,
const float* anchorUs, const float* anchorVs,
const int* anchorStarts, const int* anchorCounts,
const float* angles, const float* scales,
int count) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
std::vector<PopoutWindow::ImTrackLabel> tmpLabels;
std::vector<float> tmpAnchorUs, tmpAnchorVs;
if (namesBlobW && count > 0) {
tmpLabels.reserve(count);
const wchar_t* p = namesBlobW;
for (int i = 0; i < count; ++i) {
const wchar_t* end = p;
while (*end && *end != L'\n') ++end;
PopoutWindow::ImTrackLabel lbl{};
WideCharToMultiByte(CP_UTF8, 0, p, (int)(end - p),
lbl.name, (int)sizeof(lbl.name) - 1, nullptr, nullptr);
lbl.u = us[i]; lbl.v = vs[i];
lbl.angle = angles[i];
lbl.scale = scales ? scales[i] : 1.0f;
lbl.anchorStart = anchorStarts[i];
lbl.anchorCount = anchorCounts[i];
tmpLabels.push_back(lbl);
p = (*end == L'\n') ? end + 1 : end;
}
int totalAnchors = anchorStarts[count - 1] + anchorCounts[count - 1];
tmpAnchorUs.assign(anchorUs, anchorUs + totalAnchors);
tmpAnchorVs.assign(anchorVs, anchorVs + totalAnchors);
}
std::lock_guard<std::mutex> lk(win->imTrackLabelMutex);
win->imTrackLabels = std::move(tmpLabels);
win->imAnchorUs = std::move(tmpAnchorUs);
win->imAnchorVs = std::move(tmpAnchorVs);
}
// Enable or disable track label rendering for this window. Default: enabled.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetTrackLabelsEnabled(int windowHandle, bool enabled) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (win) win->imTrackLabelsEnabled.store(enabled);
}
// Seed all track-label style settings at once. Call on map activate and on any change.
extern "C" __declspec(dllexport)
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) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->imTrackLabelFontSize.store(fontSize);
win->imTrackLabelLineThickness.store(lineThickness);
win->imTrackLabelZoomLimit.store(zoomLimit);
win->imTrackLeaderLinesEnabled.store(leaderLinesEnabled);
win->imTrackCollisionEnabled.store(collisionEnabled);
win->imTrackParallelEnabled.store(parallelEnabled);
win->imTrackMergeEnabled.store(mergeEnabled);
win->imTrackLabelMergeZoom.store(mergeZoom);
win->imTrackIndustryZoom.store(industryZoom);
win->imTrackUtilityRepair.store(utilityRepairEnabled);
win->imTrackUtilityDiesel.store(utilityDieselEnabled);
win->imTrackUtilityLoader.store(utilityLoaderEnabled);
win->imTrackUtilityInterchange.store(utilityInterchangeEnabled);
win->imTrackUtilityZoom.store(utilityZoom);
win->imTrackAllLabelsZoom.store(allLabelsZoom);
win->imTrackLabelAvoidTrack.store(avoidTrack);
win->imTrackLabelFontSizeMin.store(fontSizeMin);
}
// ---------------------------------------------------------------------------
// Plugin_Initialize / Plugin_Shutdown (called from dllmain.cpp)
// ---------------------------------------------------------------------------
void Plugin_Initialize() { }
void Plugin_Shutdown() { Renderer_OnDeviceShutdown(); }