feat(items): port retail external-container looting

Add the ClientUISystem ground-object lifecycle, authoritative root and nested ViewContents projections, replacement and close semantics, and the DAT-authored gmExternalContainerUI strip for chests and corpses.

Route double-click loot and full or partial drag transfers through the shared retail item policy without optimistic external ownership. Remove the incorrect NoLongerViewingContents behavior from owned side packs and retire AP-106/#196.

Release build succeeds and all 5,875 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 16:18:10 +02:00
parent 48a118db91
commit 20ce67b625
27 changed files with 1750 additions and 45 deletions

View file

@ -79,7 +79,8 @@ public static class GameEventWiring
SquelchState? squelch = null,
Action<IReadOnlyList<(uint Id, uint Amount)>>? onDesiredComponents = null,
Action<uint /*options1*/, uint /*options2*/>? onCharacterOptions = null,
Func<double>? clientTime = null)
Func<double>? clientTime = null,
ExternalContainerState? externalContainers = null)
{
ArgumentNullException.ThrowIfNull(dispatcher);
ArgumentNullException.ThrowIfNull(items);
@ -364,6 +365,7 @@ public static class GameEventWiring
p.Value.Items[i].Guid,
p.Value.Items[i].ContainerType);
items.ReplaceContents(p.Value.ContainerGuid, entries);
externalContainers?.ApplyViewContents(p.Value.ContainerGuid);
});
// B-Wire: InventoryPutObjectIn3D (0x019A) — server confirms an item dropped
@ -412,13 +414,15 @@ public static class GameEventWiring
chat.OnSystemMessage(WeenieErrorText.For(err.Value), chatType: 0);
});
// B-Wire: CloseGroundContainer (0x0052) — server closed a ground-container
// view. No table change (the view is UI-only, wired in container-open); log.
// CloseGroundContainer (0x0052): clear ClientUISystem::groundObject and
// retire the root plus nested temporary ViewContents projections. Child
// objects remain until authoritative move/delete wire says otherwise.
dispatcher.Register(GameEventType.CloseGroundContainer, e =>
{
var guid = GameEvents.ParseCloseGroundContainer(e.Payload.Span);
if (guid is not null)
Console.WriteLine($"[B-Wire] CloseGroundContainer guid=0x{guid.Value:X8}");
if (guid is null) return;
externalContainers?.ApplyClose(guid.Value);
items.StopViewingContentsTree(guid.Value);
});
dispatcher.Register(GameEventType.IdentifyObjectResponse, e =>