v0.2.3 - MapEnhancer settings panel, opacity controls, right-click recenter
Extends the existing MapEnhancer gear menu integration with a full settings panel covering the options added by the community fork: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons. Also adds pan-cancels-follow and right-click to recenter on player toggles, three independent opacity controls (Window, Map Elements, Map Background), a per-element hover minimum of 50% for the window chrome so controls stay reachable at low opacity, and fixes to make the compass and toolbar correctly respond to their respective sliders. Both surfaces (in-game overlay and OS popout) maintain parity on all new settings.
This commit is contained in:
parent
b85a492bd6
commit
dd1af2ec30
12 changed files with 541 additions and 46 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"Id": "S3",
|
||||
"DisplayName": "S³ - Seton's Special Sauce",
|
||||
"Author": "seton",
|
||||
"Version": "0.2.2",
|
||||
"Version": "0.2.3",
|
||||
"ManagerVersion": "0.27.0",
|
||||
"GameVersionPoint": "0",
|
||||
"AssemblyName": "S3.dll",
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -29,7 +29,7 @@ Replaces the base-game map with a floating in-game overlay (toggle with **M** or
|
|||
|
||||
### Settings
|
||||
|
||||
Configure everything from the S³ UMM settings page: hotkey, drag-to-cancel follow behavior, theme, independent window and map opacity, the detach button, and a full per-element custom color editor.
|
||||
Configure everything from the S³ UMM settings page: hotkey, drag-to-cancel follow behavior, right-click to recenter on player, theme, three independent opacity controls (Window, Map Elements, Map Background), the detach button, and a full per-element custom color editor.
|
||||
|
||||

|
||||
|
||||
|
|
@ -52,7 +52,9 @@ Six themes built in: five named presets plus a **Custom** slot you tune with per
|
|||
|
||||
### Transparency
|
||||
|
||||
The in-game overlay has independent **Window** and **Map** opacity controls. Keep the chrome subtle while the track layout stays crisp, or dial both down to a ghost overlay sitting over the world.
|
||||
The in-game overlay has three independent opacity controls. **Window** sets the chrome opacity (toolbar, compass, and frame), with an automatic minimum of 50% while the mouse is over the overlay so the controls stay reachable at any setting. **Map Elements** fades the map image independently. **Map Background** controls how transparent the empty space behind terrain is - turn it down and the game world shows through where there is no map content.
|
||||
|
||||
<video src="https://git.farmtowntech.com/setonc/railroader-setons-special-sauce/raw/branch/main/img/map/opacity/opacity_controls.mp4" controls></video>
|
||||
|
||||

|
||||
|
||||
|
|
@ -72,9 +74,9 @@ Pop the map into a detached native OS window. Drag it to any monitor, resize it
|
|||
|
||||
### MapEnhancer Integration
|
||||
|
||||
If [MapEnhancer](https://github.com/Refizar08/rr-mapenhancer-fix) is installed, the toolbar gear menu unlocks a full **Follow** submenu: follow player camera, follow the selected locomotive, or pick any consist from a live-updated list. Jump to named locations is also available.
|
||||
If [MapEnhancer](https://github.com/Refizar08/rr-mapenhancer-fix) is installed, the toolbar gear menu expands with follow controls and a settings panel. The **Follow** submenu lets you follow the player camera, the selected locomotive, or any consist picked from a live-updated list. **Jump to Location** is also available. The **Settings** panel gives you live control over all MapEnhancer rendering options without leaving the map: marker visibility toggles, scale sliders for flares, junctions, track lines, and crossings, and bulk switch reset buttons.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,36 @@ enum UICmd : int32_t {
|
|||
SetAlpha = 13, // (in-game only) set overlay (chrome) alpha; y = [0.1, 1.0]
|
||||
Close = 14, // (in-game only) hide the overlay (user clicked [X])
|
||||
SetMapAlpha = 15, // (in-game only) set map image alpha; y = [0.0, 1.0]
|
||||
// MapEnhancer settings (C# MapEnhancerBridge mirrors these indices)
|
||||
SetMEBool = 16, // toggle ME bool setting; y = MEBoolBit index, delta = 0|1
|
||||
SetMEFloat = 17, // set ME float setting; y = MEFloatIdx index, delta = value
|
||||
MEResetSwitchesNormal = 18, // bulk reset: all switches to Normal
|
||||
MEResetSwitchesThrown = 19, // bulk reset: all switches to Thrown
|
||||
TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode
|
||||
ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player
|
||||
SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0]
|
||||
};
|
||||
|
||||
// Bit indices for ME bool settings packed into PopoutWindow::imMEFlags.
|
||||
// Must match s_boolFieldNames[] in MapEnhancerBridge.cs exactly.
|
||||
enum MEBoolBit : int {
|
||||
MB_TurntableMarkers = 0, // ShowTurntableMarkers
|
||||
MB_TurntableControl = 1, // EnableTurntableControl
|
||||
MB_CheckClearance = 2, // CheckTurntableClearance
|
||||
MB_CrossingMarkers = 3, // ShowRoadCrossingMarkers
|
||||
MB_PassengerStops = 4, // EnablePassengerStopTracking
|
||||
MB_IndustryAreaColors = 5, // EnableIndustryAreaColors
|
||||
MB_ModdedSpawnPoints = 6, // EnableModdedSpawnPoints
|
||||
MB_DoubleClick = 7, // DoubleClick (require double-click for interactions)
|
||||
};
|
||||
|
||||
// Float setting indices for ME sliders.
|
||||
// Must match s_floatFieldNames[] in MapEnhancerBridge.cs exactly.
|
||||
enum MEFloatIdx : int {
|
||||
MF_FlareScale = 0, // FlareScale [0.1, 1.0]
|
||||
MF_JunctionScale = 1, // JunctionMarkerScale [0.5, 1.0]
|
||||
MF_TrackThickness = 2, // TrackLineThickness [0.5, 2.0]
|
||||
MF_CrossingScale = 3, // CrossingMarkerScale [0.1, 1.0]
|
||||
};
|
||||
|
||||
// Theme data pushed from C# to native. Controls ImGui style colours and the
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ static std::atomic<bool> g_themeChanged {false};
|
|||
static std::atomic<int> g_currentThemePreset {0};
|
||||
static std::atomic<float> g_ovAlpha {1.0f}; // chrome (window + toolbar + compass) alpha
|
||||
static std::atomic<float> g_mapAlpha {1.0f}; // map Image() alpha, independent of chrome
|
||||
static std::atomic<float> g_mapBgAlpha {1.0f}; // camera clear colour opacity
|
||||
|
||||
// cbuffer layout — must be 16-byte aligned
|
||||
struct alignas(16) UVRectCB { float u0, v0, u1, v1; };
|
||||
|
|
@ -430,7 +431,7 @@ void Renderer_ReleaseSwapchain(PopoutWindow* win) {
|
|||
// resize grip stays grabbable (the popout uses its OS window border instead).
|
||||
static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
||||
bool reserveResizeGrip, bool showPopOutBtn,
|
||||
const MapThemeData& theme) {
|
||||
const MapThemeData& theme, float mapAlpha = 1.0f) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Shared command helper — usable from any window in this frame
|
||||
|
|
@ -492,9 +493,10 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
ImVec2 wp = ImGui::GetWindowPos();
|
||||
ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f };
|
||||
|
||||
// Derive compass colours from theme
|
||||
auto col32 = [](float r, float g, float b, float a) -> ImU32 {
|
||||
return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a*255));
|
||||
// Derive compass colours from theme; scale alpha by mapAlpha so the
|
||||
// compass responds to the Map Elements opacity slider.
|
||||
auto col32 = [&](float r, float g, float b, float a) -> ImU32 {
|
||||
return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a * mapAlpha * 255));
|
||||
};
|
||||
ImU32 bgCol = cAct ? col32(theme.cmpR*1.6f, theme.cmpG*1.6f, theme.cmpB*1.6f, theme.cmpA)
|
||||
: cHov ? col32(theme.cmpR*1.3f, theme.cmpG*1.3f, theme.cmpB*1.3f, theme.cmpA)
|
||||
|
|
@ -560,7 +562,6 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
const float kGripReserve = reserveResizeGrip ? 18.f : 0.f;
|
||||
ImGui::SetNextWindowPos(ImVec2(origin.x, origin.y + (float)h - kBarH));
|
||||
ImGui::SetNextWindowSize(ImVec2((float)w - kGripReserve, kBarH));
|
||||
ImGui::SetNextWindowBgAlpha(1.f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 2.f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 2.f));
|
||||
|
|
@ -640,8 +641,32 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
win->imMapSyncPlayer.load()))
|
||||
pushCmd(UICmd::RotateSyncPlayer);
|
||||
|
||||
if (ImGui::MenuItem("Pan cancels follow", nullptr,
|
||||
win->imPanDisablesFollow.load()))
|
||||
pushCmd(UICmd::TogglePanDisablesFollow);
|
||||
|
||||
if (ImGui::MenuItem("Right-click recenters", nullptr,
|
||||
win->imRightClickRecenter.load()))
|
||||
pushCmd(UICmd::ToggleRightClickRecenter);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Helper lambdas for ME setting commands (y = index, delta = value)
|
||||
auto pushMEBool = [&](int idx, bool val) {
|
||||
InputEvent ev{}; ev.type = UICommand;
|
||||
ev.x = static_cast<float>(UICmd::SetMEBool);
|
||||
ev.y = static_cast<float>(idx);
|
||||
ev.delta = val ? 1.f : 0.f;
|
||||
win->inputQueue.push(ev);
|
||||
};
|
||||
auto pushMEFloat = [&](int idx, float val) {
|
||||
InputEvent ev{}; ev.type = UICommand;
|
||||
ev.x = static_cast<float>(UICmd::SetMEFloat);
|
||||
ev.y = static_cast<float>(idx);
|
||||
ev.delta = val;
|
||||
win->inputQueue.push(ev);
|
||||
};
|
||||
|
||||
bool meOn = win->imMapEnhancerInstalled.load();
|
||||
if (meOn) {
|
||||
if (ImGui::BeginMenu("Map Enhancer")) {
|
||||
|
|
@ -666,7 +691,7 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Jump to location")) {
|
||||
if (ImGui::BeginMenu("Jump to Location")) {
|
||||
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
||||
if (win->imLocationList.empty())
|
||||
ImGui::TextDisabled("(loading...)");
|
||||
|
|
@ -676,10 +701,106 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
pushCmd(UICmd::JumpToLocation, (float)i);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::BeginMenu("Settings")) {
|
||||
uint32_t meFlags = win->imMEFlags.load();
|
||||
auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; };
|
||||
|
||||
// -- Markers --
|
||||
ImGui::TextDisabled("Markers");
|
||||
{
|
||||
bool v = meBit(MB_TurntableMarkers);
|
||||
if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v);
|
||||
}
|
||||
{
|
||||
bool v = meBit(MB_TurntableControl);
|
||||
if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v);
|
||||
if (v) {
|
||||
bool cl = meBit(MB_CheckClearance);
|
||||
if (ImGui::MenuItem(" Check Clearance", nullptr, cl)) pushMEBool(MB_CheckClearance, !cl);
|
||||
}
|
||||
}
|
||||
{
|
||||
bool v = meBit(MB_CrossingMarkers);
|
||||
if (ImGui::MenuItem("Road Crossing Markers", nullptr, v)) pushMEBool(MB_CrossingMarkers, !v);
|
||||
}
|
||||
{
|
||||
bool v = meBit(MB_PassengerStops);
|
||||
if (ImGui::MenuItem("Passenger Stop Tracking", nullptr, v)) pushMEBool(MB_PassengerStops, !v);
|
||||
}
|
||||
{
|
||||
bool v = meBit(MB_IndustryAreaColors);
|
||||
if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// -- Behavior --
|
||||
ImGui::TextDisabled("Behavior");
|
||||
{
|
||||
bool v = meBit(MB_ModdedSpawnPoints);
|
||||
if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v);
|
||||
}
|
||||
{
|
||||
bool v = meBit(MB_DoubleClick);
|
||||
if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// -- Scales -- (live-update atomic during drag; push on release)
|
||||
ImGui::TextDisabled("Scales");
|
||||
{
|
||||
float v = win->imMEFlareScale.load();
|
||||
ImGui::SetNextItemWidth(110.f);
|
||||
if (ImGui::SliderFloat("Flare##me", &v, 0.1f, 1.0f, "%.2f"))
|
||||
win->imMEFlareScale.store(v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushMEFloat(MF_FlareScale, win->imMEFlareScale.load());
|
||||
}
|
||||
{
|
||||
float v = win->imMEJunctionSc.load();
|
||||
ImGui::SetNextItemWidth(110.f);
|
||||
if (ImGui::SliderFloat("Junction##me", &v, 0.5f, 1.0f, "%.2f"))
|
||||
win->imMEJunctionSc.store(v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushMEFloat(MF_JunctionScale, win->imMEJunctionSc.load());
|
||||
}
|
||||
{
|
||||
float v = win->imMETrackThick.load();
|
||||
ImGui::SetNextItemWidth(110.f);
|
||||
if (ImGui::SliderFloat("Track Width##me", &v, 0.5f, 2.0f, "%.2f"))
|
||||
win->imMETrackThick.store(v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushMEFloat(MF_TrackThickness, win->imMETrackThick.load());
|
||||
}
|
||||
{
|
||||
float v = win->imMECrossingSc.load();
|
||||
ImGui::SetNextItemWidth(110.f);
|
||||
if (ImGui::SliderFloat("Crossing##me", &v, 0.1f, 1.0f, "%.2f"))
|
||||
win->imMECrossingSc.store(v);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// -- Switch Reset --
|
||||
ImGui::TextDisabled("Switch Reset");
|
||||
if (ImGui::MenuItem("All Switches - Normal"))
|
||||
pushCmd(UICmd::MEResetSwitchesNormal);
|
||||
if (ImGui::MenuItem("All Switches - Thrown"))
|
||||
pushCmd(UICmd::MEResetSwitchesThrown);
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
} else {
|
||||
if (ImGui::BeginMenu("Jump to location")) {
|
||||
if (ImGui::BeginMenu("Jump to Location")) {
|
||||
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
||||
if (win->imLocationList.empty())
|
||||
ImGui::TextDisabled("(loading...)");
|
||||
|
|
@ -719,10 +840,17 @@ static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|||
// Map image alpha — independent of chrome
|
||||
float mAlpha = g_mapAlpha.load();
|
||||
ImGui::SetNextItemWidth(100.f);
|
||||
if (ImGui::SliderFloat("Map##ov", &mAlpha, 0.0f, 1.0f, "%.2f"))
|
||||
if (ImGui::SliderFloat("Map Elements##ov", &mAlpha, 0.0f, 1.0f, "%.2f"))
|
||||
g_mapAlpha.store(mAlpha);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushCmd(UICmd::SetMapAlpha, g_mapAlpha.load());
|
||||
// Map background alpha — camera clear colour opacity (0 = transparent void)
|
||||
float bgAlpha = g_mapBgAlpha.load();
|
||||
ImGui::SetNextItemWidth(100.f);
|
||||
if (ImGui::SliderFloat("Map Background##ov", &bgAlpha, 0.0f, 1.0f, "%.2f"))
|
||||
g_mapBgAlpha.store(bgAlpha);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
pushCmd(UICmd::SetMapBgAlpha, g_mapBgAlpha.load());
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
|
|
@ -865,6 +993,7 @@ void Overlay_GetMapView(float* outW, float* outH) {
|
|||
|
||||
void Overlay_SetAlpha(float alpha) { g_ovAlpha.store(std::max(0.1f, std::min(1.0f, alpha))); }
|
||||
void Overlay_SetMapAlpha(float alpha) { g_mapAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
||||
void Overlay_SetMapBgAlpha(float alpha) { g_mapBgAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
||||
|
||||
// Map-image interaction (drag/zoom) queued here for C# to forward to the map
|
||||
// camera. Same InputEvent contract the popout uses, so C# can share the logic.
|
||||
|
|
@ -933,9 +1062,15 @@ void Renderer_PresentOverlay() {
|
|||
// image has its own independent alpha, then popped finally after BuildMapUI.
|
||||
const float ovAlpha = g_ovAlpha.load();
|
||||
const float mapAlpha = g_mapAlpha.load();
|
||||
const bool hasAlpha = ovAlpha < 0.999f;
|
||||
const float mapBgA = g_mapBgAlpha.load();
|
||||
// While the mouse is over the overlay, clamp chrome alpha to 50% so the UI
|
||||
// remains readable even when the user has set a very low window opacity.
|
||||
// Uses last frame's WantCaptureMouse (one-frame lag; imperceptible in practice).
|
||||
const bool mouseOver = g_ovWantMouse.load();
|
||||
const float effectiveAlpha = mouseOver ? std::max(ovAlpha, 0.5f) : ovAlpha;
|
||||
const bool hasAlpha = effectiveAlpha < 0.999f;
|
||||
if (hasAlpha)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ovAlpha);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
||||
|
||||
g_ovMapSRV.Reset(); // release last frame's view before building this frame's
|
||||
|
||||
|
|
@ -954,6 +1089,11 @@ void Renderer_PresentOverlay() {
|
|||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
||||
ImGui::SetNextWindowPos(ImVec2(80.f, 80.f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(720.f, 480.f), ImGuiCond_FirstUseEver);
|
||||
// Scale window bg alpha by mapBgA so the map background slider makes the void
|
||||
// behind the camera transparent (showing Unity's scene), not the window chrome.
|
||||
if (mapBgA < 0.999f)
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
||||
ImVec4(theme.wBgR, theme.wBgG, theme.wBgB, theme.wBgA * mapBgA));
|
||||
// p_open = true: ImGui adds an [X] close button to the title bar. When the user
|
||||
// clicks it ImGui sets windowOpen=false; we push UICmd::Close so C# can call
|
||||
// HideIfVisible() and DeactivateMap(). NoBringToFrontOnFocus keeps this window
|
||||
|
|
@ -963,6 +1103,7 @@ void Renderer_PresentOverlay() {
|
|||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
// WindowPadding is read; pop now so ovAlpha is the only active style var.
|
||||
ImGui::PopStyleVar();
|
||||
if (mapBgA < 0.999f) ImGui::PopStyleColor();
|
||||
|
||||
if (windowVisible) {
|
||||
void* mapTex = g_ovMapTex.load();
|
||||
|
|
@ -1003,6 +1144,7 @@ void Renderer_PresentOverlay() {
|
|||
pushOv(MouseMove, 0.f);
|
||||
if (ImGui::IsItemDeactivated()) pushOv(LButtonUp, 0.f);
|
||||
if (hov && io.MouseWheel != 0.f) pushOv(MouseWheel, io.MouseWheel);
|
||||
if (hov && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) pushOv(RButtonUp, 0.f);
|
||||
|
||||
// Draw the map over the same rect the hit-button occupies.
|
||||
// Map alpha is independent of chrome alpha: pop ovAlpha so
|
||||
|
|
@ -1013,7 +1155,7 @@ void Renderer_PresentOverlay() {
|
|||
ImVec4 tint(theme.mapR, theme.mapG, theme.mapB, theme.mapA * mapAlpha);
|
||||
if (hasAlpha) ImGui::PopStyleVar(); // lift chrome alpha
|
||||
ImGui::Image((ImTextureID)g_ovMapSRV.Get(), sz, uv0, uv1, tint);
|
||||
if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ovAlpha);
|
||||
if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
||||
|
||||
// Visible resize-grip indicator. ImGui draws its own grip during
|
||||
// Begin() — behind our opaque map image, so invisible. We draw one
|
||||
|
|
@ -1050,7 +1192,7 @@ void Renderer_PresentOverlay() {
|
|||
// in-game map and the popout present the same UI. Reads the shared state holder.
|
||||
if (mapShown && stateWin)
|
||||
BuildMapUI(stateWin, mapScreenPos, mapSize.x, mapSize.y,
|
||||
/*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme);
|
||||
/*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme, mapAlpha);
|
||||
|
||||
// Pop the global alpha AFTER all chrome windows, before Render().
|
||||
if (hasAlpha)
|
||||
|
|
|
|||
|
|
@ -60,5 +60,8 @@ void Overlay_SetAlpha(float alpha);
|
|||
// Set the map image alpha [0.0, 1.0]. Independent of chrome alpha. Thread-safe via atomic.
|
||||
void Overlay_SetMapAlpha(float alpha);
|
||||
|
||||
// Set the map camera clear-colour opacity [0.0, 1.0]. Thread-safe via atomic.
|
||||
void Overlay_SetMapBgAlpha(float alpha);
|
||||
|
||||
// Render the overlay into the currently bound RTV. Render thread only.
|
||||
void Renderer_PresentOverlay();
|
||||
|
|
|
|||
|
|
@ -209,6 +209,26 @@ void RRPOPOUT_SetMapEnhancerInstalled(int windowHandle, bool installed) {
|
|||
if (win) win->imMapEnhancerInstalled.store(installed);
|
||||
}
|
||||
|
||||
// Tell native whether panning the map should cancel follow mode (for the gear menu checkmark).
|
||||
extern "C" __declspec(dllexport)
|
||||
void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active) {
|
||||
PopoutWindow* win = GetPopoutWindow(windowHandle);
|
||||
if (win) win->imPanDisablesFollow.store(active);
|
||||
}
|
||||
|
||||
// Tell native whether right-clicking the map recenters on the player (for the gear menu checkmark).
|
||||
extern "C" __declspec(dllexport)
|
||||
void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active) {
|
||||
PopoutWindow* win = GetPopoutWindow(windowHandle);
|
||||
if (win) win->imRightClickRecenter.store(active);
|
||||
}
|
||||
|
||||
// Seed the map camera clear-colour opacity slider (shared global, affects both surfaces).
|
||||
extern "C" __declspec(dllexport)
|
||||
void RRPOPOUT_SetOverlayMapBgAlpha(float alpha) {
|
||||
Overlay_SetMapBgAlpha(alpha);
|
||||
}
|
||||
|
||||
// Sync the compass display to the current map rotation (degrees, [0,360)).
|
||||
// Called by C# after it applies a rotation or while in sync-to-player mode.
|
||||
extern "C" __declspec(dllexport)
|
||||
|
|
@ -276,6 +296,24 @@ void RRPOPOUT_SetTheme(const MapThemeData* data, int presetIndex) {
|
|||
Renderer_SetTheme(data, presetIndex);
|
||||
}
|
||||
|
||||
// Push MapEnhancer settings state to a window's render-thread atomics.
|
||||
// flags: bit-packed bool settings (see MEBoolBit in shared_types.h).
|
||||
// The scale values are used to initialise / update the slider positions;
|
||||
// the render thread writes them back during drag and only pushes UICmd::SetMEFloat
|
||||
// when the slider is released (IsItemDeactivatedAfterEdit).
|
||||
extern "C" __declspec(dllexport)
|
||||
void RRPOPOUT_SetMEState(int windowHandle, uint32_t flags,
|
||||
float flareScale, float junctionScale,
|
||||
float trackThickness, float crossingScale) {
|
||||
PopoutWindow* win = GetPopoutWindow(windowHandle);
|
||||
if (!win) return;
|
||||
win->imMEFlags.store(flags);
|
||||
win->imMEFlareScale.store(flareScale);
|
||||
win->imMEJunctionSc.store(junctionScale);
|
||||
win->imMETrackThick.store(trackThickness);
|
||||
win->imMECrossingSc.store(crossingScale);
|
||||
}
|
||||
|
||||
// Set the in-game overlay's chrome (window + toolbar + compass) alpha [0.1, 1.0].
|
||||
extern "C" __declspec(dllexport)
|
||||
void RRPOPOUT_SetOverlayAlpha(float alpha) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,20 @@ struct PopoutWindow {
|
|||
std::atomic<bool> imMapEnhancerInstalled {false};
|
||||
std::atomic<bool> imFollowPlayer {false};
|
||||
std::atomic<bool> imAlwaysOnTop {false};
|
||||
std::atomic<bool> imPanDisablesFollow {true};
|
||||
std::atomic<bool> imRightClickRecenter {true};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MapEnhancer settings state (C# → render thread via RRPOPOUT_SetMEState)
|
||||
// Bit layout of imMEFlags matches MEBoolBit enum in shared_types.h.
|
||||
// Scale atomics are live-updated during slider drag so the slider doesn't
|
||||
// snap back between frames, then the final value is pushed as UICmd::SetMEFloat.
|
||||
// -----------------------------------------------------------------------
|
||||
std::atomic<uint32_t> imMEFlags {0};
|
||||
std::atomic<float> imMEFlareScale {0.6f};
|
||||
std::atomic<float> imMEJunctionSc {0.6f};
|
||||
std::atomic<float> imMETrackThick {1.25f};
|
||||
std::atomic<float> imMECrossingSc {0.3f};
|
||||
|
||||
// Loaded geometry from the save file — consumed by MessagePumpThread before CreateWindowExW.
|
||||
std::atomic<int> lastWinX {-1}, lastWinY {-1};
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ internal sealed class UiHost : MonoBehaviour
|
|||
private bool _followPlayer;
|
||||
private float _listTimer;
|
||||
private bool _locationsSent;
|
||||
private bool _meDirty;
|
||||
private string _lastStatus = "";
|
||||
|
||||
private void Start()
|
||||
|
|
@ -195,10 +196,11 @@ internal sealed class UiHost : MonoBehaviour
|
|||
|
||||
_nativeReady = true;
|
||||
|
||||
// Apply saved theme and both alpha settings so the overlay opens with the right look.
|
||||
// Apply saved theme and opacity settings so the overlay opens with the right look.
|
||||
MapThemes.ApplyFromSettings();
|
||||
Native.RRPOPOUT_SetOverlayAlpha(PopoutModule.Settings.overlayAlpha);
|
||||
Native.RRPOPOUT_SetOverlayMapAlpha(PopoutModule.Settings.overlayMapAlpha);
|
||||
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);
|
||||
|
||||
StartCoroutine(RenderLoop());
|
||||
Log.Info("[ui] in-game overlay ready - press F10 to toggle.");
|
||||
|
|
@ -337,6 +339,7 @@ internal sealed class UiHost : MonoBehaviour
|
|||
int cn = Native.RRPOPOUT_PollInputEvents(_overlayHandle, s_inputBuf, kMaxInput);
|
||||
for (int i = 0; i < cn; i++)
|
||||
ForwardCommand(s_inputBuf[i]);
|
||||
if (_meDirty) { PushMEState(); _meDirty = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -387,6 +390,17 @@ internal sealed class UiHost : MonoBehaviour
|
|||
Native.RRPOPOUT_SetMapRotation(_overlayHandle, _mapRotationDeg);
|
||||
}
|
||||
|
||||
private void PushMEState()
|
||||
{
|
||||
if (!MapEnhancerBridge.IsInstalled) return;
|
||||
Native.RRPOPOUT_SetMEState(_overlayHandle,
|
||||
MapEnhancerBridge.GetMEBoolFlags(),
|
||||
MapEnhancerBridge.GetMEFloat(0),
|
||||
MapEnhancerBridge.GetMEFloat(1),
|
||||
MapEnhancerBridge.GetMEFloat(2),
|
||||
MapEnhancerBridge.GetMEFloat(3));
|
||||
}
|
||||
|
||||
// Pushes the toolbar status line (zoom + map/cam coordinates) when it changes.
|
||||
private void UpdateMapStatus()
|
||||
{
|
||||
|
|
@ -494,11 +508,11 @@ internal sealed class UiHost : MonoBehaviour
|
|||
|
||||
case UICmd.SetTheme:
|
||||
MapThemes.Apply((MapTheme)(int)e.y);
|
||||
// Also update the map camera background colour immediately so it
|
||||
// matches the new theme even before the next ActivateMap.
|
||||
if (_mapCamera != null)
|
||||
_mapCamera.backgroundColor =
|
||||
MapThemes.GetMapBgColor((MapTheme)(int)e.y);
|
||||
if (_mapCamera != null) {
|
||||
var c = MapThemes.GetMapBgColor((MapTheme)(int)e.y);
|
||||
c.a *= PopoutModule.Settings.overlayMapBgAlpha;
|
||||
_mapCamera.backgroundColor = c;
|
||||
}
|
||||
break;
|
||||
|
||||
case UICmd.SetAlpha:
|
||||
|
|
@ -518,6 +532,42 @@ internal sealed class UiHost : MonoBehaviour
|
|||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetOverlayMapAlpha(mapAlpha);
|
||||
break;
|
||||
|
||||
case UICmd.SetMEBool:
|
||||
MapEnhancerBridge.SetMEBool((int)e.y, e.delta != 0f);
|
||||
if ((int)e.y == 6) _locationsSent = false; // EnableModdedSpawnPoints — re-push location list
|
||||
_meDirty = true;
|
||||
break;
|
||||
case UICmd.SetMEFloat:
|
||||
MapEnhancerBridge.SetMEFloat((int)e.y, e.delta);
|
||||
_meDirty = true;
|
||||
break;
|
||||
case UICmd.MEResetSwitchesNormal:
|
||||
MapEnhancerBridge.ResetAllSwitchesToNormal();
|
||||
break;
|
||||
case UICmd.MEResetSwitchesThrown:
|
||||
MapEnhancerBridge.ResetAllSwitchesToThrown();
|
||||
break;
|
||||
case UICmd.TogglePanDisablesFollow:
|
||||
PopoutModule.Settings.panDisablesFollow = !PopoutModule.Settings.panDisablesFollow;
|
||||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow);
|
||||
break;
|
||||
case UICmd.ToggleRightClickRecenter:
|
||||
PopoutModule.Settings.rightClickRecenter = !PopoutModule.Settings.rightClickRecenter;
|
||||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter);
|
||||
break;
|
||||
case UICmd.SetMapBgAlpha:
|
||||
float bgAlpha = Mathf.Clamp(e.y, 0f, 1f);
|
||||
PopoutModule.Settings.overlayMapBgAlpha = bgAlpha;
|
||||
PopoutModule.Persist();
|
||||
if (_mapCamera != null) {
|
||||
var bgc2 = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
|
||||
bgc2.a *= bgAlpha;
|
||||
_mapCamera.backgroundColor = bgc2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -531,6 +581,11 @@ internal sealed class UiHost : MonoBehaviour
|
|||
|
||||
switch ((InputEventType)e.type)
|
||||
{
|
||||
case InputEventType.RButtonUp:
|
||||
if (PopoutModule.Settings.rightClickRecenter)
|
||||
UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
|
||||
break;
|
||||
|
||||
case InputEventType.MouseWheel:
|
||||
// Lower floor than the popout's 100 so you can zoom in close like the
|
||||
// base game map; UpdateForZoom rescales icons to match.
|
||||
|
|
@ -628,11 +683,6 @@ internal sealed class UiHost : MonoBehaviour
|
|||
_mapCamera.targetTexture = _ownRT;
|
||||
_mapCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
|
||||
// Apply the theme's map camera background colour (visible as empty-space
|
||||
// colour behind terrain/water). Safe to set anytime after camera takeover.
|
||||
_mapCamera.backgroundColor =
|
||||
MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
PanelFinder.UpdateMapForZoom();
|
||||
|
||||
|
|
@ -649,6 +699,13 @@ internal sealed class UiHost : MonoBehaviour
|
|||
Native.RRPOPOUT_SetMapRotation(_overlayHandle, 0f);
|
||||
Native.RRPOPOUT_SetMapSyncPlayer(_overlayHandle, false);
|
||||
Native.RRPOPOUT_SetFollowPlayer(_overlayHandle, false);
|
||||
Native.RRPOPOUT_SetPanDisablesFollow(_overlayHandle, PopoutModule.Settings.panDisablesFollow);
|
||||
Native.RRPOPOUT_SetRightClickRecenter(_overlayHandle, PopoutModule.Settings.rightClickRecenter);
|
||||
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);
|
||||
var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
|
||||
bgc.a *= PopoutModule.Settings.overlayMapBgAlpha;
|
||||
_mapCamera!.backgroundColor = bgc;
|
||||
PushMEState();
|
||||
|
||||
_mapActive = true;
|
||||
Log.Info("[ui] in-game map activated.");
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ namespace S3.Modules.Popout {
|
|||
|
||||
// Follow player position (independent of MapEnhancer)
|
||||
private bool _followPlayer = false;
|
||||
|
||||
private bool _meDirty = false;
|
||||
|
||||
private const int kMaxInputEvents = 64;
|
||||
private static readonly InputEvent[] s_eventBuffer = new InputEvent[kMaxInputEvents];
|
||||
|
|
@ -71,6 +71,13 @@ namespace S3.Modules.Popout {
|
|||
_mapCamEulerZ = _mapCamera.transform.eulerAngles.z;
|
||||
|
||||
Native.RRPOPOUT_SetMapEnhancerInstalled(_windowHandle, MapEnhancerBridge.IsInstalled);
|
||||
Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow);
|
||||
Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter);
|
||||
Native.RRPOPOUT_SetOverlayMapBgAlpha(PopoutModule.Settings.overlayMapBgAlpha);
|
||||
var bgc = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
|
||||
bgc.a *= PopoutModule.Settings.overlayMapBgAlpha;
|
||||
_mapCamera.backgroundColor = bgc;
|
||||
if (MapEnhancerBridge.IsInstalled) PushMEState();
|
||||
|
||||
// Native loaded window_state.ini before showing the window (position/size/topmost
|
||||
// already applied). Read back the C#-side state and apply it here.
|
||||
|
|
@ -159,6 +166,7 @@ namespace S3.Modules.Popout {
|
|||
int count = Native.RRPOPOUT_PollInputEvents(_windowHandle, s_eventBuffer, kMaxInputEvents);
|
||||
for (int i = 0; i < count; i++)
|
||||
ForwardInputEvent(s_eventBuffer[i]);
|
||||
if (_meDirty) { PushMEState(); _meDirty = false; }
|
||||
|
||||
// Hotkey mirror
|
||||
bool hotkeyNow = IsHotkeyDown();
|
||||
|
|
@ -167,6 +175,16 @@ namespace S3.Modules.Popout {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
private void PushMEState() {
|
||||
if (!MapEnhancerBridge.IsInstalled) return;
|
||||
Native.RRPOPOUT_SetMEState(_windowHandle,
|
||||
MapEnhancerBridge.GetMEBoolFlags(),
|
||||
MapEnhancerBridge.GetMEFloat(0),
|
||||
MapEnhancerBridge.GetMEFloat(1),
|
||||
MapEnhancerBridge.GetMEFloat(2),
|
||||
MapEnhancerBridge.GetMEFloat(3));
|
||||
}
|
||||
|
||||
private void ApplyMapRotation(float deg) {
|
||||
_mapRotationDeg = ((deg % 360f) + 360f) % 360f;
|
||||
if (_mapCamera != null)
|
||||
|
|
@ -331,6 +349,7 @@ namespace S3.Modules.Popout {
|
|||
break;
|
||||
|
||||
case InputEventType.RButtonUp:
|
||||
if (PopoutModule.Settings.rightClickRecenter)
|
||||
UnityEngine.Object.FindObjectOfType<MapWindow>()?.ClickLocateMe();
|
||||
break;
|
||||
|
||||
|
|
@ -380,8 +399,11 @@ namespace S3.Modules.Popout {
|
|||
break;
|
||||
case UICmd.SetTheme:
|
||||
MapThemes.Apply((MapTheme)(int)e.y);
|
||||
if (_mapCamera != null)
|
||||
_mapCamera.backgroundColor = MapThemes.GetMapBgColor((MapTheme)(int)e.y);
|
||||
if (_mapCamera != null) {
|
||||
var c = MapThemes.GetMapBgColor((MapTheme)(int)e.y);
|
||||
c.a *= PopoutModule.Settings.overlayMapBgAlpha;
|
||||
_mapCamera.backgroundColor = c;
|
||||
}
|
||||
break;
|
||||
case UICmd.SetAlpha:
|
||||
float alpha = Mathf.Clamp(e.y, 0.1f, 1.0f);
|
||||
|
|
@ -395,6 +417,41 @@ namespace S3.Modules.Popout {
|
|||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetOverlayMapAlpha(mapAlpha);
|
||||
break;
|
||||
case UICmd.SetMEBool:
|
||||
MapEnhancerBridge.SetMEBool((int)e.y, e.delta != 0f);
|
||||
if ((int)e.y == 6) _locationsSent = false; // EnableModdedSpawnPoints — re-push location list
|
||||
_meDirty = true;
|
||||
break;
|
||||
case UICmd.SetMEFloat:
|
||||
MapEnhancerBridge.SetMEFloat((int)e.y, e.delta);
|
||||
_meDirty = true;
|
||||
break;
|
||||
case UICmd.MEResetSwitchesNormal:
|
||||
MapEnhancerBridge.ResetAllSwitchesToNormal();
|
||||
break;
|
||||
case UICmd.MEResetSwitchesThrown:
|
||||
MapEnhancerBridge.ResetAllSwitchesToThrown();
|
||||
break;
|
||||
case UICmd.TogglePanDisablesFollow:
|
||||
PopoutModule.Settings.panDisablesFollow = !PopoutModule.Settings.panDisablesFollow;
|
||||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow);
|
||||
break;
|
||||
case UICmd.ToggleRightClickRecenter:
|
||||
PopoutModule.Settings.rightClickRecenter = !PopoutModule.Settings.rightClickRecenter;
|
||||
PopoutModule.Persist();
|
||||
Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter);
|
||||
break;
|
||||
case UICmd.SetMapBgAlpha:
|
||||
float bgAlpha = Mathf.Clamp(e.y, 0f, 1f);
|
||||
PopoutModule.Settings.overlayMapBgAlpha = bgAlpha;
|
||||
PopoutModule.Persist();
|
||||
if (_mapCamera != null) {
|
||||
var bgc2 = MapThemes.GetMapBgColor((MapTheme)PopoutModule.Settings.mapTheme);
|
||||
bgc2.a *= bgAlpha;
|
||||
_mapCamera.backgroundColor = bgc2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Character; // CameraSelector
|
||||
using Game.State; // SpawnPoint
|
||||
using HarmonyLib;
|
||||
|
|
@ -22,6 +23,25 @@ namespace S3.Modules.Popout {
|
|||
|
||||
private static bool? _installed;
|
||||
private static MonoBehaviour? _instance;
|
||||
private static object? _meSettings;
|
||||
|
||||
// Ordered arrays — indices must match MEBoolBit / MEFloatIdx in shared_types.h.
|
||||
private static readonly string[] s_boolFieldNames = {
|
||||
"ShowTurntableMarkers", // 0 MB_TurntableMarkers
|
||||
"EnableTurntableControl", // 1 MB_TurntableControl
|
||||
"CheckTurntableClearance", // 2 MB_CheckClearance
|
||||
"ShowRoadCrossingMarkers", // 3 MB_CrossingMarkers
|
||||
"EnablePassengerStopTracking", // 4 MB_PassengerStops
|
||||
"EnableIndustryAreaColors", // 5 MB_IndustryAreaColors
|
||||
"EnableModdedSpawnPoints", // 6 MB_ModdedSpawnPoints
|
||||
"DoubleClick", // 7 MB_DoubleClick
|
||||
};
|
||||
private static readonly string[] s_floatFieldNames = {
|
||||
"FlareScale", // 0 MF_FlareScale
|
||||
"JunctionMarkerScale", // 1 MF_JunctionScale
|
||||
"TrackLineThickness", // 2 MF_TrackThickness
|
||||
"CrossingMarkerScale", // 3 MF_CrossingScale
|
||||
};
|
||||
|
||||
// True if MapEnhancer is installed and active this session.
|
||||
public static bool IsInstalled {
|
||||
|
|
@ -125,6 +145,90 @@ namespace S3.Modules.Popout {
|
|||
} catch { return Array.Empty<string>(); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MapEnhancer settings access (via Loader.Settings reflection + Traverse).
|
||||
// All methods are no-ops when ME is not installed.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public static bool GetMEBool(int idx) {
|
||||
var s = GetMESettingsObj();
|
||||
if (s == null || idx < 0 || idx >= s_boolFieldNames.Length) return false;
|
||||
try { return Traverse.Create(s).Field<bool>(s_boolFieldNames[idx]).Value; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public static void SetMEBool(int idx, bool val) {
|
||||
var s = GetMESettingsObj();
|
||||
if (s == null || idx < 0 || idx >= s_boolFieldNames.Length) return;
|
||||
try {
|
||||
Traverse.Create(s).Field(s_boolFieldNames[idx]).SetValue(val);
|
||||
// EnableTurntableControl (1) also enables ShowTurntableMarkers when turned on,
|
||||
// matching MapEnhancer's own OnSettingsChanged logic.
|
||||
if (idx == 1 && val)
|
||||
Traverse.Create(s).Field("ShowTurntableMarkers").SetValue(true);
|
||||
// EnableModdedSpawnPoints (6) must flush ME's cached spawn-point list
|
||||
// so it re-scans mods when the map is next opened.
|
||||
if (idx == 6)
|
||||
ClearModdedSpawnPointsCache();
|
||||
ApplyAndSaveMESettings();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
public static float GetMEFloat(int idx) {
|
||||
var s = GetMESettingsObj();
|
||||
if (s == null || idx < 0 || idx >= s_floatFieldNames.Length) return 0f;
|
||||
try { return Traverse.Create(s).Field<float>(s_floatFieldNames[idx]).Value; }
|
||||
catch { return 0f; }
|
||||
}
|
||||
|
||||
public static void SetMEFloat(int idx, float val) {
|
||||
var s = GetMESettingsObj();
|
||||
if (s == null || idx < 0 || idx >= s_floatFieldNames.Length) return;
|
||||
try {
|
||||
Traverse.Create(s).Field(s_floatFieldNames[idx]).SetValue(val);
|
||||
ApplyAndSaveMESettings();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Pack all bool settings into a uint32 bitmask for the native atomic.
|
||||
public static uint GetMEBoolFlags() {
|
||||
uint flags = 0;
|
||||
for (int i = 0; i < s_boolFieldNames.Length; i++)
|
||||
if (GetMEBool(i)) flags |= (1u << i);
|
||||
return flags;
|
||||
}
|
||||
|
||||
public static void ResetAllSwitchesToNormal() {
|
||||
var me = GetInstance();
|
||||
if (me == null) return;
|
||||
try { Traverse.Create(me).Method("ResetAllSwitchesToNormal").GetValue(); } catch { }
|
||||
}
|
||||
|
||||
public static void ResetAllSwitchesToThrown() {
|
||||
var me = GetInstance();
|
||||
if (me == null) return;
|
||||
try { Traverse.Create(me).Method("ResetAllSwitchesToThrown").GetValue(); } catch { }
|
||||
}
|
||||
|
||||
// Force ME to re-scan modded spawn points on next map open.
|
||||
public static void ClearModdedSpawnPointsCache() {
|
||||
var me = GetInstance();
|
||||
if (me == null) return;
|
||||
try { Traverse.Create(me).Field("_modSpawnPointsLoaded").SetValue(false); } catch { }
|
||||
}
|
||||
|
||||
// Trigger ME's in-memory apply (OnChange → OnSettingsChanged) then persist to XML.
|
||||
public static void ApplyAndSaveMESettings() {
|
||||
var s = GetMESettingsObj();
|
||||
if (s == null) return;
|
||||
try {
|
||||
Traverse.Create(s).Method("OnChange").GetValue();
|
||||
var modEntry = FindMod("MapEnhancer");
|
||||
if (modEntry != null)
|
||||
Traverse.Create(s).Method("Save", new object[] { modEntry }).GetValue();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
private static Car[] GetLocosSorted() =>
|
||||
TrainController.Shared?.Cars
|
||||
|
|
@ -148,6 +252,17 @@ namespace S3.Modules.Popout {
|
|||
} catch { }
|
||||
}
|
||||
|
||||
private static object? GetMESettingsObj() {
|
||||
if (_meSettings != null) return _meSettings;
|
||||
if (!IsInstalled) return null;
|
||||
var loaderType = Type.GetType("MapEnhancer.UMM.Loader, MapEnhancer");
|
||||
if (loaderType == null) return null;
|
||||
var fi = loaderType.GetField("Settings", BindingFlags.Public | BindingFlags.Static);
|
||||
if (fi == null) return null;
|
||||
_meSettings = fi.GetValue(null);
|
||||
return _meSettings;
|
||||
}
|
||||
|
||||
private static UnityModManager.ModEntry? FindMod(string id) {
|
||||
foreach (var m in UnityModManager.modEntries)
|
||||
if (m.Info.Id == id) return m;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ namespace S3.Modules.Popout {
|
|||
SetAlpha = 13, // (in-game only) set chrome alpha; InputEvent.y = [0.1, 1.0]
|
||||
Close = 14, // (in-game only) user clicked [X] — hide the overlay
|
||||
SetMapAlpha = 15, // (in-game only) set map image alpha; InputEvent.y = [0.0, 1.0]
|
||||
// MapEnhancer settings
|
||||
SetMEBool = 16, // toggle ME bool setting; y = MEBoolBit index, delta = 0|1
|
||||
SetMEFloat = 17, // set ME float setting; y = MEFloatIdx index, delta = value
|
||||
MEResetSwitchesNormal = 18, // bulk reset all switches to Normal
|
||||
MEResetSwitchesThrown = 19, // bulk reset all switches to Thrown
|
||||
TogglePanDisablesFollow = 20, // toggle whether panning cancels follow mode
|
||||
ToggleRightClickRecenter = 21, // toggle whether right-clicking recenters on player
|
||||
SetMapBgAlpha = 22, // set map camera clear-colour opacity; y = [0.0, 1.0]
|
||||
}
|
||||
|
||||
// Must match MapThemeData in native/include/shared_types.h exactly (36 floats = 144 bytes).
|
||||
|
|
@ -202,5 +210,27 @@ namespace S3.Modules.Popout {
|
|||
// Set the map image alpha [0.0, 1.0]. Independent of chrome alpha.
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void RRPOPOUT_SetOverlayMapAlpha(float alpha);
|
||||
|
||||
// Tell native whether panning should cancel follow mode (for the gear menu checkmark).
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void RRPOPOUT_SetPanDisablesFollow(int windowHandle, bool active);
|
||||
|
||||
// Tell native whether right-clicking the map recenters on the player (gear menu checkmark).
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void RRPOPOUT_SetRightClickRecenter(int windowHandle, bool active);
|
||||
|
||||
// Seed the map camera clear-colour opacity slider (shared global, overlay + popout).
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void RRPOPOUT_SetOverlayMapBgAlpha(float alpha);
|
||||
|
||||
// Push MapEnhancer settings state to a window's render-thread atomics.
|
||||
// flags: bit-packed booleans (see MEBoolBit in shared_types.h).
|
||||
// Scale values seed slider positions; sliders update them live during drag
|
||||
// and push UICmd::SetMEFloat only on release (IsItemDeactivatedAfterEdit).
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void RRPOPOUT_SetMEState(
|
||||
int windowHandle, uint flags,
|
||||
float flareScale, float junctionScale,
|
||||
float trackThickness, float crossingScale);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ public class PopoutSettings
|
|||
// In-game overlay map image alpha [0.0, 1.0]. Independent of chrome alpha.
|
||||
public float overlayMapAlpha = 1.0f;
|
||||
|
||||
// Map camera clear-colour opacity [0.0, 1.0]. At 0 the void/sky areas of the
|
||||
// map are transparent, letting the game world show through in empty regions.
|
||||
public float overlayMapBgAlpha = 1.0f;
|
||||
|
||||
// When true, right-clicking anywhere on the map image recenters on the player.
|
||||
public bool rightClickRecenter = true;
|
||||
|
||||
// Custom theme colors — edited live in the Settings color picker.
|
||||
// Initialized to S3 Dark so first-launch looks reasonable before the user tunes it.
|
||||
public MapThemeData customTheme = new MapThemeData {
|
||||
|
|
|
|||
Loading…
Reference in a new issue