Compare commits

..

No commits in common. "2343f05b1886562c073cdc318d2f411fe1cc61c8" and "c8918851aa07071ffaeae7159e96335d2921c89c" have entirely different histories.

20 changed files with 18 additions and 825 deletions

View file

@ -2,7 +2,7 @@
"Id": "S3", "Id": "S3",
"DisplayName": "S³ - Seton's Special Sauce", "DisplayName": "S³ - Seton's Special Sauce",
"Author": "seton", "Author": "seton",
"Version": "0.2.5", "Version": "0.2.4",
"ManagerVersion": "0.27.0", "ManagerVersion": "0.27.0",
"GameVersionPoint": "0", "GameVersionPoint": "0",
"AssemblyName": "S3.dll", "AssemblyName": "S3.dll",

View file

@ -29,9 +29,7 @@ Replaces the base-game map with a floating in-game overlay (toggle with **M** or
### Settings ### Settings
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), icon culling and EOTD settings, 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.
Most settings are also reachable directly from the gear menu on the map toolbar without opening UMM.
![Map Module settings panel](img/map/map_settings_umm.png) ![Map Module settings panel](img/map/map_settings_umm.png)
@ -66,18 +64,6 @@ Rotate the map manually with the rotation control, or lock it to your player cam
<video src="https://git.farmtowntech.com/setonc/railroader-setons-special-sauce/raw/branch/main/img/map/map_rotation/rotate_manual_and_player_locked.mp4" controls></video> <video src="https://git.farmtowntech.com/setonc/railroader-setons-special-sauce/raw/branch/main/img/map/map_rotation/rotate_manual_and_player_locked.mp4" controls></video>
### Icon Controls
Each car icon on the map is a live Canvas element. On a large session with many cars visible, the rendering cost adds up - testing showed a **3-12 FPS improvement** when zoomed out over a busy portion of the map with culling enabled.
When zoomed out past a configurable threshold (default 40%), car icons fade out and only locomotive icons remain. Locomotives scale up automatically so they stay easy to spot. A separate option hides MU trailing units, keeping just the lead locomotive per consist. A blinking red EOTD dot marks the rear of each consist so you can still gauge train length at a glance.
All of these options are available from the gear menu Icons submenu on the map toolbar, or from the S³ UMM settings page.
<video src="https://git.farmtowntech.com/setonc/railroader-setons-special-sauce/raw/branch/main/img/map/map_optimizations/hiding_cars_and_eotd_demo.mp4" controls></video>
![Icon controls settings](img/map/map_icons_config.png)
### Popout Window ### Popout Window
Pop the map into a detached native OS window. Drag it to any monitor, resize it freely, and pin it always-on-top via the window's right-click title bar menu. Re-attach it back into the game overlay at any time from the settings panel without losing your position, zoom, or rotation. Pop the map into a detached native OS window. Drag it to any monitor, resize it freely, and pin it always-on-top via the window's right-click title bar menu. Re-attach it back into the game overlay at any time from the settings panel without losing your position, zoom, or rotation.

View file

