v0.2.4 - MapEnhancer version detection

Probe for version-specific types at startup to determine which MapEnhancer
build is installed, then gray out features unavailable in that build.

Three tiers: official v1.5.2 (minimal), v1.6.0 (adds turntable markers),
and the community fork (full feature set). Detection uses type probes -
TurntableHelper for the v1.6.0+ tier, SwitchResetAuditState for community.

Adds HasTurntable and IsCommunity capability flags on MapEnhancerBridge,
imMEHasTurntable/imMECommunity atomics on PopoutWindow, and a two-arg
RRPOPOUT_SetMapEnhancerCaps export. The renderer reads both flags and wraps
community-only menu items in BeginDisabled blocks accordingly.

Updates README with version compatibility notes and comparison screenshots.
This commit is contained in:
Seton Carmichael 2026-06-21 15:12:56 -04:00
parent 4391d96e70
commit c8918851aa
11 changed files with 74 additions and 4 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.3", "Version": "0.2.4",
"ManagerVersion": "0.27.0", "ManagerVersion": "0.27.0",
"GameVersionPoint": "0", "GameVersionPoint": "0",
"AssemblyName": "S3.dll", "AssemblyName": "S3.dll",

View file

@ -74,10 +74,19 @@ Pop the map into a detached native OS window. Drag it to any monitor, resize it
### MapEnhancer Integration ### MapEnhancer Integration
If [MapEnhancer](https://github.com/Refizar08/rr-mapenhancer-fix) is installed, the toolbar gear menu expands with follow controls and a settings panel. The **Follow** submenu lets you follow the player camera, the selected locomotive, or any consist picked from a live-updated list. **Jump to Location** is also available. The **Settings** panel gives you live control over all MapEnhancer rendering options without leaving the map: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons. S³ auto-detects any installed version of MapEnhancer at startup with no configuration required. If MapEnhancer is installed, the toolbar gear menu expands with follow controls and a settings panel. The **Follow** submenu lets you follow the player camera, the selected locomotive, or any consist picked from a live-updated list. **Jump to Location** is also available. The **Settings** panel gives you live control over all MapEnhancer rendering options without leaving the map: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons.
![MapEnhancer gear menu showing follow submenu and settings panel](img/map/map_enhancer_follow_mode.png) ![MapEnhancer gear menu showing follow submenu and settings panel](img/map/map_enhancer_follow_mode.png)
Both the original official builds and the [community fork](https://github.com/Refizar08/rr-mapenhancer-fix) are supported. Features added by the community fork - turntable control, road crossing markers, passenger stop tracking, industry area colors, modded spawn points, and bulk switch reset - are grayed out when an older build is detected. All controls that the installed version supports remain fully functional.
<table>
<tr>
<td align="center"><img src="img/map/map_enhancer_full.png" alt="Community build - all features available"><br><b>Community build</b></td>
<td align="center"><img src="img/map/map_enhancer_grayedout.png" alt="Older official build - community features grayed out"><br><b>Older build - community features grayed</b></td>
</tr>
</table>
--- ---
## Physics Optimizer ## Physics Optimizer

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View file

@ -668,6 +668,8 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
}; };
bool meOn = win->imMapEnhancerInstalled.load(); bool meOn = win->imMapEnhancerInstalled.load();
bool meTurntable = win->imMEHasTurntable.load(); // v1.6.0 or community
bool meCom = win->imMECommunity.load(); // community only
if (meOn) { if (meOn) {
if (ImGui::BeginMenu("Map Enhancer")) { if (ImGui::BeginMenu("Map Enhancer")) {
if (ImGui::MenuItem("Toggle follow mode")) if (ImGui::MenuItem("Toggle follow mode"))
@ -709,11 +711,16 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; }; auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; };
// -- Markers -- // -- Markers --
// Turntable Markers: available on v1.6.0 and community (ShowTurntableMarkers field).
// Everything else in this block is community-only.
ImGui::TextDisabled("Markers"); ImGui::TextDisabled("Markers");
ImGui::BeginDisabled(!meTurntable);
{ {
bool v = meBit(MB_TurntableMarkers); bool v = meBit(MB_TurntableMarkers);
if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v); if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v);
} }
ImGui::EndDisabled();
ImGui::BeginDisabled(!meCom);
{ {
bool v = meBit(MB_TurntableControl); bool v = meBit(MB_TurntableControl);
if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v); if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v);
@ -734,15 +741,18 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
bool v = meBit(MB_IndustryAreaColors); bool v = meBit(MB_IndustryAreaColors);
if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v); if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v);
} }
ImGui::EndDisabled();
ImGui::Separator(); ImGui::Separator();
// -- Behavior -- // -- Behavior --
ImGui::TextDisabled("Behavior"); ImGui::TextDisabled("Behavior");
ImGui::BeginDisabled(!meCom);
{ {
bool v = meBit(MB_ModdedSpawnPoints); bool v = meBit(MB_ModdedSpawnPoints);
if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v); if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v);
} }
ImGui::EndDisabled();
{ {
bool v = meBit(MB_DoubleClick); bool v = meBit(MB_DoubleClick);
if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v); if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v);
@ -776,6 +786,7 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
if (ImGui::IsItemDeactivatedAfterEdit()) if (ImGui::IsItemDeactivatedAfterEdit())
pushMEFloat(MF_TrackThickness, win->imMETrackThick.load()); pushMEFloat(MF_TrackThickness, win->imMETrackThick.load());
} }
ImGui::BeginDisabled(!meCom);
{ {
float v = win->imMECrossingSc.load(); float v = win->imMECrossingSc.load();
ImGui::SetNextItemWidth(110.f); ImGui::SetNextItemWidth(110.f);
@ -784,15 +795,18 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
if (ImGui::IsItemDeactivatedAfterEdit()) if (ImGui::IsItemDeactivatedAfterEdit())
pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load()); pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load());
} }
ImGui::EndDisabled();
ImGui::Separator(); ImGui::Separator();
// -- Switch Reset -- // -- Switch Reset (community build only) --
ImGui::BeginDisabled(!meCom);
ImGui::TextDisabled("Switch Reset"); ImGui::TextDisabled("Switch Reset");
if (ImGui::MenuItem("All Switches - Normal")) if (ImGui::MenuItem("All Switches - Normal"))
pushCmd(UICmd::MEResetSwitchesNormal); pushCmd(UICmd::MEResetSwitchesNormal);
if (ImGui::MenuItem("All Switches - Thrown")) if (ImGui::MenuItem("All Switches - Thrown"))
pushCmd(UICmd::MEResetSwitchesThrown); pushCmd(UICmd::MEResetSwitchesThrown);
ImGui::EndDisabled();
ImGui::EndMenu(); ImGui::EndMenu();
} }

