826 lines
22 KiB
C#
826 lines
22 KiB
C#
using System.Collections.Generic;
|
|
using NeoModLoader.services;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public enum ChronicleKind
|
|
{
|
|
Death,
|
|
Kill,
|
|
Lover,
|
|
Friend,
|
|
World,
|
|
Other
|
|
}
|
|
|
|
public enum ChronicleTab
|
|
{
|
|
History,
|
|
World
|
|
}
|
|
|
|
public sealed class ChronicleEntry
|
|
{
|
|
public float CreatedAt;
|
|
/// <summary>WorldBox sim time when the event was recorded (<see cref="MapBox.getCurWorldTime"/>).</summary>
|
|
public double WorldTime;
|
|
/// <summary>In-world month + year, e.g. <c>January 1</c> (no brackets).</summary>
|
|
public string DateLabel = "";
|
|
public ChronicleKind Kind;
|
|
public long SubjectId;
|
|
public long OtherId;
|
|
public string Name = "";
|
|
public string SpeciesId = "";
|
|
public string Line = "";
|
|
public Vector3 Position;
|
|
|
|
/// <summary>Back-compat for harness / jump helpers.</summary>
|
|
public long UnitId => SubjectId;
|
|
|
|
/// <summary>Plain date-prefixed line for logs / asserts.</summary>
|
|
public string DisplayLine
|
|
{
|
|
get
|
|
{
|
|
if (string.IsNullOrEmpty(DateLabel))
|
|
{
|
|
return Line ?? "";
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(Line))
|
|
{
|
|
return "[" + DateLabel + "]";
|
|
}
|
|
|
|
return "[" + DateLabel + "] " + Line;
|
|
}
|
|
}
|
|
|
|
/// <summary>HUD line with a muted colored date in brackets (Unity rich text).</summary>
|
|
public string DisplayLineRich
|
|
{
|
|
get
|
|
{
|
|
if (string.IsNullOrEmpty(DateLabel))
|
|
{
|
|
return Line ?? "";
|
|
}
|
|
|
|
const string color = "#A8B4C8";
|
|
string stamped = "<color=" + color + ">[" + DateLabel + "]</color>";
|
|
if (string.IsNullOrEmpty(Line))
|
|
{
|
|
return stamped;
|
|
}
|
|
|
|
return stamped + " " + Line;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session chronicles: per-character History + separate World feed (WorldLog Story/Epic).
|
|
/// </summary>
|
|
public static class Chronicle
|
|
{
|
|
public const KeyCode ToggleKey = KeyCode.F9;
|
|
private const int MaxHistoryPerUnit = 40;
|
|
private const int MaxWorldEntries = 100;
|
|
|
|
private static readonly Dictionary<long, List<ChronicleEntry>> Histories =
|
|
new Dictionary<long, List<ChronicleEntry>>();
|
|
private static readonly List<ChronicleEntry> WorldEntries = new List<ChronicleEntry>();
|
|
private static readonly HashSet<string> LoverPairKeys = new HashSet<string>();
|
|
private static readonly HashSet<string> FriendPairKeys = new HashSet<string>();
|
|
private static readonly HashSet<long> DeathLogged = new HashSet<long>();
|
|
private static bool _hudReady;
|
|
private static long _lastHistorySubjectId;
|
|
private static object _boundWorld;
|
|
|
|
public static void OnSpectatorEnabled()
|
|
{
|
|
object world = World.world;
|
|
if (!ReferenceEquals(world, _boundWorld))
|
|
{
|
|
ClearSession();
|
|
_boundWorld = world;
|
|
LogService.LogInfo("[IdleSpectator] Chronicle cleared for new world session");
|
|
}
|
|
}
|
|
|
|
public static int Count => HistoryCountFor(CurrentHistorySubjectId()) + WorldCount;
|
|
|
|
public static int WorldCount
|
|
{
|
|
get
|
|
{
|
|
lock (WorldEntries)
|
|
{
|
|
return WorldEntries.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool HudVisible => ChronicleHud.Visible;
|
|
|
|
public static long LastHistorySubjectId => _lastHistorySubjectId;
|
|
|
|
public static int HistoryCountFor(long subjectId)
|
|
{
|
|
if (subjectId == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
lock (Histories)
|
|
{
|
|
return Histories.TryGetValue(subjectId, out List<ChronicleEntry> list) ? list.Count : 0;
|
|
}
|
|
}
|
|
|
|
public static IReadOnlyList<ChronicleEntry> SnapshotForFocus()
|
|
{
|
|
return SnapshotForSubject(CurrentHistorySubjectId());
|
|
}
|
|
|
|
public static IReadOnlyList<ChronicleEntry> SnapshotForSubject(long subjectId)
|
|
{
|
|
if (subjectId == 0)
|
|
{
|
|
return System.Array.Empty<ChronicleEntry>();
|
|
}
|
|
|
|
lock (Histories)
|
|
{
|
|
if (!Histories.TryGetValue(subjectId, out List<ChronicleEntry> list))
|
|
{
|
|
return System.Array.Empty<ChronicleEntry>();
|
|
}
|
|
|
|
return list.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>Newest-first slice for compact dossier HUD.</summary>
|
|
public static IReadOnlyList<ChronicleEntry> LatestForSubject(long subjectId, int max)
|
|
{
|
|
if (max <= 0)
|
|
{
|
|
return System.Array.Empty<ChronicleEntry>();
|
|
}
|
|
|
|
IReadOnlyList<ChronicleEntry> all = SnapshotForSubject(subjectId);
|
|
if (all.Count == 0)
|
|
{
|
|
return all;
|
|
}
|
|
|
|
int take = Mathf.Min(max, all.Count);
|
|
ChronicleEntry[] newest = new ChronicleEntry[take];
|
|
for (int i = 0; i < take; i++)
|
|
{
|
|
newest[i] = all[all.Count - 1 - i];
|
|
}
|
|
|
|
return newest;
|
|
}
|
|
|
|
public static IReadOnlyList<ChronicleEntry> SnapshotWorld()
|
|
{
|
|
lock (WorldEntries)
|
|
{
|
|
return WorldEntries.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>Latest history entry for the current/last focus (harness + jump).</summary>
|
|
public static ChronicleEntry Latest
|
|
{
|
|
get
|
|
{
|
|
IReadOnlyList<ChronicleEntry> list = SnapshotForFocus();
|
|
return list.Count == 0 ? null : list[list.Count - 1];
|
|
}
|
|
}
|
|
|
|
public static ChronicleEntry LatestWorld
|
|
{
|
|
get
|
|
{
|
|
lock (WorldEntries)
|
|
{
|
|
return WorldEntries.Count == 0 ? null : WorldEntries[WorldEntries.Count - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
public static long CurrentHistorySubjectId()
|
|
{
|
|
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
|
{
|
|
try
|
|
{
|
|
long id = MoveCamera._focus_unit.getID();
|
|
if (id != 0)
|
|
{
|
|
_lastHistorySubjectId = id;
|
|
return id;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
return _lastHistorySubjectId;
|
|
}
|
|
|
|
public static string CurrentHistoryTitle()
|
|
{
|
|
long id = CurrentHistorySubjectId();
|
|
if (id == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
Actor unit = FindUnitById(id);
|
|
if (unit != null)
|
|
{
|
|
return $"{SafeName(unit)} ({SpeciesOf(unit)})";
|
|
}
|
|
|
|
IReadOnlyList<ChronicleEntry> list = SnapshotForSubject(id);
|
|
if (list.Count > 0)
|
|
{
|
|
ChronicleEntry e = list[list.Count - 1];
|
|
if (!string.IsNullOrEmpty(e.Name))
|
|
{
|
|
return string.IsNullOrEmpty(e.SpeciesId) ? e.Name : $"{e.Name} ({e.SpeciesId})";
|
|
}
|
|
}
|
|
|
|
return "Unit #" + id;
|
|
}
|
|
|
|
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, History|World)");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
LogService.LogInfo("[IdleSpectator] Chronicle HUD init failed: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
public static void ClearSession()
|
|
{
|
|
lock (Histories)
|
|
{
|
|
Histories.Clear();
|
|
}
|
|
|
|
lock (WorldEntries)
|
|
{
|
|
WorldEntries.Clear();
|
|
}
|
|
|
|
LoverPairKeys.Clear();
|
|
FriendPairKeys.Clear();
|
|
DeathLogged.Clear();
|
|
_lastHistorySubjectId = 0;
|
|
// Keep _boundWorld so re-enable on same map does not loop-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.SubjectId);
|
|
if (unit != null && unit.isAlive())
|
|
{
|
|
CameraDirector.FocusUnit(unit);
|
|
WatchCaption.SetFromActor(unit);
|
|
return true;
|
|
}
|
|
|
|
if (entry.OtherId != 0)
|
|
{
|
|
Actor other = FindUnitById(entry.OtherId);
|
|
if (other != null && other.isAlive())
|
|
{
|
|
CameraDirector.FocusUnit(other);
|
|
WatchCaption.SetFromActor(other);
|
|
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;
|
|
}
|
|
|
|
// Keep last history subject sticky when focus dies / drops briefly.
|
|
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
|
{
|
|
try
|
|
{
|
|
long id = MoveCamera._focus_unit.getID();
|
|
if (id != 0)
|
|
{
|
|
_lastHistorySubjectId = id;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
ChronicleHud.UpdateLive();
|
|
}
|
|
|
|
/// <summary>Killer POV - character history only.</summary>
|
|
public static void NoteKill(Actor killer, Actor victim)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || killer == null || victim == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AppendHistory(
|
|
ChronicleKind.Kill,
|
|
killer,
|
|
victim,
|
|
$"Killed {SafeName(victim)} ({SpeciesOf(victim)})");
|
|
}
|
|
|
|
/// <summary>Victim POV - cause of death on character history.</summary>
|
|
public static void NoteDeath(Actor victim, AttackType attackType, Actor killer)
|
|
{
|
|
if (!ModSettings.ChronicleEnabled || victim == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long id = 0;
|
|
try
|
|
{
|
|
id = victim.getID();
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (id == 0 || !DeathLogged.Add(id))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string line = FormatDeathCause(victim, attackType, killer);
|
|
AppendHistory(ChronicleKind.Death, victim, killer, line);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
AppendHistory(ChronicleKind.Lover, a, b, $"Became lovers with {SafeName(b)}");
|
|
AppendHistory(ChronicleKind.Lover, b, a, $"Became lovers with {SafeName(a)}");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
AppendHistory(ChronicleKind.Friend, a, b, $"Befriended {SafeName(b)}");
|
|
AppendHistory(ChronicleKind.Friend, b, a, $"Befriended {SafeName(a)}");
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
string line = label;
|
|
AppendWorld(unit, line, pos);
|
|
|
|
// Mirror onto that unit's History when WorldLog points at someone.
|
|
if (unit != null)
|
|
{
|
|
AppendHistory(ChronicleKind.World, unit, null, line);
|
|
}
|
|
}
|
|
|
|
/// <summary>Harness: inject a life-event style line on the focused unit's History.</summary>
|
|
public static bool ForceNoteCurrentFocus(string prefix = "Killed")
|
|
{
|
|
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Actor unit = MoveCamera._focus_unit;
|
|
string kindPrefix = string.IsNullOrEmpty(prefix) ? "Killed" : prefix;
|
|
// Short tags keep the old "Killed: Name (species) - life event" shape.
|
|
// Long strings are treated as the full event line (dossier wrap tests).
|
|
string line;
|
|
if (kindPrefix.Length <= 24 && kindPrefix.IndexOf(' ') < 0)
|
|
{
|
|
UnitDossier dossier = UnitDossier.FromActor(unit);
|
|
line = $"{kindPrefix}: {dossier.Name} ({dossier.SpeciesId}) - life event";
|
|
}
|
|
else
|
|
{
|
|
line = kindPrefix;
|
|
}
|
|
|
|
AppendHistory(ChronicleKind.Other, unit, null, line);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Harness: inject a World-feed line (does not touch character History).</summary>
|
|
public static bool ForceWorldNote(string label)
|
|
{
|
|
if (string.IsNullOrEmpty(label))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AppendWorld(null, label, Vector3.zero);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Harness: append a formatted death-cause line for the focused unit.</summary>
|
|
public static bool ForceDeathSample(string attackTypeName, Actor killer = null)
|
|
{
|
|
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!System.Enum.TryParse(attackTypeName, ignoreCase: true, out AttackType attackType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Actor victim = MoveCamera._focus_unit;
|
|
string line = FormatDeathCause(victim, attackType, killer);
|
|
AppendHistory(ChronicleKind.Death, victim, killer, line);
|
|
return true;
|
|
}
|
|
|
|
public static string FormatDeathCause(Actor victim, AttackType attackType, Actor killer)
|
|
{
|
|
string killerLabel = killer != null
|
|
? $"{SafeName(killer)} ({SpeciesOf(killer)})"
|
|
: "";
|
|
|
|
switch (attackType)
|
|
{
|
|
case AttackType.Age:
|
|
return "Died of old age";
|
|
case AttackType.Starvation:
|
|
return "Starved to death";
|
|
case AttackType.Fire:
|
|
return IsOnLava(victim) ? "Burned in lava" : "Burned to death";
|
|
case AttackType.Drowning:
|
|
case AttackType.Water:
|
|
return "Drowned";
|
|
case AttackType.Poison:
|
|
return "Died of poison";
|
|
case AttackType.Plague:
|
|
return "Died of plague";
|
|
case AttackType.Infection:
|
|
return "Died of infection";
|
|
case AttackType.Tumor:
|
|
return "Died of a tumor";
|
|
case AttackType.Acid:
|
|
return "Dissolved in acid";
|
|
case AttackType.AshFever:
|
|
return "Died of ash fever";
|
|
case AttackType.Gravity:
|
|
return "Fell to their death";
|
|
case AttackType.Explosion:
|
|
return "Died in an explosion";
|
|
case AttackType.Divine:
|
|
return "Struck down by divine power";
|
|
case AttackType.Eaten:
|
|
return string.IsNullOrEmpty(killerLabel)
|
|
? "Was eaten"
|
|
: $"Eaten by {killerLabel}";
|
|
case AttackType.Weapon:
|
|
case AttackType.Other:
|
|
case AttackType.Smile:
|
|
if (!string.IsNullOrEmpty(killerLabel))
|
|
{
|
|
return $"Killed by {killerLabel}";
|
|
}
|
|
|
|
return attackType == AttackType.Weapon ? "Killed in combat" : "Died";
|
|
case AttackType.Metamorphosis:
|
|
return "Transformed away";
|
|
case AttackType.None:
|
|
return "Died";
|
|
default:
|
|
return string.IsNullOrEmpty(killerLabel)
|
|
? $"Died ({attackType})"
|
|
: $"Killed by {killerLabel}";
|
|
}
|
|
}
|
|
|
|
private static bool IsOnLava(Actor victim)
|
|
{
|
|
try
|
|
{
|
|
WorldTile tile = victim.current_tile;
|
|
return tile != null && tile.Type != null && tile.Type.lava;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void AppendHistory(ChronicleKind kind, Actor subject, Actor other, string line)
|
|
{
|
|
if (subject == null || string.IsNullOrEmpty(line))
|
|
{
|
|
return;
|
|
}
|
|
|
|
long subjectId = subject.getID();
|
|
if (subjectId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StampWorldDate(out double worldTime, out string dateLabel);
|
|
ChronicleEntry entry = new ChronicleEntry
|
|
{
|
|
CreatedAt = Time.unscaledTime,
|
|
WorldTime = worldTime,
|
|
DateLabel = dateLabel,
|
|
Kind = kind,
|
|
SubjectId = subjectId,
|
|
OtherId = other != null ? other.getID() : 0,
|
|
Name = SafeName(subject),
|
|
SpeciesId = SpeciesOf(subject),
|
|
Line = line,
|
|
Position = subject.current_position
|
|
};
|
|
|
|
lock (Histories)
|
|
{
|
|
if (!Histories.TryGetValue(subjectId, out List<ChronicleEntry> list))
|
|
{
|
|
list = new List<ChronicleEntry>();
|
|
Histories[subjectId] = list;
|
|
}
|
|
|
|
list.Add(entry);
|
|
while (list.Count > MaxHistoryPerUnit)
|
|
{
|
|
list.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
LogService.LogInfo($"[IdleSpectator][CHRONICLE][History:{subjectId}] {entry.DisplayLine}");
|
|
}
|
|
|
|
private static void AppendWorld(Actor unit, string line, Vector3 fallbackPosition)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
return;
|
|
}
|
|
|
|
StampWorldDate(out double worldTime, out string dateLabel);
|
|
ChronicleEntry entry = new ChronicleEntry
|
|
{
|
|
CreatedAt = Time.unscaledTime,
|
|
WorldTime = worldTime,
|
|
DateLabel = dateLabel,
|
|
Kind = ChronicleKind.World,
|
|
SubjectId = unit != null ? unit.getID() : 0,
|
|
OtherId = 0,
|
|
Name = unit != null ? SafeName(unit) : "",
|
|
SpeciesId = unit != null ? SpeciesOf(unit) : "",
|
|
Line = line,
|
|
Position = unit != null ? unit.current_position : fallbackPosition
|
|
};
|
|
|
|
lock (WorldEntries)
|
|
{
|
|
WorldEntries.Add(entry);
|
|
while (WorldEntries.Count > MaxWorldEntries)
|
|
{
|
|
WorldEntries.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
LogService.LogInfo("[IdleSpectator][CHRONICLE][World] " + entry.DisplayLine);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Capture current WorldBox calendar as month + year (e.g. <c>January 1</c>).
|
|
/// </summary>
|
|
private static void StampWorldDate(out double worldTime, out string dateLabel)
|
|
{
|
|
worldTime = 0;
|
|
dateLabel = "";
|
|
try
|
|
{
|
|
if (World.world == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
worldTime = World.world.getCurWorldTime();
|
|
int[] raw = Date.getRawDate(worldTime);
|
|
if (raw == null || raw.Length < 3)
|
|
{
|
|
dateLabel = "y" + Date.getCurrentYear() + " m" + Date.getCurrentMonth();
|
|
return;
|
|
}
|
|
|
|
// raw: [day, month, year] - UI uses month + year only
|
|
string monthName = Date.formatMonth(raw[1]);
|
|
if (string.IsNullOrEmpty(monthName))
|
|
{
|
|
monthName = "m" + raw[1];
|
|
}
|
|
|
|
dateLabel = monthName + " " + raw[2];
|
|
}
|
|
catch
|
|
{
|
|
dateLabel = "";
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|