@ -84,11 +84,5 @@ function New-ModLayout {
Copy-Item $ManagedDll (Join-Path $DestModDir "S3.dll") -Force Copy-Item $ManagedDll (Join-Path $DestModDir "S3.dll") -Force
if ($NativeDll) { if ($NativeDll) {
Copy-Item $NativeDll (Join-Path $DestModDir "S3Native.dll") -Force Copy-Item $NativeDll (Join-Path $DestModDir "S3Native.dll") -Force
# Ship ProggyClean.ttf alongside the native DLL so IMGUI_DISABLE_DEFAULT_FONT
# can load it at runtime without the base85 blob in the binary.
$fontSrc = Join-Path $RepoRoot "native\fonts\ProggyClean.ttf"
if (Test-Path $fontSrc) {
Copy-Item $fontSrc (Join-Path $DestModDir "ProggyClean.ttf") -Force
}
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

View file

@ -31,11 +31,6 @@ target_include_directories(imgui PUBLIC
# imgui_impl_dx11.cpp calls D3DCompile at runtime to build its own shaders. # imgui_impl_dx11.cpp calls D3DCompile at runtime to build its own shaders.
target_link_libraries(imgui PUBLIC d3d11 d3dcompiler) target_link_libraries(imgui PUBLIC d3d11 d3dcompiler)
# Remove the built-in ProggyClean base85 blob. That blob incidentally contains
# the 3-char string "UPX" which triggers AV false-positives on every release zip.
# We ship ProggyClean.ttf as a standalone file and load it at runtime instead.
target_compile_definitions(imgui PUBLIC IMGUI_DISABLE_DEFAULT_FONT)
# Suppress MSVC warnings inside third-party ImGui source. # Suppress MSVC warnings inside third-party ImGui source.
if(MSVC) if(MSVC)
target_compile_options(imgui PRIVATE /W0) target_compile_options(imgui PRIVATE /W0)

Binary file not shown.

View file

@ -41,14 +41,6 @@ enum UICmd : int32_t {
TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode
ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player
SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0] SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0]
// Icon culling + EOTD (gear menu Icons submenu)
ToggleIconCulling = 23, // toggle car-icon culling
SetCullThreshold = 24, // set cull zoom threshold; y = [0.3, 1.0]
ToggleHideMuLocos = 25, // toggle hiding MU trailing units
SetLocoBoostedScale = 26, // set loco icon boost when culling; y = [1.0, 3.0]
ToggleEotd = 27, // toggle EOTD dot visibility
ToggleEotdOnlyWhenCulled = 28, // toggle hiding EOTD when car icons are visible
SetEotdSizeScale = 29, // set EOTD dot size; y = [1.0, 10.0]
}; };
// Bit indices for ME bool settings packed into PopoutWindow::imMEFlags. // Bit indices for ME bool settings packed into PopoutWindow::imMEFlags.

View file

@ -276,29 +276,9 @@ static void InitImGui(ID3D11Device* device, ID3D11DeviceContext* context) {
{ std::lock_guard<std::mutex> lk(g_themeMutex); if (g_theme.accA == 0.f) g_theme = kS3DarkTheme; } { std::lock_guard<std::mutex> lk(g_themeMutex); if (g_theme.accA == 0.f) g_theme = kS3DarkTheme; }
ApplyThemeToStyle(g_theme); ApplyThemeToStyle(g_theme);
// Locate ProggyClean.ttf relative to this DLL. IMGUI_DISABLE_DEFAULT_FONT strips // Fonts: default (ASCII) + merge the ⚙ gear glyph from Segoe UI Symbol.
// the built-in base85 blob from the binary (that blob incidentally contains "UPX", // If seguisym.ttf is absent the button renders a box — fully functional fallback.
// which AV scanners mistake for a UPX-packed executable). We ship the TTF file io.Fonts->AddFontDefault();
// alongside the DLL so end users see identical visual output.
{
char fontPath[MAX_PATH] = {};
HMODULE self = GetModuleHandleA("S3Native.dll");
if (self && GetModuleFileNameA(self, fontPath, MAX_PATH)) {
char* slash = strrchr(fontPath, '\\');
if (slash) { *++slash = '\0'; strcat_s(fontPath, "ProggyClean.ttf"); }
}
// Fallback chain: mod dir → Consolas → Lucida Console.
// At least one must succeed or ImGui renders blank glyphs (no crash).
static const char* kFallbacks[] = {
"C:\\Windows\\Fonts\\consola.ttf",
"C:\\Windows\\Fonts\\lucon.ttf",
nullptr
};
if (!fontPath[0] || !io.Fonts->AddFontFromFileTTF(fontPath, 13.f)) {
for (const char** fb = kFallbacks; *fb; ++fb)
if (io.Fonts->AddFontFromFileTTF(*fb, 13.f)) break;
}
}
{ {
ImFontConfig fc; ImFontConfig fc;
fc.MergeMode = true; fc.MergeMode = true;
@ -671,69 +651,6 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginMenu("Icons")) {
bool cullOn = win->imIconCullingEnabled.load();
if (ImGui::MenuItem("Hide cars when zoomed out", nullptr, cullOn)) {
win->imIconCullingEnabled.store(!cullOn);
pushCmd(UICmd::ToggleIconCulling);
}
ImGui::BeginDisabled(!cullOn);
// Display as percentage (0.40 → 40%); store as fraction.
float threshPct = win->imCullThreshold.load() * 100.f;
ImGui::SetNextItemWidth(100.f);
if (ImGui::SliderFloat("Cull threshold##icons", &threshPct, 30.f, 100.f, "%.0f%%"))
win->imCullThreshold.store(threshPct / 100.f);
if (ImGui::IsItemDeactivatedAfterEdit())
pushCmd(UICmd::SetCullThreshold, win->imCullThreshold.load());
ImGui::Separator();
bool hideMU = win->imHideMuLocos.load();
if (ImGui::MenuItem("Hide MU trailing units", nullptr, hideMU)) {
win->imHideMuLocos.store(!hideMU);
pushCmd(UICmd::ToggleHideMuLocos);
}
float boost = win->imLocoBoostedScale.load();
ImGui::SetNextItemWidth(100.f);
if (ImGui::SliderFloat("Loco icon boost##icons", &boost, 1.0f, 3.0f, "%.1fx"))
win->imLocoBoostedScale.store(boost);
if (ImGui::IsItemDeactivatedAfterEdit())
pushCmd(UICmd::SetLocoBoostedScale, win->imLocoBoostedScale.load());
ImGui::Separator();
bool eotdOn = win->imEotdEnabled.load();
if (ImGui::MenuItem("Show EOTD", nullptr, eotdOn)) {
win->imEotdEnabled.store(!eotdOn);
pushCmd(UICmd::ToggleEotd);
}
ImGui::BeginDisabled(!eotdOn);
bool eotdWC = win->imEotdOnlyWhenCulled.load();
if (ImGui::MenuItem("Hide EOTD when icons visible", nullptr, eotdWC)) {
win->imEotdOnlyWhenCulled.store(!eotdWC);
pushCmd(UICmd::ToggleEotdOnlyWhenCulled);
}
float eotdSz = win->imEotdSizeScale.load();
ImGui::SetNextItemWidth(100.f);
if (ImGui::SliderFloat("EOTD dot size##icons", &eotdSz, 1.0f, 10.0f, "%.1f"))
win->imEotdSizeScale.store(eotdSz);
if (ImGui::IsItemDeactivatedAfterEdit())
pushCmd(UICmd::SetEotdSizeScale, win->imEotdSizeScale.load());
ImGui::EndDisabled(); // !eotdOn
ImGui::EndDisabled(); // !cullOn
ImGui::EndMenu();
}
ImGui::Separator();
// Helper lambdas for ME setting commands (y = index, delta = value) // Helper lambdas for ME setting commands (y = index, delta = value)
auto pushMEBool = [&](int idx, bool val) { auto pushMEBool = [&](int idx, bool val) {
InputEvent ev{}; ev.type = UICommand; InputEvent ev{}; ev.type = UICommand;

View file

@ -234,24 +234,6 @@ void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active) {
if (win) win->imRightClickRecenter.store(active); 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). // Seed the map camera clear-colour opacity slider (shared global, affects both surfaces).
extern "C" __declspec(dllexport) extern "C" __declspec(dllexport)
void RRPOPOUT_SetOverlayMapBgAlpha(float alpha) { void RRPOPOUT_SetOverlayMapBgAlpha(float alpha) {

View file

@ -82,17 +82,6 @@ struct PopoutWindow {
std::atomic<bool> imPanDisablesFollow {true}; std::atomic<bool> imPanDisablesFollow {true};
std::atomic<bool> imRightClickRecenter {true}; std::atomic<bool> imRightClickRecenter {true};
// -----------------------------------------------------------------------
// Icon culling + EOTD settings (C# → render thread via RRPOPOUT_SetIconCullingState)
// -----------------------------------------------------------------------
std::atomic<bool> imIconCullingEnabled {true};
std::atomic<float> imCullThreshold {0.40f};
std::atomic<bool> imHideMuLocos {true};
std::atomic<float> imLocoBoostedScale {1.5f};
std::atomic<bool> imEotdEnabled {true};
std::atomic<bool> imEotdOnlyWhenCulled {true};
std::atomic<float> imEotdSizeScale {8.0f};
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// MapEnhancer settings state (C# → render thread via RRPOPOUT_SetMEState) // MapEnhancer settings state (C# → render thread via RRPOPOUT_SetMEState)
// Bit layout of imMEFlags matches MEBoolBit enum in shared_types.h. // Bit layout of imMEFlags matches MEBoolBit enum in shared_types.h.

View file

@ -469,7 +469,7 @@ internal sealed class UiHost : MonoBehaviour
switch ((UICmd)(int)e.x) switch ((UICmd)(int)e.x)
{ {
case UICmd.Recenter: case UICmd.Recenter:
PanelFinder.GetMapWindow()?.ClickLocateMe(); UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
break; break;
case UICmd.FollowMode: MapEnhancerBridge.ToggleFollowMode(); break; case UICmd.FollowMode: MapEnhancerBridge.ToggleFollowMode(); break;
case UICmd.FollowPlayerCam: MapEnhancerBridge.FollowPlayerCamera(); break; case UICmd.FollowPlayerCam: MapEnhancerBridge.FollowPlayerCamera(); break;
@ -568,34 +568,6 @@ internal sealed class UiHost : MonoBehaviour
_mapCamera.backgroundColor = bgc2; _mapCamera.backgroundColor = bgc2;
} }
break; break;
case UICmd.ToggleIconCulling:
PopoutModule.Settings.iconCullingEnabled = !PopoutModule.Settings.iconCullingEnabled;
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.SetCullThreshold:
PopoutModule.Settings.iconCullingThreshold = Mathf.Clamp(e.y, 0.3f, 1.0f);
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.ToggleHideMuLocos:
PopoutModule.Settings.hideMuLocos = !PopoutModule.Settings.hideMuLocos;
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.SetLocoBoostedScale:
PopoutModule.Settings.locoBoostedScale = Mathf.Clamp(e.y, 1.0f, 3.0f);
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.ToggleEotd:
PopoutModule.Settings.eotdEnabled = !PopoutModule.Settings.eotdEnabled;
PopoutModule.Persist();
break;
case UICmd.ToggleEotdOnlyWhenCulled:
PopoutModule.Settings.eotdOnlyWhenCulled = !PopoutModule.Settings.eotdOnlyWhenCulled;
PopoutModule.Persist();
break;
case UICmd.SetEotdSizeScale:
PopoutModule.Settings.eotdSizeScale = Mathf.Clamp(e.y, 1.0f, 10.0f);
PopoutModule.Persist();
break;
} }
} }
@ -611,7 +583,7 @@ internal sealed class UiHost : MonoBehaviour
{ {
case InputEventType.RButtonUp: case InputEventType.RButtonUp:
if (PopoutModule.Settings.rightClickRecenter) if (PopoutModule.Settings.rightClickRecenter)
PanelFinder.GetMapWindow()?.ClickLocateMe(); UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
break; break;
case InputEventType.MouseWheel: case InputEventType.MouseWheel:
@ -730,7 +702,6 @@ internal sealed class UiHost : MonoBehaviour
Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false); Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false);
Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow); Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow);
Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter); Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter);
SeedIconCullingState(_overlayHandle);
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);
var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
bgc.a *= PopoutModule.Settings.overlayMapBgAlpha; bgc.a *= PopoutModule.Settings.overlayMapBgAlpha;
@ -792,12 +763,4 @@ internal sealed class UiHost : MonoBehaviour
_drag = false; _drag = false;
_mapActive = false; _mapActive = false;
} }
private static void SeedIconCullingState(int handle)
{
var s = PopoutModule.Settings;
Native.RRPOPOUT_SetIconCullingState(handle,
s.iconCullingEnabled, s.iconCullingThreshold, s.hideMuLocos, s.locoBoostedScale,
s.eotdEnabled, s.eotdOnlyWhenCulled, s.eotdSizeScale);
}
} }

