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.
347 lines
10 KiB
C#
347 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Helpers;
|
|
using UI.Map;
|
|
using UnityEngine;
|
|
|
|
namespace S3.Modules.Popout;
|
|
|
|
internal struct MapViewPreset
|
|
{
|
|
public string name;
|
|
public float x, z, zoom, rotationY;
|
|
}
|
|
|
|
internal static class MapViewPresets
|
|
{
|
|
private static bool _stashValid;
|
|
private static float _stashX, _stashZ, _stashZoom, _stashRot;
|
|
|
|
public static void PushList(int handle)
|
|
{
|
|
try
|
|
{
|
|
var presets = List();
|
|
if (presets.Count == 0)
|
|
{
|
|
Native.RRPOPOUT_SetPresetList(handle, "");
|
|
return;
|
|
}
|
|
var sb = new StringBuilder();
|
|
for (int i = 0; i < presets.Count; i++)
|
|
{
|
|
if (i > 0) sb.Append('\n');
|
|
sb.Append(string.IsNullOrEmpty(presets[i].name) ? $"View {i + 1}" : presets[i].name);
|
|
}
|
|
Native.RRPOPOUT_SetPresetList(handle, sb.ToString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
S3.Core.Log.Error($"[popout] preset list push failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static string ReadRename(int handle)
|
|
{
|
|
try
|
|
{
|
|
var buf = new StringBuilder(128);
|
|
Native.RRPOPOUT_GetPresetRenameName(handle, buf, 128);
|
|
return buf.ToString().Trim();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
S3.Core.Log.Error($"[popout] preset rename read failed: {ex.Message}");
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public static bool TryHandleCommand(UICmd cmd, float y, int handle, Camera? cam,
|
|
Action<float> applyRotation, Action disableFollow)
|
|
{
|
|
int index = (int)y;
|
|
switch (cmd)
|
|
{
|
|
case UICmd.PresetAdd:
|
|
if (cam == null) return true;
|
|
AddCurrent(cam);
|
|
PushList(handle);
|
|
return true;
|
|
case UICmd.PresetApply:
|
|
if (cam == null) return true;
|
|
disableFollow();
|
|
Apply(index, cam, applyRotation);
|
|
return true;
|
|
case UICmd.PresetDelete:
|
|
Delete(index);
|
|
CancelStash(cam, applyRotation);
|
|
PushList(handle);
|
|
return true;
|
|
case UICmd.PresetRename:
|
|
Rename(index, ReadRename(handle));
|
|
PushList(handle);
|
|
return true;
|
|
case UICmd.PresetPreview:
|
|
if (cam == null) return true;
|
|
disableFollow();
|
|
Preview(index, cam, applyRotation);
|
|
return true;
|
|
case UICmd.PresetCommitEdit:
|
|
if (cam == null) return true;
|
|
Rename(index, ReadRename(handle));
|
|
CommitEdit(index, cam, applyRotation);
|
|
PushList(handle);
|
|
return true;
|
|
case UICmd.PresetCancelEdit:
|
|
CancelStash(cam, applyRotation);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static List<MapViewPreset> List()
|
|
{
|
|
var s = PopoutModule.Settings;
|
|
var names = s.presetNames ?? Array.Empty<string>();
|
|
var xs = s.presetX ?? Array.Empty<float>();
|
|
var zs = s.presetZ ?? Array.Empty<float>();
|
|
var zooms = s.presetZoom ?? Array.Empty<float>();
|
|
var rots = s.presetRot ?? Array.Empty<float>();
|
|
int n = Math.Min(names.Length, Math.Min(xs.Length, Math.Min(zs.Length, Math.Min(zooms.Length, rots.Length))));
|
|
var list = new List<MapViewPreset>(n);
|
|
for (int i = 0; i < n; i++)
|
|
list.Add(new MapViewPreset
|
|
{
|
|
name = names[i] ?? $"View {i + 1}",
|
|
x = xs[i], z = zs[i], zoom = zooms[i], rotationY = rots[i]
|
|
});
|
|
return list;
|
|
}
|
|
|
|
private static void Save(List<MapViewPreset> list)
|
|
{
|
|
int n = list.Count;
|
|
var names = new string[n];
|
|
var xs = new float[n];
|
|
var zs = new float[n];
|
|
var zooms = new float[n];
|
|
var rots = new float[n];
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
names[i] = list[i].name ?? "";
|
|
xs[i] = list[i].x;
|
|
zs[i] = list[i].z;
|
|
zooms[i] = list[i].zoom;
|
|
rots[i] = list[i].rotationY;
|
|
}
|
|
var s = PopoutModule.Settings;
|
|
s.presetNames = names;
|
|
s.presetX = xs;
|
|
s.presetZ = zs;
|
|
s.presetZoom = zooms;
|
|
s.presetRot = rots;
|
|
PopoutModule.Persist();
|
|
}
|
|
|
|
private static MapViewPreset Capture(Camera cam)
|
|
{
|
|
MigrateLegacyIfNeeded(cam);
|
|
ToGameXZ(cam.transform.position, out float x, out float z);
|
|
return new MapViewPreset
|
|
{
|
|
name = "",
|
|
x = x,
|
|
z = z,
|
|
zoom = cam.orthographicSize,
|
|
rotationY = cam.transform.eulerAngles.y,
|
|
};
|
|
}
|
|
|
|
private static void Apply(int index, Camera cam, Action<float> applyRotation)
|
|
{
|
|
MigrateLegacyIfNeeded(cam);
|
|
var presets = List();
|
|
if (index < 0 || index >= presets.Count) return;
|
|
ApplyPreset(presets[index], cam, applyRotation);
|
|
}
|
|
|
|
private static void ApplyPreset(MapViewPreset p, Camera cam, Action<float> applyRotation)
|
|
{
|
|
SetCameraXZ(cam, p.x, p.z, PopoutModule.Settings.presetUseGameCoords);
|
|
cam.orthographicSize = Mathf.Clamp(p.zoom, 25f, 10000f);
|
|
applyRotation(p.rotationY);
|
|
PanelFinder.UpdateMapForZoom();
|
|
}
|
|
|
|
// Old presets stored Unity world XZ. After a floating-origin rebase those
|
|
// numbers no longer map to the same place on the railroad. Convert the
|
|
// whole list the first time we save in game space.
|
|
private static void MigrateLegacyIfNeeded(Camera cam)
|
|
{
|
|
var s = PopoutModule.Settings;
|
|
if (s.presetUseGameCoords) return;
|
|
var list = List();
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
var p = list[i];
|
|
var world = new Vector3(p.x, cam.transform.position.y, p.z);
|
|
ToGameXZ(world, out p.x, out p.z);
|
|
list[i] = p;
|
|
}
|
|
s.presetUseGameCoords = true;
|
|
if (list.Count > 0) Save(list);
|
|
else PopoutModule.Persist();
|
|
}
|
|
|
|
internal static void ToGameXZ(Vector3 world, out float x, out float z)
|
|
{
|
|
try
|
|
{
|
|
var g = WorldTransformer.WorldToGame(world);
|
|
x = g.x; z = g.z;
|
|
}
|
|
catch
|
|
{
|
|
x = world.x; z = world.z;
|
|
}
|
|
}
|
|
|
|
internal static void SetCameraXZ(Camera cam, float x, float z, bool gameSpace)
|
|
{
|
|
var t = cam.transform;
|
|
if (!gameSpace)
|
|
{
|
|
t.position = new Vector3(x, t.position.y, z);
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
var world = WorldTransformer.GameToWorld(new Vector3(x, 0f, z));
|
|
world.y = t.position.y;
|
|
t.position = world;
|
|
}
|
|
catch
|
|
{
|
|
t.position = new Vector3(x, t.position.y, z);
|
|
}
|
|
}
|
|
|
|
private static void AddCurrent(Camera cam)
|
|
{
|
|
var list = List();
|
|
var p = Capture(cam);
|
|
p.name = NextName(list);
|
|
list.Add(p);
|
|
Save(list);
|
|
}
|
|
|
|
private static string NextName(List<MapViewPreset> list)
|
|
{
|
|
int n = list.Count + 1;
|
|
string name = $"View {n}";
|
|
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var p in list)
|
|
if (!string.IsNullOrEmpty(p.name)) used.Add(p.name);
|
|
while (used.Contains(name))
|
|
{
|
|
n++;
|
|
name = $"View {n}";
|
|
}
|
|
return name;
|
|
}
|
|
|
|
private static void Delete(int index)
|
|
{
|
|
var list = List();
|
|
if (index < 0 || index >= list.Count) return;
|
|
list.RemoveAt(index);
|
|
Save(list);
|
|
}
|
|
|
|
private static void Rename(int index, string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return;
|
|
var list = List();
|
|
if (index < 0 || index >= list.Count) return;
|
|
var p = list[index];
|
|
p.name = name.Trim();
|
|
list[index] = p;
|
|
Save(list);
|
|
}
|
|
|
|
private static void Preview(int index, Camera cam, Action<float> applyRotation)
|
|
{
|
|
if (!_stashValid)
|
|
{
|
|
var cur = Capture(cam);
|
|
_stashX = cur.x; _stashZ = cur.z; _stashZoom = cur.zoom; _stashRot = cur.rotationY;
|
|
_stashValid = true;
|
|
}
|
|
Apply(index, cam, applyRotation);
|
|
}
|
|
|
|
private static void CommitEdit(int index, Camera cam, Action<float> applyRotation)
|
|
{
|
|
var list = List();
|
|
if (index < 0 || index >= list.Count)
|
|
{
|
|
CancelStash(cam, applyRotation);
|
|
return;
|
|
}
|
|
var p = Capture(cam);
|
|
p.name = list[index].name;
|
|
list[index] = p;
|
|
Save(list);
|
|
RestoreStash(cam, applyRotation);
|
|
}
|
|
|
|
private static void CancelStash(Camera? cam, Action<float> applyRotation)
|
|
{
|
|
if (cam != null) RestoreStash(cam, applyRotation);
|
|
else _stashValid = false;
|
|
}
|
|
|
|
private static void RestoreStash(Camera cam, Action<float> applyRotation)
|
|
{
|
|
if (!_stashValid) return;
|
|
ApplyPreset(new MapViewPreset
|
|
{
|
|
x = _stashX, z = _stashZ, zoom = _stashZoom, rotationY = _stashRot
|
|
}, cam, applyRotation);
|
|
_stashValid = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remembers the map camera between overlay/popout close and the next open.
|
|
/// MapEnhancer patches OnWindowShown to snap the camera to the player; we
|
|
/// capture before teardown and write the view back after Show() returns.
|
|
/// </summary>
|
|
internal static class MapCameraMemory
|
|
{
|
|
public static void Capture(Camera? cam, float rotationY)
|
|
{
|
|
if (cam == null) return;
|
|
var s = PopoutModule.Settings;
|
|
MapViewPresets.ToGameXZ(cam.transform.position, out s.lastViewX, out s.lastViewZ);
|
|
s.lastViewZoom = cam.orthographicSize;
|
|
s.lastViewRot = rotationY;
|
|
s.lastViewValid = true;
|
|
s.lastViewIsGame = true;
|
|
PopoutModule.Persist();
|
|
}
|
|
|
|
public static bool TryRestore(Camera? cam, Action<float> applyRotation)
|
|
{
|
|
if (cam == null) return false;
|
|
var s = PopoutModule.Settings;
|
|
if (!s.lastViewValid) return false;
|
|
MapViewPresets.SetCameraXZ(cam, s.lastViewX, s.lastViewZ, s.lastViewIsGame);
|
|
cam.orthographicSize = Mathf.Clamp(s.lastViewZoom, 25f, 10000f);
|
|
applyRotation(s.lastViewRot);
|
|
PanelFinder.UpdateMapForZoom();
|
|
return true;
|
|
}
|
|
}
|