feat(D.2b): ClientObjectTable.ReplaceContents — authoritative full-replace for ViewContents
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
24a0b84162
commit
48bf752cf1
2 changed files with 85 additions and 0 deletions
|
|
@ -314,6 +314,52 @@ public sealed class ClientObjectTable
|
||||||
_containerIndex.TryGetValue(containerId, out var l)
|
_containerIndex.TryGetValue(containerId, out var l)
|
||||||
? l.ToArray() : System.Array.Empty<uint>();
|
? l.ToArray() : System.Array.Empty<uint>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replace a container's entire membership with <paramref name="guids"/> (in order) — the
|
||||||
|
/// authoritative full snapshot the server sends in ViewContents (0x0196) when you open a
|
||||||
|
/// container. Members no longer present are detached (ContainerId → 0); new members are
|
||||||
|
/// recorded with ContainerSlot = list index. ACE writes ViewContents entries
|
||||||
|
/// OrderBy(PlacementPosition) with NO explicit slot field (GameEventViewContents.cs), so the
|
||||||
|
/// list order IS the slot order. Fires ObjectAdded/ObjectMoved so bound UI repaints.
|
||||||
|
/// Retail consumer: ClientUISystem::OnViewContents.
|
||||||
|
/// </summary>
|
||||||
|
public void ReplaceContents(uint containerId, IReadOnlyList<uint> guids)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(guids);
|
||||||
|
if (containerId == 0) return;
|
||||||
|
|
||||||
|
var keep = new HashSet<uint>(guids);
|
||||||
|
|
||||||
|
// Detach prior members no longer present (they left the container server-side). GetContents
|
||||||
|
// returns a snapshot, so mutating the index inside the loop is safe.
|
||||||
|
foreach (var old in GetContents(containerId))
|
||||||
|
{
|
||||||
|
if (keep.Contains(old)) continue;
|
||||||
|
if (!_objects.TryGetValue(old, out var o) || o.ContainerId != containerId) continue;
|
||||||
|
o.ContainerId = 0;
|
||||||
|
Reindex(o, containerId);
|
||||||
|
ObjectMoved?.Invoke(o, containerId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record new members in order; ContainerSlot = index reconstructs the server PlacementPosition.
|
||||||
|
for (int i = 0; i < guids.Count; i++)
|
||||||
|
{
|
||||||
|
uint g = guids[i];
|
||||||
|
bool existed = _objects.TryGetValue(g, out var obj);
|
||||||
|
if (!existed || obj is null)
|
||||||
|
{
|
||||||
|
obj = new ClientObject { ObjectId = g };
|
||||||
|
_objects[g] = obj;
|
||||||
|
}
|
||||||
|
uint oldContainer = obj.ContainerId;
|
||||||
|
obj.ContainerId = containerId;
|
||||||
|
obj.ContainerSlot = i;
|
||||||
|
Reindex(obj, oldContainer);
|
||||||
|
if (!existed) ObjectAdded?.Invoke(obj);
|
||||||
|
else ObjectMoved?.Invoke(obj, oldContainer, containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Σ Burden over every object carried by <paramref name="ownerGuid"/>: items
|
/// Σ Burden over every object carried by <paramref name="ownerGuid"/>: items
|
||||||
/// whose container chain roots at the owner (pack + side-bag contents, retail
|
/// whose container chain roots at the owner (pack + side-bag contents, retail
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,45 @@ public sealed class ClientObjectTableTests
|
||||||
Assert.True((o.Type & ItemType.Creature) != 0); // LiveItemType(guid) & Creature drives creature targeting
|
Assert.True((o.Type & ItemType.Creature) != 0); // LiveItemType(guid) & Creature drives creature targeting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceContents_RecordsAllInListOrder()
|
||||||
|
{
|
||||||
|
var table = new ClientObjectTable();
|
||||||
|
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u, 0xA03u });
|
||||||
|
Assert.Equal(new[] { 0xA01u, 0xA02u, 0xA03u }, table.GetContents(0xC9u));
|
||||||
|
Assert.Equal(0xC9u, table.Get(0xA01u)!.ContainerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceContents_SecondSmallerList_DetachesDropped() // the full-REPLACE semantics (vs the old additive merge)
|
||||||
|
{
|
||||||
|
var table = new ClientObjectTable();
|
||||||
|
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u, 0xA03u });
|
||||||
|
table.ReplaceContents(0xC9u, new uint[] { 0xA02u }); // A01 + A03 removed server-side
|
||||||
|
Assert.Equal(new[] { 0xA02u }, table.GetContents(0xC9u));
|
||||||
|
Assert.Equal(0u, table.Get(0xA01u)!.ContainerId); // detached, not lingering
|
||||||
|
Assert.Equal(0u, table.Get(0xA03u)!.ContainerId);
|
||||||
|
Assert.Equal(0xC9u, table.Get(0xA02u)!.ContainerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceContents_ReordersToNewListOrder()
|
||||||
|
{
|
||||||
|
var table = new ClientObjectTable();
|
||||||
|
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u });
|
||||||
|
table.ReplaceContents(0xC9u, new uint[] { 0xA02u, 0xA01u });
|
||||||
|
Assert.Equal(new[] { 0xA02u, 0xA01u }, table.GetContents(0xC9u));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceContents_ZeroContainer_NoOp()
|
||||||
|
{
|
||||||
|
var table = new ClientObjectTable();
|
||||||
|
table.ReplaceContents(0u, new uint[] { 0xA01u });
|
||||||
|
Assert.Empty(table.GetContents(0u));
|
||||||
|
Assert.Null(table.Get(0xA01u));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ContainerIndex_SlotChange_ResortsInPlace() // guards the Reindex same-container early-out
|
public void ContainerIndex_SlotChange_ResortsInPlace() // guards the Reindex same-container early-out
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue