using System;
using System.Collections.Generic;
namespace AcDream.Core.Allegiance;
///
/// Per-character allegiance node. Retail allegiance is a strict tree:
/// one patron per character, max 11 direct vassals, recursive depth.
/// Rank capped at 10 (r11 §1, §4).
///
///
/// Nodes know their own name + rank + allegiance XP pass-through
/// history. The root (monarch) has = 0.
///
///
public sealed class AllegianceNode
{
public uint Guid;
public string Name = "";
public uint PatronGuid; // 0 if this node IS the monarch
public int Rank; // 0 = unpledged / monarch; capped at 10
public long ExperienceCached; // XP the client has seen pass up so far
public List VassalGuids { get; } = new(); // direct vassals only
}
///
/// Client-side mirror of the player's allegiance tree. Updates come
/// from
/// (0x0020). The full wire blob is complex — r11 §5 — so this class
/// exposes a minimal API the UI can grow into:
///
///
/// -
/// UpsertNode — called by the AllegianceUpdate handler
/// when the server pushes a node refresh.
///
/// -
/// SetMonarch — establishes the root.
///
/// -
/// RemoveNode — handles break / leave.
///
/// -
/// GetAncestors — walks up to the monarch.
///
/// -
/// GetDescendants — flattens the subtree.
///
///
///
///
/// Thread safety: single-threaded use from the render thread. Clone
/// before reading from another thread.
///
///
public sealed class AllegianceTree
{
private readonly Dictionary _byGuid = new();
public uint MonarchGuid { get; private set; }
public uint PlayerGuid { get; set; } // whose-allegiance-this-is
public event Action? TreeChanged;
public int NodeCount => _byGuid.Count;
public AllegianceNode? Get(uint guid) =>
_byGuid.TryGetValue(guid, out var n) ? n : null;
public void SetMonarch(uint guid, string name)
{
MonarchGuid = guid;
var node = UpsertNodeInternal(guid, name, 0, rank: 0);
TreeChanged?.Invoke();
}
public AllegianceNode UpsertNode(uint guid, string name, uint patronGuid, int rank)
{
var node = UpsertNodeInternal(guid, name, patronGuid, rank);
TreeChanged?.Invoke();
return node;
}
private AllegianceNode UpsertNodeInternal(uint guid, string name, uint patronGuid, int rank)
{
if (!_byGuid.TryGetValue(guid, out var node))
{
node = new AllegianceNode { Guid = guid };
_byGuid[guid] = node;
}
node.Name = name;
node.PatronGuid = patronGuid;
node.Rank = Math.Clamp(rank, 0, 10);
// Keep parent's vassal list in sync with this node's declared
// patron. A node that changes patron is removed from the old
// patron's list and added to the new one.
if (patronGuid != 0 && _byGuid.TryGetValue(patronGuid, out var parent))
{
if (!parent.VassalGuids.Contains(guid))
parent.VassalGuids.Add(guid);
}
return node;
}
public bool RemoveNode(uint guid)
{
if (!_byGuid.TryGetValue(guid, out var node)) return false;
if (node.PatronGuid != 0 && _byGuid.TryGetValue(node.PatronGuid, out var parent))
parent.VassalGuids.Remove(guid);
_byGuid.Remove(guid);
// Cascade: orphaned descendants stay in the map with their
// patron pointer dangling; the UI can hide them or the next
// AllegianceUpdate will refresh them.
TreeChanged?.Invoke();
return true;
}
///
/// Walk from up to the monarch. Yields node
/// then patron then grand-patron, etc. Includes the starting node.
/// Stops if a cycle is detected (shouldn't happen — server enforces
/// acyclicity — but be defensive).
///
public IEnumerable GetAncestors(uint guid)
{
var seen = new HashSet();
uint cursor = guid;
while (cursor != 0 && seen.Add(cursor) && _byGuid.TryGetValue(cursor, out var node))
{
yield return node;
cursor = node.PatronGuid;
}
}
///
/// Flatten the subtree rooted at in BFS
/// order. Includes the root itself.
///
public IEnumerable GetDescendants(uint rootGuid)
{
if (!_byGuid.TryGetValue(rootGuid, out var root)) yield break;
var queue = new Queue();
queue.Enqueue(root);
while (queue.Count > 0)
{
var node = queue.Dequeue();
yield return node;
foreach (var vGuid in node.VassalGuids)
if (_byGuid.TryGetValue(vGuid, out var vassal))
queue.Enqueue(vassal);
}
}
public void Clear()
{
_byGuid.Clear();
MonarchGuid = 0;
TreeChanged?.Invoke();
}
}
///
/// Retail XP passup formula (r11 §3.2). Vassal passes up a fraction of
/// their earned XP each level:
///
/// passup = ((50 + 22.5 × loyaltyLevel) / 291) ×
/// (1 + realTimeDays/730 × inGameDays/720) × earnedXp
///
///
public static class AllegianceMath
{
public static long ComputePassup(long earnedXp, int loyaltyLevel,
double realTimeDays, double inGameDays)
{
double loyaltyScale = (50.0 + 22.5 * loyaltyLevel) / 291.0;
double ageScale = 1.0 + (realTimeDays / 730.0) * (inGameDays / 720.0);
double passup = earnedXp * loyaltyScale * ageScale;
if (passup < 0) passup = 0;
return (long)Math.Round(passup);
}
}