From dd1af2ec30fb7bf63fec65771032975022d125c6 Mon Sep 17 00:00:00 2001 From: seton Date: Fri, 19 Jun 2026 06:08:24 -0400 Subject: [PATCH] v0.2.3 - MapEnhancer settings panel, opacity controls, right-click recenter Extends the existing MapEnhancer gear menu integration with a full settings panel covering the options added by the community fork: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons. Also adds pan-cancels-follow and right-click to recenter on player toggles, three independent opacity controls (Window, Map Elements, Map Background), a per-element hover minimum of 50% for the window chrome so controls stay reachable at low opacity, and fixes to make the compass and toolbar correctly respond to their respective sliders. Both surfaces (in-game overlay and OS popout) maintain parity on all new settings. --- Info.json | 2 +- README.md | 10 +- native/include/shared_types.h | 42 +++++- native/src/d3d11_renderer.cpp | 170 ++++++++++++++++++++++-- native/src/d3d11_renderer.h | 3 + native/src/exports.cpp | 38 ++++++ native/src/popout_window.h | 26 +++- src/Core/Ui/UiService.cs | 79 +++++++++-- src/Modules/Popout/DetachedPanel.cs | 65 ++++++++- src/Modules/Popout/MapEnhancerBridge.cs | 115 ++++++++++++++++ src/Modules/Popout/NativeInterop.cs | 30 +++++ src/Modules/Popout/PopoutSettings.cs | 7 + 12 files changed, 541 insertions(+), 46 deletions(-) diff --git a/Info.json b/Info.json index 81834aa..c0369be 100644 --- a/Info.json +++ b/Info.json @@ -2,7 +2,7 @@ "Id": "S3", "DisplayName": "S³ - Seton's Special Sauce", "Author": "seton", - "Version": "0.2.2", + "Version": "0.2.3", "ManagerVersion": "0.27.0", "GameVersionPoint": "0", "AssemblyName": "S3.dll", diff --git a/README.md b/README.md index 202e497..8c67344 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Replaces the base-game map with a floating in-game overlay (toggle with **M** or ### Settings -Configure everything from the S³ UMM settings page: hotkey, drag-to-cancel follow behavior, theme, independent window and map opacity, the detach button, and a full per-element custom color editor. +Configure everything from the S³ UMM settings page: hotkey, drag-to-cancel follow behavior, right-click to recenter on player, theme, three independent opacity controls (Window, Map Elements, Map Background), the detach button, and a full per-element custom color editor. ![Map Module settings panel](img/map/map_settings_umm.png) @@ -52,7 +52,9 @@ Six themes built in: five named presets plus a **Custom** slot you tune with per ### Transparency -The in-game overlay has independent **Window** and **Map** opacity controls. Keep the chrome subtle while the track layout stays crisp, or dial both down to a ghost overlay sitting over the world. +The in-game overlay has three independent opacity controls. **Window** sets the chrome opacity (toolbar, compass, and frame), with an automatic minimum of 50% while the mouse is over the overlay so the controls stay reachable at any setting. **Map Elements** fades the map image independently. **Map Background** controls how transparent the empty space behind terrain is - turn it down and the game world shows through where there is no map content. + + ![Overlay transparency with the map visible through the game world](img/map/transparency.png) @@ -72,9 +74,9 @@ Pop the map into a detached native OS window. Drag it to any monitor, resize it ### MapEnhancer Integration -If [MapEnhancer](https://github.com/Refizar08/rr-mapenhancer-fix) is installed, the toolbar gear menu unlocks a full **Follow** submenu: follow player camera, follow the selected locomotive, or pick any consist from a live-updated list. Jump to named locations is also available. +If [MapEnhancer](https://github.com/Refizar08/rr-mapenhancer-fix) is installed, the toolbar gear menu expands with follow controls and a settings panel. The **Follow** submenu lets you follow the player camera, the selected locomotive, or any consist picked from a live-updated list. **Jump to Location** is also available. The **Settings** panel gives you live control over all MapEnhancer rendering options without leaving the map: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons. -![MapEnhancer follow menu with locomotive list](img/map/map_enhancer_follow_mode.png) +![MapEnhancer gear menu showing follow submenu and settings panel](img/map/map_enhancer_follow_mode.png) --- diff --git a/native/include/shared_types.h b/native/include/shared_types.h index 28e8b12..6f87834 100644 --- a/native/include/shared_types.h +++ b/native/include/shared_types.h @@ -18,12 +18,12 @@ enum InputEventType : int32_t { // Command IDs for UICommand events. // For FollowLoco / JumpToLocation the list index is carried in InputEvent::y. enum UICmd : int32_t { - 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; y = 0-based index in loco list - JumpToLocation = 6, // jump map camera to named location; y = 0-based index + 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; y = 0-based index in loco list + JumpToLocation = 6, // jump map camera to named location; y = 0-based index SetRotation = 7, // set map rotation; y = degrees [0,360) RotateReset = 8, // reset map rotation to 0 RotateSyncPlayer = 9, // toggle sync-rotation-to-player-camera mode @@ -33,6 +33,36 @@ enum UICmd : int32_t { SetAlpha = 13, // (in-game only) set overlay (chrome) alpha; y = [0.1, 1.0] Close = 14, // (in-game only) hide the overlay (user clicked [X]) SetMapAlpha = 15, // (in-game only) set map image alpha; y = [0.0, 1.0] + // MapEnhancer settings (C# MapEnhancerBridge mirrors these indices) + 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] +}; + +// Bit indices for ME bool settings packed into PopoutWindow::imMEFlags. +// Must match s_boolFieldNames[] in MapEnhancerBridge.cs exactly. +enum MEBoolBit : int { + MB_TurntableMarkers = 0, // ShowTurntableMarkers + MB_TurntableControl = 1, // EnableTurntableControl + MB_CheckClearance = 2, // CheckTurntableClearance + MB_CrossingMarkers = 3, // ShowRoadCrossingMarkers + MB_PassengerStops = 4, // EnablePassengerStopTracking + MB_IndustryAreaColors = 5, // EnableIndustryAreaColors + MB_ModdedSpawnPoints = 6, // EnableModdedSpawnPoints + MB_DoubleClick = 7, // DoubleClick (require double-click for interactions) +}; + +// Float setting indices for ME sliders. +// Must match s_floatFieldNames[] in MapEnhancerBridge.cs exactly. +enum MEFloatIdx : int { + MF_FlareScale = 0, // FlareScale [0.1, 1.0] + MF_JunctionScale = 1, // JunctionMarkerScale [0.5, 1.0] + MF_TrackThickness = 2, // TrackLineThickness [0.5, 2.0] + MF_CrossingScale = 3, // CrossingMarkerScale [0.1, 1.0] }; // Theme data pushed from C# to native. Controls ImGui style colours and the diff --git a/native/src/d3d11_renderer.cpp b/native/src/d3d11_renderer.cpp index 7032445..f972673 100644 --- a/native/src/d3d11_renderer.cpp +++ b/native/src/d3d11_renderer.cpp @@ -49,6 +49,7 @@ static std::atomic g_themeChanged {false}; static std::atomic g_currentThemePreset {0}; static std::atomic g_ovAlpha {1.0f}; // chrome (window + toolbar + compass) alpha static std::atomic g_mapAlpha {1.0f}; // map Image() alpha, independent of chrome +static std::atomic g_mapBgAlpha {1.0f}; // camera clear colour opacity // cbuffer layout — must be 16-byte aligned struct alignas(16) UVRectCB { float u0, v0, u1, v1; }; @@ -430,7 +431,7 @@ void Renderer_ReleaseSwapchain(PopoutWindow* win) { // resize grip stays grabbable (the popout uses its OS window border instead). static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, bool reserveResizeGrip, bool showPopOutBtn, - const MapThemeData& theme) { + const MapThemeData& theme, float mapAlpha = 1.0f) { ImGuiIO& io = ImGui::GetIO(); // Shared command helper — usable from any window in this frame @@ -492,9 +493,10 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, ImVec2 wp = ImGui::GetWindowPos(); ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f }; - // Derive compass colours from theme - auto col32 = [](float r, float g, float b, float a) -> ImU32 { - return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a*255)); + // Derive compass colours from theme; scale alpha by mapAlpha so the + // compass responds to the Map Elements opacity slider. + auto col32 = [&](float r, float g, float b, float a) -> ImU32 { + return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a * mapAlpha * 255)); }; ImU32 bgCol = cAct ? col32(theme.cmpR*1.6f, theme.cmpG*1.6f, theme.cmpB*1.6f, theme.cmpA) : cHov ? col32(theme.cmpR*1.3f, theme.cmpG*1.3f, theme.cmpB*1.3f, theme.cmpA) @@ -560,7 +562,6 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, const float kGripReserve = reserveResizeGrip ? 18.f : 0.f; ImGui::SetNextWindowPos(ImVec2(origin.x, origin.y + (float)h - kBarH)); ImGui::SetNextWindowSize(ImVec2((float)w - kGripReserve, kBarH)); - ImGui::SetNextWindowBgAlpha(1.f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 2.f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 2.f)); @@ -640,8 +641,32 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, win->imMapSyncPlayer.load())) pushCmd(UICmd::RotateSyncPlayer); + if (ImGui::MenuItem("Pan cancels follow", nullptr, + win->imPanDisablesFollow.load())) + pushCmd(UICmd::TogglePanDisablesFollow); + + if (ImGui::MenuItem("Right-click recenters", nullptr, + win->imRightClickRecenter.load())) + pushCmd(UICmd::ToggleRightClickRecenter); + ImGui::Separator(); + // Helper lambdas for ME setting commands (y = index, delta = value) + auto pushMEBool = [&](int idx, bool val) { + InputEvent ev{}; ev.type = UICommand; + ev.x = static_cast(UICmd::SetMEBool); + ev.y = static_cast(idx); + ev.delta = val ? 1.f : 0.f; + win->inputQueue.push(ev); + }; + auto pushMEFloat = [&](int idx, float val) { + InputEvent ev{}; ev.type = UICommand; + ev.x = static_cast(UICmd::SetMEFloat); + ev.y = static_cast(idx); + ev.delta = val; + win->inputQueue.push(ev); + }; + bool meOn = win->imMapEnhancerInstalled.load(); if (meOn) { if (ImGui::BeginMenu("Map Enhancer")) { @@ -666,7 +691,7 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, } ImGui::EndMenu(); } - if (ImGui::BeginMenu("Jump to location")) { + if (ImGui::BeginMenu("Jump to Location")) { std::lock_guard lk(win->imLocationMutex); if (win->imLocationList.empty()) ImGui::TextDisabled("(loading...)"); @@ -676,10 +701,106 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, pushCmd(UICmd::JumpToLocation, (float)i); ImGui::EndMenu(); } + + ImGui::Separator(); + + if (ImGui::BeginMenu("Settings")) { + uint32_t meFlags = win->imMEFlags.load(); + auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; }; + + // -- Markers -- + ImGui::TextDisabled("Markers"); + { + bool v = meBit(MB_TurntableMarkers); + if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v); + } + { + bool v = meBit(MB_TurntableControl); + if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v); + if (v) { + bool cl = meBit(MB_CheckClearance); + if (ImGui::MenuItem(" Check Clearance", nullptr, cl)) pushMEBool(MB_CheckClearance, !cl); + } + } + { + bool v = meBit(MB_CrossingMarkers); + if (ImGui::MenuItem("Road Crossing Markers", nullptr, v)) pushMEBool(MB_CrossingMarkers, !v); + } + { + bool v = meBit(MB_PassengerStops); + if (ImGui::MenuItem("Passenger Stop Tracking", nullptr, v)) pushMEBool(MB_PassengerStops, !v); + } + { + bool v = meBit(MB_IndustryAreaColors); + if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v); + } + + ImGui::Separator(); + + // -- Behavior -- + ImGui::TextDisabled("Behavior"); + { + bool v = meBit(MB_ModdedSpawnPoints); + if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v); + } + { + bool v = meBit(MB_DoubleClick); + if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v); + } + + ImGui::Separator(); + + // -- Scales -- (live-update atomic during drag; push on release) + ImGui::TextDisabled("Scales"); + { + float v = win->imMEFlareScale.load(); + ImGui::SetNextItemWidth(110.f); + if (ImGui::SliderFloat("Flare##me", &v, 0.1f, 1.0f, "%.2f")) + win->imMEFlareScale.store(v); + if (ImGui::IsItemDeactivatedAfterEdit()) + pushMEFloat(MF_FlareScale, win->imMEFlareScale.load()); + } + { + float v = win->imMEJunctionSc.load(); + ImGui::SetNextItemWidth(110.f); + if (ImGui::SliderFloat("Junction##me", &v, 0.5f, 1.0f, "%.2f")) + win->imMEJunctionSc.store(v); + if (ImGui::IsItemDeactivatedAfterEdit()) + pushMEFloat(MF_JunctionScale, win->imMEJunctionSc.load()); + } + { + float v = win->imMETrackThick.load(); + ImGui::SetNextItemWidth(110.f); + if (ImGui::SliderFloat("Track Width##me", &v, 0.5f, 2.0f, "%.2f")) + win->imMETrackThick.store(v); + if (ImGui::IsItemDeactivatedAfterEdit()) + pushMEFloat(MF_TrackThickness, win->imMETrackThick.load()); + } + { + float v = win->imMECrossingSc.load(); + ImGui::SetNextItemWidth(110.f); + if (ImGui::SliderFloat("Crossing##me", &v, 0.1f, 1.0f, "%.2f")) + win->imMECrossingSc.store(v); + if (ImGui::IsItemDeactivatedAfterEdit()) + pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load()); + } + + ImGui::Separator(); + + // -- Switch Reset -- + ImGui::TextDisabled("Switch Reset"); + if (ImGui::MenuItem("All Switches - Normal")) + pushCmd(UICmd::MEResetSwitchesNormal); + if (ImGui::MenuItem("All Switches - Thrown")) + pushCmd(UICmd::MEResetSwitchesThrown); + + ImGui::EndMenu(); + } + ImGui::EndMenu(); } } else { - if (ImGui::BeginMenu("Jump to location")) { + if (ImGui::BeginMenu("Jump to Location")) { std::lock_guard lk(win->imLocationMutex); if (win->imLocationList.empty()) ImGui::TextDisabled("(loading...)"); @@ -719,10 +840,17 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h, // Map image alpha — independent of chrome float mAlpha = g_mapAlpha.load(); ImGui::SetNextItemWidth(100.f); - if (ImGui::SliderFloat("Map##ov", &mAlpha, 0.0f, 1.0f, "%.2f")) + if (ImGui::SliderFloat("Map Elements##ov", &mAlpha, 0.0f, 1.0f, "%.2f")) g_mapAlpha.store(mAlpha); if (ImGui::IsItemDeactivatedAfterEdit()) pushCmd(UICmd::SetMapAlpha, g_mapAlpha.load()); + // Map background alpha — camera clear colour opacity (0 = transparent void) + float bgAlpha = g_mapBgAlpha.load(); + ImGui::SetNextItemWidth(100.f); + if (ImGui::SliderFloat("Map Background##ov", &bgAlpha, 0.0f, 1.0f, "%.2f")) + g_mapBgAlpha.store(bgAlpha); + if (ImGui::IsItemDeactivatedAfterEdit()) + pushCmd(UICmd::SetMapBgAlpha, g_mapBgAlpha.load()); } ImGui::EndPopup(); @@ -863,8 +991,9 @@ void Overlay_GetMapView(float* outW, float* outH) { if (outH) *outH = g_ovViewH.load(); } -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_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_SetMapBgAlpha(float alpha) { g_mapBgAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); } // Map-image interaction (drag/zoom) queued here for C# to forward to the map // camera. Same InputEvent contract the popout uses, so C# can share the logic. @@ -933,9 +1062,15 @@ void Renderer_PresentOverlay() { // image has its own independent alpha, then popped finally after BuildMapUI. const float ovAlpha = g_ovAlpha.load(); const float mapAlpha = g_mapAlpha.load(); - const bool hasAlpha = ovAlpha < 0.999f; + const float mapBgA = g_mapBgAlpha.load(); + // While the mouse is over the overlay, clamp chrome alpha to 50% so the UI + // remains readable even when the user has set a very low window opacity. + // Uses last frame's WantCaptureMouse (one-frame lag; imperceptible in practice). + const bool mouseOver = g_ovWantMouse.load(); + const float effectiveAlpha = mouseOver ? std::max(ovAlpha, 0.5f) : ovAlpha; + const bool hasAlpha = effectiveAlpha < 0.999f; if (hasAlpha) - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ovAlpha); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha); g_ovMapSRV.Reset(); // release last frame's view before building this frame's @@ -954,6 +1089,11 @@ void Renderer_PresentOverlay() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); ImGui::SetNextWindowPos(ImVec2(80.f, 80.f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(720.f, 480.f), ImGuiCond_FirstUseEver); + // Scale window bg alpha by mapBgA so the map background slider makes the void + // behind the camera transparent (showing Unity's scene), not the window chrome. + if (mapBgA < 0.999f) + ImGui::PushStyleColor(ImGuiCol_WindowBg, + ImVec4(theme.wBgR, theme.wBgG, theme.wBgB, theme.wBgA * mapBgA)); // p_open = true: ImGui adds an [X] close button to the title bar. When the user // clicks it ImGui sets windowOpen=false; we push UICmd::Close so C# can call // HideIfVisible() and DeactivateMap(). NoBringToFrontOnFocus keeps this window @@ -963,6 +1103,7 @@ void Renderer_PresentOverlay() { ImGuiWindowFlags_NoBringToFrontOnFocus); // WindowPadding is read; pop now so ovAlpha is the only active style var. ImGui::PopStyleVar(); + if (mapBgA < 0.999f) ImGui::PopStyleColor(); if (windowVisible) { void* mapTex = g_ovMapTex.load(); @@ -1003,6 +1144,7 @@ void Renderer_PresentOverlay() { pushOv(MouseMove, 0.f); if (ImGui::IsItemDeactivated()) pushOv(LButtonUp, 0.f); if (hov && io.MouseWheel != 0.f) pushOv(MouseWheel, io.MouseWheel); + if (hov && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) pushOv(RButtonUp, 0.f); // Draw the map over the same rect the hit-button occupies. // Map alpha is independent of chrome alpha: pop ovAlpha so @@ -1013,7 +1155,7 @@ void Renderer_PresentOverlay() { ImVec4 tint(theme.mapR, theme.mapG, theme.mapB, theme.mapA * mapAlpha); if (hasAlpha) ImGui::PopStyleVar(); // lift chrome alpha ImGui::Image((ImTextureID)g_ovMapSRV.Get(), sz, uv0, uv1, tint); - if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ovAlpha); + if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha); // Visible resize-grip indicator. ImGui draws its own grip during // Begin() — behind our opaque map image, so invisible. We draw one @@ -1050,7 +1192,7 @@ void Renderer_PresentOverlay() { // in-game map and the popout present the same UI. Reads the shared state holder. if (mapShown && stateWin) BuildMapUI(stateWin, mapScreenPos, mapSize.x, mapSize.y, - /*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme); + /*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme, mapAlpha); // Pop the global alpha AFTER all chrome windows, before Render(). if (hasAlpha) diff --git a/native/src/d3d11_renderer.h b/native/src/d3d11_renderer.h index 5777fe4..c40725d 100644 --- a/native/src/d3d11_renderer.h +++ b/native/src/d3d11_renderer.h @@ -60,5 +60,8 @@ void Overlay_SetAlpha(float alpha); // Set the map image alpha [0.0, 1.0]. Independent of chrome alpha. Thread-safe via atomic. void Overlay_SetMapAlpha(float alpha); +// Set the map camera clear-colour opacity [0.0, 1.0]. Thread-safe via atomic. +void Overlay_SetMapBgAlpha(float alpha); + // Render the overlay into the currently bound RTV. Render thread only. void Renderer_PresentOverlay(); diff --git a/native/src/exports.cpp b/native/src/exports.cpp index 43ab527..8c2e5dc 100644 --- a/native/src/exports.cpp +++ b/native/src/exports.cpp @@ -209,6 +209,26 @@ void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed) { if (win) win->imMapEnhancerInstalled.store(installed); } +// 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 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) @@ -276,6 +296,24 @@ 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) { diff --git a/native/src/popout_window.h b/native/src/popout_window.h index 1ee8359..1871af0 100644 --- a/native/src/popout_window.h +++ b/native/src/popout_window.h @@ -71,12 +71,26 @@ struct PopoutWindow { // ----------------------------------------------------------------------- // Map rotation + ME flag (C# → render thread via exports) // ----------------------------------------------------------------------- - std::atomic imMapRotationDeg {0.f}; - std::atomic imMapZoom {500.f}; - std::atomic imMapSyncPlayer {false}; - std::atomic imMapEnhancerInstalled{false}; - std::atomic imFollowPlayer {false}; - std::atomic imAlwaysOnTop {false}; + std::atomic imMapRotationDeg {0.f}; + std::atomic imMapZoom {500.f}; + std::atomic imMapSyncPlayer {false}; + std::atomic imMapEnhancerInstalled {false}; + std::atomic imFollowPlayer {false}; + std::atomic imAlwaysOnTop {false}; + std::atomic imPanDisablesFollow {true}; + std::atomic imRightClickRecenter {true}; + + // ----------------------------------------------------------------------- + // MapEnhancer settings state (C# → render thread via RRPOPOUT_SetMEState) + // Bit layout of imMEFlags matches MEBoolBit enum in shared_types.h. + // Scale atomics are live-updated during slider drag so the slider doesn't + // snap back between frames, then the final value is pushed as UICmd::SetMEFloat. + // ----------------------------------------------------------------------- + std::atomic imMEFlags {0}; + std::atomic imMEFlareScale {0.6f}; + std::atomic imMEJunctionSc {0.6f}; + std::atomic imMETrackThick {1.25f}; + std::atomic imMECrossingSc {0.3f}; // Loaded geometry from the save file — consumed by MessagePumpThread before CreateWindowExW. std::atomic lastWinX {-1}, lastWinY {-1}; diff --git a/src/Core/Ui/UiService.cs b/src/Core/Ui/UiService.cs index 7cc6ee4..07a62b0 100644 --- a/src/Core/Ui/UiService.cs +++ b/src/Core/Ui/UiService.cs @@ -170,6 +170,7 @@ internal sealed class UiHost : MonoBehaviour private bool _followPlayer; private float _listTimer; private bool _locationsSent; + private bool _meDirty; private string _lastStatus = ""; private void Start() @@ -195,10 +196,11 @@ internal sealed class UiHost : MonoBehaviour _nativeReady = true; - // Apply saved theme and both alpha settings so the overlay opens with the right look. + // Apply saved theme and opacity settings so the overlay opens with the right look. MapThemes.ApplyFromSettings(); Native.RRPOPOUT_SetOverlayAlpha(PopoutModule.Settings.overlayAlpha); Native.RRPOPOUT_SetOverlayMapAlpha(PopoutModule.Settings.overlayMapAlpha); + Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); StartCoroutine(RenderLoop()); Log.Info("[ui] in-game overlay ready - press F10 to toggle."); @@ -337,6 +339,7 @@ internal sealed class UiHost : MonoBehaviour int cn = Native.RRPOPOUT_PollInputEvents(_overlayHandle, s_inputBuf, kMaxInput); for (int i = 0; i < cn; i++) ForwardCommand(s_inputBuf[i]); + if (_meDirty) { PushMEState(); _meDirty = false; } } } } @@ -387,6 +390,17 @@ internal sealed class UiHost : MonoBehaviour Native.RRPOPOUT_SetMapRotation(_overlayHandle, _mapRotationDeg); } + private void PushMEState() + { + if (!MapEnhancerBridge.IsInstalled) return; + Native.RRPOPOUT_SetMEState(_overlayHandle, + MapEnhancerBridge.GetMEBoolFlags(), + MapEnhancerBridge.GetMEFloat(0), + MapEnhancerBridge.GetMEFloat(1), + MapEnhancerBridge.GetMEFloat(2), + MapEnhancerBridge.GetMEFloat(3)); + } + // Pushes the toolbar status line (zoom + map/cam coordinates) when it changes. private void UpdateMapStatus() { @@ -494,11 +508,11 @@ internal sealed class UiHost : MonoBehaviour case UICmd.SetTheme: MapThemes.Apply((MapTheme)(int)e.y); - // Also update the map camera background colour immediately so it - // matches the new theme even before the next ActivateMap. - if (_mapCamera != null) - _mapCamera.backgroundColor = - MapThemes.GetMapBgColor((MapTheme)(int)e.y); + if (_mapCamera != null) { + var c = MapThemes.GetMapBgColor((MapTheme)(int)e.y); + c.a *= PopoutModule.Settings.overlayMapBgAlpha; + _mapCamera.backgroundColor = c; + } break; case UICmd.SetAlpha: @@ -518,6 +532,42 @@ internal sealed class UiHost : MonoBehaviour PopoutModule.Persist(); Native.RRPOPOUT_SetOverlayMapAlpha(mapAlpha); break; + + case UICmd.SetMEBool: + MapEnhancerBridge.SetMEBool((int)e.y, e.delta != 0f); + if ((int)e.y == 6) _locationsSent = false; // EnableModdedSpawnPoints — re-push location list + _meDirty = true; + break; + case UICmd.SetMEFloat: + MapEnhancerBridge.SetMEFloat((int)e.y, e.delta); + _meDirty = true; + break; + case UICmd.MEResetSwitchesNormal: + MapEnhancerBridge.ResetAllSwitchesToNormal(); + break; + case UICmd.MEResetSwitchesThrown: + MapEnhancerBridge.ResetAllSwitchesToThrown(); + break; + case UICmd.TogglePanDisablesFollow: + PopoutModule.Settings.panDisablesFollow = !PopoutModule.Settings.panDisablesFollow; + PopoutModule.Persist(); + Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow); + break; + case UICmd.ToggleRightClickRecenter: + PopoutModule.Settings.rightClickRecenter = !PopoutModule.Settings.rightClickRecenter; + PopoutModule.Persist(); + Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter); + break; + case UICmd.SetMapBgAlpha: + float bgAlpha = Mathf.Clamp(e.y, 0f, 1f); + PopoutModule.Settings.overlayMapBgAlpha = bgAlpha; + PopoutModule.Persist(); + if (_mapCamera != null) { + var bgc2 = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); + bgc2.a *= bgAlpha; + _mapCamera.backgroundColor = bgc2; + } + break; } } @@ -531,6 +581,11 @@ internal sealed class UiHost : MonoBehaviour switch ((InputEventType)e.type) { + case InputEventType.RButtonUp: + if (PopoutModule.Settings.rightClickRecenter) + UnityEngine.Object.FindObjectOfType()?.ClickLocateMe(); + break; + case InputEventType.MouseWheel: // Lower floor than the popout's 100 so you can zoom in close like the // base game map; UpdateForZoom rescales icons to match. @@ -628,11 +683,6 @@ internal sealed class UiHost : MonoBehaviour _mapCamera.targetTexture = _ownRT; _mapCamera.rect = new Rect(0f, 0f, 1f, 1f); - // Apply the theme's map camera background colour (visible as empty-space - // colour behind terrain/water). Safe to set anytime after camera takeover. - _mapCamera.backgroundColor = - MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); - Canvas.ForceUpdateCanvases(); PanelFinder.UpdateMapForZoom(); @@ -649,6 +699,13 @@ internal sealed class UiHost : MonoBehaviour Native.RRPOPOUT_SetMapRotation(_overlayHandle, 0f); Native.RRPOPOUT_SetMapSyncPlayer(_overlayHandle, false); Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false); + Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow); + Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter); + Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); + var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); + bgc.a *= PopoutModule.Settings.overlayMapBgAlpha; + _mapCamera!.backgroundColor = bgc; + PushMEState(); _mapActive = true; Log.Info("[ui] in-game map activated."); diff --git a/src/Modules/Popout/DetachedPanel.cs b/src/Modules/Popout/DetachedPanel.cs index e4da26b..4ee9905 100644 --- a/src/Modules/Popout/DetachedPanel.cs +++ b/src/Modules/Popout/DetachedPanel.cs @@ -54,7 +54,7 @@ namespace S3.Modules.Popout { // Follow player position (independent of MapEnhancer) private bool _followPlayer = false; - + private bool _meDirty = false; private const int kMaxInputEvents = 64; private static readonly InputEvent[] s_eventBuffer = new InputEvent[kMaxInputEvents]; @@ -71,6 +71,13 @@ namespace S3.Modules.Popout { _mapCamEulerZ = _mapCamera.transform.eulerAngles.z; Native.RRPOPOUT_SetMapEnhancerInstalled(_windowHandle, MapEnhancerBridge.IsInstalled); + Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow); + Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter); + Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); + var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); + bgc.a *= PopoutModule.Settings.overlayMapBgAlpha; + _mapCamera.backgroundColor = bgc; + if (MapEnhancerBridge.IsInstalled) PushMEState(); // Native loaded window_state.ini before showing the window (position/size/topmost // already applied). Read back the C#-side state and apply it here. @@ -159,6 +166,7 @@ namespace S3.Modules.Popout { int count = Native.RRPOPOUT_PollInputEvents(_windowHandle, s_eventBuffer, kMaxInputEvents); for (int i = 0; i < count; i++) ForwardInputEvent(s_eventBuffer[i]); + if (_meDirty) { PushMEState(); _meDirty = false; } // Hotkey mirror bool hotkeyNow = IsHotkeyDown(); @@ -167,6 +175,16 @@ namespace S3.Modules.Popout { } // --------------------------------------------------------------------------- + private void PushMEState() { + if (!MapEnhancerBridge.IsInstalled) return; + Native.RRPOPOUT_SetMEState(_windowHandle, + MapEnhancerBridge.GetMEBoolFlags(), + MapEnhancerBridge.GetMEFloat(0), + MapEnhancerBridge.GetMEFloat(1), + MapEnhancerBridge.GetMEFloat(2), + MapEnhancerBridge.GetMEFloat(3)); + } + private void ApplyMapRotation(float deg) { _mapRotationDeg = ((deg % 360f) + 360f) % 360f; if (_mapCamera != null) @@ -331,7 +349,8 @@ namespace S3.Modules.Popout { break; case InputEventType.RButtonUp: - UnityEngine.Object.FindObjectOfType()?.ClickLocateMe(); + if (PopoutModule.Settings.rightClickRecenter) + UnityEngine.Object.FindObjectOfType()?.ClickLocateMe(); break; case InputEventType.UICommand: @@ -380,8 +399,11 @@ namespace S3.Modules.Popout { break; case UICmd.SetTheme: MapThemes.Apply((MapTheme)(int)e.y); - if (_mapCamera != null) - _mapCamera.backgroundColor = MapThemes.GetMapBgColor((MapTheme)(int)e.y); + if (_mapCamera != null) { + var c = MapThemes.GetMapBgColor((MapTheme)(int)e.y); + c.a *= PopoutModule.Settings.overlayMapBgAlpha; + _mapCamera.backgroundColor = c; + } break; case UICmd.SetAlpha: float alpha = Mathf.Clamp(e.y, 0.1f, 1.0f); @@ -395,6 +417,41 @@ namespace S3.Modules.Popout { PopoutModule.Persist(); Native.RRPOPOUT_SetOverlayMapAlpha(mapAlpha); break; + case UICmd.SetMEBool: + MapEnhancerBridge.SetMEBool((int)e.y, e.delta != 0f); + if ((int)e.y == 6) _locationsSent = false; // EnableModdedSpawnPoints — re-push location list + _meDirty = true; + break; + case UICmd.SetMEFloat: + MapEnhancerBridge.SetMEFloat((int)e.y, e.delta); + _meDirty = true; + break; + case UICmd.MEResetSwitchesNormal: + MapEnhancerBridge.ResetAllSwitchesToNormal(); + break; + case UICmd.MEResetSwitchesThrown: + MapEnhancerBridge.ResetAllSwitchesToThrown(); + break; + case UICmd.TogglePanDisablesFollow: + PopoutModule.Settings.panDisablesFollow = !PopoutModule.Settings.panDisablesFollow; + PopoutModule.Persist(); + Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow); + break; + case UICmd.ToggleRightClickRecenter: + PopoutModule.Settings.rightClickRecenter = !PopoutModule.Settings.rightClickRecenter; + PopoutModule.Persist(); + Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter); + break; + case UICmd.SetMapBgAlpha: + float bgAlpha = Mathf.Clamp(e.y, 0f, 1f); + PopoutModule.Settings.overlayMapBgAlpha = bgAlpha; + PopoutModule.Persist(); + if (_mapCamera != null) { + var bgc2 = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); + bgc2.a *= bgAlpha; + _mapCamera.backgroundColor = bgc2; + } + break; } break; } diff --git a/src/Modules/Popout/MapEnhancerBridge.cs b/src/Modules/Popout/MapEnhancerBridge.cs index e3a1d14..c5615b2 100644 --- a/src/Modules/Popout/MapEnhancerBridge.cs +++ b/src/Modules/Popout/MapEnhancerBridge.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using Character; // CameraSelector using Game.State; // SpawnPoint using HarmonyLib; @@ -22,6 +23,25 @@ namespace S3.Modules.Popout { private static bool? _installed; private static MonoBehaviour? _instance; + private static object? _meSettings; + + // Ordered arrays — indices must match MEBoolBit / MEFloatIdx in shared_types.h. + private static readonly string[] s_boolFieldNames = { + "ShowTurntableMarkers", // 0 MB_TurntableMarkers + "EnableTurntableControl", // 1 MB_TurntableControl + "CheckTurntableClearance", // 2 MB_CheckClearance + "ShowRoadCrossingMarkers", // 3 MB_CrossingMarkers + "EnablePassengerStopTracking", // 4 MB_PassengerStops + "EnableIndustryAreaColors", // 5 MB_IndustryAreaColors + "EnableModdedSpawnPoints", // 6 MB_ModdedSpawnPoints + "DoubleClick", // 7 MB_DoubleClick + }; + private static readonly string[] s_floatFieldNames = { + "FlareScale", // 0 MF_FlareScale + "JunctionMarkerScale", // 1 MF_JunctionScale + "TrackLineThickness", // 2 MF_TrackThickness + "CrossingMarkerScale", // 3 MF_CrossingScale + }; // True if MapEnhancer is installed and active this session. public static bool IsInstalled { @@ -125,6 +145,90 @@ namespace S3.Modules.Popout { } catch { return Array.Empty(); } } + // --------------------------------------------------------------------------- + // MapEnhancer settings access (via Loader.Settings reflection + Traverse). + // All methods are no-ops when ME is not installed. + // --------------------------------------------------------------------------- + + public static bool GetMEBool(int idx) { + var s = GetMESettingsObj(); + if (s == null || idx < 0 || idx >= s_boolFieldNames.Length) return false; + try { return Traverse.Create(s).Field(s_boolFieldNames[idx]).Value; } + catch { return false; } + } + + public static void SetMEBool(int idx, bool val) { + var s = GetMESettingsObj(); + if (s == null || idx < 0 || idx >= s_boolFieldNames.Length) return; + try { + Traverse.Create(s).Field(s_boolFieldNames[idx]).SetValue(val); + // EnableTurntableControl (1) also enables ShowTurntableMarkers when turned on, + // matching MapEnhancer's own OnSettingsChanged logic. + if (idx == 1 && val) + Traverse.Create(s).Field("ShowTurntableMarkers").SetValue(true); + // EnableModdedSpawnPoints (6) must flush ME's cached spawn-point list + // so it re-scans mods when the map is next opened. + if (idx == 6) + ClearModdedSpawnPointsCache(); + ApplyAndSaveMESettings(); + } catch { } + } + + public static float GetMEFloat(int idx) { + var s = GetMESettingsObj(); + if (s == null || idx < 0 || idx >= s_floatFieldNames.Length) return 0f; + try { return Traverse.Create(s).Field(s_floatFieldNames[idx]).Value; } + catch { return 0f; } + } + + public static void SetMEFloat(int idx, float val) { + var s = GetMESettingsObj(); + if (s == null || idx < 0 || idx >= s_floatFieldNames.Length) return; + try { + Traverse.Create(s).Field(s_floatFieldNames[idx]).SetValue(val); + ApplyAndSaveMESettings(); + } catch { } + } + + // Pack all bool settings into a uint32 bitmask for the native atomic. + public static uint GetMEBoolFlags() { + uint flags = 0; + for (int i = 0; i < s_boolFieldNames.Length; i++) + if (GetMEBool(i)) flags |= (1u << i); + return flags; + } + + public static void ResetAllSwitchesToNormal() { + var me = GetInstance(); + if (me == null) return; + try { Traverse.Create(me).Method("ResetAllSwitchesToNormal").GetValue(); } catch { } + } + + public static void ResetAllSwitchesToThrown() { + var me = GetInstance(); + if (me == null) return; + try { Traverse.Create(me).Method("ResetAllSwitchesToThrown").GetValue(); } catch { } + } + + // Force ME to re-scan modded spawn points on next map open. + public static void ClearModdedSpawnPointsCache() { + var me = GetInstance(); + if (me == null) return; + try { Traverse.Create(me).Field("_modSpawnPointsLoaded").SetValue(false); } catch { } + } + + // Trigger ME's in-memory apply (OnChange → OnSettingsChanged) then persist to XML. + public static void ApplyAndSaveMESettings() { + var s = GetMESettingsObj(); + if (s == null) return; + try { + Traverse.Create(s).Method("OnChange").GetValue(); + var modEntry = FindMod("MapEnhancer"); + if (modEntry != null) + Traverse.Create(s).Method("Save", new object[] { modEntry }).GetValue(); + } catch { } + } + // --------------------------------------------------------------------------- private static Car[] GetLocosSorted() => TrainController.Shared?.Cars @@ -148,6 +252,17 @@ namespace S3.Modules.Popout { } catch { } } + private static object? GetMESettingsObj() { + if (_meSettings != null) return _meSettings; + if (!IsInstalled) return null; + var loaderType = Type.GetType("MapEnhancer.UMM.Loader, MapEnhancer"); + if (loaderType == null) return null; + var fi = loaderType.GetField("Settings", BindingFlags.Public | BindingFlags.Static); + if (fi == null) return null; + _meSettings = fi.GetValue(null); + return _meSettings; + } + private static UnityModManager.ModEntry? FindMod(string id) { foreach (var m in UnityModManager.modEntries) if (m.Info.Id == id) return m; diff --git a/src/Modules/Popout/NativeInterop.cs b/src/Modules/Popout/NativeInterop.cs index 6433cf7..b00dd73 100644 --- a/src/Modules/Popout/NativeInterop.cs +++ b/src/Modules/Popout/NativeInterop.cs @@ -38,6 +38,14 @@ namespace S3.Modules.Popout { 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] } // Must match MapThemeData in native/include/shared_types.h exactly (36 floats = 144 bytes). @@ -202,5 +210,27 @@ namespace S3.Modules.Popout { // 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 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); } } diff --git a/src/Modules/Popout/PopoutSettings.cs b/src/Modules/Popout/PopoutSettings.cs index a382650..9b3770a 100644 --- a/src/Modules/Popout/PopoutSettings.cs +++ b/src/Modules/Popout/PopoutSettings.cs @@ -31,6 +31,13 @@ public class PopoutSettings // In-game overlay map image alpha [0.0, 1.0]. Independent of chrome alpha. public float overlayMapAlpha = 1.0f; + // Map camera clear-colour opacity [0.0, 1.0]. At 0 the void/sky areas of the + // map are transparent, letting the game world show through in empty regions. + public float overlayMapBgAlpha = 1.0f; + + // When true, right-clicking anywhere on the map image recenters on the player. + public bool rightClickRecenter = true; + // Custom theme colors — edited live in the Settings color picker. // Initialized to S3 Dark so first-launch looks reasonable before the user tunes it. public MapThemeData customTheme = new MapThemeData {