worldbox-observer-mod/IdleSpectator/HappinessEventRouter.cs
DazedAnon fcf2445cd0 fix(narrative): improve caption relevance and combat continuity
- Limit saga context to relevant beats and suppress repeated facts
- Stabilize combat labels and commit telemetry after focus changes
- Preserve relationship participants and normalize event prose
- Compact narrative sidecars and extend presentation coverage
2026-07-23 14:52:27 -05:00

944 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Sole writer from happiness hooks into Activity / Chronicle Life.
/// Patches publish occurrences; presentation tiers and overlap live here.
/// </summary>
public static class HappinessEventRouter
{
public const float AmbientRingCooldown = 25f;
public const float CorrelationWindow = 3f;
public const float AggregateWindow = 0.75f;
public const int AggregateSampleCap = 8;
public const int MaxRecentOccurrences = 256;
private static readonly List<HappinessOccurrence> Recent =
new List<HappinessOccurrence>(MaxRecentOccurrences);
private static readonly Dictionary<string, float> AmbientRingAt =
new Dictionary<string, float>();
private static readonly Dictionary<string, float> CorrelationAt =
new Dictionary<string, float>();
private static readonly Dictionary<string, AggregateBucket> Aggregates =
new Dictionary<string, AggregateBucket>();
private static int _notesSinceClear;
private static int _ringWritesSinceClear;
private static int _lifeWritesSinceClear;
private static int _mergedSinceClear;
private static int _suppressedSinceClear;
private static int _aggregateSummariesSinceClear;
private static string _lastEffectId = "";
private static long _counterSubjectId;
private static ulong _nextSequence = 1;
private static Action<HappinessOccurrence> _subscribers;
private sealed class AggregateBucket
{
public string EffectId;
public float StartedAt;
public int Total;
public int Sampled;
public Actor FirstNotable;
public string CivKey = "";
}
public static int NotesSinceClear => _notesSinceClear;
public static int RingWritesSinceClear => _ringWritesSinceClear;
public static int LifeWritesSinceClear => _lifeWritesSinceClear;
public static int MergedSinceClear => _mergedSinceClear;
public static int SuppressedSinceClear => _suppressedSinceClear;
public static int AggregateSummariesSinceClear => _aggregateSummariesSinceClear;
public static string LastEffectId => _lastEffectId ?? "";
public static ulong CurrentSequence => _nextSequence > 0 ? _nextSequence - 1 : 0;
public static void ClearSession()
{
Recent.Clear();
AmbientRingAt.Clear();
CorrelationAt.Clear();
Aggregates.Clear();
_notesSinceClear = 0;
_ringWritesSinceClear = 0;
_lifeWritesSinceClear = 0;
_mergedSinceClear = 0;
_suppressedSinceClear = 0;
_aggregateSummariesSinceClear = 0;
_lastEffectId = "";
_counterSubjectId = 0;
_nextSequence = 1;
InterestFeeds.OnHappinessRouterCleared();
}
/// <summary>Push listener for spectator interest intake (in addition to <see cref="DrainSince"/>).</summary>
public static void Subscribe(Action<HappinessOccurrence> handler)
{
if (handler == null)
{
return;
}
_subscribers += handler;
}
public static void Unsubscribe(Action<HappinessOccurrence> handler)
{
if (handler == null)
{
return;
}
_subscribers -= handler;
}
/// <summary>
/// Drain occurrences with Sequence &gt; <paramref name="sinceSeq"/>.
/// Returns the highest sequence drained (pass back next call). Diagnostics may still use <see cref="Latest"/>.
/// </summary>
public static ulong DrainSince(ulong sinceSeq, List<HappinessOccurrence> destination)
{
if (destination == null)
{
return sinceSeq;
}
destination.Clear();
ulong max = sinceSeq;
for (int i = 0; i < Recent.Count; i++)
{
HappinessOccurrence occ = Recent[i];
if (occ == null || occ.Sequence <= sinceSeq)
{
continue;
}
destination.Add(occ);
if (occ.Sequence > max)
{
max = occ.Sequence;
}
}
return max;
}
public static void ResetCountersForSubject(long subjectId)
{
_counterSubjectId = subjectId;
_notesSinceClear = 0;
_ringWritesSinceClear = 0;
_lifeWritesSinceClear = 0;
_mergedSinceClear = 0;
_suppressedSinceClear = 0;
_aggregateSummariesSinceClear = 0;
_lastEffectId = "";
}
public static IReadOnlyList<HappinessOccurrence> Latest(int max = 32)
{
if (max <= 0 || Recent.Count == 0)
{
return Array.Empty<HappinessOccurrence>();
}
int start = Math.Max(0, Recent.Count - max);
var list = new List<HappinessOccurrence>(Recent.Count - start);
for (int i = start; i < Recent.Count; i++)
{
list.Add(Recent[i]);
}
return list;
}
/// <summary>Publish a happiness effect that the game actually applied (or harness force).</summary>
public static HappinessOccurrence PublishApplied(
Actor subject,
string effectId,
int amount,
HappinessSourceHook hook,
Actor related = null,
HappinessRelationRole role = HappinessRelationRole.None,
bool forceRing = false,
string relatedNameOverride = null)
{
return PublishCore(
subject,
effectId,
amount,
hook,
related,
role,
applied: true,
suppressedByPsychopath: false,
forceRing,
relatedNameOverride);
}
/// <summary>Record that the game refused to apply happiness (psychopath/egg/no emotions).</summary>
public static void PublishSuppressed(Actor subject, string effectId, HappinessSourceHook hook)
{
if (_counterSubjectId == 0 || (subject != null && subject.getID() == _counterSubjectId))
{
_suppressedSinceClear++;
}
InterestDropLog.Record("no_emotions_or_egg", effectId ?? "");
var occ = BuildOccurrence(
subject,
effectId,
amount: 0,
hook,
related: null,
role: HappinessRelationRole.None,
applied: false,
suppressedByPsychopath: true);
occ.Merged = true;
Remember(occ);
}
private static HappinessOccurrence PublishCore(
Actor subject,
string effectId,
int amount,
HappinessSourceHook hook,
Actor related,
HappinessRelationRole role,
bool applied,
bool suppressedByPsychopath,
bool forceRing,
string relatedNameOverride = null)
{
if (subject == null || string.IsNullOrEmpty(effectId) || !applied)
{
return null;
}
FlushStaleAggregates();
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(effectId);
if (related == null
&& HappinessContextBag.TryGetRelated(subject, out Actor bagRelated, out HappinessRelationRole bagRole))
{
related = bagRelated;
if (role == HappinessRelationRole.None)
{
role = bagRole;
}
}
if (role == HappinessRelationRole.None)
{
role = entry.RelationRole != HappinessRelationRole.None
? entry.RelationRole
: HappinessContextBag.RoleForDeathEffect(effectId);
}
if (entry.Presentation == HappinessPresentationTier.Aggregate && !forceRing)
{
return PublishAggregateMember(subject, effectId, amount, hook, related, role, entry);
}
HappinessOccurrence occ = BuildOccurrence(
subject,
effectId,
amount,
hook,
related,
role,
applied,
suppressedByPsychopath,
relatedNameOverride);
FillProse(occ, entry);
if (ShouldMerge(occ, entry))
{
occ.Merged = true;
EnrichEarlierOccurrence(occ);
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
{
_mergedSinceClear++;
_notesSinceClear++;
_lastEffectId = effectId;
}
Remember(occ);
return occ;
}
bool writeRing = forceRing || ShouldWriteRing(occ, entry);
if (writeRing)
{
ActivityLog.NoteHappiness(occ);
occ.WroteActivityRing = true;
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
{
_ringWritesSinceClear++;
}
MarkAmbient(occ);
}
if (entry.Life == HappinessLifePolicy.ProjectToLife
&& entry.Overlap != HappinessOverlapPolicy.CanonicalMilestone)
{
NoteSagaFromHappiness(occ, subject, related, effectId);
if (Chronicle.NoteHappinessLife(occ, subject, related))
{
occ.WroteLife = true;
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
{
_lifeWritesSinceClear++;
}
}
}
MarkCorrelation(occ, entry);
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
{
_notesSinceClear++;
_lastEffectId = effectId;
}
Remember(occ);
return occ;
}
private static void NoteSagaFromHappiness(
HappinessOccurrence occ,
Actor subject,
Actor related,
string effectId)
{
if (subject == null || string.IsNullOrEmpty(effectId))
{
return;
}
string id = effectId;
if (id.IndexOf("become_king", System.StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("became_king", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
LifeSagaMemory.RecordRole(subject, "become_king", "happiness:" + id);
return;
}
if (id.IndexOf("become_leader", System.StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("became_leader", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
LifeSagaMemory.RecordRole(subject, "become_leader", "happiness:" + id);
return;
}
if (id.IndexOf("become_alpha", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
LifeSagaMemory.RecordRole(subject, "become_alpha", "happiness:" + id);
return;
}
if (id.IndexOf("just_found_house", System.StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("found_house", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
LifeSagaMemory.RecordHome(subject, gained: true, provenance: "happiness:" + id);
return;
}
if (id.IndexOf("just_lost_house", System.StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("lost_house", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
LifeSagaMemory.RecordHome(subject, gained: false, provenance: "happiness:" + id);
return;
}
if (id.IndexOf("death_lover", System.StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("grief", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
if (related != null)
{
LifeSagaMemory.RecordBondBroken(subject, related, "happiness:" + id);
}
}
_ = occ;
}
private static HappinessOccurrence PublishAggregateMember(
Actor subject,
string effectId,
int amount,
HappinessSourceHook hook,
Actor related,
HappinessRelationRole role,
HappinessCatalogEntry entry)
{
string civKey = CivKey(subject, effectId);
float now = Time.unscaledTime;
if (!Aggregates.TryGetValue(civKey, out AggregateBucket bucket)
|| now - bucket.StartedAt > AggregateWindow)
{
bucket = new AggregateBucket
{
EffectId = effectId,
StartedAt = now,
CivKey = civKey
};
Aggregates[civKey] = bucket;
}
bucket.Total++;
bool notable = IsNotable(subject);
bool takeSample = bucket.Sampled < AggregateSampleCap && (notable ? bucket.FirstNotable == null : true);
if (notable && bucket.FirstNotable == null)
{
bucket.FirstNotable = subject;
}
if (takeSample)
{
bucket.Sampled++;
// Notables and early samples get individual ring lines (one notable max).
HappinessOccurrence sample = BuildOccurrence(
subject, effectId, amount, hook, related, role, applied: true, suppressedByPsychopath: false);
sample.Presentation = HappinessPresentationTier.Aggregate;
sample.CivEventKey = civKey;
sample.SampledCount = 1;
sample.TotalAffectedCount = bucket.Total;
FillProse(sample, entry);
bool writeRing = notable || bucket.Sampled <= 3;
if (writeRing)
{
ActivityLog.NoteHappiness(sample);
sample.WroteActivityRing = true;
if (_counterSubjectId == 0 || sample.SubjectId == _counterSubjectId)
{
_ringWritesSinceClear++;
}
}
if (_counterSubjectId == 0 || sample.SubjectId == _counterSubjectId)
{
_notesSinceClear++;
_lastEffectId = effectId;
}
Remember(sample);
return sample;
}
// Overflow: count only; summary flushed later.
HappinessOccurrence skipped = BuildOccurrence(
subject, effectId, amount, hook, related, role, applied: true, suppressedByPsychopath: false);
skipped.Presentation = HappinessPresentationTier.Aggregate;
skipped.CivEventKey = civKey;
skipped.TotalAffectedCount = bucket.Total;
skipped.Merged = true;
FillProse(skipped, entry);
Remember(skipped);
return skipped;
}
public static void FlushAggregatesNow()
{
FlushStaleAggregates(force: true);
}
private static void FlushStaleAggregates(bool force = false)
{
float now = Time.unscaledTime;
if (Aggregates.Count == 0)
{
return;
}
var done = new List<string>();
foreach (KeyValuePair<string, AggregateBucket> kv in Aggregates)
{
if (force || now - kv.Value.StartedAt >= AggregateWindow)
{
done.Add(kv.Key);
}
}
for (int i = 0; i < done.Count; i++)
{
string key = done[i];
AggregateBucket bucket = Aggregates[key];
Aggregates.Remove(key);
if (bucket.Total <= bucket.Sampled)
{
continue;
}
Actor subject = bucket.FirstNotable;
if (subject == null)
{
continue;
}
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(bucket.EffectId);
HappinessOccurrence summary = BuildOccurrence(
subject,
bucket.EffectId,
amount: HappinessGameApi.ResolveAmount(bucket.EffectId),
HappinessSourceHook.AggregateSummary,
related: null,
role: HappinessRelationRole.None,
applied: true,
suppressedByPsychopath: false);
summary.Presentation = HappinessPresentationTier.Aggregate;
summary.CivEventKey = bucket.CivKey;
summary.SampledCount = bucket.Sampled;
summary.TotalAffectedCount = bucket.Total;
int others = Math.Max(0, bucket.Total - bucket.Sampled);
string baseClause = ActivityHappinessProse.Clause(entry, "", out _, out _);
summary.PlainClause = baseClause + " (and " + others + " others)";
summary.RichClause = summary.PlainClause;
summary.FactLine = "Happiness " + bucket.EffectId + " x" + bucket.Total;
ActivityLog.NoteHappiness(summary);
summary.WroteActivityRing = true;
_aggregateSummariesSinceClear++;
if (_counterSubjectId == 0 || summary.SubjectId == _counterSubjectId)
{
_ringWritesSinceClear++;
_notesSinceClear++;
_lastEffectId = bucket.EffectId;
}
Remember(summary);
}
}
private static HappinessOccurrence BuildOccurrence(
Actor subject,
string effectId,
int amount,
HappinessSourceHook hook,
Actor related,
HappinessRelationRole role,
bool applied,
bool suppressedByPsychopath,
string relatedNameOverride = null)
{
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(effectId);
long subjectId = 0;
string subjectName = "Someone";
string species = "";
Vector3 pos = Vector3.zero;
try
{
subjectId = subject.getID();
subjectName = subject.getName();
if (string.IsNullOrEmpty(subjectName))
{
subjectName = subject.asset != null ? subject.asset.id : "creature";
}
species = subject.asset != null ? subject.asset.id : "";
pos = subject.current_position;
}
catch
{
// keep defaults
}
long relatedId = 0;
string relatedName = "";
string relatedSpecies = "";
if (related != null)
{
try
{
relatedId = related.getID();
relatedName = related.getName();
if (string.IsNullOrEmpty(relatedName))
{
relatedName = related.asset != null ? related.asset.id : "creature";
}
relatedSpecies = related.asset != null ? related.asset.id : "";
}
catch
{
// ignore
}
}
if (string.IsNullOrEmpty(relatedName) && !string.IsNullOrEmpty(relatedNameOverride))
{
relatedName = relatedNameOverride.Trim();
}
StampWorldDate(out double worldTime, out string dateLabel);
string corr = subjectId + "|" + effectId + "|" + relatedId + "|" + Math.Floor(worldTime);
return new HappinessOccurrence
{
EffectId = effectId.Trim(),
Amount = amount,
SubjectId = subjectId,
SubjectName = subjectName,
SubjectSpecies = species,
RelatedId = relatedId,
RelatedName = relatedName,
RelatedSpecies = relatedSpecies,
RelationRole = role,
Position = pos,
WorldTime = worldTime,
DateLabel = dateLabel,
SourceHook = hook,
CorrelationKey = corr,
SuppressedByPsychopath = suppressedByPsychopath,
Applied = applied,
Presentation = entry.Presentation,
Life = entry.Life,
Overlap = entry.Overlap,
Context = entry.Context,
InterestCategory = entry.InterestCategory,
CanonicalMilestoneKey = entry.CanonicalMilestoneKey,
StatusOverlapId = entry.StatusOverlapId,
StatusOverlapKind = entry.StatusOverlapKind
};
}
private static void FillProse(HappinessOccurrence occ, HappinessCatalogEntry entry)
{
string clause = ActivityHappinessProse.Clause(
entry, occ.RelatedName, out bool usedRelated, out _);
if (entry.Context == HappinessContextRequirement.RequiresRelatedActor
&& string.IsNullOrEmpty(occ.RelatedName))
{
// Still author a subject-only line at runtime for safety; audit fails separately.
clause = ActivityHappinessProse.Clause(entry, "", out _, out _);
usedRelated = false;
}
occ.PlainClause = clause;
if (usedRelated && !string.IsNullOrEmpty(occ.RelatedName))
{
occ.RichClause = clause.Replace(
occ.RelatedName,
ActivityProse.ColorName(occ.RelatedName));
}
else
{
occ.RichClause = clause;
}
occ.FactLine = "Happiness " + occ.EffectId
+ (occ.Amount != 0 ? " " + (occ.Amount > 0 ? "+" : "") + occ.Amount : "");
}
private static bool ShouldMerge(HappinessOccurrence occ, HappinessCatalogEntry entry)
{
float now = Time.unscaledTime;
if (entry.Overlap == HappinessOverlapPolicy.CanonicalMilestone
&& !string.IsNullOrEmpty(entry.CanonicalMilestoneKey))
{
string key = "canon|" + occ.SubjectId + "|" + entry.CanonicalMilestoneKey;
if (CorrelationAt.TryGetValue(key, out float at) && now - at <= CorrelationWindow)
{
return true;
}
}
if (entry.Overlap == HappinessOverlapPolicy.Continuation
&& !string.IsNullOrEmpty(entry.StatusOverlapId))
{
string key = "status|" + occ.SubjectId + "|" + entry.StatusOverlapId;
if (CorrelationAt.TryGetValue(key, out float at) && now - at <= CorrelationWindow)
{
// Same-second status+happiness: keep status as the visible phase; merge happiness ring.
return true;
}
}
string selfKey = "happ|" + occ.SubjectId + "|" + occ.EffectId + "|" + occ.RelatedId;
if (CorrelationAt.TryGetValue(selfKey, out float selfAt) && now - selfAt <= 0.35f)
{
return true;
}
return false;
}
private static bool ShouldWriteRing(HappinessOccurrence occ, HappinessCatalogEntry entry)
{
if (entry.Presentation == HappinessPresentationTier.Signal)
{
return true;
}
if (entry.Presentation == HappinessPresentationTier.Ambient)
{
string key = occ.SubjectId + "|" + occ.EffectId;
float now = Time.unscaledTime;
if (AmbientRingAt.TryGetValue(key, out float at) && now - at < AmbientRingCooldown)
{
return false;
}
return true;
}
return true;
}
/// <summary>
/// A bridge callback can know the named partner after the first canonical signal was
/// recorded. The drain stores occurrence references, so enriching the earlier survivor
/// preserves that participant without creating a duplicate camera beat.
/// </summary>
private static void EnrichEarlierOccurrence(HappinessOccurrence incoming)
{
if (incoming == null
|| incoming.RelatedId == 0
|| string.IsNullOrEmpty(incoming.RelatedName))
{
return;
}
for (int i = Recent.Count - 1; i >= 0 && i >= Recent.Count - 12; i--)
{
HappinessOccurrence prior = Recent[i];
if (prior == null
|| prior.Merged
|| prior.SubjectId != incoming.SubjectId
|| !string.Equals(prior.EffectId, incoming.EffectId, StringComparison.OrdinalIgnoreCase)
|| prior.RelatedId != 0)
{
continue;
}
prior.RelatedId = incoming.RelatedId;
prior.RelatedName = incoming.RelatedName;
prior.RelatedSpecies = incoming.RelatedSpecies;
if (prior.RelationRole == HappinessRelationRole.None)
{
prior.RelationRole = incoming.RelationRole;
}
if (!string.IsNullOrEmpty(incoming.PlainClause))
{
prior.PlainClause = incoming.PlainClause;
prior.RichClause = incoming.RichClause;
}
return;
}
}
private static void MarkAmbient(HappinessOccurrence occ)
{
if (occ.Presentation != HappinessPresentationTier.Ambient)
{
return;
}
AmbientRingAt[occ.SubjectId + "|" + occ.EffectId] = Time.unscaledTime;
}
private static void MarkCorrelation(HappinessOccurrence occ, HappinessCatalogEntry entry)
{
float now = Time.unscaledTime;
CorrelationAt["happ|" + occ.SubjectId + "|" + occ.EffectId + "|" + occ.RelatedId] = now;
if (!string.IsNullOrEmpty(entry.CanonicalMilestoneKey))
{
CorrelationAt["canon|" + occ.SubjectId + "|" + entry.CanonicalMilestoneKey] = now;
}
if (!string.IsNullOrEmpty(entry.StatusOverlapId))
{
CorrelationAt["status|" + occ.SubjectId + "|" + entry.StatusOverlapId] = now;
}
PruneDict(CorrelationAt, now, CorrelationWindow * 4f);
PruneDict(AmbientRingAt, now, AmbientRingCooldown * 4f);
}
/// <summary>Called when Chronicle logs a canonical lover/friend/kill milestone.</summary>
public static void NoteCanonicalMilestone(long subjectId, string milestoneKey)
{
if (subjectId == 0 || string.IsNullOrEmpty(milestoneKey))
{
return;
}
CorrelationAt["canon|" + subjectId + "|" + milestoneKey] = Time.unscaledTime;
}
/// <summary>Called when status gain/loss is logged so Continuation overlaps can merge.</summary>
public static void NoteStatusPhase(long subjectId, string statusId)
{
if (subjectId == 0 || string.IsNullOrEmpty(statusId))
{
return;
}
CorrelationAt["status|" + subjectId + "|" + statusId] = Time.unscaledTime;
}
private static void Remember(HappinessOccurrence occ)
{
if (occ == null)
{
return;
}
occ.Sequence = _nextSequence++;
Recent.Add(occ);
while (Recent.Count > MaxRecentOccurrences)
{
Recent.RemoveAt(0);
}
Action<HappinessOccurrence> handlers = _subscribers;
if (handlers != null)
{
try
{
handlers(occ);
}
catch
{
// Interest consumers must not break happiness logging.
}
}
}
/// <summary>Harness-only: assign sequence and remember an already-built occurrence.</summary>
public static void RememberForHarness(HappinessOccurrence occ)
{
if (occ == null)
{
return;
}
occ.Applied = true;
Remember(occ);
}
private static bool IsNotable(Actor actor)
{
if (actor == null)
{
return false;
}
try
{
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null
&& MoveCamera._focus_unit.getID() == actor.getID())
{
return true;
}
}
catch
{
// ignore
}
try
{
if (actor.isFavorite())
{
return true;
}
}
catch
{
// ignore
}
try
{
if (actor.isKing() || actor.isCityLeader())
{
return true;
}
}
catch
{
// ignore
}
return false;
}
private static string CivKey(Actor subject, string effectId)
{
string kingdom = "";
try
{
if (subject.kingdom != null)
{
kingdom = subject.kingdom.name ?? "";
}
}
catch
{
kingdom = "";
}
return effectId + "|" + kingdom + "|" + Math.Floor(Time.unscaledTime / AggregateWindow);
}
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;
}
string monthName = Date.formatMonth(raw[1]);
if (string.IsNullOrEmpty(monthName))
{
monthName = "m" + raw[1];
}
dateLabel = monthName + " " + raw[2];
}
catch
{
dateLabel = "";
}
}
private static void PruneDict(Dictionary<string, float> dict, float now, float maxAge)
{
if (dict.Count < 256)
{
return;
}
var remove = new List<string>();
foreach (KeyValuePair<string, float> kv in dict)
{
if (now - kv.Value > maxAge)
{
remove.Add(kv.Key);
}
}
for (int i = 0; i < remove.Count; i++)
{
dict.Remove(remove[i]);
}
}
}