Compare commits
No commits in common. "2343f05b1886562c073cdc318d2f411fe1cc61c8" and "c8918851aa07071ffaeae7159e96335d2921c89c" have entirely different histories.
2343f05b18
...
c8918851aa
20 changed files with 18 additions and 825 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
16
README.md
16
README.md
|
|
@ -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.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
|
||||||
6
dist/build-common.ps1
vendored
6
dist/build-common.ps1
vendored
|
|
@ -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 |
Binary file not shown.
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 1–10; 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 ────────────────────────────────────────────
|
||||||
|
|
@ -172,7 +95,6 @@ static class PopoutSettingsUI
|
||||||
// ── 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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue