diff --git a/src/AcDream.Core/Items/ClientObjectTable.cs b/src/AcDream.Core/Items/ClientObjectTable.cs index d8ed56ba..aba45de5 100644 --- a/src/AcDream.Core/Items/ClientObjectTable.cs +++ b/src/AcDream.Core/Items/ClientObjectTable.cs @@ -1206,7 +1206,7 @@ public IReadOnlyList GetEquippedBy(uint wielderId) => .ToArray() : Array.Empty(); - /// Whether server listings cover the owner and every nested pack. + /// Whether server listings cover the owner and every nested pack (a focus needs none). public bool HasCompleteInventory(uint ownerId) { if (ownerId == 0 || _pendingMoves.Count != 0) @@ -1225,7 +1225,10 @@ public bool HasCompleteInventory(uint ownerId) { if (!_objects.TryGetValue(id, out ClientObject? item)) return false; - if (IsContainerListMember(item)) + // A focus takes a container slot but holds nothing, and the + // server never sends a listing for one. + if (IsContainerListMember(item) + && !InventoryContainerPlacementPolicy.IsFocus(item)) pending.Push(id); } } diff --git a/tests/AcDream.Core.Tests/Items/InventoryCompletenessTests.cs b/tests/AcDream.Core.Tests/Items/InventoryCompletenessTests.cs index 00656b51..60cd62bb 100644 --- a/tests/AcDream.Core.Tests/Items/InventoryCompletenessTests.cs +++ b/tests/AcDream.Core.Tests/Items/InventoryCompletenessTests.cs @@ -42,4 +42,17 @@ public void EmptyAuthoritativeInventoryIsComplete() table.InitializeInventoryManifest(1, Array.Empty()); Assert.True(table.HasCompleteInventory(1)); } + + [Fact] + public void SpellFocusNeverListedDoesNotBlockCompleteness() + { + // ACE lists a focus (container type 2) in the container slots but + // never sends its contents, since a focus holds nothing. + var table = new ClientObjectTable(); + table.InitializeInventoryManifest(1, + [new ContainerContentEntry(2, 1), new ContainerContentEntry(3, 2)]); + Assert.False(table.HasCompleteInventory(1)); + table.ReplaceContents(2, Array.Empty()); + Assert.True(table.HasCompleteInventory(1)); + } }