worldbox-observer-mod/IdleSpectator/ActorRelation.cs
DazedAnon 44999fb653 feat(actor-relation): enhance parent-child relationship tracking and event handling
- Introduce methods to observe and remember parent-child links for actors
- Implement caching for observed relationships to improve performance
- Update narrative event store to index child events by parent and child
- Refactor LifeSagaPanel to ensure stable cast membership during saga sessions
- Enhance documentation to clarify relationship handling and event indexing
2026-07-24 16:21:49 -05:00

603 lines
15 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
namespace IdleSpectator;
/// <summary>
/// Live-game relation resolver. Labels that claim a second party must resolve through here.
/// </summary>
public static class ActorRelation
{
private static readonly MethodInfo GetChildrenMethod = typeof(Actor).GetMethod(
"getChildren",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
types: Type.EmptyTypes,
modifiers: null);
private static readonly MethodInfo GetParentsMethod = typeof(Actor).GetMethod(
"getParents",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
types: Type.EmptyTypes,
modifiers: null);
private static readonly Dictionary<long, List<long>> ObservedChildrenByParent =
new Dictionary<long, List<long>>();
private static readonly HashSet<long> ParentLinksScanned =
new HashSet<long>();
public static void ClearObservedRelations()
{
ObservedChildrenByParent.Clear();
ParentLinksScanned.Clear();
}
public static void ObserveParentLinks(Actor child)
{
if (child == null)
{
return;
}
long childId = EventFeedUtil.SafeId(child);
if (childId == 0 || !ParentLinksScanned.Add(childId))
{
return;
}
foreach (Actor parent in EnumerateParents(child, 2))
{
RememberParentChild(parent, child);
}
}
public static void RememberParentChild(Actor parent, Actor child)
{
long parentId = EventFeedUtil.SafeId(parent);
long childId = EventFeedUtil.SafeId(child);
if (parentId == 0 || childId == 0 || parentId == childId)
{
return;
}
ParentLinksScanned.Add(childId);
if (!ObservedChildrenByParent.TryGetValue(parentId, out List<long> children))
{
children = new List<long>(2);
ObservedChildrenByParent[parentId] = children;
}
if (!children.Contains(childId))
{
children.Add(childId);
}
}
public static Actor GetLover(Actor actor)
{
if (actor == null)
{
return null;
}
try
{
if (!actor.hasLover())
{
return null;
}
}
catch
{
// fall through to field/property
}
try
{
Actor lover = null;
try
{
lover = actor.lover;
}
catch
{
lover = null;
}
if (lover == null)
{
lover = ReadActorMember(actor, "lover", "get_lover", "getLover");
}
if (lover != null && lover.isAlive() && lover != actor)
{
return lover;
}
}
catch
{
// ignore
}
return null;
}
public static bool TryGetLover(Actor actor, out Actor lover)
{
lover = GetLover(actor);
return lover != null;
}
public static Actor GetBestFriend(Actor actor)
{
if (actor == null)
{
return null;
}
try
{
long id = 0;
object data = actor.data;
if (data != null)
{
object raw = data.GetType().GetProperty("best_friend_id")?.GetValue(data, null)
?? data.GetType().GetField("best_friend_id")?.GetValue(data);
if (raw is long l)
{
id = l;
}
else if (raw is int i)
{
id = i;
}
}
if (id == 0)
{
return null;
}
return FindUnitById(id);
}
catch
{
return null;
}
}
/// <summary>First living peer in the same family (not self).</summary>
public static Actor FirstFamilyPeer(Actor actor)
{
foreach (Actor peer in EnumerateFamilyMembers(actor, max: 8))
{
if (peer != null && peer != actor && peer.isAlive())
{
return peer;
}
}
return null;
}
public static IEnumerable<Actor> EnumerateFamilyMembers(Actor actor, int max = 12)
{
if (actor == null || max <= 0)
{
yield break;
}
object family = ReadMember(actor, "family", "get_family", "getFamily");
if (family == null)
{
yield break;
}
int yielded = 0;
foreach (Actor member in EnumerateActorsFromMeta(family))
{
if (member == null || !member.isAlive())
{
continue;
}
yield return member;
yielded++;
if (yielded >= max)
{
yield break;
}
}
}
public static IEnumerable<Actor> EnumerateChildren(Actor actor, int max = 12)
{
if (actor == null || max <= 0)
{
yield break;
}
var seen = new HashSet<long>();
int yielded = 0;
IEnumerable children = null;
try
{
if (GetChildrenMethod != null)
{
children = GetChildrenMethod.Invoke(actor, null) as IEnumerable;
}
}
catch
{
children = null;
}
if (children != null)
{
foreach (object raw in children)
{
Actor child = raw as Actor ?? ResolveActor(raw);
if (!IsDistinctLivingRelation(actor, child, seen))
{
continue;
}
yield return child;
yielded++;
if (yielded >= max)
{
yield break;
}
}
}
if (ObservedChildrenByParent.TryGetValue(
EventFeedUtil.SafeId(actor),
out List<long> observedChildren))
{
for (int i = 0; i < observedChildren.Count; i++)
{
Actor child = EventFeedUtil.FindAliveById(observedChildren[i]);
if (!IsDistinctLivingRelation(actor, child, seen))
{
continue;
}
yield return child;
yielded++;
if (yielded >= max)
{
yield break;
}
}
}
// Some saved civilized bloodlines retain parent links on each child while
// the parent's getChildren() view is empty or incomplete. Family membership
// is not directional, so only promote a peer after its own parent data
// explicitly points back to this actor.
long actorId = EventFeedUtil.SafeId(actor);
int familyScanMax = Math.Min(64, Math.Max(16, max * 8));
foreach (Actor peer in EnumerateFamilyMembers(actor, familyScanMax))
{
if (peer == null || peer == actor)
{
continue;
}
bool namesActorAsParent = false;
foreach (Actor parent in EnumerateParents(peer, 2))
{
if (EventFeedUtil.SafeId(parent) == actorId)
{
namesActorAsParent = true;
break;
}
}
if (!namesActorAsParent || !IsDistinctLivingRelation(actor, peer, seen))
{
continue;
}
yield return peer;
yielded++;
if (yielded >= max)
{
yield break;
}
}
}
/// <summary>Living parents reported by the running game's relation data.</summary>
public static IEnumerable<Actor> EnumerateParents(Actor actor, int max = 2)
{
if (actor == null || max <= 0)
{
yield break;
}
object parents = null;
try
{
parents = GetParentsMethod?.Invoke(actor, null);
}
catch
{
parents = null;
}
if (parents == null)
{
parents = ReadMember(actor, "get_parents", "parents");
}
if (parents == null)
{
parents = ReadMember(actor.data, "parents", "parent_ids", "parents_ids");
}
int yielded = 0;
var seen = new HashSet<long>();
if (parents is IEnumerable enumerable)
{
foreach (object raw in enumerable)
{
Actor parent = ResolveActor(raw) ?? ResolveActorId(raw);
if (!IsDistinctLivingRelation(actor, parent, seen))
{
continue;
}
yield return parent;
yielded++;
if (yielded >= max)
{
yield break;
}
}
}
if (yielded >= max)
{
yield break;
}
string[] singular =
{
"mother", "father", "parent", "mother_id", "father_id", "parent_id",
"parent_1", "parent_2", "parent1", "parent2"
};
for (int i = 0; i < singular.Length && yielded < max; i++)
{
object raw = ReadMember(actor, singular[i])
?? ReadMember(actor.data, singular[i]);
Actor parent = ResolveActor(raw) ?? ResolveActorId(raw);
if (!IsDistinctLivingRelation(actor, parent, seen))
{
continue;
}
yield return parent;
yielded++;
}
}
/// <summary>Current living combat target, when the live attack target is an actor.</summary>
public static Actor GetCombatOpponent(Actor actor)
{
if (actor == null)
{
return null;
}
try
{
if (!actor.has_attack_target || actor.attack_target == null)
{
return null;
}
Actor opponent = ResolveActor(actor.attack_target);
return opponent != null && opponent != actor && opponent.isAlive()
? opponent
: null;
}
catch
{
return null;
}
}
/// <summary>
/// Resolve a related unit for pack/family labels. Prefers lover, then family peer, then child.
/// </summary>
public static Actor ResolvePackRelated(Actor subject)
{
if (subject == null)
{
return null;
}
Actor lover = GetLover(subject);
if (lover != null)
{
return lover;
}
Actor peer = FirstFamilyPeer(subject);
if (peer != null)
{
return peer;
}
foreach (Actor child in EnumerateChildren(subject, max: 1))
{
return child;
}
return null;
}
public static Actor ResolveActor(object raw)
{
if (raw == null)
{
return null;
}
if (raw is Actor actor)
{
return actor;
}
try
{
if (raw is BaseSimObject sim && sim.isActor())
{
return sim.a;
}
}
catch
{
// fall through
}
return ReadActorMember(raw, "actor", "Actor", "a", "getActor", "GetActor");
}
private static IEnumerable<Actor> EnumerateActorsFromMeta(object meta)
{
if (meta == null)
{
yield break;
}
// Common: units / members / getUnits / getSimpleList
object list = ReadMember(meta, "units", "members", "getUnits", "getSimpleList", "getList");
if (list is IEnumerable enumerable)
{
foreach (object raw in enumerable)
{
Actor a = ResolveActor(raw);
if (a != null)
{
yield return a;
}
}
yield break;
}
Actor founder = ReadActorMember(meta, "founder", "creator", "leader", "getFounder");
if (founder != null)
{
yield return founder;
}
}
private static Actor FindUnitById(long id)
{
return EventFeedUtil.FindAliveById(id);
}
private static Actor ResolveActorId(object raw)
{
if (raw is long longId)
{
return FindUnitById(longId);
}
if (raw is int intId)
{
return FindUnitById(intId);
}
try
{
if (raw != null && long.TryParse(raw.ToString(), out long parsed))
{
return FindUnitById(parsed);
}
}
catch
{
// ignore
}
return null;
}
private static bool IsDistinctLivingRelation(Actor subject, Actor related, HashSet<long> seen)
{
if (related == null || related == subject || !related.isAlive())
{
return false;
}
try
{
return seen.Add(related.getID());
}
catch
{
return false;
}
}
private static Actor ReadActorMember(object target, params string[] names)
{
object raw = ReadMember(target, names);
return raw as Actor;
}
private static object ReadMember(object target, params string[] names)
{
if (target == null || names == null)
{
return null;
}
Type type = target as Type ?? target.GetType();
object instance = target is Type ? null : target;
foreach (string name in names)
{
if (string.IsNullOrEmpty(name))
{
continue;
}
try
{
PropertyInfo prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (prop != null)
{
return prop.GetValue(instance, null);
}
FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null)
{
return field.GetValue(instance);
}
MethodInfo method = type.GetMethod(
name,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
types: Type.EmptyTypes,
modifiers: null);
if (method != null)
{
return method.Invoke(instance, null);
}
}
catch
{
// try next
}
}
return null;
}
}