View file

@ -209,6 +209,17 @@ void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed) {
if (win) win->imMapEnhancerInstalled.store(installed); if (win) win->imMapEnhancerInstalled.store(installed);
} }
// Set ME capability flags.
// hasTurntable — ShowTurntableMarkers field exists (v1.6.0 or community); gates Turntable Markers toggle.
// community — full community build (crossing/spawn/switch-reset); gates all other community items.
extern "C" __declspec(dllexport)
void RRPOPOUT_SetMapEnhancerCaps(int windowHandle, bool hasTurntable, bool community) {
PopoutWindow* win = GetPopoutWindow(windowHandle);
if (!win) return;
win->imMEHasTurntable.store(hasTurntable);
win->imMECommunity.store(community);
}
// Tell native whether panning the map should cancel follow mode (for the gear menu checkmark). // Tell native whether panning the map should cancel follow mode (for the gear menu checkmark).
extern "C" __declspec(dllexport) extern "C" __declspec(dllexport)
void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active) { void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active) {

View file

@ -75,6 +75,8 @@ struct PopoutWindow {
std::atomic<float> imMapZoom {500.f}; std::atomic<float> imMapZoom {500.f};
std::atomic<bool> imMapSyncPlayer {false}; std::atomic<bool> imMapSyncPlayer {false};
std::atomic<bool> imMapEnhancerInstalled {false}; std::atomic<bool> imMapEnhancerInstalled {false};
std::atomic<bool> imMEHasTurntable {false}; // true if ShowTurntableMarkers exists (v1.6.0 or community)
std::atomic<bool> imMECommunity {false}; // true = community build (has crossing/spawn/switch-reset)
std::atomic<bool> imFollowPlayer {false}; std::atomic<bool> imFollowPlayer {false};
std::atomic<bool> imAlwaysOnTop {false}; std::atomic<bool> imAlwaysOnTop {false};
std::atomic<bool> imPanDisablesFollow {true}; std::atomic<bool> imPanDisablesFollow {true};

View file

@ -696,6 +696,7 @@ internal sealed class UiHost : MonoBehaviour
_locationsSent = false; _locationsSent = false;
_lastStatus = ""; _lastStatus = "";
Native.RRPOPOUT_SetMapEnhancerInstalled(_overlayHandle, MapEnhancerBridge.IsInstalled); Native.RRPOPOUT_SetMapEnhancerInstalled(_overlayHandle, MapEnhancerBridge.IsInstalled);
Native.RRPOPOUT_SetMapEnhancerCaps(_overlayHandle, MapEnhancerBridge.HasTurntable, MapEnhancerBridge.IsCommunity);
Native.RRPOPOUT_SetMapRotation(_overlayHandle, 0f); Native.RRPOPOUT_SetMapRotation(_overlayHandle, 0f);
Native.RRPOPOUT_SetMapSyncPlayer(_overlayHandle, false); Native.RRPOPOUT_SetMapSyncPlayer(_overlayHandle, false);
Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false); Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false);

