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

@ -222,13 +222,13 @@ internal sealed class AutoWieldController : IDisposable
return true;
}
private void OnObjectMoved(ClientObject item, uint _, uint newContainerId)
private void OnObjectMoved(ClientObjectMove move)
{
if (_pendingSwitch is not { } pending
|| pending.BlockingItemId == 0
|| item.ObjectId != pending.BlockingItemId
|| newContainerId != _playerGuid()
|| item.CurrentlyEquippedLocation != EquipMask.None)
|| move.ItemId != pending.BlockingItemId
|| move.Current.ContainerId != _playerGuid()
|| move.Current.EquipLocation != EquipMask.None)
return;
// Clear before re-entry: the second AutoWield pass must see the newly
@ -238,10 +238,10 @@ internal sealed class AutoWieldController : IDisposable
TryWield(requested, pending.RequestedMask);
}
private void OnWieldConfirmed(ClientObject item)
private void OnWieldConfirmed(uint itemId)
{
if (_pendingSwitch is { BlockingItemId: 0 } pending
&& item.ObjectId == pending.RequestedItemId)
&& itemId == pending.RequestedItemId)
_pendingSwitch = null;
}

View file

@ -170,6 +170,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
// Rebuild on any change to the player's possessions.
_objects.ObjectAdded += OnObjectChanged;
_objects.ObjectMoved += OnObjectMoved;
_objects.ContainerContentsReplaced += OnContainerContentsReplaced;
_objects.ObjectRemoved += OnObjectRemoved;
_objects.ObjectUpdated += OnObjectChanged;
_objects.Cleared += OnObjectsCleared;
@ -239,8 +240,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
}
if (Concerns(o)) Populate();
}
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
private void OnObjectMoved(ClientObjectMove move)
{
uint player = _playerGuid();
if ((move.Item is { } item && Concerns(item))
|| move.Previous.ContainerId == player
|| move.Current.ContainerId == player
|| move.Previous.WielderId == player
|| move.Current.WielderId == player)
Populate();
}
private void OnContainerContentsReplaced(uint containerId)
{
if (containerId == EffectiveOpen() || containerId == _playerGuid())
Populate();
}
private void OnInteractionStateChanged() => ApplyIndicators();
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
private void OnObjectsCleared() => Populate();
@ -702,6 +716,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_disposed = true;
_objects.ObjectAdded -= OnObjectChanged;
_objects.ObjectMoved -= OnObjectMoved;
_objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
_objects.ObjectRemoved -= OnObjectRemoved;
_objects.ObjectUpdated -= OnObjectChanged;
_objects.Cleared -= OnObjectsCleared;

View file

@ -211,8 +211,16 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
else if (Concerns(o))
Populate();
}
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
private void OnObjectMoved(ClientObjectMove move)
{
uint player = _playerGuid();
if ((move.Item is { } item && Concerns(item))
|| move.Previous.ContainerId == player
|| move.Current.ContainerId == player
|| move.Previous.WielderId == player
|| move.Current.WielderId == player)
Populate();
}
private void OnSelectionChanged(SelectionTransition _) => ApplySelectionIndicators();
private void OnObjectsCleared()
{
@ -224,8 +232,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
/// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always
/// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by
/// WieldItemOptimistic), so the equip-location need not be tested here; OnObjectMoved adds the from/to
/// player backstop for moves that transiently satisfy neither (e.g. unwield into a side bag).</summary>
/// WieldItemOptimistic), so the equip-location need not be tested here; OnObjectMoved carries the
/// complete old/new retail placement for transitions that satisfy neither after mutation.</summary>
private bool Concerns(ClientObject o)
{
uint p = _playerGuid();

View file

@ -216,13 +216,17 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Populate();
}
private void OnRepositoryObjectMoved(ClientObject obj, uint oldContainer, uint newContainer)
private void OnRepositoryObjectMoved(ClientObjectMove move)
{
uint player = _playerGuid?.Invoke() ?? 0u;
if (obj.ObjectId == _ammoObjectId
|| (player != 0 && (oldContainer == player || newContainer == player)))
if (move.ItemId == _ammoObjectId
|| (player != 0
&& (move.Previous.ContainerId == player
|| move.Current.ContainerId == player
|| move.Previous.WielderId == player
|| move.Current.WielderId == player)))
RefreshAmmo();
if (IsShortcutGuid(obj.ObjectId))
if (IsShortcutGuid(move.ItemId))
Populate();
}
@ -238,7 +242,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
return obj.ObjectId == _ammoObjectId
|| (player != 0 && obj.ObjectId == player)
|| (player != 0
&& obj.ContainerId == player
&& (obj.WielderId == player || obj.ContainerId == player)
&& (obj.CurrentlyEquippedLocation
& (EquipMask.MissileWeapon | EquipMask.MissileAmmo)) != 0);
}
@ -246,13 +250,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private void RefreshAmmo()
{
uint player = _playerGuid?.Invoke() ?? 0u;
var placements = new List<ClientObject>();
if (player != 0)
{
foreach (uint objectId in _repo.GetContents(player))
if (_repo.Get(objectId) is { } item)
placements.Add(item);
}
IReadOnlyList<ClientObject> placements = player != 0
? _repo.GetEquippedBy(player)
: Array.Empty<ClientObject>();
ToolbarAmmoPolicy.Result result = ToolbarAmmoPolicy.Resolve(placements);
_ammoObjectId = result.ObjectId;