using System; using System.Collections.Generic; namespace IdleSpectator; /// /// Adds a concise, evidence-backed explanation when an exact event participant matters /// because of a current MC. Generic identity (species/job/traits) never belongs in the Beat. /// public static class BeatSignificance { private static readonly List Slots = new List(LifeSagaRoster.Cap); /// /// Character grounding is footage, not an event. Its live task already appears in the /// green task chip, so it must never manufacture an orange Beat. /// public static bool SuppressBeat(InterestCandidate candidate) { return candidate != null && (candidate.LeadKind == InterestLeadKind.CharacterLed || candidate.Completion == InterestCompletionKind.CharacterVignette); } public static string Enrich( Actor focus, InterestCandidate candidate, string beat) { if (string.IsNullOrEmpty(beat) || candidate == null || SuppressBeat(candidate)) { return SuppressBeat(candidate) ? "" : beat ?? ""; } long focusId = EventFeedUtil.SafeId(focus); long participantId = ExactOtherParticipant(candidate, focusId); if (participantId == 0 || participantId == focusId || LifeSagaRoster.IsMc(participantId) || SemanticsAlreadyExplainRelation(candidate, beat)) { return beat; } string participantName = ExactOtherName(candidate, participantId); if (string.IsNullOrEmpty(participantName) || !ContainsWholeName(StripRichText(beat), participantName)) { return beat; } if (!TryDescribeMcRelation( participantId, out long anchorMcId, out string anchorName, out RelationEvidence relation)) { return beat; } string qualifier = FormatQualifier(relation, anchorName, colorName: true); if (string.IsNullOrEmpty(qualifier) || StripRichText(beat).IndexOf( StripRichText(qualifier), StringComparison.OrdinalIgnoreCase) >= 0) { return beat; } // Preserve click ownership on the event's named participant. The qualifier explains // significance but does not replace the event's subject/other ids. return beat.TrimEnd() + " (" + qualifier + ")"; } private static long ExactOtherParticipant(InterestCandidate candidate, long focusId) { if (candidate?.NarrativePresentation != null) { long semanticOther = candidate.NarrativePresentation.OtherId; if (semanticOther != 0 && semanticOther != focusId) { return semanticOther; } } if (candidate == null) { return 0; } long[] exact = { candidate.RelatedId, EventFeedUtil.SafeId(candidate.RelatedUnit), candidate.PairOwnerId == focusId ? candidate.PairPartnerId : 0, candidate.PairPartnerId == focusId ? candidate.PairOwnerId : 0 }; for (int i = 0; i < exact.Length; i++) { if (exact[i] != 0 && exact[i] != focusId) { return exact[i]; } } return 0; } private static string ExactOtherName(InterestCandidate candidate, long participantId) { NarrativePresentationModel presentation = candidate?.NarrativePresentation; if (presentation != null && presentation.OtherId == participantId && !string.IsNullOrEmpty(presentation.Other.Name)) { return presentation.Other.Name.Trim(); } if (candidate?.RelatedUnit != null && EventFeedUtil.SafeId(candidate.RelatedUnit) == participantId) { string related = EventFeedUtil.SafeName(candidate.RelatedUnit); if (!string.IsNullOrEmpty(related)) { return related; } } Actor live = EventFeedUtil.FindAliveById(participantId); return EventFeedUtil.SafeName(live); } private static bool TryDescribeMcRelation( long participantId, out long anchorMcId, out string anchorName, out RelationEvidence relation) { anchorMcId = 0; anchorName = ""; relation = RelationEvidence.None; Actor participant = EventFeedUtil.FindAliveById(participantId); Slots.Clear(); LifeSagaRoster.CopySlots(Slots); int bestPriority = int.MaxValue; for (int i = 0; i < Slots.Count; i++) { LifeSagaSlot slot = Slots[i]; if (slot == null || slot.UnitId == 0 || slot.UnitId == participantId) { continue; } Actor mc = EventFeedUtil.FindAliveById(slot.UnitId); RelationEvidence evidence = SagaProse.ResolveRelation(slot.UnitId, participantId, mc, participant); string name = !string.IsNullOrEmpty(slot.DisplayName) ? slot.DisplayName : EventFeedUtil.SafeName(mc); string candidateQualifier = FormatQualifier(evidence, name, colorName: false); if (string.IsNullOrEmpty(candidateQualifier)) { continue; } int priority = evidence.Priority > 0 ? evidence.Priority : 9; if (anchorMcId != 0 && priority >= bestPriority) { continue; } anchorMcId = slot.UnitId; anchorName = name; relation = evidence; bestPriority = priority; } return anchorMcId != 0 && !string.IsNullOrEmpty(anchorName); } private static string FormatQualifier( RelationEvidence relation, string anchorName, bool colorName) { if (relation == null || relation.Class == RelationClass.None || string.IsNullOrEmpty(anchorName)) { return ""; } string who = colorName ? ActivityProse.ColorName(anchorName) : anchorName; switch (relation.Class) { case RelationClass.Bond: return (relation.BondBroken ? "former partner of " : "partner of ") + who; case RelationClass.Friend: return (relation.BestFriend ? "best friend of " : "friend of ") + who; case RelationClass.Kin: switch (relation.KinKind) { case KinKind.Parent: return "parent of " + who; case KinKind.Child: return "child of " + who; case KinKind.Heir: return "heir of " + who; case KinKind.Packmate: return "packmate of " + who; default: return "kin of " + who; } case RelationClass.Foe: switch (relation.FoeKind) { case FoeKind.Blood: return "blood foe of " + who; case FoeKind.War: return "war enemy of " + who; case FoeKind.Plot: return "plot enemy of " + who; default: return "rival of " + who; } case RelationClass.PlotPartner: return "plot ally of " + who; default: return ""; } } private static bool SemanticsAlreadyExplainRelation( InterestCandidate candidate, string beat) { NarrativePresentationModel presentation = candidate?.NarrativePresentation; if (presentation != null && presentation.RelationRole != NarrativeRelationRole.None) { return true; } string plain = StripRichText(beat).ToLowerInvariant(); string[] relationWords = { "child", "parent", "lover", "partner", "friend", "kin", "heir", "rival", "enemy", "foe", "ally" }; for (int i = 0; i < relationWords.Length; i++) { if (plain.IndexOf(relationWords[i], StringComparison.Ordinal) >= 0) { return true; } } return false; } private static bool ContainsWholeName(string text, string name) { if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(name)) { return false; } int start = 0; while (start <= text.Length - name.Length) { int at = text.IndexOf(name, start, StringComparison.OrdinalIgnoreCase); if (at < 0) { return false; } bool left = at == 0 || !IsNameChar(text[at - 1]); int end = at + name.Length; bool right = end >= text.Length || !IsNameChar(text[end]); if (left && right) { return true; } start = at + 1; } return false; } private static bool IsNameChar(char value) { return char.IsLetterOrDigit(value) || value == '\'' || value == '-'; } private static string StripRichText(string value) { if (string.IsNullOrEmpty(value) || value.IndexOf('<') < 0) { return value ?? ""; } var chars = new System.Text.StringBuilder(value.Length); bool tag = false; for (int i = 0; i < value.Length; i++) { char ch = value[i]; if (ch == '<') { tag = true; } else if (ch == '>') { tag = false; } else if (!tag) { chars.Append(ch); } } return chars.ToString(); } public static bool HarnessProbePolicy(out string detail) { var fill = new InterestCandidate { LeadKind = InterestLeadKind.CharacterLed, Completion = InterestCompletionKind.CharacterVignette, Label = "Elf", NarrativePresentation = new NarrativePresentationModel { Kind = NarrativePresentationKind.AuthoredObservation, AuthoredClause = "Elf" } }; var child = new RelationEvidence { Class = RelationClass.Kin, KinKind = KinKind.Child, Priority = 4 }; var rival = new RelationEvidence { Class = RelationClass.Foe, FoeKind = FoeKind.Rematch, Priority = 2 }; bool fillSuppressed = SuppressBeat(fill); string childQualifier = FormatQualifier(child, "Norron", colorName: false); string rivalQualifier = FormatQualifier(rival, "Woof", colorName: false); bool pass = fillSuppressed && childQualifier == "child of Norron" && rivalQualifier == "rival of Woof"; detail = "fillSuppressed=" + fillSuppressed + " child='" + childQualifier + "' rival='" + rivalQualifier + "' pass=" + pass; return pass; } public static bool HarnessProbeLiveRelation( Actor focus, Actor participant, out string detail) { long participantId = EventFeedUtil.SafeId(participant); string participantName = EventFeedUtil.SafeName(participant); var candidate = new InterestCandidate { LeadKind = InterestLeadKind.EventLed, Completion = InterestCompletionKind.FixedDwell, SubjectId = EventFeedUtil.SafeId(focus), RelatedId = participantId, RelatedUnit = participant, NarrativePresentation = new NarrativePresentationModel { Kind = NarrativePresentationKind.AuthoredObservation, PerspectiveId = EventFeedUtil.SafeId(focus), Perspective = LifeSagaMemory.Snapshot(focus), OtherId = participantId, Other = LifeSagaMemory.Snapshot(participant), AuthoredClause = "Kills " + participantName } }; string raw = "Kills " + participantName; string enriched = Enrich(focus, candidate, raw); string focusName = EventFeedUtil.SafeName(focus); bool pass = focus != null && participant != null && LifeSagaRoster.IsMc(EventFeedUtil.SafeId(focus)) && enriched.IndexOf("child of", StringComparison.OrdinalIgnoreCase) >= 0 && enriched.IndexOf(focusName, StringComparison.OrdinalIgnoreCase) >= 0; detail = "raw='" + raw + "' enriched='" + enriched + "' pass=" + pass; return pass; } }