377 lines
10 KiB
C#
377 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Bounded merge/expiry store for <see cref="InterestCandidate"/>. Sole intake for the director.
|
|
/// </summary>
|
|
public static class InterestRegistry
|
|
{
|
|
public const int MaxCandidates = 96;
|
|
public const float DefaultTtlSeconds = 45f;
|
|
|
|
private static readonly object Gate = new object();
|
|
private static readonly Dictionary<string, InterestCandidate> ByKey =
|
|
new Dictionary<string, InterestCandidate>(128);
|
|
private static readonly List<InterestCandidate> Scratch = new List<InterestCandidate>(128);
|
|
|
|
public static int Count
|
|
{
|
|
get
|
|
{
|
|
lock (Gate)
|
|
{
|
|
return ByKey.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Unselected candidates still eligible for scheduling.</summary>
|
|
public static int PendingCount
|
|
{
|
|
get
|
|
{
|
|
lock (Gate)
|
|
{
|
|
int n = 0;
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
if (kv.Value != null && !kv.Value.Selected && kv.Value.HasValidPosition)
|
|
{
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
lock (Gate)
|
|
{
|
|
ByKey.Clear();
|
|
}
|
|
}
|
|
|
|
public static InterestCandidate Upsert(InterestCandidate incoming)
|
|
{
|
|
if (incoming == null || string.IsNullOrEmpty(incoming.Key) || !incoming.HasValidPosition)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
float now = Time.unscaledTime;
|
|
if (incoming.CreatedAt <= 0f)
|
|
{
|
|
incoming.CreatedAt = now;
|
|
}
|
|
|
|
if (incoming.LastSeenAt <= 0f)
|
|
{
|
|
incoming.LastSeenAt = now;
|
|
}
|
|
|
|
if (incoming.ExpiresAt <= 0f)
|
|
{
|
|
incoming.ExpiresAt = now + DefaultTtlSeconds;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
if (ByKey.TryGetValue(incoming.Key, out InterestCandidate existing) && existing != null)
|
|
{
|
|
// Ownership merge: same key must not adopt a different subject's FollowUnit/Label.
|
|
bool sameSubject = incoming.SubjectId == 0
|
|
|| existing.SubjectId == 0
|
|
|| incoming.SubjectId == existing.SubjectId;
|
|
if (!sameSubject)
|
|
{
|
|
// Reject subject hijack; keep existing ownership, still refresh TTL/scores lightly.
|
|
existing.LastSeenAt = now;
|
|
existing.ExpiresAt = Mathf.Max(existing.ExpiresAt, incoming.ExpiresAt);
|
|
existing.EventStrength = Mathf.Max(existing.EventStrength, incoming.EventStrength);
|
|
existing.TotalScore = Mathf.Max(existing.TotalScore, incoming.TotalScore);
|
|
existing.Selected = false;
|
|
return existing;
|
|
}
|
|
|
|
existing.LastSeenAt = now;
|
|
existing.ExpiresAt = Mathf.Max(existing.ExpiresAt, incoming.ExpiresAt);
|
|
existing.EventStrength = Mathf.Max(existing.EventStrength, incoming.EventStrength);
|
|
existing.CharacterSignificance = Mathf.Max(
|
|
existing.CharacterSignificance, incoming.CharacterSignificance);
|
|
existing.VisualConfidence = Mathf.Max(existing.VisualConfidence, incoming.VisualConfidence);
|
|
existing.TotalScore = Mathf.Max(existing.TotalScore, incoming.TotalScore);
|
|
|
|
if (!string.IsNullOrEmpty(incoming.Label))
|
|
{
|
|
existing.Label = incoming.Label;
|
|
}
|
|
|
|
if (incoming.FollowUnit != null && incoming.FollowUnit.isAlive())
|
|
{
|
|
existing.FollowUnit = incoming.FollowUnit;
|
|
existing.SubjectId = incoming.SubjectId != 0
|
|
? incoming.SubjectId
|
|
: EventFeedUtil.SafeId(incoming.FollowUnit);
|
|
existing.Position = incoming.FollowUnit.current_position;
|
|
}
|
|
else if (incoming.Position != Vector3.zero)
|
|
{
|
|
existing.Position = incoming.Position;
|
|
}
|
|
|
|
if (incoming.RelatedUnit != null)
|
|
{
|
|
existing.RelatedUnit = incoming.RelatedUnit;
|
|
existing.RelatedId = incoming.RelatedId;
|
|
}
|
|
|
|
if (incoming.ForceActive)
|
|
{
|
|
existing.ForceActive = true;
|
|
}
|
|
|
|
existing.Selected = false;
|
|
return existing;
|
|
}
|
|
|
|
ByKey[incoming.Key] = incoming;
|
|
TrimLocked(now);
|
|
return incoming;
|
|
}
|
|
}
|
|
|
|
public static int CountKeysContaining(string needle)
|
|
{
|
|
if (string.IsNullOrEmpty(needle))
|
|
{
|
|
return Count;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
int n = 0;
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c?.Key != null
|
|
&& c.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
n++;
|
|
continue;
|
|
}
|
|
|
|
if (c?.HappinessEffectId != null
|
|
&& c.HappinessEffectId.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
}
|
|
|
|
public static void ForceExpireContaining(string needle, float now)
|
|
{
|
|
lock (Gate)
|
|
{
|
|
Scratch.Clear();
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool match = string.IsNullOrEmpty(needle)
|
|
|| (c.Key != null
|
|
&& c.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0);
|
|
if (match)
|
|
{
|
|
c.ExpiresAt = now - 0.01f;
|
|
Scratch.Add(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
ExpireStale(now);
|
|
}
|
|
|
|
public static bool TryGet(string key, out InterestCandidate candidate)
|
|
{
|
|
lock (Gate)
|
|
{
|
|
return ByKey.TryGetValue(key ?? "", out candidate) && candidate != null;
|
|
}
|
|
}
|
|
|
|
public static void MarkSelected(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
if (ByKey.TryGetValue(key, out InterestCandidate c) && c != null)
|
|
{
|
|
c.Selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void MarkUnselected(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
if (ByKey.TryGetValue(key, out InterestCandidate c) && c != null)
|
|
{
|
|
c.Selected = false;
|
|
c.LastSeenAt = Time.unscaledTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Remove(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
ByKey.Remove(key);
|
|
}
|
|
}
|
|
|
|
public static void ExpireStale(float now)
|
|
{
|
|
lock (Gate)
|
|
{
|
|
Scratch.Clear();
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c == null || now >= c.ExpiresAt || !c.HasValidPosition)
|
|
{
|
|
Scratch.Add(c);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < Scratch.Count; i++)
|
|
{
|
|
InterestCandidate c = Scratch[i];
|
|
if (c != null && !string.IsNullOrEmpty(c.Key))
|
|
{
|
|
ByKey.Remove(c.Key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Copy live unselected candidates into <paramref name="dest"/> (cleared first).</summary>
|
|
public static void CopyPending(List<InterestCandidate> dest)
|
|
{
|
|
if (dest == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dest.Clear();
|
|
lock (Gate)
|
|
{
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c == null || c.Selected || !c.HasValidPosition)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dest.Add(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool HasPendingScoreAtLeast(float minScore)
|
|
{
|
|
lock (Gate)
|
|
{
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c != null && !c.Selected && c.HasValidPosition && c.TotalScore >= minScore)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>True when any unselected EventLed candidate is waiting for the camera.</summary>
|
|
public static bool HasPendingEvent()
|
|
{
|
|
lock (Gate)
|
|
{
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
InterestCandidate c = kv.Value;
|
|
if (c == null || c.Selected || !c.HasValidPosition)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (c.LeadKind == InterestLeadKind.EventLed)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void TrimLocked(float now)
|
|
{
|
|
if (ByKey.Count <= MaxCandidates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Scratch.Clear();
|
|
foreach (KeyValuePair<string, InterestCandidate> kv in ByKey)
|
|
{
|
|
if (kv.Value != null)
|
|
{
|
|
Scratch.Add(kv.Value);
|
|
}
|
|
}
|
|
|
|
Scratch.Sort(InterestScoring.CompareByScore);
|
|
|
|
for (int i = MaxCandidates; i < Scratch.Count; i++)
|
|
{
|
|
InterestCandidate drop = Scratch[i];
|
|
if (drop != null && !string.IsNullOrEmpty(drop.Key))
|
|
{
|
|
ByKey.Remove(drop.Key);
|
|
}
|
|
}
|
|
}
|
|
}
|