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); } }