fix(gameplay): reconcile wield ownership and target facing

Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks.

Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state.

Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration.

Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 08:19:23 +02:00
parent b26f84cc69
commit 7b7ffcd278
36 changed files with 3884 additions and 761 deletions

View file

@ -305,33 +305,29 @@ public static class GameEventWiring
var p = GameEvents.ParseWieldObject(e.Payload.Span);
if (p is null) return;
uint wielderGuid = p.Value.WielderGuid != 0u
? p.Value.WielderGuid
: (playerGuid?.Invoke() ?? 0u);
items.MoveItem(
uint wielderGuid = playerGuid?.Invoke() ?? 0u;
items.ApplyConfirmedServerWield(
p.Value.ItemGuid,
newContainerId: wielderGuid,
newEquipLocation: (AcDream.Core.Items.EquipMask)p.Value.EquipLoc);
items.ConfirmWield(p.Value.ItemGuid); // Exact IR_WIELD completion + optimistic reconciliation.
wielderGuid,
(AcDream.Core.Items.EquipMask)p.Value.EquipLoc);
});
dispatcher.Register(GameEventType.InventoryPutObjInContainer, e =>
{
var p = GameEvents.ParsePutObjInContainer(e.Payload.Span);
if (p is null) return;
items.MoveItem(
items.ApplyConfirmedServerMove(
p.Value.ItemGuid,
p.Value.ContainerGuid,
newWielderId: 0u,
newSlot: (int)p.Value.Placement,
containerTypeHint: p.Value.ContainerType);
items.ConfirmMove(p.Value.ItemGuid); // B-Drag: the server confirmed our optimistic move
});
// ViewContents (0x0196) — the server's AUTHORITATIVE full contents list for a container you
// opened (Use 0x0036). Treat it as a full REPLACE: flush the container's prior membership and
// record the new entries in order (ContainerSlot = index — ACE writes entries
// OrderBy(PlacementPosition) with no slot field). The container-open UI (InventoryController)
// repaints the grid off the resulting ObjectAdded/Moved events. Retail: ClientUISystem::OnViewContents.
// opened (Use 0x0036). Treat it as a full projection-only REPLACE: update membership without
// inventing ContainerSlot values, then publish one ContainerContentsReplaced notification so
// every UI consumer repaints from the same snapshot. Retail: ClientUISystem::OnViewContents.
dispatcher.Register(GameEventType.ViewContents, e =>
{
var p = GameEvents.ParseViewContents(e.Payload.Span);
@ -352,8 +348,10 @@ public static class GameEventWiring
var guid = GameEvents.ParsePutObjectIn3D(e.Payload.Span);
if (guid is not null)
{
items.MoveItem(guid.Value, newContainerId: 0u);
items.ConfirmMove(guid.Value);
items.ApplyConfirmedServerMove(
guid.Value,
newContainerId: 0u,
newWielderId: 0u);
}
});
@ -596,20 +594,35 @@ public static class GameEventWiring
entries[i] = new ContainerContentEntry(
p.Value.Inventory[i].Guid,
p.Value.Inventory[i].ContainerType);
items.ReplaceContents(ownerGuid, entries);
items.InitializeInventoryManifest(ownerGuid, entries);
}
else
{
foreach (var inv in p.Value.Inventory)
items.RecordMembership(inv.Guid, containerTypeHint: inv.ContainerType);
}
foreach (var eq in p.Value.Equipped)
if (ownerGuid != 0u)
{
items.RecordMembership(
eq.Guid,
containerId: ownerGuid,
equip: (EquipMask)eq.EquipLocation,
priority: eq.Priority);
var equipment = new EquipmentManifestEntry[p.Value.Equipped.Count];
for (int i = 0; i < equipment.Length; i++)
{
var eq = p.Value.Equipped[i];
equipment[i] = new EquipmentManifestEntry(
eq.Guid,
(EquipMask)eq.EquipLocation,
eq.Priority);
}
items.InitializeEquipmentManifest(ownerGuid, equipment);
}
else
{
foreach (var eq in p.Value.Equipped)
{
items.RecordMembership(
eq.Guid,
equip: (EquipMask)eq.EquipLocation,
priority: eq.Priority);
}
}
// D.5.1 Task 4: forward shortcut bar entries to the caller so the