840 lines
25 KiB
C#
840 lines
25 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 > <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;
|
|
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)
|
|
{
|
|
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 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;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|
|
}
|