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:
parent
5f9aa16f43
commit
4281750b4d
2 changed files with 0 additions and 293 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue