Named camera bookmarks, Auto Engineer / WaypointQueue pins, and extracted track labels. Stock MapWindow stays collapsed while S3 owns the camera. Radio Runner is not in this drop.
613 lines
29 KiB
C#
613 lines
29 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using HarmonyLib;
|
|
using S3.Core; // Log
|
|
using UI.Map;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
namespace S3.Modules.Popout {
|
|
|
|
// Manages one detached map popout window.
|
|
internal class DetachedPanel {
|
|
|
|
public bool IsAlive => _windowHandle > 0;
|
|
|
|
private static IntPtr _renderEventFunc = IntPtr.Zero;
|
|
|
|
private int _windowHandle;
|
|
private Camera? _mapCamera;
|
|
private RenderTexture? _ownRT;
|
|
|
|
private RenderTexture? _savedTargetTexture;
|
|
private Rect _savedRect;
|
|
private float _savedAspect;
|
|
|
|
// Drag / click state
|
|
private bool _isDragging;
|
|
private bool _didDrag;
|
|
private float _dragStartX, _dragStartY;
|
|
private float _camStartX, _camStartZ;
|
|
|
|
private const float kClickThreshold = 0.02f;
|
|
private const float kZoomFactor = 0.9f;
|
|
private const float kZoomMin = 100f;
|
|
private const float kZoomMax = 10000f;
|
|
|
|
// GetAsyncKeyState works regardless of which window has focus.
|
|
[DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey);
|
|
|
|
private bool _prevHotkeyDown;
|
|
private bool _prevTeleportDown;
|
|
public bool CloseRequested { get; private set; }
|
|
|
|
// Last mouse position (normalised [0,1], top-left origin) received via MouseMove.
|
|
// Updated by ForwardInputEvent; used for popout teleport key handling.
|
|
private float _lastMouseX = -1f;
|
|
private float _lastMouseY = -1f;
|
|
|
|
// Status bar: only push to native when the string changes.
|
|
private string _lastStatusText = "";
|
|
|
|
// Menu list refresh — locos every 3 s, locations once.
|
|
private float _listTimer = 3f; // trigger immediately on first Update
|
|
private bool _locationsSent = false;
|
|
|
|
// Map rotation
|
|
private float _mapRotationDeg = 0f;
|
|
private float _mapCamEulerX = 0f;
|
|
private float _mapCamEulerZ = 0f;
|
|
private bool _mapSyncPlayer = false;
|
|
|
|
// 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];
|
|
|
|
public DetachedPanel(string title) {
|
|
if (_renderEventFunc == IntPtr.Zero)
|
|
_renderEventFunc = Native.RRPOPOUT_GetRenderEventFunc();
|
|
|
|
_mapCamera = MapBuilder.Shared?.mapCamera;
|
|
_windowHandle = Native.RRPOPOUT_CreateWindow(title, 900, 700);
|
|
if (_windowHandle == 0 || _mapCamera == null) return;
|
|
|
|
_mapCamEulerX = _mapCamera.transform.eulerAngles.x;
|
|
_mapCamEulerZ = _mapCamera.transform.eulerAngles.z;
|
|
|
|
Native.RRPOPOUT_SetMapEnhancerInstalled(_windowHandle, MapEnhancerBridge.IsInstalled);
|
|
Native.RRPOPOUT_SetMapEnhancerCaps(_windowHandle, MapEnhancerBridge.HasTurntable, MapEnhancerBridge.IsCommunity);
|
|
Native.RRPOPOUT_SetPanDisablesFollow(_windowHandle, PopoutModule.Settings.panDisablesFollow);
|
|
Native.RRPOPOUT_SetRightClickRecenter(_windowHandle, PopoutModule.Settings.rightClickRecenter);
|
|
SeedIconCullingState(_windowHandle);
|
|
try
|
|
{
|
|
TrackLabelService.SeedEnabled(_windowHandle);
|
|
TrackLabelService.SeedStyle(_windowHandle);
|
|
TrackLabelService.Reset();
|
|
MapViewPresets.PushList(_windowHandle);
|
|
MapWaypointSystem.SeedState(_windowHandle);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error($"[popout] map chrome seed failed (popout still active): {ex}");
|
|
}
|
|
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.
|
|
Native.RRPOPOUT_GetPersistedState(_windowHandle,
|
|
out int followPlayer, out int syncRotation,
|
|
out float mapRotation, out float mapZoom);
|
|
|
|
// Preserve the game's original RT dimensions exactly so Camera.pixelWidth and
|
|
// Camera.pixelHeight are identical to the in-game map. Icon scripts that scale
|
|
// based on pixelWidth/Height will then produce the same sizes as in-game.
|
|
// We still set camera.aspect = window ratio so the correct FOV fills the window;
|
|
// the RT-vs-window aspect mismatch is cancelled by the camera's horizontal FOV
|
|
// expansion and the native blit's corresponding horizontal stretch.
|
|
_savedTargetTexture = _mapCamera.targetTexture;
|
|
_savedRect = _mapCamera.rect;
|
|
_savedAspect = _mapCamera.aspect;
|
|
Log.Info($"[popout] mapCam RT={((_mapCamera.targetTexture != null) ? $"{_mapCamera.targetTexture.width}x{_mapCamera.targetTexture.height}" : "null(screen)")} screen={Screen.width}x{Screen.height} rect={_mapCamera.rect} aspect={_mapCamera.aspect:F3}");
|
|
|
|
_mapCamera.orthographicSize = Mathf.Clamp(mapZoom, 100f, 10000f);
|
|
if (mapRotation != 0f) ApplyMapRotation(mapRotation);
|
|
MapCameraMemory.TryRestore(_mapCamera, ApplyMapRotation);
|
|
if (syncRotation != 0) { _mapSyncPlayer = true; Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, true); }
|
|
if (followPlayer != 0) { _followPlayer = true; Native.RRPOPOUT_SetFollowPlayer(_windowHandle, true); }
|
|
|
|
CreateOwnRT();
|
|
_mapCamera.enabled = true; // SetVisible(false) may have disabled it; re-enable now
|
|
_prevHotkeyDown = IsHotkeyDown();
|
|
}
|
|
|
|
public void Update() {
|
|
if (_windowHandle == 0 || _mapCamera == null || _ownRT == null) return;
|
|
|
|
Native.RRPOPOUT_GetWindowSize(_windowHandle, out int w, out int h);
|
|
if (w == 0 && h == 0) { _windowHandle = 0; return; }
|
|
|
|
// RT dimensions are fixed at the game's original map RT size — never resized.
|
|
// Only aspect changes as the window is resized, so the camera renders the
|
|
// correct FOV for the current window shape.
|
|
_mapCamera.enabled = true;
|
|
_mapCamera.targetTexture = _ownRT;
|
|
_mapCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
|
_mapCamera.aspect = (float)w / h;
|
|
|
|
Native.RRPOPOUT_SetFrameTexture(_windowHandle,
|
|
_ownRT.GetNativeTexturePtr(), 0f, 1f, 1f, 0f);
|
|
GL.IssuePluginEvent(_renderEventFunc, _windowHandle);
|
|
|
|
// --- Status bar ---
|
|
UpdateStatusText();
|
|
TrackLabelService.Push(_windowHandle, _mapCamera);
|
|
|
|
// --- Menu list refresh ---
|
|
_listTimer += Time.deltaTime;
|
|
if (_listTimer >= 3f) {
|
|
_listTimer = 0f;
|
|
PushLocoList();
|
|
if (!_locationsSent) { PushLocationList(); _locationsSent = true; }
|
|
}
|
|
|
|
// --- Follow player position ---
|
|
if (_followPlayer) {
|
|
if (Camera.main != null) {
|
|
var p = _mapCamera!.transform.position;
|
|
p.x = Camera.main.transform.position.x;
|
|
p.z = Camera.main.transform.position.z;
|
|
_mapCamera.transform.position = p;
|
|
} else {
|
|
_followPlayer = false;
|
|
Native.RRPOPOUT_SetFollowPlayer(_windowHandle, false);
|
|
}
|
|
}
|
|
|
|
// --- Map rotation sync ---
|
|
if (_mapSyncPlayer) {
|
|
if (Camera.main != null)
|
|
ApplyMapRotation(Camera.main.transform.eulerAngles.y);
|
|
else {
|
|
_mapSyncPlayer = false;
|
|
Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, false);
|
|
}
|
|
}
|
|
|
|
// --- Input ---
|
|
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();
|
|
if (hotkeyNow && !_prevHotkeyDown) CloseRequested = true;
|
|
_prevHotkeyDown = hotkeyNow;
|
|
|
|
// "Jump to Mouse": teleport the player to the last-known cursor position on the
|
|
// map. We use GetAsyncKeyState so this fires even when the game window doesn't
|
|
// have focus. Unity's Input System won't fire Teleport from the popout window,
|
|
// so we poll here instead, using the key + modifier actually bound to Game/Teleport
|
|
// (resolved once via ResolveTeleportBinding) rather than a hardcoded key — a plain
|
|
// VK_T poll ignored whatever modifier the binding requires (default Shift+T, or
|
|
// any rebind), so the popout would fire on the bare key alone.
|
|
ResolveTeleportBinding();
|
|
bool teleportNow = IsDown(_teleportVk) &&
|
|
(_teleportModifierVk == 0 || IsDown(_teleportModifierVk));
|
|
if (teleportNow && !_prevTeleportDown && _lastMouseX >= 0f)
|
|
PanelFinder.GetMapDrag()?.OnTeleport?.Invoke(new Vector2(_lastMouseX, 1f - _lastMouseY));
|
|
_prevTeleportDown = teleportNow;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
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)
|
|
_mapCamera.transform.rotation = Quaternion.Euler(_mapCamEulerX, _mapRotationDeg, _mapCamEulerZ);
|
|
Native.RRPOPOUT_SetMapRotation(_windowHandle, _mapRotationDeg);
|
|
}
|
|
|
|
private void DisableFollowForPreset() {
|
|
if (_followPlayer) {
|
|
_followPlayer = false;
|
|
Native.RRPOPOUT_SetFollowPlayer(_windowHandle, false);
|
|
}
|
|
if (_mapSyncPlayer) {
|
|
_mapSyncPlayer = false;
|
|
Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, false);
|
|
}
|
|
if (MapEnhancerBridge.IsInstalled && MapEnhancerBridge.FollowMode)
|
|
MapEnhancerBridge.ToggleFollowMode();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
private void UpdateStatusText() {
|
|
string status = PanelFinder.BuildStatusText(_mapCamera!);
|
|
if (status == _lastStatusText) return;
|
|
Native.RRPOPOUT_SetStatusText(_windowHandle, status);
|
|
Native.RRPOPOUT_SetMapZoom(_windowHandle, _mapCamera!.orthographicSize);
|
|
_lastStatusText = status;
|
|
}
|
|
|
|
private void PushLocoList() {
|
|
try {
|
|
var names = MapEnhancerBridge.GetLocoNames();
|
|
Native.RRPOPOUT_SetLocoList(_windowHandle,
|
|
names.Length > 0 ? string.Join("\n", names) : "");
|
|
} catch { }
|
|
}
|
|
|
|
private void PushLocationList() {
|
|
try {
|
|
var names = MapEnhancerBridge.GetLocationNames();
|
|
Native.RRPOPOUT_SetLocationList(_windowHandle,
|
|
names.Length > 0 ? string.Join("\n", names) : "");
|
|
} catch { }
|
|
}
|
|
|
|
// Returns true while the configured hotkey combination is held.
|
|
private static bool IsHotkeyDown() {
|
|
var kb = PopoutModule.Hotkey;
|
|
bool wantShift = (kb.modifiers & 1) != 0;
|
|
bool wantCtrl = (kb.modifiers & 2) != 0;
|
|
bool wantAlt = (kb.modifiers & 4) != 0;
|
|
if (IsDown(0x10) != wantShift) return false;
|
|
if (IsDown(0x11) != wantCtrl ) return false;
|
|
if (IsDown(0x12) != wantAlt ) return false;
|
|
return IsDown(ToVirtualKey(kb.keyCode));
|
|
}
|
|
|
|
private static bool IsDown(int vk) => (GetAsyncKeyState(vk) & 0x8000) != 0;
|
|
|
|
// Cache of the actual Game/Teleport keybind (resolved once, lazily, from the game's
|
|
// live InputAction so a rebind is picked up on the next popout open). Falls back to
|
|
// Shift+T — the game's own default — if resolution fails for any reason.
|
|
private static bool _teleportBindingResolved;
|
|
private static int _teleportVk = 0x54; // VK_T
|
|
private static int _teleportModifierVk = 0x10; // VK_SHIFT
|
|
|
|
private static void ResolveTeleportBinding() {
|
|
if (_teleportBindingResolved) return;
|
|
_teleportBindingResolved = true;
|
|
try {
|
|
var action = Traverse.Create(UI.GameInput.shared).Field("_teleportAction").GetValue<InputAction>();
|
|
if (action == null) return;
|
|
|
|
int mainVk = 0, modVk = 0;
|
|
foreach (var binding in action.bindings) {
|
|
int vk = KeyPathToVirtualKey(binding.effectivePath);
|
|
if (vk == 0) continue;
|
|
if (binding.isPartOfComposite && string.Equals(binding.name, "modifier", StringComparison.OrdinalIgnoreCase))
|
|
modVk = vk;
|
|
else if (!binding.isComposite)
|
|
mainVk = vk;
|
|
}
|
|
if (mainVk != 0) {
|
|
_teleportVk = mainVk;
|
|
_teleportModifierVk = modVk;
|
|
}
|
|
} catch (Exception ex) {
|
|
Log.Error($"[popout] failed to resolve Teleport keybind, falling back to Shift+T: {ex}");
|
|
}
|
|
}
|
|
|
|
// Maps an Input System key path (e.g. "<Keyboard>/t", "<Keyboard>/leftShift",
|
|
// "<Keyboard>/f5") to its Win32 virtual-key code. Returns 0 if unrecognised.
|
|
private static int KeyPathToVirtualKey(string? effectivePath) {
|
|
if (string.IsNullOrEmpty(effectivePath)) return 0;
|
|
int slash = effectivePath!.LastIndexOf('/');
|
|
string key = (slash >= 0 ? effectivePath.Substring(slash + 1) : effectivePath).ToLowerInvariant();
|
|
switch (key) {
|
|
case "leftshift": case "rightshift": case "shift": return 0x10;
|
|
case "leftctrl": case "rightctrl": case "ctrl": return 0x11;
|
|
case "leftalt": case "rightalt": case "alt": return 0x12;
|
|
}
|
|
if (key.Length == 1) {
|
|
char c = key[0];
|
|
if (c >= 'a' && c <= 'z') return char.ToUpperInvariant(c);
|
|
if (c >= '0' && c <= '9') return c;
|
|
}
|
|
if (key.Length >= 2 && key[0] == 'f' && int.TryParse(key.Substring(1), out int fn) && fn is >= 1 and <= 24)
|
|
return 0x70 + (fn - 1);
|
|
return 0;
|
|
}
|
|
|
|
private static int ToVirtualKey(KeyCode k) {
|
|
int c = (int)k;
|
|
if (c >= 97 && c <= 122) return c - 32;
|
|
if (c >= 282 && c <= 296) return 0x70 + (c - 282);
|
|
return c;
|
|
}
|
|
|
|
public void Destroy() {
|
|
if (_mapCamera != null)
|
|
MapCameraMemory.Capture(_mapCamera, _mapRotationDeg);
|
|
if (_mapCamera != null) {
|
|
_mapCamera.targetTexture = _savedTargetTexture;
|
|
_mapCamera.rect = _savedRect;
|
|
_mapCamera.aspect = _savedAspect;
|
|
_mapCamera.transform.rotation = Quaternion.Euler(_mapCamEulerX, 0f, _mapCamEulerZ);
|
|
_mapCamera = null;
|
|
}
|
|
if (_ownRT != null) {
|
|
// Skip Release() — let Destroy handle GPU teardown so the address stays
|
|
// live until end-of-frame. This prevents the pool from reissuing the same
|
|
// D3D handle to whoever creates a new RT in the same Update call.
|
|
UnityEngine.Object.Destroy(_ownRT);
|
|
_ownRT = null;
|
|
}
|
|
if (_windowHandle != 0) {
|
|
Native.RRPOPOUT_DestroyWindow(_windowHandle);
|
|
_windowHandle = 0;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
private void CreateOwnRT() {
|
|
if (_ownRT != null) { _ownRT.Release(); UnityEngine.Object.Destroy(_ownRT); }
|
|
// Use the game's original RT dimensions so Camera.pixelWidth AND pixelHeight
|
|
// are identical to the in-game map. camera.aspect is updated each frame in
|
|
// Update() to match the current window shape.
|
|
int rtW = _savedTargetTexture != null ? _savedTargetTexture.width : Mathf.Min(Screen.width, 1920);
|
|
int rtH = _savedTargetTexture != null ? _savedTargetTexture.height : Mathf.Min(Screen.height, 1080);
|
|
_ownRT = new RenderTexture(rtW, rtH, 24, RenderTextureFormat.ARGB32) { name = "RRPopout_MapRT" };
|
|
_ownRT.Create();
|
|
if (_mapCamera != null) {
|
|
_mapCamera.targetTexture = _ownRT;
|
|
_mapCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
|
}
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
// Apply the correct icon scale for the current zoom now that the map renders
|
|
// into our RT. MapBuilder.UpdateForZoom() recomputes MapBuilder.IconScale from
|
|
// the camera's orthographic size and rescales every map icon, matching how the
|
|
// game refreshes the map after a zoom.
|
|
PanelFinder.UpdateMapForZoom();
|
|
}
|
|
|
|
private void ForwardInputEvent(in InputEvent e) {
|
|
var cam = _mapCamera;
|
|
if (cam == null) return;
|
|
|
|
var viewportPos = new Vector2(e.x, 1f - e.y);
|
|
|
|
switch ((InputEventType)e.type) {
|
|
|
|
case InputEventType.MouseWheel:
|
|
cam.orthographicSize = Mathf.Clamp(
|
|
cam.orthographicSize * Mathf.Pow(kZoomFactor, e.delta),
|
|
kZoomMin, kZoomMax);
|
|
// The game rescales every map icon (locos, junctions, stations, flares)
|
|
// inside MapBuilder.UpdateForZoom(), which is normally driven by
|
|
// MapWindow.OnZoom(). Because we set orthographicSize directly, we must
|
|
// trigger it ourselves — otherwise icons keep their previous pixel size
|
|
// as you zoom in/out. This is the same call MapEnhancer makes after it
|
|
// adjusts the orthographic size.
|
|
PanelFinder.UpdateMapForZoom();
|
|
break;
|
|
|
|
case InputEventType.LButtonDown:
|
|
_isDragging = true;
|
|
_didDrag = false;
|
|
_dragStartX = e.x; _dragStartY = e.y;
|
|
_camStartX = cam.transform.position.x;
|
|
_camStartZ = cam.transform.position.z;
|
|
// Dragging cancels position follow (same behaviour as Google Maps grab-to-pan)
|
|
if (_followPlayer) {
|
|
_followPlayer = false;
|
|
Native.RRPOPOUT_SetFollowPlayer(_windowHandle, false);
|
|
}
|
|
break;
|
|
|
|
case InputEventType.LButtonUp:
|
|
if (!_didDrag)
|
|
{
|
|
var vp = new Vector2(e.x, 1f - e.y);
|
|
PanelFinder.GetMapDrag()?.OnClick?.Invoke(vp);
|
|
}
|
|
_isDragging = false;
|
|
_didDrag = false;
|
|
break;
|
|
|
|
case InputEventType.MouseMove:
|
|
_lastMouseX = e.x;
|
|
_lastMouseY = e.y;
|
|
if (!_isDragging) break;
|
|
if (!_didDrag &&
|
|
(Mathf.Abs(e.x - _dragStartX) > kClickThreshold ||
|
|
Mathf.Abs(e.y - _dragStartY) > kClickThreshold))
|
|
{
|
|
_didDrag = true;
|
|
if (PopoutModule.Settings.panDisablesFollow &&
|
|
MapEnhancerBridge.IsInstalled && MapEnhancerBridge.FollowMode)
|
|
MapEnhancerBridge.ToggleFollowMode();
|
|
}
|
|
if (!_didDrag) break;
|
|
float orthoSize = cam.orthographicSize;
|
|
float aspect = cam.aspect > 0f ? cam.aspect : 1.7f;
|
|
// Screen-space drag scaled to world units
|
|
float dxScreen = (e.x - _dragStartX) * orthoSize * 2f * aspect;
|
|
float dzScreen = (e.y - _dragStartY) * orthoSize * 2f;
|
|
// Rotate screen drag into world space using camera orientation.
|
|
// For top-down camera: right and up are horizontal (Y≈0) so no projection needed.
|
|
// Formula preserves original sign conventions at rotDeg=0.
|
|
Vector3 worldDelta = -cam.transform.right * dxScreen + cam.transform.up * dzScreen;
|
|
cam.transform.position = new Vector3(
|
|
_camStartX + worldDelta.x, cam.transform.position.y, _camStartZ + worldDelta.z);
|
|
break;
|
|
|
|
case InputEventType.RButtonUp:
|
|
if (PopoutModule.Settings.rightClickRecenter)
|
|
PanelFinder.GetMapWindow()?.ClickLocateMe();
|
|
break;
|
|
|
|
case InputEventType.UICommand:
|
|
switch ((UICmd)(int)e.x) {
|
|
case UICmd.Recenter:
|
|
PanelFinder.GetMapWindow()?.ClickLocateMe();
|
|
break;
|
|
case UICmd.FollowMode:
|
|
MapEnhancerBridge.ToggleFollowMode();
|
|
break;
|
|
case UICmd.FollowPlayerCam:
|
|
MapEnhancerBridge.FollowPlayerCamera();
|
|
break;
|
|
case UICmd.FollowSelectedLoco:
|
|
MapEnhancerBridge.FollowSelectedLoco();
|
|
break;
|
|
case UICmd.FollowLoco:
|
|
MapEnhancerBridge.FollowLoco((int)e.y);
|
|
break;
|
|
case UICmd.JumpToLocation:
|
|
MapEnhancerBridge.JumpToLocation((int)e.y);
|
|
break;
|
|
case UICmd.SetRotation:
|
|
_mapSyncPlayer = false;
|
|
Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, false);
|
|
ApplyMapRotation(e.y);
|
|
break;
|
|
case UICmd.RotateReset:
|
|
_mapSyncPlayer = false;
|
|
Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, false);
|
|
ApplyMapRotation(0f);
|
|
break;
|
|
case UICmd.RotateSyncPlayer:
|
|
_mapSyncPlayer = !_mapSyncPlayer;
|
|
Native.RRPOPOUT_SetMapSyncPlayer(_windowHandle, _mapSyncPlayer);
|
|
break;
|
|
case UICmd.ToggleFollowPlayer:
|
|
_followPlayer = !_followPlayer;
|
|
// Cancel any ME follow mode to avoid conflicting camera updates
|
|
if (_followPlayer && MapEnhancerBridge.IsInstalled && MapEnhancerBridge.FollowMode)
|
|
MapEnhancerBridge.ToggleFollowMode();
|
|
Native.RRPOPOUT_SetFollowPlayer(_windowHandle, _followPlayer);
|
|
break;
|
|
case UICmd.Close:
|
|
CloseRequested = true;
|
|
break;
|
|
case UICmd.SetTheme:
|
|
MapThemes.Apply((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);
|
|
PopoutModule.Settings.overlayAlpha = alpha;
|
|
PopoutModule.Persist();
|
|
Native.RRPOPOUT_SetOverlayAlpha(alpha);
|
|
break;
|
|
case UICmd.SetMapAlpha:
|
|
float mapAlpha = Mathf.Clamp(e.y, 0.0f, 1.0f);
|
|
PopoutModule.Settings.overlayMapAlpha = mapAlpha;
|
|
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;
|
|
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;
|
|
default:
|
|
{
|
|
var cmd = (UICmd)(int)e.x;
|
|
if (TrackLabelService.TryHandleCommand(cmd, e.y, _windowHandle))
|
|
break;
|
|
if (MapViewPresets.TryHandleCommand(cmd, e.y, _windowHandle, _mapCamera,
|
|
ApplyMapRotation, DisableFollowForPreset))
|
|
break;
|
|
MapWaypointSystem.TryHandleCommand(cmd, _windowHandle);
|
|
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);
|
|
}
|
|
}
|
|
}
|