View file

@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
using S3.Core; // Log using S3.Core; // Log
using UI.Map; using UI.Map;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering;
namespace S3.Modules.Popout { namespace S3.Modules.Popout {
@ -73,7 +74,6 @@ namespace S3.Modules.Popout {
Native.RRPOPOUT_SetMapEnhancerCaps(_windowHandle, MapEnhancerBridge.HasTurntable, MapEnhancerBridge.IsCommunity); Native.RRPOPOUT_SetMapEnhancerCaps(_windowHandle, MapEnhancerBridge.HasTurntable, MapEnhancerBridge.IsCommunity);
Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow); Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow);
Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter); Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter);
SeedIconCullingState(_windowHandle);
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);
var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme); var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
bgc.a *= PopoutModule.Settings.overlayMapBgAlpha; bgc.a *= PopoutModule.Settings.overlayMapBgAlpha;
@ -121,6 +121,10 @@ namespace S3.Modules.Popout {
_mapCamera.rect = new Rect(0f, 0f, 1f, 1f); _mapCamera.rect = new Rect(0f, 0f, 1f, 1f);
_mapCamera.aspect = (float)w / h; _mapCamera.aspect = (float)w / h;
var inGameRT = PanelFinder.GetMapRenderTexture();
if (inGameRT != null && inGameRT != _ownRT)
Graphics.Blit(_ownRT, inGameRT);
Native.RRPOPOUT_SetFrameTexture(_windowHandle, Native.RRPOPOUT_SetFrameTexture(_windowHandle,
_ownRT.GetNativeTexturePtr(), 0f, 1f, 1f, 0f); _ownRT.GetNativeTexturePtr(), 0f, 1f, 1f, 0f);
GL.IssuePluginEvent(_renderEventFunc, _windowHandle); GL.IssuePluginEvent(_renderEventFunc, _windowHandle);
@ -347,13 +351,13 @@ namespace S3.Modules.Popout {
case InputEventType.RButtonUp: case InputEventType.RButtonUp:
if (PopoutModule.Settings.rightClickRecenter) if (PopoutModule.Settings.rightClickRecenter)
PanelFinder.GetMapWindow()?.ClickLocateMe(); UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
break; break;
case InputEventType.UICommand: case InputEventType.UICommand:
switch ((UICmd)(int)e.x) { switch ((UICmd)(int)e.x) {
case UICmd.Recenter: case UICmd.Recenter:
PanelFinder.GetMapWindow()?.ClickLocateMe(); UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
break; break;
case UICmd.FollowMode: case UICmd.FollowMode:
MapEnhancerBridge.ToggleFollowMode(); MapEnhancerBridge.ToggleFollowMode();
@ -449,45 +453,9 @@ namespace S3.Modules.Popout {
_mapCamera.backgroundColor = bgc2; _mapCamera.backgroundColor = bgc2;
} }
break; break;
case UICmd.ToggleIconCulling:
PopoutModule.Settings.iconCullingEnabled = !PopoutModule.Settings.iconCullingEnabled;
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.SetCullThreshold:
PopoutModule.Settings.iconCullingThreshold = Mathf.Clamp(e.y, 0.3f, 1.0f);
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.ToggleHideMuLocos:
PopoutModule.Settings.hideMuLocos = !PopoutModule.Settings.hideMuLocos;
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.SetLocoBoostedScale:
PopoutModule.Settings.locoBoostedScale = Mathf.Clamp(e.y, 1.0f, 3.0f);
PopoutModule.Persist(); MapIconCuller.Refresh();
break;
case UICmd.ToggleEotd:
PopoutModule.Settings.eotdEnabled = !PopoutModule.Settings.eotdEnabled;
PopoutModule.Persist();
break;
case UICmd.ToggleEotdOnlyWhenCulled:
PopoutModule.Settings.eotdOnlyWhenCulled = !PopoutModule.Settings.eotdOnlyWhenCulled;
PopoutModule.Persist();
break;
case UICmd.SetEotdSizeScale:
PopoutModule.Settings.eotdSizeScale = Mathf.Clamp(e.y, 1.0f, 10.0f);
PopoutModule.Persist();
break;
} }
break; break;
} }
} }
private static void SeedIconCullingState(int handle)
{
var s = PopoutModule.Settings;
Native.RRPOPOUT_SetIconCullingState(handle,
s.iconCullingEnabled, s.iconCullingThreshold, s.hideMuLocos, s.locoBoostedScale,
s.eotdEnabled, s.eotdOnlyWhenCulled, s.eotdSizeScale);
}
} }
} }

