fix(core): FA1 -- delete dead AllegianceTree scaffold

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 00:13:39 +02:00
parent 5f9aa16f43
commit 4281750b4d
2 changed files with 0 additions and 293 deletions

View file

@ -1,183 +0,0 @@
using System;
using System.Collections.Generic;
namespace AcDream.Core.Allegiance;
/// <summary>
/// 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).
///
/// <para>
/// Nodes know their own name + rank + allegiance XP pass-through
/// history. The root (monarch) has <see cref="PatronGuid"/> = 0.
/// </para>
/// </summary>
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<uint> VassalGuids { get; } = new(); // direct vassals only
}
/// <summary>
/// Client-side mirror of the player's allegiance tree. Updates come
/// from <see cref="AcDream.Core.Net.Messages.GameEventType.AllegianceUpdate"/>
/// (0x0020). The full wire blob is complex — r11 §5 — so this class
/// exposes a minimal API the UI can grow into:
///
/// <list type="bullet">
/// <item><description>
/// <c>UpsertNode</c> — called by the AllegianceUpdate handler
/// when the server pushes a node refresh.
/// </description></item>
/// <item><description>
/// <c>SetMonarch</c> — establishes the root.
/// </description></item>
/// <item><description>
/// <c>RemoveNode</c> — handles break / leave.
/// </description></item>
/// <item><description>
/// <c>GetAncestors</c> — walks up to the monarch.
/// </description></item>
/// <item><description>
/// <c>GetDescendants</c> — flattens the subtree.
/// </description></item>
/// </list>
///
/// <para>
/// Thread safety: single-threaded use from the render thread. Clone
/// before reading from another thread.
/// </para>
/// </summary>
public sealed class AllegianceTree
{
private readonly Dictionary<uint, AllegianceNode> _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;
}
/// <summary>
/// Walk from <paramref name="guid"/> 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).
/// </summary>
public IEnumerable<AllegianceNode> GetAncestors(uint guid)
{
var seen = new HashSet<uint>();
uint cursor = guid;
while (cursor != 0 && seen.Add(cursor) && _byGuid.TryGetValue(cursor, out var node))
{
yield return node;
cursor = node.PatronGuid;
}
}
/// <summary>
/// Flatten the subtree rooted at <paramref name="rootGuid"/> in BFS
/// order. Includes the root itself.
/// </summary>
public IEnumerable<AllegianceNode> GetDescendants(uint rootGuid)
{
if (!_byGuid.TryGetValue(rootGuid, out var root)) yield break;
var queue = new Queue<AllegianceNode>();
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();
}
}
/// <summary>
/// Retail XP passup formula (r11 §3.2). Vassal passes up a fraction of
/// their earned XP each level:
/// <code>
/// passup = ((50 + 22.5 × loyaltyLevel) / 291) ×
/// (1 + realTimeDays/730 × inGameDays/720) × earnedXp
/// </code>
/// </summary>
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);
}
}

View file

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