- Soft-cool kill, courtship, intent, construction, status fx, hatch, rest, and worldlog meta - Block cooled tips from resuming after a cut-in - Name only dead theater partners in stands-over aftermath - Add harness gates through 0.28.54
327 lines
9.1 KiB
C#
327 lines
9.1 KiB
C#
using System;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Sole unit-led publish API for the idle director.
|
|
/// Every registration is EventLed and may own the camera (score ranks).
|
|
/// FollowUnit owns the reason; RelatedUnit required when the label claims a second party;
|
|
/// location-only civic events may omit FollowUnit but never invent a stranger subject.
|
|
/// </summary>
|
|
public static class EventFeedUtil
|
|
{
|
|
public static InterestCandidate Register(
|
|
string key,
|
|
string source,
|
|
string category,
|
|
string label,
|
|
string assetId,
|
|
float eventStrength,
|
|
Vector3 position,
|
|
Actor follow,
|
|
Actor related = null,
|
|
bool locationOnly = false,
|
|
float minWatch = 4f,
|
|
float maxWatch = 22f,
|
|
InterestCompletionKind completion = InterestCompletionKind.FixedDwell)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Actor subject = follow;
|
|
if (subject != null && !subject.isAlive())
|
|
{
|
|
subject = null;
|
|
}
|
|
|
|
Actor relatedAlive = related;
|
|
if (relatedAlive != null && !relatedAlive.isAlive())
|
|
{
|
|
relatedAlive = null;
|
|
}
|
|
|
|
Vector3 pos = position;
|
|
if (subject != null)
|
|
{
|
|
pos = subject.current_position;
|
|
}
|
|
|
|
if (locationOnly)
|
|
{
|
|
if (pos == Vector3.zero || float.IsNaN(pos.x))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unit-led: require a living subject. Never invent a stranger.
|
|
if (subject == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (pos == Vector3.zero || float.IsNaN(pos.x))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
float strength = eventStrength;
|
|
if (strength < 8f)
|
|
{
|
|
strength = 8f;
|
|
}
|
|
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = string.IsNullOrEmpty(category) ? "Event" : category,
|
|
Source = source ?? "event",
|
|
EventStrength = strength,
|
|
CharacterSignificance = 0f,
|
|
VisualConfidence = subject != null ? 0.7f : 0.35f,
|
|
Position = pos,
|
|
FollowUnit = subject,
|
|
RelatedUnit = relatedAlive,
|
|
SubjectId = subject != null ? SafeId(subject) : 0,
|
|
RelatedId = relatedAlive != null ? SafeId(relatedAlive) : 0,
|
|
Label = label ?? assetId ?? key,
|
|
AssetId = assetId ?? "",
|
|
SpeciesId = subject?.asset != null ? subject.asset.id : "",
|
|
// StatusPhase publishes the status id as AssetId - stamp StatusId before Upsert
|
|
// so NoteSelection / beat cools see the class on first SwitchTo.
|
|
StatusId = completion == InterestCompletionKind.StatusPhase ? (assetId ?? "") : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 35f,
|
|
MinWatch = minWatch,
|
|
MaxWatch = maxWatch,
|
|
Completion = completion
|
|
};
|
|
if (!EventPresentability.WouldShow(candidate))
|
|
{
|
|
InterestDropLog.Record("unpresentable", candidate.Label ?? key);
|
|
return null;
|
|
}
|
|
|
|
InterestScoring.ScoreCheap(candidate);
|
|
return InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publish a fully built candidate through the registry (still the sole intake).
|
|
/// Prefer the typed Register overload for new feeds.
|
|
/// </summary>
|
|
public static InterestCandidate RegisterCandidate(InterestCandidate candidate)
|
|
{
|
|
if (candidate == null || string.IsNullOrEmpty(candidate.Key) || !candidate.HasValidPosition)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Reject stranger subjects: dead follow with no position is already invalid via HasValidPosition.
|
|
if (candidate.FollowUnit != null && !candidate.FollowUnit.isAlive())
|
|
{
|
|
candidate.FollowUnit = null;
|
|
candidate.SubjectId = 0;
|
|
if (!candidate.HasValidPosition)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (candidate.RelatedUnit != null && !candidate.RelatedUnit.isAlive())
|
|
{
|
|
candidate.RelatedUnit = null;
|
|
candidate.RelatedId = 0;
|
|
}
|
|
|
|
if (candidate.FollowUnit != null && candidate.SubjectId == 0)
|
|
{
|
|
candidate.SubjectId = SafeId(candidate.FollowUnit);
|
|
}
|
|
|
|
if (candidate.RelatedUnit != null && candidate.RelatedId == 0)
|
|
{
|
|
candidate.RelatedId = SafeId(candidate.RelatedUnit);
|
|
}
|
|
|
|
// Harness injects synthetic tip labels for director tests; dossier still blanks them.
|
|
// Production feeds (Source != harness) must be presentable to enter the registry.
|
|
if (!IsHarnessSource(candidate.Source) && !EventPresentability.WouldShow(candidate))
|
|
{
|
|
InterestDropLog.Record("unpresentable", candidate.Label ?? candidate.Key);
|
|
return null;
|
|
}
|
|
|
|
InterestScoring.ScoreCheap(candidate);
|
|
return InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
private static bool IsHarnessSource(string source) =>
|
|
string.Equals(source, "harness", StringComparison.OrdinalIgnoreCase);
|
|
|
|
public static long SafeId(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
try
|
|
{
|
|
return actor.getID();
|
|
}
|
|
catch
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// O(1) living actor lookup via <c>ActorManager.get</c>. Prefer this over scanning
|
|
/// <see cref="WorldActivityScanner.EnumerateAliveUnitsPublic"/>.
|
|
/// </summary>
|
|
public static Actor FindAliveById(long id)
|
|
{
|
|
Actor actor = FindUnitById(id);
|
|
if (actor == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
return actor.isAlive() ? actor : null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// O(1) actor lookup including dead units (naming fallen duel partners, etc.).
|
|
/// </summary>
|
|
public static Actor FindUnitById(long id)
|
|
{
|
|
if (id == 0 || World.world?.units == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
return World.world.units.get(id);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static string SafeName(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
try
|
|
{
|
|
return actor.getName() ?? "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public static Actor NearestUnit(Vector3 near, float radius = 200f)
|
|
{
|
|
try
|
|
{
|
|
return WorldActivityScanner.FindNearestAliveUnit(near, radius);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Harness / diagnostics only. Must not be used as a silent camera subject for EventLed events.
|
|
/// </summary>
|
|
public static Actor AnyAliveUnit()
|
|
{
|
|
try
|
|
{
|
|
if (World.world?.units == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (Actor actor in World.world.units)
|
|
{
|
|
if (actor != null && actor.isAlive())
|
|
{
|
|
return actor;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve a unit-led anchor without stranger fallback.
|
|
/// Preferred unit if alive; else nearest at position; else fail (caller may location-only).
|
|
/// </summary>
|
|
public static bool TryResolveOwnedAnchor(
|
|
Actor preferred,
|
|
Vector3 position,
|
|
out Actor follow,
|
|
out Vector3 pos,
|
|
bool allowNearestAtPosition = true)
|
|
{
|
|
follow = null;
|
|
pos = position;
|
|
|
|
if (preferred != null && preferred.isAlive())
|
|
{
|
|
follow = preferred;
|
|
pos = preferred.current_position;
|
|
return pos != Vector3.zero && !float.IsNaN(pos.x);
|
|
}
|
|
|
|
if (allowNearestAtPosition && position != Vector3.zero && !float.IsNaN(position.x))
|
|
{
|
|
follow = NearestUnit(position, 48f);
|
|
if (follow != null && follow.isAlive())
|
|
{
|
|
pos = follow.current_position;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
follow = null;
|
|
pos = position;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Obsolete stranger path. Prefer TryResolveOwnedAnchor or location-only Register.</summary>
|
|
[System.Obsolete("Use TryResolveOwnedAnchor or location-only Register. Do not invent subjects.")]
|
|
public static bool TryResolveAnchor(Actor preferred, Vector3 position, out Actor follow, out Vector3 pos)
|
|
{
|
|
return TryResolveOwnedAnchor(preferred, position, out follow, out pos, allowNearestAtPosition: true);
|
|
}
|
|
}
|