309 lines
7.8 KiB
C#
309 lines
7.8 KiB
C#
using System;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Live-relevance policy for Activity History <b>peek</b> (dossier).
|
|
/// The ring stays append-only; Lore Activity still shows the full journal.
|
|
/// Peek only keeps lines that still match what the unit is doing (or just did).
|
|
/// </summary>
|
|
public static class ActivityRelevance
|
|
{
|
|
/// <summary>
|
|
/// Lines newer than this always pass (beat just happened; AI may not have settled).
|
|
/// </summary>
|
|
public const float SoftWindowSeconds = 8f;
|
|
|
|
/// <summary>How many newest ring lines to scan when filling a peek of size N.</summary>
|
|
public const int PeekScanMax = 24;
|
|
|
|
public enum Family
|
|
{
|
|
Ambient,
|
|
Combat,
|
|
Haul,
|
|
Social,
|
|
Romance,
|
|
Work,
|
|
Care,
|
|
Travel,
|
|
Milestone,
|
|
Status,
|
|
Happiness
|
|
}
|
|
|
|
public static Family FamilyForEntry(ActivityEntry entry)
|
|
{
|
|
if (entry == null)
|
|
{
|
|
return Family.Ambient;
|
|
}
|
|
|
|
switch (entry.Kind)
|
|
{
|
|
case ActivityKind.StatusGain:
|
|
case ActivityKind.StatusLoss:
|
|
return Family.Status;
|
|
case ActivityKind.Happiness:
|
|
return Family.Happiness;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
string key = (entry.TaskId ?? "").Trim();
|
|
if (key.StartsWith("milestone_", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Family.Milestone;
|
|
}
|
|
|
|
return FamilyForVerb(ActivityVerbMap.Resolve(key));
|
|
}
|
|
|
|
public static Family FamilyForVerb(string verb)
|
|
{
|
|
if (string.IsNullOrEmpty(verb))
|
|
{
|
|
return Family.Ambient;
|
|
}
|
|
|
|
switch (verb)
|
|
{
|
|
case "hunt":
|
|
case "fight":
|
|
case "flee":
|
|
case "fire":
|
|
case "ignite":
|
|
case "extinguish":
|
|
case "warrior":
|
|
case "loot":
|
|
case "steal":
|
|
return Family.Combat;
|
|
case "haul":
|
|
return Family.Haul;
|
|
case "social":
|
|
case "group":
|
|
return Family.Social;
|
|
case "lover":
|
|
return Family.Romance;
|
|
case "farm":
|
|
case "work":
|
|
case "pollinate":
|
|
case "fish":
|
|
case "trade":
|
|
case "build":
|
|
case "read":
|
|
case "heal":
|
|
return Family.Work;
|
|
case "eat":
|
|
case "sleep":
|
|
case "play":
|
|
case "relieve":
|
|
case "laugh":
|
|
case "sing":
|
|
case "cry":
|
|
case "swear":
|
|
case "wait":
|
|
case "recharge":
|
|
case "dream":
|
|
return Family.Care;
|
|
case "move":
|
|
case "settle":
|
|
case "boat":
|
|
return Family.Travel;
|
|
default:
|
|
return Family.Ambient;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this ring line should appear in the dossier History peek for <paramref name="actor"/>.
|
|
/// </summary>
|
|
public static bool IsPeekRelevant(Actor actor, ActivityEntry entry, float now)
|
|
{
|
|
if (entry == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (now - entry.CreatedAt <= SoftWindowSeconds)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Family family = FamilyForEntry(entry);
|
|
if (family == Family.Milestone)
|
|
{
|
|
// Life beats stay peek-worthy; Chronicle also covers them.
|
|
return true;
|
|
}
|
|
|
|
if (actor == null || !actor.isAlive())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (family == Family.Status)
|
|
{
|
|
return StatusStillMatches(actor, entry);
|
|
}
|
|
|
|
if (family == Family.Happiness)
|
|
{
|
|
// Discrete mood beats age out of peek after the soft window.
|
|
return false;
|
|
}
|
|
|
|
string entryVerb = ActivityVerbMap.Resolve(entry.TaskId ?? "");
|
|
string liveTaskId = ActivityInterestTable.SafeTaskId(actor);
|
|
string liveVerb = ActivityVerbMap.Resolve(liveTaskId);
|
|
|
|
if (!string.IsNullOrEmpty(entryVerb)
|
|
&& !string.IsNullOrEmpty(liveVerb)
|
|
&& (string.Equals(entryVerb, liveVerb, StringComparison.OrdinalIgnoreCase)
|
|
|| FamilyForVerb(entryVerb) == FamilyForVerb(liveVerb)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return FamilyLivePredicate(actor, family);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fingerprint of live AI bits that can change peek without adding ring lines.
|
|
/// </summary>
|
|
public static int LiveFingerprint(Actor actor, float now)
|
|
{
|
|
int h = 17;
|
|
h = h * 31 + (int)(now / SoftWindowSeconds);
|
|
if (actor == null)
|
|
{
|
|
return h;
|
|
}
|
|
|
|
try
|
|
{
|
|
h = h * 31 + (actor.has_attack_target ? 1 : 0);
|
|
h = h * 31 + (actor.isCarryingResources() ? 1 : 0);
|
|
string task = ActivityInterestTable.SafeTaskId(actor) ?? "";
|
|
h = h * 31 + (task?.GetHashCode() ?? 0);
|
|
if (actor.hasTask() && actor.ai?.task != null)
|
|
{
|
|
h = h * 31 + (actor.ai.task.in_combat ? 1 : 0);
|
|
h = h * 31 + (actor.ai.task.is_fireman ? 1 : 0);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return h;
|
|
}
|
|
|
|
private static bool FamilyLivePredicate(Actor actor, Family family)
|
|
{
|
|
switch (family)
|
|
{
|
|
case Family.Combat:
|
|
return CombatLive(actor);
|
|
case Family.Haul:
|
|
return HaulLive(actor);
|
|
case Family.Social:
|
|
case Family.Romance:
|
|
case Family.Work:
|
|
case Family.Care:
|
|
case Family.Travel:
|
|
case Family.Ambient:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool CombatLive(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
if (actor.has_attack_target)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (actor.hasTask() && actor.ai?.task != null
|
|
&& (actor.ai.task.in_combat || actor.ai.task.is_fireman))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool HaulLive(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
return actor.isCarryingResources();
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool StatusStillMatches(Actor actor, ActivityEntry entry)
|
|
{
|
|
string statusId = StatusIdFromEntry(entry);
|
|
if (string.IsNullOrEmpty(statusId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
bool has = actor.hasStatus(statusId);
|
|
if (entry.Kind == ActivityKind.StatusGain)
|
|
{
|
|
return has;
|
|
}
|
|
|
|
if (entry.Kind == ActivityKind.StatusLoss)
|
|
{
|
|
return !has;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string StatusIdFromEntry(ActivityEntry entry)
|
|
{
|
|
string key = (entry?.TaskId ?? "").Trim();
|
|
const string gain = "status_gain:";
|
|
const string loss = "status_loss:";
|
|
if (key.StartsWith(gain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return key.Substring(gain.Length).Trim();
|
|
}
|
|
|
|
if (key.StartsWith(loss, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return key.Substring(loss.Length).Trim();
|
|
}
|
|
|
|
// NoteStatusChange may store bare status id or ActivityStatusProse.TaskKey.
|
|
if (key.IndexOf(':') < 0 && !string.IsNullOrEmpty(key))
|
|
{
|
|
return key;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
}
|