Enhance Chronicle and HUD functionality with new subject management features. Implemented subject cap adjustments, timed rebuilds for Fallen/Living lists, and added refresh capabilities to the HUD. Updated test scenarios to validate new behaviors and ensure proper subject handling.
This commit is contained in:
parent
88cf59a6aa
commit
0b23a15618
6 changed files with 695 additions and 36 deletions
|
|
@ -112,6 +112,7 @@ A loaded world is still required.
|
||||||
| `discovery` | Present dedupe + force drain + tip match |
|
| `discovery` | Present dedupe + force drain + tip match |
|
||||||
| `world_log` | Story/Epic interest interrupt via collector |
|
| `world_log` | Story/Epic interest interrupt via collector |
|
||||||
| `chronicle_smoke` | History inject, death-cause samples, World feed isolation, History\|World tabs, HUD jump |
|
| `chronicle_smoke` | History inject, death-cause samples, World feed isolation, History\|World tabs, HUD jump |
|
||||||
|
| `chronicle_subject_bench` | Timed Fallen list rebuilds at rising subject caps; recommends a budgeted max |
|
||||||
| `smoke` | Short enable/focus health check |
|
| `smoke` | Short enable/focus health check |
|
||||||
| `tip_match` | Tip asset must match focused unit |
|
| `tip_match` | Tip asset must match focused unit |
|
||||||
| `regression` | Shell suite of all gate scenarios above |
|
| `regression` | Shell suite of all gate scenarios above |
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,19 @@ public static class AgentHarness
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "lore_refresh":
|
||||||
|
case "chronicle_refresh":
|
||||||
|
{
|
||||||
|
ChronicleHud.RefreshCastList();
|
||||||
|
_cmdOk++;
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok: true,
|
||||||
|
detail:
|
||||||
|
$"stale={ChronicleHud.CastListStale} top='{ChronicleHud.LastListTopTitle}' tab={ChronicleHud.ActiveTab}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "lore_search":
|
case "lore_search":
|
||||||
{
|
{
|
||||||
Chronicle.ShowHud();
|
Chronicle.ShowHud();
|
||||||
|
|
@ -581,6 +594,166 @@ public static class AgentHarness
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "chronicle_orphan_flood":
|
||||||
|
{
|
||||||
|
// Stress subject-cap pruning (does not need living units).
|
||||||
|
int n = Math.Max(1, cmd.count);
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
long id = Chronicle.ForceOrphanHistory(
|
||||||
|
"Flood" + i,
|
||||||
|
"human",
|
||||||
|
1,
|
||||||
|
"Flood",
|
||||||
|
AttackType.Age);
|
||||||
|
if (id != 0)
|
||||||
|
{
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = made == n
|
||||||
|
&& Chronicle.HistorySubjectCount <= Chronicle.MaxHistorySubjects;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"made={made}/{n} subjects={Chronicle.HistorySubjectCount} max={Chronicle.MaxHistorySubjects} dossiers={Chronicle.FinalDossierCount}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "chronicle_set_subject_cap":
|
||||||
|
{
|
||||||
|
int cap = Math.Max(40, cmd.count);
|
||||||
|
if (!string.IsNullOrEmpty(cmd.value)
|
||||||
|
&& int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
||||||
|
{
|
||||||
|
cap = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Chronicle.SetMaxHistorySubjects(cap);
|
||||||
|
bool ok = Chronicle.MaxHistorySubjects == Mathf.Clamp(cap, 40, Chronicle.AbsoluteMaxHistorySubjects)
|
||||||
|
&& Chronicle.HistorySubjectCount <= Chronicle.MaxHistorySubjects;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"cap={Chronicle.MaxHistorySubjects} subjects={Chronicle.HistorySubjectCount}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "chronicle_subject_bench":
|
||||||
|
{
|
||||||
|
// value: comma-separated sizes, e.g. "320,640,1280,2560"
|
||||||
|
// count: events per subject (default 3). expect: max avg fallen ms budget (default 2).
|
||||||
|
string sizesRaw = string.IsNullOrEmpty(cmd.value)
|
||||||
|
? "320,640,1280,2560,4096,8192"
|
||||||
|
: cmd.value;
|
||||||
|
int eventsPer = Math.Max(1, cmd.count);
|
||||||
|
if (eventsPer == 1 && string.IsNullOrEmpty(cmd.label))
|
||||||
|
{
|
||||||
|
eventsPer = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
float budgetMs = 2f;
|
||||||
|
if (!string.IsNullOrEmpty(cmd.expect)
|
||||||
|
&& float.TryParse(cmd.expect, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedBudget))
|
||||||
|
{
|
||||||
|
budgetMs = parsedBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] parts = sizesRaw.Split(',');
|
||||||
|
var reports = new List<string>();
|
||||||
|
int best = 0;
|
||||||
|
float bestAvg = -1f;
|
||||||
|
bool any = false;
|
||||||
|
for (int i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
string p = parts[i].Trim();
|
||||||
|
if (!int.TryParse(p, NumberStyles.Integer, CultureInfo.InvariantCulture, out int size)
|
||||||
|
|| size < 40)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string report = Chronicle.BenchSubjectLoad(size, eventsPer, iters: 25);
|
||||||
|
reports.Add(report);
|
||||||
|
any = true;
|
||||||
|
|
||||||
|
float avg = float.MaxValue;
|
||||||
|
const string needle = "fallenAvgMs=";
|
||||||
|
int idx = report.IndexOf(needle, StringComparison.Ordinal);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
int start = idx + needle.Length;
|
||||||
|
int end = start;
|
||||||
|
while (end < report.Length
|
||||||
|
&& (char.IsDigit(report[end]) || report[end] == '.'))
|
||||||
|
{
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
float.TryParse(
|
||||||
|
report.Substring(start, end - start),
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out avg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highest size that stays within the ms budget.
|
||||||
|
if (avg <= budgetMs && size > best)
|
||||||
|
{
|
||||||
|
best = size;
|
||||||
|
bestAvg = avg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best <= 0)
|
||||||
|
{
|
||||||
|
best = Chronicle.DefaultMaxHistorySubjects;
|
||||||
|
bestAvg = budgetMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
Chronicle.SetMaxHistorySubjects(best);
|
||||||
|
Chronicle.ClearSession();
|
||||||
|
bool ok = any && reports.Count > 0;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
string joined = string.Join(" || ", reports);
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"budgetMs={budgetMs:0.00} recommend={best} recommendAvgMs={bestAvg:0.00} capNow={Chronicle.MaxHistorySubjects} | {joined}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "lore_close":
|
case "lore_close":
|
||||||
case "chronicle_close":
|
case "chronicle_close":
|
||||||
{
|
{
|
||||||
|
|
@ -1545,6 +1718,23 @@ public static class AgentHarness
|
||||||
detail = $"badKinds={bad} worldRows={worldRows} memory={snap.Count}";
|
detail = $"badKinds={bad} worldRows={worldRows} memory={snap.Count}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "chronicle_subjects_capped":
|
||||||
|
{
|
||||||
|
int subjects = Chronicle.HistorySubjectCount;
|
||||||
|
int dossiers = Chronicle.FinalDossierCount;
|
||||||
|
pass = subjects <= Chronicle.MaxHistorySubjects
|
||||||
|
&& dossiers <= Chronicle.MaxHistorySubjects;
|
||||||
|
detail =
|
||||||
|
$"subjects={subjects} dossiers={dossiers} max={Chronicle.MaxHistorySubjects} revision={Chronicle.HistoryRevision}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "chronicle_memory_capped":
|
||||||
|
{
|
||||||
|
int memory = Chronicle.MemoryCount;
|
||||||
|
pass = memory <= Chronicle.MaxLandmarks;
|
||||||
|
detail = $"memory={memory} max={Chronicle.MaxLandmarks}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "dossier_favorite":
|
case "dossier_favorite":
|
||||||
case "is_favorite":
|
case "is_favorite":
|
||||||
{
|
{
|
||||||
|
|
@ -1911,6 +2101,23 @@ public static class AgentHarness
|
||||||
detail = $"top='{have}' want='{want}' tab={ChronicleHud.ActiveTab}";
|
detail = $"top='{have}' want='{want}' tab={ChronicleHud.ActiveTab}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "lore_list_stale":
|
||||||
|
{
|
||||||
|
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||||
|
if (string.IsNullOrEmpty(cmd.value) && !string.IsNullOrEmpty(cmd.expect)
|
||||||
|
&& cmd.expect != "lore_list_stale")
|
||||||
|
{
|
||||||
|
want = ParseBool(cmd.expect, defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool have = ChronicleHud.CastListStale;
|
||||||
|
bool castTab = ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Living
|
||||||
|
|| ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen;
|
||||||
|
pass = Chronicle.HudVisible && castTab && ChronicleHud.DetailUnitId == 0 && have == want;
|
||||||
|
detail =
|
||||||
|
$"stale={have} want={want} tab={ChronicleHud.ActiveTab} revision={Chronicle.HistoryRevision}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "lore_recent":
|
case "lore_recent":
|
||||||
{
|
{
|
||||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,16 @@ public static class Chronicle
|
||||||
{
|
{
|
||||||
public const KeyCode ToggleKey = KeyCode.L;
|
public const KeyCode ToggleKey = KeyCode.L;
|
||||||
public const int MaxHistoryPerUnit = 40;
|
public const int MaxHistoryPerUnit = 40;
|
||||||
private const int MaxLandmarks = 80;
|
/// <summary>Default soft cap on keyed character histories (Living + Fallen archive subjects).</summary>
|
||||||
|
public const int DefaultMaxHistorySubjects = 100000;
|
||||||
|
/// <summary>Hard safety ceiling for <see cref="SetMaxHistorySubjects"/>.</summary>
|
||||||
|
public const int AbsoluteMaxHistorySubjects = 200000;
|
||||||
|
/// <summary>Soft cap on keyed character histories. Tunable via harness.</summary>
|
||||||
|
public static int MaxHistorySubjects { get; private set; } = DefaultMaxHistorySubjects;
|
||||||
|
public const int MaxLandmarks = 80;
|
||||||
private const int MaxLandmarksPerAge = 8;
|
private const int MaxLandmarksPerAge = 8;
|
||||||
|
private const int MaxPairKeys = 1500;
|
||||||
|
private const int MaxLandmarkDedupe = 400;
|
||||||
|
|
||||||
private static readonly Dictionary<long, List<ChronicleEntry>> Histories =
|
private static readonly Dictionary<long, List<ChronicleEntry>> Histories =
|
||||||
new Dictionary<long, List<ChronicleEntry>>();
|
new Dictionary<long, List<ChronicleEntry>>();
|
||||||
|
|
@ -151,6 +159,9 @@ public static class Chronicle
|
||||||
|
|
||||||
public const int MaxRecentFocus = 24;
|
public const int MaxRecentFocus = 24;
|
||||||
|
|
||||||
|
/// <summary>Monotonic counter bumped on history/memory writes (cheap HUD dirty check).</summary>
|
||||||
|
public static int HistoryRevision { get; private set; }
|
||||||
|
|
||||||
private static bool _hudReady;
|
private static bool _hudReady;
|
||||||
private static long _lastHistorySubjectId;
|
private static long _lastHistorySubjectId;
|
||||||
private static long _trackedFocusId;
|
private static long _trackedFocusId;
|
||||||
|
|
@ -219,6 +230,137 @@ public static class Chronicle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>How many distinct characters currently have History rows stored.</summary>
|
||||||
|
public static int HistorySubjectCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (Histories)
|
||||||
|
{
|
||||||
|
return Histories.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int FinalDossierCount => FinalDossiers.Count;
|
||||||
|
|
||||||
|
/// <summary>Harness / tuning: change subject cap and prune immediately.</summary>
|
||||||
|
public static void SetMaxHistorySubjects(int max)
|
||||||
|
{
|
||||||
|
MaxHistorySubjects = Mathf.Clamp(max, 40, AbsoluteMaxHistorySubjects);
|
||||||
|
lock (Histories)
|
||||||
|
{
|
||||||
|
PruneHistorySubjectsUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
PruneFinalDossiers();
|
||||||
|
BumpRevision();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Harness: fill to <paramref name="subjects"/> and time Fallen/Living list rebuilds.
|
||||||
|
/// Returns a single-line report of avg/max ms per size.
|
||||||
|
/// </summary>
|
||||||
|
public static string BenchSubjectLoad(int subjects, int eventsPerSubject = 3, int iters = 25)
|
||||||
|
{
|
||||||
|
subjects = Mathf.Clamp(subjects, 40, AbsoluteMaxHistorySubjects);
|
||||||
|
eventsPerSubject = Mathf.Clamp(eventsPerSubject, 1, MaxHistoryPerUnit);
|
||||||
|
iters = Mathf.Clamp(iters, 5, 200);
|
||||||
|
|
||||||
|
int previousCap = MaxHistorySubjects;
|
||||||
|
ClearSession();
|
||||||
|
SetMaxHistorySubjects(subjects);
|
||||||
|
FillBenchSubjectsUnlocked(subjects, eventsPerSubject);
|
||||||
|
|
||||||
|
// Warmup
|
||||||
|
ListCharacters(max: 40, scope: CharacterListScope.Fallen);
|
||||||
|
ListCharacters(max: 40, scope: CharacterListScope.Living);
|
||||||
|
|
||||||
|
int have = HistorySubjectCount;
|
||||||
|
|
||||||
|
double fallenSum = 0;
|
||||||
|
double fallenMax = 0;
|
||||||
|
double livingSum = 0;
|
||||||
|
double livingMax = 0;
|
||||||
|
for (int i = 0; i < iters; i++)
|
||||||
|
{
|
||||||
|
float t0 = Time.realtimeSinceStartup;
|
||||||
|
ListCharacters(max: 40, scope: CharacterListScope.Fallen);
|
||||||
|
float fallenMs = (Time.realtimeSinceStartup - t0) * 1000f;
|
||||||
|
fallenSum += fallenMs;
|
||||||
|
if (fallenMs > fallenMax)
|
||||||
|
{
|
||||||
|
fallenMax = fallenMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
t0 = Time.realtimeSinceStartup;
|
||||||
|
ListCharacters(max: 40, scope: CharacterListScope.Living);
|
||||||
|
float livingMs = (Time.realtimeSinceStartup - t0) * 1000f;
|
||||||
|
livingSum += livingMs;
|
||||||
|
if (livingMs > livingMax)
|
||||||
|
{
|
||||||
|
livingMax = livingMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float pruneMs;
|
||||||
|
{
|
||||||
|
float t0 = Time.realtimeSinceStartup;
|
||||||
|
SetMaxHistorySubjects(Mathf.Max(40, subjects / 2));
|
||||||
|
pruneMs = (Time.realtimeSinceStartup - t0) * 1000f;
|
||||||
|
}
|
||||||
|
|
||||||
|
string report =
|
||||||
|
$"n={subjects} events={eventsPerSubject} have={have} "
|
||||||
|
+ $"fallenAvgMs={fallenSum / iters:0.00} fallenMaxMs={fallenMax:0.00} "
|
||||||
|
+ $"livingAvgMs={livingSum / iters:0.00} livingMaxMs={livingMax:0.00} "
|
||||||
|
+ $"pruneMs={pruneMs:0.00}";
|
||||||
|
|
||||||
|
LogService.LogInfo("[IdleSpectator][BENCH] " + report);
|
||||||
|
SetMaxHistorySubjects(previousCap);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fast synthetic fill for benches (skips lore rewrite / age refresh).</summary>
|
||||||
|
private static void FillBenchSubjectsUnlocked(int subjects, int eventsPerSubject)
|
||||||
|
{
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
lock (Histories)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < subjects; i++)
|
||||||
|
{
|
||||||
|
long id = _orphanSeq--;
|
||||||
|
var list = new List<ChronicleEntry>(eventsPerSubject);
|
||||||
|
for (int e = 0; e < eventsPerSubject; e++)
|
||||||
|
{
|
||||||
|
list.Add(new ChronicleEntry
|
||||||
|
{
|
||||||
|
CreatedAt = now + (i * 0.0001f) + (e * 0.00001f),
|
||||||
|
WorldTime = i,
|
||||||
|
DateLabel = "Bench",
|
||||||
|
Kind = ChronicleKind.Death,
|
||||||
|
SubjectId = id,
|
||||||
|
Name = "Bench" + i,
|
||||||
|
SpeciesId = "human",
|
||||||
|
Line = "Bench death",
|
||||||
|
LoreLine = "Bench death",
|
||||||
|
DeathManner = DeathManner.OldAge,
|
||||||
|
AttackTypeName = "Age"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Histories[id] = list;
|
||||||
|
FinalDossiers[id] = UnitDossier.FromFallenArchive(
|
||||||
|
id, "Bench" + i, "human", DeathManner.OldAge);
|
||||||
|
}
|
||||||
|
|
||||||
|
PruneHistorySubjectsUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
PruneFinalDossiers();
|
||||||
|
BumpRevision();
|
||||||
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<ChronicleEntry> SnapshotForFocus()
|
public static IReadOnlyList<ChronicleEntry> SnapshotForFocus()
|
||||||
{
|
{
|
||||||
return SnapshotForSubject(CurrentHistorySubjectId());
|
return SnapshotForSubject(CurrentHistorySubjectId());
|
||||||
|
|
@ -400,9 +542,18 @@ public static class Chronicle
|
||||||
_trackedFocusId = 0;
|
_trackedFocusId = 0;
|
||||||
_currentAgeId = "";
|
_currentAgeId = "";
|
||||||
_currentAgeName = "";
|
_currentAgeName = "";
|
||||||
|
BumpRevision();
|
||||||
// Keep _boundWorld so re-enable on same map does not loop-clear.
|
// Keep _boundWorld so re-enable on same map does not loop-clear.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void BumpRevision()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
HistoryRevision++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void PollInput()
|
public static void PollInput()
|
||||||
{
|
{
|
||||||
if (!Config.game_loaded || !ModSettings.ChronicleEnabled)
|
if (!Config.game_loaded || !ModSettings.ChronicleEnabled)
|
||||||
|
|
@ -1015,6 +1166,8 @@ public static class Chronicle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CapPairKeys(LoverPairKeys);
|
||||||
|
|
||||||
AppendHistory(ChronicleKind.Lover, a, b, $"Became lovers with {SafeName(b)}");
|
AppendHistory(ChronicleKind.Lover, a, b, $"Became lovers with {SafeName(b)}");
|
||||||
AppendHistory(ChronicleKind.Lover, b, a, $"Became lovers with {SafeName(a)}");
|
AppendHistory(ChronicleKind.Lover, b, a, $"Became lovers with {SafeName(a)}");
|
||||||
}
|
}
|
||||||
|
|
@ -1032,6 +1185,8 @@ public static class Chronicle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CapPairKeys(FriendPairKeys);
|
||||||
|
|
||||||
AppendHistory(ChronicleKind.Friend, a, b, $"Befriended {SafeName(b)}");
|
AppendHistory(ChronicleKind.Friend, a, b, $"Befriended {SafeName(b)}");
|
||||||
AppendHistory(ChronicleKind.Friend, b, a, $"Befriended {SafeName(a)}");
|
AppendHistory(ChronicleKind.Friend, b, a, $"Befriended {SafeName(a)}");
|
||||||
}
|
}
|
||||||
|
|
@ -1201,13 +1356,14 @@ public static class Chronicle
|
||||||
{
|
{
|
||||||
list.RemoveAt(0);
|
list.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PruneHistorySubjectsUnlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteFocusId(id);
|
BumpRevision();
|
||||||
LogService.LogInfo(
|
|
||||||
$"[IdleSpectator][CHRONICLE] orphan history id={id} count={count} name={who} manner={manner} killer={killerId}");
|
|
||||||
|
|
||||||
FinalDossiers[id] = UnitDossier.FromFallenArchive(id, who, sp, manner);
|
FinalDossiers[id] = UnitDossier.FromFallenArchive(id, who, sp, manner);
|
||||||
|
PruneFinalDossiers();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1459,6 +1615,7 @@ public static class Chronicle
|
||||||
}
|
}
|
||||||
|
|
||||||
FinalDossiers[id] = dossier;
|
FinalDossiers[id] = dossier;
|
||||||
|
PruneFinalDossiers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Last living dossier for a fallen subject, or a minimal archive rebuild.</summary>
|
/// <summary>Last living dossier for a fallen subject, or a minimal archive rebuild.</summary>
|
||||||
|
|
@ -1720,9 +1877,171 @@ public static class Chronicle
|
||||||
{
|
{
|
||||||
list.RemoveAt(0);
|
list.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PruneHistorySubjectsUnlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE][History:{subjectId}] {entry.DisplayLine}");
|
_lastHistorySubjectId = subjectId;
|
||||||
|
BumpRevision();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drop oldest dormant subjects when over <see cref="MaxHistorySubjects"/>.
|
||||||
|
/// Caller must hold the Histories lock.
|
||||||
|
/// </summary>
|
||||||
|
private static void PruneHistorySubjectsUnlocked()
|
||||||
|
{
|
||||||
|
if (Histories.Count <= MaxHistorySubjects)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keep = new HashSet<long>();
|
||||||
|
if (_trackedFocusId != 0)
|
||||||
|
{
|
||||||
|
keep.Add(_trackedFocusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastHistorySubjectId != 0)
|
||||||
|
{
|
||||||
|
keep.Add(_lastHistorySubjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (RecentFocusIds)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < RecentFocusIds.Count; i++)
|
||||||
|
{
|
||||||
|
keep.Add(RecentFocusIds[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranked = new List<(long id, float lastAt)>(Histories.Count);
|
||||||
|
foreach (KeyValuePair<long, List<ChronicleEntry>> kv in Histories)
|
||||||
|
{
|
||||||
|
if (keep.Contains(kv.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float lastAt = 0f;
|
||||||
|
List<ChronicleEntry> list = kv.Value;
|
||||||
|
if (list != null && list.Count > 0)
|
||||||
|
{
|
||||||
|
ChronicleEntry last = list[list.Count - 1];
|
||||||
|
if (last != null)
|
||||||
|
{
|
||||||
|
lastAt = last.CreatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ranked.Add((kv.Key, lastAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
ranked.Sort((a, b) => a.lastAt.CompareTo(b.lastAt));
|
||||||
|
|
||||||
|
int need = Histories.Count - MaxHistorySubjects;
|
||||||
|
for (int i = 0; i < ranked.Count && need > 0; i++)
|
||||||
|
{
|
||||||
|
long id = ranked[i].id;
|
||||||
|
Histories.Remove(id);
|
||||||
|
DeathLogged.Remove(id);
|
||||||
|
FinalDossiers.Remove(id);
|
||||||
|
need--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PruneFinalDossiers()
|
||||||
|
{
|
||||||
|
if (FinalDossiers.Count <= MaxHistorySubjects)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<long> histIds;
|
||||||
|
lock (Histories)
|
||||||
|
{
|
||||||
|
histIds = new HashSet<long>(Histories.Keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
var orphan = new List<long>();
|
||||||
|
foreach (long id in FinalDossiers.Keys)
|
||||||
|
{
|
||||||
|
if (!histIds.Contains(id))
|
||||||
|
{
|
||||||
|
orphan.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < orphan.Count; i++)
|
||||||
|
{
|
||||||
|
FinalDossiers.Remove(orphan[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FinalDossiers.Count <= MaxHistorySubjects)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still over: drop extras not recently focused.
|
||||||
|
var protect = new HashSet<long>();
|
||||||
|
if (_trackedFocusId != 0)
|
||||||
|
{
|
||||||
|
protect.Add(_trackedFocusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastHistorySubjectId != 0)
|
||||||
|
{
|
||||||
|
protect.Add(_lastHistorySubjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (RecentFocusIds)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < RecentFocusIds.Count; i++)
|
||||||
|
{
|
||||||
|
protect.Add(RecentFocusIds[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var drop = new List<long>();
|
||||||
|
foreach (long id in FinalDossiers.Keys)
|
||||||
|
{
|
||||||
|
if (protect.Contains(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.Add(id);
|
||||||
|
if (FinalDossiers.Count - drop.Count <= MaxHistorySubjects)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < drop.Count; i++)
|
||||||
|
{
|
||||||
|
FinalDossiers.Remove(drop[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CapPairKeys(HashSet<string> keys)
|
||||||
|
{
|
||||||
|
if (keys == null || keys.Count <= MaxPairKeys)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedupes are best-effort; clearing under pressure avoids unbounded growth.
|
||||||
|
keys.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CapLandmarkDedupe()
|
||||||
|
{
|
||||||
|
if (LandmarkDedupe.Count <= MaxLandmarkDedupe)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LandmarkDedupe.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendLandmark(
|
private static void AppendLandmark(
|
||||||
|
|
@ -1749,6 +2068,8 @@ public static class Chronicle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CapLandmarkDedupe();
|
||||||
|
|
||||||
lock (MemoryLandmarks)
|
lock (MemoryLandmarks)
|
||||||
{
|
{
|
||||||
if (!LandmarksPerAge.TryGetValue(ageId, out int ageCount))
|
if (!LandmarksPerAge.TryGetValue(ageId, out int ageCount))
|
||||||
|
|
@ -1758,6 +2079,7 @@ public static class Chronicle
|
||||||
|
|
||||||
if (ageCount >= MaxLandmarksPerAge)
|
if (ageCount >= MaxLandmarksPerAge)
|
||||||
{
|
{
|
||||||
|
LandmarkDedupe.Remove(dedupeKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1788,16 +2110,22 @@ public static class Chronicle
|
||||||
{
|
{
|
||||||
ChronicleEntry removed = MemoryLandmarks[0];
|
ChronicleEntry removed = MemoryLandmarks[0];
|
||||||
MemoryLandmarks.RemoveAt(0);
|
MemoryLandmarks.RemoveAt(0);
|
||||||
if (!string.IsNullOrEmpty(removed.AgeId)
|
if (removed != null)
|
||||||
&& LandmarksPerAge.TryGetValue(removed.AgeId, out int c)
|
|
||||||
&& c > 0)
|
|
||||||
{
|
{
|
||||||
LandmarksPerAge[removed.AgeId] = c - 1;
|
if (!string.IsNullOrEmpty(removed.AgeId)
|
||||||
|
&& LandmarksPerAge.TryGetValue(removed.AgeId, out int c)
|
||||||
|
&& c > 0)
|
||||||
|
{
|
||||||
|
LandmarksPerAge[removed.AgeId] = c - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string oldKey = (removed.AgeId ?? "") + "|" + (removed.AssetId ?? "") + "|" + (removed.Line ?? "");
|
||||||
|
LandmarkDedupe.Remove(oldKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService.LogInfo("[IdleSpectator][MEMORY] " + entry.DisplayLine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BumpRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RefreshAgeChapter(bool force)
|
private static void RefreshAgeChapter(bool force)
|
||||||
|
|
@ -1867,7 +2195,7 @@ public static class Chronicle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService.LogInfo("[IdleSpectator][MEMORY] Age chapter: " + ageName);
|
BumpRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadCurrentAge(out string ageId, out string ageName)
|
private static bool TryReadCurrentAge(out string ageId, out string ageName)
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,13 @@ public static class ChronicleHud
|
||||||
private static InputField _searchField;
|
private static InputField _searchField;
|
||||||
private static Button _favFilterBtn;
|
private static Button _favFilterBtn;
|
||||||
private static Image _favFilterIcon;
|
private static Image _favFilterIcon;
|
||||||
|
private static Button _refreshBtn;
|
||||||
|
private static Image _refreshBg;
|
||||||
|
private static Text _refreshLabel;
|
||||||
private static bool _favoritesOnly;
|
private static bool _favoritesOnly;
|
||||||
private static string _searchText = "";
|
private static string _searchText = "";
|
||||||
private static int _lastCharFingerprint = int.MinValue;
|
private static int _lastCharFingerprint = int.MinValue;
|
||||||
|
private static int _builtAtRevision = int.MinValue;
|
||||||
private static long _detailUnitId;
|
private static long _detailUnitId;
|
||||||
private static bool _followFocus;
|
private static bool _followFocus;
|
||||||
private static GameObject _backBtn;
|
private static GameObject _backBtn;
|
||||||
|
|
@ -71,6 +75,10 @@ public static class ChronicleHud
|
||||||
/// <summary>Title of the first row in the last Living/Fallen list rebuild (harness).</summary>
|
/// <summary>Title of the first row in the last Living/Fallen list rebuild (harness).</summary>
|
||||||
public static string LastListTopTitle { get; private set; } = "";
|
public static string LastListTopTitle { get; private set; } = "";
|
||||||
|
|
||||||
|
/// <summary>True when Living/Fallen list is older than new chronicle events (needs Refresh).</summary>
|
||||||
|
public static bool CastListStale =>
|
||||||
|
Visible && IsCastTab && _detailUnitId == 0 && Chronicle.HistoryRevision != _builtAtRevision;
|
||||||
|
|
||||||
public static bool Visible => _visible && _root != null && _root.activeSelf;
|
public static bool Visible => _visible && _root != null && _root.activeSelf;
|
||||||
|
|
||||||
public static bool IsReady => _root != null;
|
public static bool IsReady => _root != null;
|
||||||
|
|
@ -280,7 +288,7 @@ public static class ChronicleHud
|
||||||
searchRt.anchorMin = new Vector2(0f, 0f);
|
searchRt.anchorMin = new Vector2(0f, 0f);
|
||||||
searchRt.anchorMax = new Vector2(1f, 1f);
|
searchRt.anchorMax = new Vector2(1f, 1f);
|
||||||
searchRt.offsetMin = Vector2.zero;
|
searchRt.offsetMin = Vector2.zero;
|
||||||
searchRt.offsetMax = new Vector2(-20f, 0f);
|
searchRt.offsetMax = new Vector2(-40f, 0f);
|
||||||
Image searchBg = searchGo.GetComponent<Image>();
|
Image searchBg = searchGo.GetComponent<Image>();
|
||||||
searchBg.color = new Color(0.05f, 0.06f, 0.08f, 0.95f);
|
searchBg.color = new Color(0.05f, 0.06f, 0.08f, 0.95f);
|
||||||
searchBg.raycastTarget = true;
|
searchBg.raycastTarget = true;
|
||||||
|
|
@ -306,6 +314,16 @@ public static class ChronicleHud
|
||||||
_searchField.customCaretColor = true;
|
_searchField.customCaretColor = true;
|
||||||
_searchField.onValueChanged.AddListener(OnSearchChanged);
|
_searchField.onValueChanged.AddListener(OnSearchChanged);
|
||||||
|
|
||||||
|
_refreshBtn = BuildTextTool(_charTools.transform, "Refresh", "↻", RefreshCastList);
|
||||||
|
_refreshBg = _refreshBtn.GetComponent<Image>();
|
||||||
|
_refreshLabel = _refreshBtn.transform.Find("Label")?.GetComponent<Text>();
|
||||||
|
RectTransform refreshRt = _refreshBtn.GetComponent<RectTransform>();
|
||||||
|
refreshRt.anchorMin = new Vector2(1f, 0f);
|
||||||
|
refreshRt.anchorMax = new Vector2(1f, 1f);
|
||||||
|
refreshRt.pivot = new Vector2(1f, 0.5f);
|
||||||
|
refreshRt.sizeDelta = new Vector2(18f, 0f);
|
||||||
|
refreshRt.anchoredPosition = new Vector2(-19f, 0f);
|
||||||
|
|
||||||
_favFilterBtn = BuildIconTool(_charTools.transform, "FavFilter", HudIcons.Favorite(), ToggleFavoritesOnly);
|
_favFilterBtn = BuildIconTool(_charTools.transform, "FavFilter", HudIcons.Favorite(), ToggleFavoritesOnly);
|
||||||
_favFilterIcon = _favFilterBtn.transform.Find("Icon")?.GetComponent<Image>();
|
_favFilterIcon = _favFilterBtn.transform.Find("Icon")?.GetComponent<Image>();
|
||||||
RectTransform favRt = _favFilterBtn.GetComponent<RectTransform>();
|
RectTransform favRt = _favFilterBtn.GetComponent<RectTransform>();
|
||||||
|
|
@ -326,6 +344,25 @@ public static class ChronicleHud
|
||||||
|
|
||||||
_charTools.SetActive(false);
|
_charTools.SetActive(false);
|
||||||
RefreshFavFilterVisual();
|
RefreshFavFilterVisual();
|
||||||
|
RefreshStaleVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button BuildTextTool(Transform parent, string name, string label, UnityAction onClick)
|
||||||
|
{
|
||||||
|
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
Image bg = go.GetComponent<Image>();
|
||||||
|
bg.color = new Color(0.12f, 0.13f, 0.16f, 0.95f);
|
||||||
|
bg.raycastTarget = true;
|
||||||
|
Button button = go.GetComponent<Button>();
|
||||||
|
button.targetGraphic = bg;
|
||||||
|
button.onClick.AddListener(onClick);
|
||||||
|
Text text = HudCanvas.MakeText(go.transform, "Label", label, 10);
|
||||||
|
StretchFull(text.GetComponent<RectTransform>(), 0f);
|
||||||
|
text.alignment = TextAnchor.MiddleCenter;
|
||||||
|
text.color = new Color(0.78f, 0.8f, 0.84f, 1f);
|
||||||
|
text.raycastTarget = false;
|
||||||
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Button BuildIconTool(Transform parent, string name, Sprite icon, UnityAction onClick)
|
private static Button BuildIconTool(Transform parent, string name, Sprite icon, UnityAction onClick)
|
||||||
|
|
@ -815,6 +852,19 @@ public static class ChronicleHud
|
||||||
_favFilterBtn.gameObject.SetActive(_tab == LoreTab.Living && !detail);
|
_favFilterBtn.gameObject.SetActive(_tab == LoreTab.Living && !detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_refreshBtn != null)
|
||||||
|
{
|
||||||
|
_refreshBtn.gameObject.SetActive(chars && !detail);
|
||||||
|
// On Fallen (no fav), park refresh on the far right.
|
||||||
|
RectTransform refreshRt = _refreshBtn.GetComponent<RectTransform>();
|
||||||
|
if (refreshRt != null)
|
||||||
|
{
|
||||||
|
refreshRt.anchoredPosition = _tab == LoreTab.Living
|
||||||
|
? new Vector2(-19f, 0f)
|
||||||
|
: Vector2.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_backBtn != null)
|
if (_backBtn != null)
|
||||||
{
|
{
|
||||||
_backBtn.SetActive(detail);
|
_backBtn.SetActive(detail);
|
||||||
|
|
@ -827,6 +877,7 @@ public static class ChronicleHud
|
||||||
StyleTab(_worldTabBtn, _tab == LoreTab.World);
|
StyleTab(_worldTabBtn, _tab == LoreTab.World);
|
||||||
StyleTab(_livingTabBtn, _tab == LoreTab.Living);
|
StyleTab(_livingTabBtn, _tab == LoreTab.Living);
|
||||||
StyleTab(_fallenTabBtn, _tab == LoreTab.Fallen);
|
StyleTab(_fallenTabBtn, _tab == LoreTab.Fallen);
|
||||||
|
RefreshStaleVisual();
|
||||||
ApplyScrollInsets();
|
ApplyScrollInsets();
|
||||||
RefreshTitle();
|
RefreshTitle();
|
||||||
}
|
}
|
||||||
|
|
@ -903,6 +954,37 @@ public static class ChronicleHud
|
||||||
: new Color(0.55f, 0.58f, 0.62f, 1f);
|
: new Color(0.55f, 0.58f, 0.62f, 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RefreshStaleVisual()
|
||||||
|
{
|
||||||
|
if (_refreshBg == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stale = CastListStale;
|
||||||
|
_refreshBg.color = stale
|
||||||
|
? new Color(0.28f, 0.34f, 0.22f, 0.98f)
|
||||||
|
: new Color(0.12f, 0.13f, 0.16f, 0.95f);
|
||||||
|
if (_refreshLabel != null)
|
||||||
|
{
|
||||||
|
_refreshLabel.color = stale
|
||||||
|
? new Color(0.85f, 0.95f, 0.55f, 1f)
|
||||||
|
: new Color(0.78f, 0.8f, 0.84f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Manual reload of Living/Fallen list (or open detail history).</summary>
|
||||||
|
public static void RefreshCastList()
|
||||||
|
{
|
||||||
|
if (!Visible || !IsCastTab)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rebuild(force: true);
|
||||||
|
LogService.LogInfo("[IdleSpectator][LORE] cast list refreshed");
|
||||||
|
}
|
||||||
|
|
||||||
public static void UpdateLive()
|
public static void UpdateLive()
|
||||||
{
|
{
|
||||||
if (!Visible)
|
if (!Visible)
|
||||||
|
|
@ -924,16 +1006,13 @@ public static class ChronicleHud
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Living/Fallen: snapshot until Refresh (or tab/search/fav). Follow mode still retargets.
|
||||||
if (TryRetargetFollowFocus())
|
if (TryRetargetFollowFocus())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int fp = CharacterFingerprint();
|
RefreshStaleVisual();
|
||||||
if (fp != _lastCharFingerprint)
|
|
||||||
{
|
|
||||||
Rebuild(force: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1002,25 +1081,14 @@ public static class ChronicleHud
|
||||||
hash = (hash * 31) + (_favoritesOnly ? 1 : 0);
|
hash = (hash * 31) + (_favoritesOnly ? 1 : 0);
|
||||||
hash = (hash * 31) + (_searchText ?? "").GetHashCode();
|
hash = (hash * 31) + (_searchText ?? "").GetHashCode();
|
||||||
hash = (hash * 31) + _detailUnitId.GetHashCode();
|
hash = (hash * 31) + _detailUnitId.GetHashCode();
|
||||||
|
hash = (hash * 31) + (int)_tab;
|
||||||
|
// Cheap dirty bits - never rescan all subjects every frame.
|
||||||
|
hash = (hash * 31) + Chronicle.HistoryRevision;
|
||||||
|
hash = (hash * 31) + Chronicle.HistorySubjectCount;
|
||||||
|
hash = (hash * 31) + Chronicle.MemoryCount;
|
||||||
if (_detailUnitId != 0)
|
if (_detailUnitId != 0)
|
||||||
{
|
{
|
||||||
hash = (hash * 31) + Chronicle.HistoryCountFor(_detailUnitId);
|
hash = (hash * 31) + Chronicle.HistoryCountFor(_detailUnitId);
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(
|
|
||||||
_searchText,
|
|
||||||
_tab == LoreTab.Living && _favoritesOnly,
|
|
||||||
max: 12,
|
|
||||||
scope: _tab == LoreTab.Fallen
|
|
||||||
? Chronicle.CharacterListScope.Fallen
|
|
||||||
: Chronicle.CharacterListScope.Living);
|
|
||||||
hash = (hash * 31) + (int)_tab;
|
|
||||||
hash = (hash * 31) + list.Count;
|
|
||||||
for (int i = 0; i < list.Count && i < 8; i++)
|
|
||||||
{
|
|
||||||
hash = (hash * 31) + list[i].UnitId.GetHashCode();
|
|
||||||
hash = (hash * 31) + list[i].HistoryCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
|
|
@ -1100,6 +1168,8 @@ public static class ChronicleHud
|
||||||
bool favOnly = scope == Chronicle.CharacterListScope.Living && _favoritesOnly;
|
bool favOnly = scope == Chronicle.CharacterListScope.Living && _favoritesOnly;
|
||||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, favOnly, scope: scope);
|
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, favOnly, scope: scope);
|
||||||
_lastCharFingerprint = CharacterFingerprint();
|
_lastCharFingerprint = CharacterFingerprint();
|
||||||
|
_builtAtRevision = Chronicle.HistoryRevision;
|
||||||
|
RefreshStaleVisual();
|
||||||
|
|
||||||
if (list.Count == 0)
|
if (list.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1150,6 +1220,8 @@ public static class ChronicleHud
|
||||||
{
|
{
|
||||||
IReadOnlyList<ChronicleEntry> entries = Chronicle.LatestForSubject(unitId, Chronicle.MaxHistoryPerUnit);
|
IReadOnlyList<ChronicleEntry> entries = Chronicle.LatestForSubject(unitId, Chronicle.MaxHistoryPerUnit);
|
||||||
_lastCharFingerprint = CharacterFingerprint();
|
_lastCharFingerprint = CharacterFingerprint();
|
||||||
|
_builtAtRevision = Chronicle.HistoryRevision;
|
||||||
|
RefreshStaleVisual();
|
||||||
if (entries == null || entries.Count == 0)
|
if (entries == null || entries.Count == 0)
|
||||||
{
|
{
|
||||||
LastDetailHistoryRows = 0;
|
LastDetailHistoryRows = 0;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ internal static class HarnessScenarios
|
||||||
case "chronicle":
|
case "chronicle":
|
||||||
case "chronicle_smoke":
|
case "chronicle_smoke":
|
||||||
return ChronicleSmoke();
|
return ChronicleSmoke();
|
||||||
|
case "chronicle_subject_bench":
|
||||||
|
case "subject_bench":
|
||||||
|
return ChronicleSubjectBench();
|
||||||
case "regression":
|
case "regression":
|
||||||
case "full":
|
case "full":
|
||||||
return Regression();
|
return Regression();
|
||||||
|
|
@ -43,6 +46,28 @@ internal static class HarnessScenarios
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timed Fallen/Living list rebuilds at rising subject counts.
|
||||||
|
/// Picks the highest size under a 2ms Fallen avg budget (runtime cap only for that session).
|
||||||
|
/// </summary>
|
||||||
|
private static List<HarnessCommand> ChronicleSubjectBench()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("sb0", "wait_world"),
|
||||||
|
Step("sb1", "dismiss_windows"),
|
||||||
|
Step("sb2", "chronicle_clear"),
|
||||||
|
Step(
|
||||||
|
"sb3",
|
||||||
|
"chronicle_subject_bench",
|
||||||
|
value: "320,640,1280,2560,4096,8192",
|
||||||
|
count: 3,
|
||||||
|
expect: "12"),
|
||||||
|
Step("sb4", "assert", expect: "chronicle_subjects_capped"),
|
||||||
|
Step("sb5", "snapshot")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full regression meta-suite (nested scenarios keep assert counters).
|
/// Full regression meta-suite (nested scenarios keep assert counters).
|
||||||
/// Battle / map-gen paths stay out of gate.
|
/// Battle / map-gen paths stay out of gate.
|
||||||
|
|
@ -442,6 +467,14 @@ internal static class HarnessScenarios
|
||||||
Step("ch16h", "assert", expect: "chronicle_latest_contains", value: "Burned"),
|
Step("ch16h", "assert", expect: "chronicle_latest_contains", value: "Burned"),
|
||||||
// Character kills/deaths must stay on character history, not World Memory.
|
// Character kills/deaths must stay on character history, not World Memory.
|
||||||
Step("ch16i", "assert", expect: "world_memory_world_only"),
|
Step("ch16i", "assert", expect: "world_memory_world_only"),
|
||||||
|
// Soft-cap prune: temporarily lower the cap, flood past it, then restore.
|
||||||
|
Step("ch16j0", "chronicle_set_subject_cap", value: "500"),
|
||||||
|
Step("ch16j", "chronicle_orphan_flood", count: 580),
|
||||||
|
Step("ch16k", "assert", expect: "chronicle_subjects_capped"),
|
||||||
|
Step(
|
||||||
|
"ch16k2",
|
||||||
|
"chronicle_set_subject_cap",
|
||||||
|
value: Chronicle.DefaultMaxHistorySubjects.ToString()),
|
||||||
|
|
||||||
// World Memory landmarks (replaces History|World tabs)
|
// World Memory landmarks (replaces History|World tabs)
|
||||||
Step("ch17a", "chronicle_world_force", label: "War: harness_alpha vs harness_beta"),
|
Step("ch17a", "chronicle_world_force", label: "War: harness_alpha vs harness_beta"),
|
||||||
|
|
@ -450,6 +483,8 @@ internal static class HarnessScenarios
|
||||||
Step("ch17d", "assert", expect: "chronicle_latest_contains", value: "Burned"),
|
Step("ch17d", "assert", expect: "chronicle_latest_contains", value: "Burned"),
|
||||||
Step("ch17e", "assert", expect: "world_memory_age"),
|
Step("ch17e", "assert", expect: "world_memory_age"),
|
||||||
Step("ch17e2", "assert", expect: "world_memory_world_only"),
|
Step("ch17e2", "assert", expect: "world_memory_world_only"),
|
||||||
|
Step("ch17e3", "assert", expect: "chronicle_memory_capped"),
|
||||||
|
Step("ch17e4", "assert", expect: "chronicle_subjects_capped"),
|
||||||
// Selecting a world event pans to its recorded place (even with no unit there).
|
// Selecting a world event pans to its recorded place (even with no unit there).
|
||||||
Step("ch17j", "lore_jump_world"),
|
Step("ch17j", "lore_jump_world"),
|
||||||
Step("ch17k", "wait", wait: 0.2f),
|
Step("ch17k", "wait", wait: 0.2f),
|
||||||
|
|
@ -638,6 +673,22 @@ internal static class HarnessScenarios
|
||||||
Step("ch18ft3", "wait", wait: 0.2f),
|
Step("ch18ft3", "wait", wait: 0.2f),
|
||||||
Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"),
|
Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"),
|
||||||
Step("ch18ft5", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
Step("ch18ft5", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
||||||
|
// Snapshot stays put until Refresh (new deaths do not auto-rebuild).
|
||||||
|
Step("ch18ft5b", "assert", expect: "lore_list_stale", value: "false"),
|
||||||
|
Step(
|
||||||
|
"ch18ft5c",
|
||||||
|
"chronicle_orphan",
|
||||||
|
label: "FreshFallen",
|
||||||
|
asset: "human",
|
||||||
|
value: "Fresh",
|
||||||
|
count: 1,
|
||||||
|
expect: "Age",
|
||||||
|
tier: "place"),
|
||||||
|
Step("ch18ft5d", "assert", expect: "lore_list_stale", value: "true"),
|
||||||
|
Step("ch18ft5e", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
||||||
|
Step("ch18ft5f", "lore_refresh"),
|
||||||
|
Step("ch18ft5g", "assert", expect: "lore_list_stale", value: "false"),
|
||||||
|
Step("ch18ft5h", "assert", expect: "fallen_list_top", value: "FreshFallen"),
|
||||||
Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"),
|
Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"),
|
||||||
Step("ch18ft7", "wait", wait: 0.55f),
|
Step("ch18ft7", "wait", wait: 0.55f),
|
||||||
Step("ch18q12", "lore_tab", value: "world"),
|
Step("ch18q12", "lore_tab", value: "world"),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.12.17",
|
"version": "0.12.22",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). World tab is world events only.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Subject soft cap 100k.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue