From 4281750b4d248028c8928b721118a820469d8cd8 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 00:13:39 +0200 Subject: [PATCH] fix(core): FA1 -- delete dead AllegianceTree scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Campaign FA slice FA1, lane C §0/§7.3 DELETE verdict. Verified zero production references (a repo-wide grep for AllegianceTree/ AllegianceNode/AllegianceMath outside the class's own file and its own test file returns nothing but docs and an unrelated test-name string coincidence in ChatChannelInfoTests.cs). Two defects made this safer to delete than fix: - AllegianceMath.ComputePassup transcribed retail's passup formula wrong by roughly 1000x: it computed (50 + 22.5*loyalty) / 291 instead of 50 + 22.5*(loyalty/291) AS A PERCENTAGE (missing the trailing / 100), and its own unit test locked the wrong value in as correct. - AllegianceTree's UpsertNode(guid, name, patronGuid, rank) modeled a patron edge the wire does not carry -- the wire names each record's TREE PARENT (§4.4), which for ACE's own writer is not always the real patron (ACE hangs a non-monarch patron directly off the monarch). The parsed record list plus its treeParent tags already IS the tree (see ClientCommandResponses.AllegianceProfileLookups, landed in the companion feat(net) commit this session) -- no separate tree class is needed. The client also never needs the passup number at all: _cp_tithed arrives pre-computed from the server. Co-Authored-By: Claude Fable 5 --- src/AcDream.Core/Allegiance/AllegianceTree.cs | 183 ------------------ .../Allegiance/AllegianceTreeTests.cs | 110 ----------- 2 files changed, 293 deletions(-) delete mode 100644 src/AcDream.Core/Allegiance/AllegianceTree.cs delete mode 100644 tests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs 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)); - } -}