- Introduce new probes for framing approval and family coalescing - Implement exact presentation replay checks to prevent duplicate events - Refactor InterestVariety to manage exact replay cooldowns effectively - Update EventPresentability to improve candidate approval logic - Enhance InterestFeeds to streamline event registration and labeling
231 lines
7.7 KiB
C#
231 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Single source of truth: would the orange dossier show this EventLed Label?
|
|
/// Selection must equal presentation - unpresentable beats must not win Watching.
|
|
/// </summary>
|
|
public static class EventPresentability
|
|
{
|
|
private const int CacheLimit = 256;
|
|
|
|
private sealed class CacheEntry
|
|
{
|
|
public string Label = "";
|
|
public long SubjectId;
|
|
public long RelatedId;
|
|
public InterestLeadKind LeadKind;
|
|
public bool Result;
|
|
public float LastAccessAt;
|
|
}
|
|
|
|
private sealed class CacheTicket
|
|
{
|
|
public string Key = "";
|
|
public CacheEntry Entry;
|
|
}
|
|
|
|
private static readonly Dictionary<string, CacheEntry> Cache =
|
|
new Dictionary<string, CacheEntry>(CacheLimit, StringComparer.Ordinal);
|
|
private static readonly Queue<CacheTicket> CacheOrder =
|
|
new Queue<CacheTicket>(CacheLimit + 1);
|
|
|
|
public static void ClearCache()
|
|
{
|
|
Cache.Clear();
|
|
CacheOrder.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when the candidate may own Layer A camera.
|
|
/// Empty Label is only allowed for CharacterLed fill (task chip, no orange reason).
|
|
/// </summary>
|
|
public static bool WouldShow(InterestCandidate scene)
|
|
{
|
|
if (scene == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsApproved(scene))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(scene.Label))
|
|
{
|
|
return scene.LeadKind == InterestLeadKind.CharacterLed;
|
|
}
|
|
|
|
string key = scene.Key ?? "";
|
|
if (!string.IsNullOrEmpty(key)
|
|
&& Cache.TryGetValue(key, out CacheEntry cached)
|
|
&& cached.SubjectId == scene.SubjectId
|
|
&& cached.RelatedId == scene.RelatedId
|
|
&& cached.LeadKind == scene.LeadKind
|
|
&& string.Equals(cached.Label, scene.Label, StringComparison.Ordinal))
|
|
{
|
|
cached.LastAccessAt = Time.unscaledTime;
|
|
return cached.Result;
|
|
}
|
|
|
|
bool result =
|
|
!string.IsNullOrEmpty(UnitDossier.EvaluateSceneLabel(scene, recordDrops: false));
|
|
if (!string.IsNullOrEmpty(key))
|
|
{
|
|
var entry = new CacheEntry
|
|
{
|
|
Label = scene.Label,
|
|
SubjectId = scene.SubjectId,
|
|
RelatedId = scene.RelatedId,
|
|
LeadKind = scene.LeadKind,
|
|
Result = result,
|
|
LastAccessAt = Time.unscaledTime
|
|
};
|
|
Cache[key] = entry;
|
|
CacheOrder.Enqueue(new CacheTicket { Key = key, Entry = entry });
|
|
TrimCache();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stamp the exact identity that passed the intake gate. Repeated scheduler/director
|
|
/// checks can trust this fingerprint without rescanning the world. Ownership changes
|
|
/// invalidate it; explicitly recognized live framing updates retain approval.
|
|
/// </summary>
|
|
public static void MarkApproved(InterestCandidate scene)
|
|
{
|
|
if (scene == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
scene.ApprovedPresentationLabel = scene.Label ?? "";
|
|
scene.ApprovedPresentationSubjectId = scene.SubjectId;
|
|
scene.ApprovedPresentationRelatedId = scene.RelatedId;
|
|
scene.ApprovedPresentationLeadKind = scene.LeadKind;
|
|
scene.HasApprovedPresentation = true;
|
|
}
|
|
|
|
public static bool IsApproved(InterestCandidate scene)
|
|
{
|
|
if (scene == null
|
|
|| !scene.HasApprovedPresentation
|
|
|| scene.ApprovedPresentationSubjectId != scene.SubjectId
|
|
|| scene.ApprovedPresentationRelatedId != scene.RelatedId
|
|
|| scene.ApprovedPresentationLeadKind != scene.LeadKind)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string approved = scene.ApprovedPresentationLabel ?? "";
|
|
string current = scene.Label ?? "";
|
|
if (string.Equals(approved, current, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Sticky scene maintenance may refresh scale/tier without changing ownership.
|
|
// Re-running UnitDossier's stranger scan across a developed world for every
|
|
// headcount tick made the scheduler proposal stage hitch badly.
|
|
bool liveFraming =
|
|
scene.Completion == InterestCompletionKind.CombatActive
|
|
|| scene.Completion == InterestCompletionKind.WarFront
|
|
|| scene.Completion == InterestCompletionKind.StatusOutbreak
|
|
|| scene.Completion == InterestCompletionKind.FamilyPack;
|
|
if (!liveFraming)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (EventReason.IsHeadcountOnlyChange(approved, current))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return scene.Completion == InterestCompletionKind.CombatActive
|
|
&& EventReason.IsCombatFramingOnlyChange(approved, current);
|
|
}
|
|
|
|
public static bool HarnessProbeFramingApproval(out string detail)
|
|
{
|
|
var scene = new InterestCandidate
|
|
{
|
|
SubjectId = 41,
|
|
RelatedId = 42,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Completion = InterestCompletionKind.CombatActive,
|
|
Label = "Battle - Alpha (8) vs Beta (4)"
|
|
};
|
|
MarkApproved(scene);
|
|
scene.Label = "Mass - Beta (3) vs Alpha (12)";
|
|
bool framingKept = IsApproved(scene);
|
|
scene.Label = "Mass - Gamma (3) vs Alpha (12)";
|
|
bool foreignCampRejected = !IsApproved(scene);
|
|
scene.Label = "Mass - Beta (3) vs Alpha (12)";
|
|
scene.SubjectId = 99;
|
|
bool ownerMutationRejected = !IsApproved(scene);
|
|
bool ok = framingKept && foreignCampRejected && ownerMutationRejected;
|
|
detail = "framing=" + framingKept
|
|
+ " foreignCamp=" + foreignCampRejected
|
|
+ " ownerMutation=" + ownerMutationRejected
|
|
+ " pass=" + ok;
|
|
return ok;
|
|
}
|
|
|
|
private static void TrimCache()
|
|
{
|
|
// Happiness bursts can publish hundreds of distinct keys. Scanning the whole
|
|
// dictionary to find the oldest entry for every insertion made cache maintenance
|
|
// quadratic. Tickets make eviction constant-time and safely ignore overwritten keys.
|
|
while (Cache.Count > CacheLimit && CacheOrder.Count > 0)
|
|
{
|
|
CacheTicket ticket = CacheOrder.Dequeue();
|
|
if (ticket != null
|
|
&& !string.IsNullOrEmpty(ticket.Key)
|
|
&& Cache.TryGetValue(ticket.Key, out CacheEntry current)
|
|
&& ReferenceEquals(current, ticket.Entry))
|
|
{
|
|
Cache.Remove(ticket.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shown orange reason, or empty when blanked by identity / ownership rules.
|
|
/// </summary>
|
|
public static string ShownReason(InterestCandidate scene, bool recordDrops = true)
|
|
{
|
|
return UnitDossier.EvaluateSceneLabel(scene, recordDrops);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Harness synthetic tips ("HoldAction") must be subject-led EventReason shape
|
|
/// so the presentability gate matches production dossier rules.
|
|
/// </summary>
|
|
public static string EnsureSubjectLedLabel(Actor subject, string label)
|
|
{
|
|
if (string.IsNullOrEmpty(label))
|
|
{
|
|
return label ?? "";
|
|
}
|
|
|
|
string name = EventFeedUtil.SafeName(subject);
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
return label;
|
|
}
|
|
|
|
if (label.StartsWith(name, System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return label;
|
|
}
|
|
|
|
// "Name is in scene: HoldAction" → EventSentence + subject-led first token.
|
|
return name + " is in scene: " + label;
|
|
}
|
|
}
|