372 lines
8.4 KiB
C#
372 lines
8.4 KiB
C#
using System.Collections.Generic;
|
|
using NeoModLoader.services;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public enum ChronicleKind
|
|
{
|
|
Kill,
|
|
Lover,
|
|
Friend,
|
|
World,
|
|
Other
|
|
}
|
|
|
|
public sealed class ChronicleEntry
|
|
{
|
|
public float CreatedAt;
|
|
public ChronicleKind Kind;
|
|
public long UnitId;
|
|
public string Name = "";
|
|
public string SpeciesId = "";
|
|
public string Line = "";
|
|
public Vector3 Position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session ring buffer of notable life/world events (kills, lovers, WorldLog), not spectator watch notes.
|
|
/// </summary>
|
|
public static class Chronicle
|
|
{
|
|
public const KeyCode ToggleKey = KeyCode.F9;
|
|
private const int MaxEntries = 300;
|
|
|
|
private static readonly List<ChronicleEntry> Entries = new List<ChronicleEntry>();
|
|
private static readonly HashSet<string> LoverPairKeys = new HashSet<string>();
|
|
private static readonly HashSet<string> FriendPairKeys = new HashSet<string>();
|
|
private static bool _hudReady;
|
|
|
|
public static int Count
|
|
{
|
|
get
|
|
{
|
|
lock (Entries)
|
|
{
|
|
return Entries.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool HudVisible => ChronicleHud.Visible;
|
|
|
|
public static IReadOnlyList<ChronicleEntry> Snapshot()
|
|
{
|
|
lock (Entries)
|
|
{
|
|
return Entries.ToArray();
|
|
}
|
|
}
|
|
|
|
public static ChronicleEntry Latest
|
|
{
|
|
get
|
|
{
|
|
lock (Entries)
|
|
{
|
|
return Entries.Count == 0 ? null : Entries[Entries.Count - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void EnsureWindow()
|
|
{
|
|
if (!ModSettings.ChronicleEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_hudReady && ChronicleHud.IsReady)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
ChronicleHud.EnsureBuilt();
|
|
if (ChronicleHud.IsReady)
|
|
{
|
|
_hudReady = true;
|
|
LogService.LogInfo("[IdleSpectator] Chronicle HUD ready (F9, life-event side panel)");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
LogService.LogInfo("[IdleSpectator] Chronicle HUD init failed: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
public static void ClearSession()
|
|
{
|
|
lock (Entries)
|
|
{
|
|
Entries.Clear();
|
|
}
|
|
|
|
LoverPairKeys.Clear();
|
|
FriendPairKeys.Clear();
|
|
}
|
|
|
|
public static void PollInput()
|
|
{
|
|
if (!Config.game_loaded || !ModSettings.ChronicleEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureWindow();
|
|
if (Input.GetKeyDown(ToggleKey))
|
|
{
|
|
ToggleWindow();
|
|
}
|
|
}
|
|
|
|
public static void ToggleWindow()
|
|
{
|
|
if (!ModSettings.ChronicleEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureWindow();
|
|
ChronicleHud.Toggle();
|
|
}
|
|
|
|
public static void ShowHud()
|
|
{
|
|
EnsureWindow();
|
|
ChronicleHud.SetVisible(true);
|
|
}
|
|
|
|
public static void HideHud()
|
|
{
|
|
ChronicleHud.SetVisible(false);
|
|
}
|
|
|
|
public static bool JumpToLatest()
|
|
{
|
|
return JumpTo(Latest);
|
|
}
|
|
|
|
public static bool JumpTo(ChronicleEntry entry)
|
|
{
|
|
if (entry == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Actor unit = FindUnitById(entry.UnitId);
|
|
if (unit != null && unit.isAlive())
|
|
{
|
|
CameraDirector.FocusUnit(unit);
|
|
WatchCaption.SetFromActor(unit);
|
|
return true;
|
|
}
|
|
|
|
if (entry.Position != Vector3.zero)
|
|
{
|
|
CameraDirector.PanTo(entry.Position);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void Update()
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || !Config.game_loaded || World.world == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChronicleHud.UpdateLive();
|
|
}
|
|
|
|
public static void NoteKill(Actor killer, Actor victim)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || killer == null || victim == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string killerName = SafeName(killer);
|
|
string victimName = SafeName(victim);
|
|
string killerSpecies = SpeciesOf(killer);
|
|
string victimSpecies = SpeciesOf(victim);
|
|
Append(
|
|
ChronicleKind.Kill,
|
|
killer,
|
|
$"Slew: {killerName} ({killerSpecies}) felled {victimName} ({victimSpecies})");
|
|
}
|
|
|
|
public static void NoteLovers(Actor a, Actor b)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || a == null || b == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string key = PairKey(a.getID(), b.getID());
|
|
if (!LoverPairKeys.Add(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Append(
|
|
ChronicleKind.Lover,
|
|
a,
|
|
$"Lovers: {SafeName(a)} and {SafeName(b)}");
|
|
}
|
|
|
|
public static void NoteBestFriends(Actor a, Actor b)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || a == null || b == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string key = PairKey(a.getID(), b.getID());
|
|
if (!FriendPairKeys.Add(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Append(
|
|
ChronicleKind.Friend,
|
|
a,
|
|
$"Friends: {SafeName(a)} befriended {SafeName(b)}");
|
|
}
|
|
|
|
public static void NoteWorldLog(WorldLogMessage message, string label)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || message == null || string.IsNullOrEmpty(label))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Actor unit = null;
|
|
try
|
|
{
|
|
if (message.hasFollowLocation())
|
|
{
|
|
unit = message.unit;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
Vector3 pos = Vector3.zero;
|
|
try
|
|
{
|
|
pos = message.getLocation();
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
Append(
|
|
ChronicleKind.World,
|
|
unit,
|
|
$"World: {label}",
|
|
fallbackPosition: pos);
|
|
}
|
|
|
|
/// <summary>Harness: inject a life-event style line for the current focus.</summary>
|
|
public static bool ForceNoteCurrentFocus(string prefix = "Slew")
|
|
{
|
|
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Actor unit = MoveCamera._focus_unit;
|
|
UnitDossier dossier = UnitDossier.FromActor(unit);
|
|
string kindPrefix = string.IsNullOrEmpty(prefix) ? "Slew" : prefix;
|
|
Append(
|
|
ChronicleKind.Other,
|
|
unit,
|
|
$"{kindPrefix}: {dossier.Name} ({dossier.SpeciesId}) - life event");
|
|
return true;
|
|
}
|
|
|
|
private static void Append(ChronicleKind kind, Actor unit, string line, Vector3 fallbackPosition = default)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChronicleEntry entry = new ChronicleEntry
|
|
{
|
|
CreatedAt = Time.unscaledTime,
|
|
Kind = kind,
|
|
UnitId = unit != null ? unit.getID() : 0,
|
|
Name = unit != null ? SafeName(unit) : "",
|
|
SpeciesId = unit != null ? SpeciesOf(unit) : "",
|
|
Line = line,
|
|
Position = unit != null ? unit.current_position : fallbackPosition
|
|
};
|
|
|
|
lock (Entries)
|
|
{
|
|
Entries.Add(entry);
|
|
while (Entries.Count > MaxEntries)
|
|
{
|
|
Entries.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
LogService.LogInfo("[IdleSpectator][CHRONICLE] " + line);
|
|
}
|
|
|
|
private static string PairKey(long a, long b)
|
|
{
|
|
long lo = a < b ? a : b;
|
|
long hi = a < b ? b : a;
|
|
return lo + ":" + hi;
|
|
}
|
|
|
|
private static Actor FindUnitById(long id)
|
|
{
|
|
if (id == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
|
{
|
|
if (actor != null && actor.getID() == id && actor.isAlive())
|
|
{
|
|
return actor;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string SpeciesOf(Actor actor)
|
|
{
|
|
return actor?.asset != null ? actor.asset.id : "creature";
|
|
}
|
|
|
|
private static string SafeName(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
string name = actor.getName();
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return SpeciesOf(actor);
|
|
}
|
|
}
|