- 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
603 lines
15 KiB
C#
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;
|
|
}
|
|
}
|