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 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 List() { var s = PopoutModule.Settings; var names = s.presetNames ?? Array.Empty(); var xs = s.presetX ?? Array.Empty(); var zs = s.presetZ ?? Array.Empty(); var zooms = s.presetZoom ?? Array.Empty(); var rots = s.presetRot ?? Array.Empty(); int n = Math.Min(names.Length, Math.Min(xs.Length, Math.Min(zs.Length, Math.Min(zooms.Length, rots.Length)))); var list = new List(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 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 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 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 list) { int n = list.Count + 1; string name = $"View {n}"; var used = new HashSet(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 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 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 applyRotation) { if (cam != null) RestoreStash(cam, applyRotation); else _stashValid = false; } private static void RestoreStash(Camera cam, Action applyRotation) { if (!_stashValid) return; ApplyPreset(new MapViewPreset { x = _stashX, z = _stashZ, zoom = _stashZoom, rotationY = _stashRot }, cam, applyRotation); _stashValid = false; } } /// /// 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. /// 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 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; } }