- Add Dossier/Saga tabs, Design A panel, and hover preview with read-pause - Persist observed saga facts across roster churn; pin subject without stale fallback - Restore Saga after Open Lore; hide redundant Kind · Climax spine under matching tips - Densify dossier header/chips and keep the cast rail as stable top chrome - Extend harness coverage for hover restore, empty pick, Evidence, and Lore return
498 lines
12 KiB
C#
498 lines
12 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
|
|
{
|
|
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;
|
|
}
|
|
|
|
IEnumerable children = null;
|
|
try
|
|
{
|
|
MethodInfo getChildren = typeof(Actor).GetMethod("getChildren", Type.EmptyTypes);
|
|
if (getChildren != null)
|
|
{
|
|
children = getChildren.Invoke(actor, null) as IEnumerable;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
children = null;
|
|
}
|
|
|
|
if (children == null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
int yielded = 0;
|
|
foreach (object raw in children)
|
|
{
|
|
Actor child = raw as Actor ?? ResolveActor(raw);
|
|
if (child == null || !child.isAlive())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return child;
|
|
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 = ReadMember(actor, "getParents", "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)
|
|
{
|
|
if (id == 0 || World.world?.units == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach (Actor unit in World.world.units)
|
|
{
|
|
if (unit == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (unit.getID() == id && unit.isAlive())
|
|
{
|
|
return unit;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// skip
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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, Type.EmptyTypes);
|
|
if (method != null)
|
|
{
|
|
return method.Invoke(instance, null);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// try next
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|