diff --git a/src/AcDream.Core/Allegiance/AllegianceTree.cs b/src/AcDream.Core/Allegiance/AllegianceTree.cs
deleted file mode 100644
index 2d68dd98..00000000
--- a/src/AcDream.Core/Allegiance/AllegianceTree.cs
+++ /dev/null
@@ -1,183 +0,0 @@
-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);
- }
-}
diff --git a/tests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs b/tests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs
deleted file mode 100644
index 826e3faf..00000000
--- a/tests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System.Linq;
-using AcDream.Core.Allegiance;
-using Xunit;
-
-namespace AcDream.Core.Tests.Allegiance;
-
-public sealed class AllegianceTreeTests
-{
- [Fact]
- public void SetMonarch_RegistersNode_FiresEvent()
- {
- var tree = new AllegianceTree();
- int changes = 0;
- tree.TreeChanged += () => changes++;
-
- tree.SetMonarch(0xAA, "Bael'Zharon");
- Assert.Equal(1, changes);
- Assert.Equal(0xAAu, tree.MonarchGuid);
- Assert.Equal(1, tree.NodeCount);
- }
-
- [Fact]
- public void UpsertNode_NewPatron_AddsToVassalList()
- {
- var tree = new AllegianceTree();
- tree.SetMonarch(0xAA, "Monarch");
- tree.UpsertNode(0xBB, "Vassal", patronGuid: 0xAA, rank: 1);
-
- var monarch = tree.Get(0xAA);
- Assert.NotNull(monarch);
- Assert.Contains(0xBBu, monarch!.VassalGuids);
- }
-
- [Fact]
- public void UpsertNode_RankClamp_CapsAt10()
- {
- var tree = new AllegianceTree();
- tree.SetMonarch(0xAA, "M");
- var node = tree.UpsertNode(0xBB, "V", 0xAA, rank: 50);
- Assert.Equal(10, node.Rank);
- }
-
- [Fact]
- public void RemoveNode_RemovesFromParentVassalList()
- {
- var tree = new AllegianceTree();
- tree.SetMonarch(0xAA, "M");
- tree.UpsertNode(0xBB, "V", 0xAA, 1);
- Assert.Single(tree.Get(0xAA)!.VassalGuids);
-
- tree.RemoveNode(0xBB);
- Assert.Empty(tree.Get(0xAA)!.VassalGuids);
- }
-
- [Fact]
- public void GetAncestors_WalksToMonarch()
- {
- var tree = new AllegianceTree();
- tree.SetMonarch(0xAA, "M");
- tree.UpsertNode(0xBB, "V", 0xAA, 1);
- tree.UpsertNode(0xCC, "VV", 0xBB, 2);
- tree.UpsertNode(0xDD, "VVV", 0xCC, 3);
-
- var chain = tree.GetAncestors(0xDD).Select(n => n.Name).ToArray();
- Assert.Equal(new[] { "VVV", "VV", "V", "M" }, chain);
- }
-
- [Fact]
- public void GetAncestors_HandlesCycleWithoutLooping()
- {
- // Corrupt data: defensive path. Treat a cycle as a stop.
- var tree = new AllegianceTree();
- tree.UpsertNode(0xAA, "A", patronGuid: 0xBB, rank: 1);
- tree.UpsertNode(0xBB, "B", patronGuid: 0xAA, rank: 1); // cycle
-
- var walk = tree.GetAncestors(0xAA).Select(n => n.Guid).ToArray();
- Assert.Equal(2, walk.Length); // visits each exactly once
- }
-
- [Fact]
- public void GetDescendants_BfsOrder()
- {
- var tree = new AllegianceTree();
- tree.SetMonarch(0xAA, "M");
- tree.UpsertNode(0xB0, "B0", 0xAA, 1);
- tree.UpsertNode(0xB1, "B1", 0xAA, 1);
- tree.UpsertNode(0xC0, "C0", 0xB0, 2);
-
- var bfs = tree.GetDescendants(0xAA).Select(n => n.Name).ToArray();
- Assert.Equal(new[] { "M", "B0", "B1", "C0" }, bfs);
- }
-
- [Fact]
- public void AllegianceMath_Passup_KnownCase()
- {
- // 1000 XP, loyalty 10, 100 real-time days, 100 in-game days.
- // loyaltyScale = (50 + 225) / 291 = 0.9450
- // ageScale = 1 + (100/730)*(100/720) = 1 + 0.0190 = 1.0190
- // passup = 1000 * 0.9450 * 1.0190 ≈ 963
- long passup = AllegianceMath.ComputePassup(1000, 10, 100, 100);
- Assert.InRange(passup, 960, 965);
- }
-
- [Fact]
- public void AllegianceMath_Passup_ZeroClamp()
- {
- // Negative XP shouldn't be possible; passup is clamped at 0.
- Assert.Equal(0, AllegianceMath.ComputePassup(-1000, 10, 0, 0));
- }
-}