View file

@ -1,261 +0,0 @@
using System.Collections.Generic;
using HarmonyLib;
using Helpers;
using Model;
using Track;
using UI.Map;
using UnityEngine;
using UnityEngine.UI;
namespace S3.Modules.Popout;
// Places a blinking red dot on the map at the rear end of each active train consist.
// The dot is a cloned car MapIcon (red Image, no label) parented to a DontDestroyOnLoad
// holder — NOT to the car's own MapIcon Canvas — so culling the car icons never hides it.
//
// Consists are rebuilt every kRebuildInterval seconds. A consist qualifies when it has a
// lead locomotive (no loco on A-end) with at least one car trailing on the A-end chain.
//
// EnumerateCoupled(A) from the lead loco walks FORWARD through the consist (lead → rear),
// ending at the last car. EnumerateCoupled(B) walks in reverse (rear → lead). We always
// use the A-end chain and take the final element as the EOTD placement car.
internal static class EotdSystem
{
private static GameObject? _holder;
private static readonly Dictionary<Car, EotdMarker> _markers = new();
private static float _rebuildTimer;
private const float kRebuildInterval = 3f;
public static void Install()
{
if (_holder != null) return;
_holder = new GameObject("S3.EOTD.Holder");
Object.DontDestroyOnLoad(_holder);
_rebuildTimer = 0f;
}
public static void Uninstall()
{
ClearAll();
if (_holder != null) { Object.Destroy(_holder); _holder = null; }
}
public static void Tick(float dt)
{
if (_holder == null) return;
if (!PopoutModule.Settings.eotdEnabled) { ClearAll(); return; }
_rebuildTimer -= dt;
if (_rebuildTimer <= 0f) { _rebuildTimer = kRebuildInterval; Rebuild(); }
}
// ---------------------------------------------------------------------------
private static void Rebuild()
{
if (_holder == null) return;
var tc = TrainController.Shared;
if (tc == null) { ClearAll(); return; }
// Identify the current set of rear cars (one per consist with a lead loco).
var rearCars = new HashSet<Car>();
foreach (Car car in tc.Cars)
{
if (!car.IsLocomotive || !MapIconCuller.IsLeadLoco(car)) continue;
var rear = FindRearCar(car);
if (rear != null) rearCars.Add(rear);
}
// Remove markers whose rear car is no longer valid.
var stale = new List<Car>();
foreach (var kv in _markers)
{
if (!rearCars.Contains(kv.Key))
{
if (kv.Value != null) Object.Destroy(kv.Value.gameObject);
stale.Add(kv.Key);
}
}
foreach (var k in stale) _markers.Remove(k);
int mapLayer = GetMapLayer(tc);
// Create markers for newly discovered rear cars.
foreach (Car rear in rearCars)
{
if (_markers.ContainsKey(rear)) continue;
var template = GetAnyCarIcon(tc);
if (template == null) continue;
var go = Object.Instantiate(template.gameObject, _holder!.transform);
go.name = "S3_EOTD_Marker";
SetLayerRecursive(go, mapLayer);
// Strip all children (car-body shape, text labels).
var children = new System.Collections.Generic.List<Transform>();
foreach (Transform child in go.transform) children.Add(child);
foreach (var child in children) Object.Destroy(child.gameObject);
// Destroy MapIcon so MapBuilder.SetZoom() never overrides our manually-set
// localScale. MapIcon.OnDisable() removes us from _mapIcons cleanly.
Object.Destroy(go.GetComponent<MapIcon>());
// sizeDelta=(1,1) — scale is set each frame in EotdMarker.Update() as a
// fraction of the map camera's orthographicSize, giving a stable ~2%
// screen-height dot at any zoom level.
var dotGo = new GameObject("dot");
dotGo.layer = mapLayer;
dotGo.transform.SetParent(go.transform, false);
var img = dotGo.AddComponent<Image>();
img.color = Color.red;
img.sprite = MakeCircleSprite(32);
img.type = Image.Type.Simple;
var dotRt = dotGo.GetComponent<RectTransform>();
dotRt.sizeDelta = new Vector2(1f, 1f);
dotRt.anchoredPosition = Vector2.zero;
var marker = go.AddComponent<EotdMarker>();
marker.Init(rear, img);
_markers[rear] = marker;
}
}
private static void ClearAll()
{
foreach (var kv in _markers)
if (kv.Value != null) Object.Destroy(kv.Value.gameObject);
_markers.Clear();
}
// EnumerateCoupled(A) starts at the lead loco and walks forward through the consist.
// The last element is the rear car. Returns null if nothing is coupled (solo loco).
private static Car? FindRearCar(Car leadLoco)
{
Car last = leadLoco;
try
{
foreach (Car c in leadLoco.EnumerateCoupled(Car.LogicalEnd.A))
last = c;
}
catch { }
return last == leadLoco ? null : last;
}
private static MapIcon? GetAnyCarIcon(TrainController tc)
{
foreach (Car car in tc.Cars)
{
if (car.IsLocomotive) continue;
var icon = Traverse.Create(car).Field<MapIcon>("MapIcon").Value;
if (icon != null) return icon;
}
foreach (Car car in tc.Cars)
{
var icon = Traverse.Create(car).Field<MapIcon>("MapIcon").Value;
if (icon != null) return icon;
}
return null;
}
private static int GetMapLayer(TrainController tc)
{
foreach (Car car in tc.Cars)
{
var icon = Traverse.Create(car).Field<MapIcon>("MapIcon").Value;
if (icon != null) return icon.gameObject.layer;
}
return LayerMask.NameToLayer("Map");
}
private static void SetLayerRecursive(GameObject go, int layer)
{
go.layer = layer;
foreach (Transform child in go.transform)
SetLayerRecursive(child.gameObject, layer);
}
// Generates a white anti-aliased circle sprite at runtime.
// Used instead of Resources.GetBuiltinResource<Sprite>("UI/Skin/Knob.psd") which
// is not available in stripped Unity builds.
private static Sprite MakeCircleSprite(int radius)
{
int size = radius * 2;
var tex = new Texture2D(size, size, TextureFormat.RGBA32, mipChain: false);
tex.filterMode = FilterMode.Bilinear;
var pixels = new Color32[size * size];
float c = radius - 0.5f;
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
float dist = Mathf.Sqrt((x - c) * (x - c) + (y - c) * (y - c));
byte a = (byte)(Mathf.Clamp01(radius - dist) * 255f);
pixels[y * size + x] = new Color32(255, 255, 255, a);
}
tex.SetPixels32(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
}
// Attached to each EOTD marker GameObject. Tracks the rear car's world position each
// frame and animates a 1.2 Hz sine-wave pulse on the red Image alpha.
internal class EotdMarker : MonoBehaviour
{
private Car? _car;
private Image? _img;
private Canvas? _canvas;
public void Init(Car rearCar, Image dot)
{
_car = rearCar;
_img = dot;
_canvas = GetComponent<Canvas>();
}
private void Update()
{
if (_car == null) { Destroy(gameObject); return; }
var s = PopoutModule.Settings;
// Optionally hide the dot when car icons are visible (zoom inside cull threshold).
bool show = true;
if (s.eotdOnlyWhenCulled && s.iconCullingEnabled)
{
var mb2 = MapBuilder.Shared;
if (mb2 != null)
{
float zoom = Traverse.Create(mb2).Property<float>("NormalizedScale").Value;
show = zoom >= s.iconCullingThreshold;
}
}
if (_canvas != null) _canvas.enabled = show;
if (!show) return;
try
{
var worldPos = WorldTransformer.GameToWorld(_car.GetCenterPosition(Graph.Shared));
worldPos.y += 3200f;
// Euler(-90, 0, 0) lays the canvas flat under the overhead map camera.
transform.SetPositionAndRotation(worldPos, Quaternion.Euler(-90f, 0f, 0f));
// Scale = orthographicSize * (eotdSizeScale * 0.002).
// eotdSizeScale ranges 110; at 3 (default) and orthoSize ~4000 this gives
// a dot ~24 world units ≈ about 3-4× the track-line width.
var mb = MapBuilder.Shared;
if (mb?.mapCamera != null)
transform.localScale = Vector3.one * (mb.mapCamera.orthographicSize * s.eotdSizeScale * 0.002f);
}
catch { }
if (_img != null)
{
float t = (Mathf.Sin(Time.time * Mathf.PI * 2.4f) + 1f) * 0.5f;
var colour = _img.color;
colour.a = t;
_img.color = colour;
}
}
}

View file

@ -1,208 +0,0 @@
using System.Collections.Generic;
using HarmonyLib;
using Model;
using UI.Map;
using UnityEngine;
using UnityEngine.UI;
namespace S3.Modules.Popout;
// Harmony patches on MapBuilder.UpdateForZoom (postfix) and MapBuilder.Add (postfix)
// that cull non-locomotive car icons when the map is zoomed far out, and optionally
// boost visible loco icon scale so they remain readable.
//
// Two patches are needed:
// UpdateForZoom postfix — handles the steady-state case (zoom scroll events).
// Add postfix — catches icons registered AFTER UpdateForZoom runs during a
// map rebuild, closing the 1-frame flash window.
//
// Fade: CanvasGroup.alpha is lerped to 0/1 each frame via TickFade(). The canvas is
// only disabled once alpha reaches 0 so the GPU stops work at the same moment the
// visual disappears. Canvas.enabled is re-set to true before fading in.
//
// Access notes (live game DLL):
// MapBuilder.NormalizedScale / IconScale — private, accessed via Traverse
// Car.MapIcon — protected field, accessed via Traverse
// Car.LogicalEnd — nested enum inside Car class
internal static class MapIconCuller
{
private static Harmony? _harmony;
// Target alpha for fading icons. Populated by SetVisible; drained by TickFade.
private static readonly Dictionary<MapIcon, float> _fadeTargets = new();
private const float kFadeSpeed = 3f; // alpha units/second → full fade in ~0.33 s
public static void Install()
{
if (_harmony != null) return;
_harmony = new Harmony("S3.popout.culler");
_harmony.CreateClassProcessor(typeof(MapBuilder_UpdateForZoom_Patch)).Patch();
_harmony.CreateClassProcessor(typeof(MapBuilder_Add_Patch)).Patch();
}
public static void Uninstall()
{
if (_harmony == null) return;
RestoreAll();
_harmony.UnpatchAll("S3.popout.culler");
_harmony = null;
_fadeTargets.Clear();
}
public static void Refresh()
{
var mb = MapBuilder.Shared;
if (mb != null) Apply(mb);
}
// Called each frame by PopoutModule.Tick() to drive icon fade animations.
public static void TickFade(float dt)
{
if (_fadeTargets.Count == 0) return;
var done = new List<MapIcon>();
foreach (var kv in _fadeTargets)
{
var icon = kv.Key;
float target = kv.Value;
if (icon == null) { done.Add(icon); continue; }
var cg = icon.GetComponent<CanvasGroup>();
if (cg == null) { done.Add(icon); continue; }
float next = Mathf.MoveTowards(cg.alpha, target, kFadeSpeed * dt);
cg.alpha = next;
if (Mathf.Approximately(next, target))
{
if (target < 0.5f)
icon.GetComponent<Canvas>().enabled = false;
done.Add(icon);
}
}
foreach (var icon in done) _fadeTargets.Remove(icon);
}
// ---------------------------------------------------------------------------
internal static void Apply(MapBuilder mb)
{
var s = PopoutModule.Settings;
if (!s.iconCullingEnabled) { RestoreAll(); return; }
float zoom = Traverse.Create(mb).Property<float>("NormalizedScale").Value;
bool culling = zoom >= s.iconCullingThreshold;
float iconScale = Traverse.Create(mb).Property<float>("IconScale").Value;
var tc = TrainController.Shared;
if (tc == null) return;
foreach (Car car in tc.Cars)
{
var icon = Traverse.Create(car).Field<MapIcon>("MapIcon").Value;
if (icon == null) continue;
if (car.IsLocomotive)
{
bool show = !culling || !s.hideMuLocos || IsLeadLoco(car);
SetVisible(icon, show);
if (show && culling)
icon.SetZoom(iconScale * s.locoBoostedScale);
}
else
{
SetVisible(icon, !culling);
}
}
}
private static void RestoreAll()
{
var tc = TrainController.Shared;
if (tc == null) return;
foreach (Car car in tc.Cars)
{
var icon = Traverse.Create(car).Field<MapIcon>("MapIcon").Value;
if (icon != null) SetVisible(icon, true);
}
}
private static void SetVisible(MapIcon icon, bool visible)
{
var canvas = icon.GetComponent<Canvas>();
if (canvas == null) return;
var cg = icon.GetComponent<CanvasGroup>();
if (cg == null) cg = icon.gameObject.AddComponent<CanvasGroup>();
if (visible)
{
canvas.enabled = true; // must re-enable before fading in
_fadeTargets[icon] = 1f;
}
else if (canvas.enabled)
{
// Only start a fade-out if the canvas is currently rendering.
_fadeTargets[icon] = 0f;
}
}
// A loco is the lead of its consist when nothing (or a non-loco) is on its A-end.
internal static bool IsLeadLoco(Car loco)
{
try
{
var atA = loco.CoupledTo(Car.LogicalEnd.A);
return atA == null || !atA.IsLocomotive;
}
catch
{
return true;
}
}
// ---------------------------------------------------------------------------
[HarmonyPatch(typeof(MapBuilder), "UpdateForZoom")]
private static class MapBuilder_UpdateForZoom_Patch
{
private static void Postfix(MapBuilder __instance) => Apply(__instance);
}
// Catches icons added to _mapIcons AFTER UpdateForZoom runs during a map rebuild.
// Car MapIcons are parented to car.transform; non-car icons (junctions, stations)
// have no Car component on their parent — those are left alone.
[HarmonyPatch(typeof(MapBuilder), nameof(MapBuilder.Add))]
private static class MapBuilder_Add_Patch
{
private static void Postfix(MapIcon icon)
{
var s = PopoutModule.Settings;
if (!s.iconCullingEnabled) return;
var mb = MapBuilder.Shared;
if (mb == null) return;
float zoom = Traverse.Create(mb).Property<float>("NormalizedScale").Value;
if (zoom < s.iconCullingThreshold) return;
// S3 EOTD clones and junction/station icons have no Car on their parent.
var car = icon.transform.parent?.GetComponent<Car>();
if (car == null) return;
float iconScale = Traverse.Create(mb).Property<float>("IconScale").Value;
if (car.IsLocomotive)
{
bool show = !s.hideMuLocos || IsLeadLoco(car);
SetVisible(icon, show);
if (show) icon.SetZoom(iconScale * s.locoBoostedScale);
}
else
{
SetVisible(icon, false);
}
}
}
}

View file

@ -46,14 +46,6 @@ namespace S3.Modules.Popout {
TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode
ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player
SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0] 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]
} }
// Must match MapThemeData in native/include/shared_types.h exactly (36 floats = 144 bytes). // Must match MapThemeData in native/include/shared_types.h exactly (36 floats = 144 bytes).
@ -233,12 +225,6 @@ namespace S3.Modules.Popout {
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active); 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). // Seed the map camera clear-colour opacity slider (shared global, overlay + popout).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetOverlayMapBgAlpha(float alpha); public static extern void RRPOPOUT_SetOverlayMapBgAlpha(float alpha);

