railroader-setons-special-s.../native/src/exports.cpp
seton edf6a8a14e Map Module 0.2.5: icon culling, EOTD, gear menu icons, font fix
Icon culling hides car icons when zoomed out past a configurable threshold
(default 40%), keeping only locomotive icons visible. Locos scale up
automatically when culling is active (configurable boost). An option also
hides MU trailing units. All managed by the new MapIconCuller class with
smooth fade transitions.

EOTD (End-of-Train Device): a blinking red dot at the rear of each consist.
Shown by default when car icons are hidden. Dot size and visibility conditions
are configurable. Implemented in EotdSystem as a programmatically generated
circle texture to avoid asset dependencies.

Added an Icons submenu to the gear menu containing all icon and EOTD settings.
Controls grey out to reflect dependencies: culling sub-settings (MU hide, loco
boost, EOTD) grey out when culling is off; EOTD dot size and hide-when-visible
grey out when EOTD is off. Changes round-trip via UICmd events, persist to
PopoutSettings, and call MapIconCuller.Refresh(). Native state is seeded from
C# on window init via RRPOPOUT_SetIconCullingState.

Settings completeness: added Right-click recenters toggle and Map BG opacity
slider to the UMM settings panel (were only accessible from the gear menu).

Replaced four FindObjectOfType<MapWindow> calls with PanelFinder.GetMapWindow().

Fixed AV false-positive on release zips: ImGui's built-in font blob contained
the substring "UPX", matching the heuristic for UPX-packed binaries. Added
IMGUI_DISABLE_DEFAULT_FONT to strip the blob; ProggyClean.ttf is now shipped
as a separate file alongside the DLL and loaded at runtime instead.
2026-06-22 18:46:12 -04:00

362 lines
14 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);
}
// 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);
}
// ---------------------------------------------------------------------------
// Plugin_Initialize / Plugin_Shutdown (called from dllmain.cpp)
// ---------------------------------------------------------------------------
void Plugin_Initialize() { }
void Plugin_Shutdown() { Renderer_OnDeviceShutdown(); }