View file

@ -71,6 +71,7 @@ namespace S3.Modules.Popout {
_mapCamEulerZ = _mapCamera.transform.eulerAngles.z; _mapCamEulerZ = _mapCamera.transform.eulerAngles.z;
Native.RRPOPOUT_SetMapEnhancerInstalled(_windowHandle, MapEnhancerBridge.IsInstalled); Native.RRPOPOUT_SetMapEnhancerInstalled(_windowHandle, MapEnhancerBridge.IsInstalled);
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);
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha); Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);

View file

@ -22,6 +22,7 @@ namespace S3.Modules.Popout {
internal static class MapEnhancerBridge { internal static class MapEnhancerBridge {
private static bool? _installed; private static bool? _installed;
private static bool? _isCommunity;
private static MonoBehaviour? _instance; private static MonoBehaviour? _instance;
private static object? _meSettings; private static object? _meSettings;
@ -53,6 +54,31 @@ namespace S3.Modules.Popout {
} }
} }
// True if ShowTurntableMarkers exists — present in v1.6.0 (lstealth) and community,
// but NOT in the original official v1.5.2 (mricher-git). Gates the Turntable Markers toggle.
public static bool HasTurntable {
get {
if (_hasTurntable.HasValue) return _hasTurntable.Value;
if (!IsInstalled) { _hasTurntable = false; return false; }
_hasTurntable = Type.GetType("MapEnhancer.TurntableHelper, MapEnhancer") != null;
return _hasTurntable.Value;
}
}
private static bool? _hasTurntable;
// True if this is the Refizar08 community build — adds crossing/spawn/switch-reset
// features absent from both official builds. Detected by SwitchResetAuditState,
// a type that only exists in that fork.
public static bool IsCommunity {
get {
if (_isCommunity.HasValue) return _isCommunity.Value;
if (!IsInstalled) { _isCommunity = false; return false; }
_isCommunity = Type.GetType("MapEnhancer.SwitchResetAuditState, MapEnhancer") != null;
S3.Core.Log.Info($"[S3] MapEnhancer: HasTurntable={HasTurntable} IsCommunity={_isCommunity.Value}");
return _isCommunity.Value;
}
}
// Current value of the private mapFollowMode field. // Current value of the private mapFollowMode field.
public static bool FollowMode { public static bool FollowMode {
get { get {

View file

@ -123,6 +123,12 @@ namespace S3.Modules.Popout {
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed); public static extern void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed);
// Push ME capability flags to native.
// hasTurntable — ShowTurntableMarkers exists (v1.6.0 or community); gates Turntable Markers.
// community — full community build (crossing/spawn/switch-reset); gates community items.
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapEnhancerCaps(int windowHandle, bool hasTurntable, bool community);
// Update the compass display to reflect the current map rotation (degrees). // Update the compass display to reflect the current map rotation (degrees).
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern void RRPOPOUT_SetMapRotation(int windowHandle, float degrees); public static extern void RRPOPOUT_SetMapRotation(int windowHandle, float degrees);