View file

@ -16,16 +16,9 @@ namespace S3.Modules.Popout {
internal static class PanelFinder { internal static class PanelFinder {
private static MapWindow? _cachedMapWindow;
// Returns the live MapWindow MonoBehaviour, or null if the map isn't loaded. // Returns the live MapWindow MonoBehaviour, or null if the map isn't loaded.
// Cached after first find; Unity's == null catches destroyed instances and public static MapWindow? GetMapWindow() =>
// triggers a fresh search on the next call. Object.FindObjectOfType<MapWindow>();
public static MapWindow? GetMapWindow() {
if (_cachedMapWindow == null)
_cachedMapWindow = Object.FindObjectOfType<MapWindow>();
return _cachedMapWindow;
}
// Returns the RenderTexture the map camera renders to each frame. // Returns the RenderTexture the map camera renders to each frame.
public static RenderTexture? GetMapRenderTexture() { public static RenderTexture? GetMapRenderTexture() {

View file

@ -81,9 +81,6 @@ public sealed class PopoutModule : IModule
return; return;
} }
MapIconCuller.Install();
EotdSystem.Install();
_host = new GameObject("S3.Popout.Host"); _host = new GameObject("S3.Popout.Host");
Object.DontDestroyOnLoad(_host); Object.DontDestroyOnLoad(_host);
_host.AddComponent<PopoutHost>(); _host.AddComponent<PopoutHost>();
@ -95,8 +92,6 @@ public sealed class PopoutModule : IModule
if (_activePanel != null) CloseActivePanel("[popout] Module disabled."); if (_activePanel != null) CloseActivePanel("[popout] Module disabled.");
_pendingOpen = false; _pendingOpen = false;
_pendingOverlayOpen = false; _pendingOverlayOpen = false;
MapIconCuller.Uninstall();
EotdSystem.Uninstall();
if (_host != null) { Object.Destroy(_host); _host = null; } if (_host != null) { Object.Destroy(_host); _host = null; }
} }
@ -176,10 +171,6 @@ public sealed class PopoutModule : IModule
_pendingRestoreWindow = null; _pendingRestoreWindow = null;
} }
float dt = UnityEngine.Time.deltaTime;
MapIconCuller.TickFade(dt);
EotdSystem.Tick(dt);
if (_hotkey.Down()) if (_hotkey.Down())
Toggle(); Toggle();

