using System; using System.Collections; using System.Collections.Generic; using System.Reflection; namespace IdleSpectator; /// /// Live-game relation resolver. Labels that claim a second party must resolve through here. /// 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> ObservedChildrenByParent = new Dictionary>(); private static readonly HashSet ParentLinksScanned = new HashSet(); 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 children)) { children = new List(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; } } /// First living peer in the same family (not self). 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 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 EnumerateChildren(Actor actor, int max = 12) { if (actor == null || max <= 0) { yield break; } var seen = new HashSet(); 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 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; } } } /// Living parents reported by the running game's relation data. public static IEnumerable 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(); 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++; } } /// Current living combat target, when the live attack target is an actor. 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; } } /// /// Resolve a related unit for pack/family labels. Prefers lover, then family peer, then child. /// 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 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 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; } }