View file

@ -38,22 +38,6 @@ public class PopoutSettings
// When true, right-clicking anywhere on the map image recenters on the player. // When true, right-clicking anywhere on the map image recenters on the player.
public bool rightClickRecenter = true; public bool rightClickRecenter = true;
// --- Icon culling ---
// When the map is zoomed out past iconCullingThreshold (NormalizedScale 0=close, 1=far),
// non-locomotive car icons are hidden to reduce Canvas draw calls.
public bool iconCullingEnabled = true;
public float iconCullingThreshold = 0.40f;
// When culling is active, hide MU trailing units (locos with another loco on their A-end).
public bool hideMuLocos = true;
// Scale multiplier applied to visible loco icons when culling is active.
public float locoBoostedScale = 1.5f;
// Show a blinking red dot on the map at the rear end of each train (EOTD).
public bool eotdEnabled = true;
// Hide the EOTD when car icons are visible (i.e. zoom is inside the cull threshold).
public bool eotdOnlyWhenCulled = true;
// Dot size: 1 (tiny) to 10 (large). Applied as orthographicSize * value * 0.002.
public float eotdSizeScale = 8f;
// Custom theme colors — edited live in the Settings color picker. // Custom theme colors — edited live in the Settings color picker.
// Initialized to S3 Dark so first-launch looks reasonable before the user tunes it. // Initialized to S3 Dark so first-launch looks reasonable before the user tunes it.
public MapThemeData customTheme = new MapThemeData { public MapThemeData customTheme = new MapThemeData {

View file

@ -76,83 +76,6 @@ static class PopoutSettingsUI
bool pan = GUILayout.Toggle(s.panDisablesFollow, " Dragging the map cancels follow mode"); bool pan = GUILayout.Toggle(s.panDisablesFollow, " Dragging the map cancels follow mode");
if (pan != s.panDisablesFollow) { s.panDisablesFollow = pan; PopoutModule.Persist(); } if (pan != s.panDisablesFollow) { s.panDisablesFollow = pan; PopoutModule.Persist(); }
bool rcr = GUILayout.Toggle(s.rightClickRecenter, " Right-click on map recenters on player");
if (rcr != s.rightClickRecenter) { s.rightClickRecenter = rcr; PopoutModule.Persist(); }
GUILayout.Space(8f);
// ── Icon culling ─────────────────────────────────────────────────────────
bool cullOn = GUILayout.Toggle(s.iconCullingEnabled, " Hide car icons when zoomed out (keep locomotives)");
if (cullOn != s.iconCullingEnabled)
{
s.iconCullingEnabled = cullOn;
PopoutModule.Persist();
MapIconCuller.Refresh();
}
if (s.iconCullingEnabled)
{
GUILayout.BeginHorizontal();
GUILayout.Space(20f);
GUILayout.Label("Cull at zoom-out:", GUILayout.Width(115f));
float newThresh = GUILayout.HorizontalSlider(s.iconCullingThreshold, 0.3f, 1.0f, GUILayout.Width(160f));
GUILayout.Label($"{s.iconCullingThreshold:P0}", GUILayout.Width(44f));
GUILayout.EndHorizontal();
if (Mathf.Abs(newThresh - s.iconCullingThreshold) > 0.005f)
{
s.iconCullingThreshold = newThresh;
PopoutModule.Persist();
MapIconCuller.Refresh();
}
GUILayout.BeginHorizontal();
GUILayout.Space(20f);
bool hideMU = GUILayout.Toggle(s.hideMuLocos, " Hide MU trailing units (show lead loco only)");
GUILayout.EndHorizontal();
if (hideMU != s.hideMuLocos)
{
s.hideMuLocos = hideMU;
PopoutModule.Persist();
MapIconCuller.Refresh();
}
GUILayout.BeginHorizontal();
GUILayout.Space(20f);
GUILayout.Label("Loco icon boost:", GUILayout.Width(115f));
float newBoost = GUILayout.HorizontalSlider(s.locoBoostedScale, 1.0f, 3.0f, GUILayout.Width(160f));
GUILayout.Label($"{s.locoBoostedScale:F1}x", GUILayout.Width(36f));
GUILayout.EndHorizontal();
if (Mathf.Abs(newBoost - s.locoBoostedScale) > 0.05f)
{
s.locoBoostedScale = newBoost;
PopoutModule.Persist();
MapIconCuller.Refresh();
}
GUILayout.BeginHorizontal();
GUILayout.Space(20f);
bool eotd = GUILayout.Toggle(s.eotdEnabled, " Show EOTD (blinking red dot at rear of each train)");
GUILayout.EndHorizontal();
if (eotd != s.eotdEnabled) { s.eotdEnabled = eotd; PopoutModule.Persist(); }
if (s.eotdEnabled)
{
GUILayout.BeginHorizontal();
GUILayout.Space(40f);
bool onlyWhenCulled = GUILayout.Toggle(s.eotdOnlyWhenCulled, " Hide when car icons are visible");
GUILayout.EndHorizontal();
if (onlyWhenCulled != s.eotdOnlyWhenCulled) { s.eotdOnlyWhenCulled = onlyWhenCulled; PopoutModule.Persist(); }
GUILayout.BeginHorizontal();
GUILayout.Space(40f);
GUILayout.Label("Dot size:", GUILayout.Width(72f));
float newSz = GUILayout.HorizontalSlider(s.eotdSizeScale, 1f, 10f, GUILayout.Width(160f));
GUILayout.Label($"{s.eotdSizeScale:F1}", GUILayout.Width(36f));
GUILayout.EndHorizontal();
if (Mathf.Abs(newSz - s.eotdSizeScale) > 0.05f) { s.eotdSizeScale = newSz; PopoutModule.Persist(); }
}
}
GUILayout.Space(8f); GUILayout.Space(8f);
// ── Theme preset quick-switch ──────────────────────────────────────────── // ── Theme preset quick-switch ────────────────────────────────────────────
@ -170,9 +93,8 @@ static class PopoutSettingsUI
GUILayout.Space(4f); GUILayout.Space(4f);
// ── Overlay opacity ────────────────────────────────────────────────────── // ── Overlay opacity ──────────────────────────────────────────────────────
OpacityRow("Window opacity:", ref s.overlayAlpha, 0.1f, 1.0f, Native.RRPOPOUT_SetOverlayAlpha); OpacityRow("Window opacity:", ref s.overlayAlpha, 0.1f, 1.0f, Native.RRPOPOUT_SetOverlayAlpha);
OpacityRow("Map opacity:", ref s.overlayMapAlpha, 0.0f, 1.0f, Native.RRPOPOUT_SetOverlayMapAlpha); OpacityRow("Map opacity:", ref s.overlayMapAlpha, 0.0f, 1.0f, Native.RRPOPOUT_SetOverlayMapAlpha);
OpacityRow("Map BG opacity:", ref s.overlayMapBgAlpha, 0.0f, 1.0f, Native.RRPOPOUT_SetOverlayMapBgAlpha);
GUILayout.Space(8f); GUILayout.Space(8f);