fix(interaction): restore retail loot placement and world-drop projection

This commit is contained in:
Erik 2026-07-27 00:03:15 +02:00
parent 4d095be286
commit 921712f412
24 changed files with 1581 additions and 34 deletions

View file

@ -214,6 +214,40 @@ public sealed class ClientObjectTable
public ClientObject? Get(uint objectId) =>
_objects.TryGetValue(objectId, out var item) ? item : null;
/// <summary>
/// Retail <c>ACCWeenieObject::IsOwnedByObject @ 0x0058CEB0</c>.
/// Direct containment/wielding counts, as does an item inside one of the
/// owner's authored pack containers or on one of the owner's locations.
/// </summary>
public bool IsOwnedByObject(uint objectId, uint ownerId)
{
if (ownerId == 0u
|| !_objects.TryGetValue(objectId, out ClientObject? item))
{
return false;
}
if (item.ObjectId == ownerId
|| item.ContainerId == ownerId
|| item.WielderId == ownerId)
{
return true;
}
if (!_objects.ContainsKey(ownerId))
return false;
if (item.ContainerId != 0u
&& _containerIndex.TryGetValue(ownerId, out List<uint>? ownerContents)
&& ownerContents.Contains(item.ContainerId))
{
return true;
}
return _equipmentIndex.TryGetValue(ownerId, out List<uint>? ownerLocations)
&& ownerLocations.Contains(item.ObjectId);
}
/// <summary>
/// Look up a container by object id, creating a lightweight stub if
/// the id doesn't match any known container (defensive — avoids losing
@ -297,26 +331,43 @@ public sealed class ClientObjectTable
newWielderId,
newEquipLocation),
containerTypeHint,
ClientObjectMoveOrigin.AuthoritativeResponse);
ClientObjectMoveOrigin.AuthoritativeResponse,
retailContainerInsert: true);
}
private bool ApplyPlacement(
ClientObject item,
ClientObjectPlacement current,
uint? containerTypeHint,
ClientObjectMoveOrigin origin)
ClientObjectMoveOrigin origin,
bool retailContainerInsert = false)
{
ClientObjectPlacement previous = ClientObjectPlacement.From(item);
List<uint>? changedContainers = RemoveFromOtherContainerIndexes(
item.ObjectId,
current.ContainerId);
bool previousWasContainer = IsContainerListMember(item);
List<uint>? changedContainers;
if (retailContainerInsert)
{
changedContainers = RemoveFromAllContainerIndexes(
item.ObjectId,
current.ContainerId,
previousWasContainer);
}
else
{
changedContainers = RemoveFromOtherContainerIndexes(
item.ObjectId,
current.ContainerId);
}
item.ContainerId = current.ContainerId;
item.ContainerSlot = current.ContainerSlot;
item.WielderId = current.WielderId;
item.CurrentlyEquippedLocation = current.EquipLocation;
if (containerTypeHint is { } hint)
item.ContainerTypeHint = hint;
Reindex(item, previous.ContainerId);
if (retailContainerInsert && item.ContainerId != 0u)
InsertContainerMember(item, current.ContainerSlot);
else
Reindex(item, previous.ContainerId);
PublishPlacementChange(item, previous, origin);
PublishContainerContentsChanges(changedContainers);
return true;
@ -927,6 +978,101 @@ public sealed class ClientObjectTable
return changed;
}
/// <summary>
/// Retail <c>ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0</c>
/// removes the object from its old <c>IDList</c> before
/// <c>AddContent @ 0x0058CCE0</c> inserts it at the server-supplied list
/// index. Remove the canonical member as well as stale ViewContents
/// projections so an authoritative same-container move can perform that
/// exact remove-then-insert transition.
/// </summary>
private List<uint>? RemoveFromAllContainerIndexes(
uint itemId,
uint destinationContainerId,
bool wasContainer)
{
List<uint>? changed = null;
foreach ((uint containerId, List<uint> members) in _containerIndex)
{
if (!members.Remove(itemId))
continue;
RenumberContainerCategory(members, wasContainer);
if (containerId != destinationContainerId)
(changed ??= new List<uint>()).Add(containerId);
}
return changed;
}
/// <summary>
/// Port of retail <c>ACCWeenieObject::AddContent @ 0x0058CCE0</c>:
/// items and child containers have separate ordered <c>IDList</c>s and
/// <c>IDList::AddAtNum</c> clamps the requested index to the list length.
/// Our compact projection stores both categories in one list, so insertion
/// and renumbering operate only inside the matching retail category.
/// </summary>
private void InsertContainerMember(ClientObject item, int requestedSlot)
{
if (!_containerIndex.TryGetValue(item.ContainerId, out List<uint>? members))
_containerIndex[item.ContainerId] = members = new List<uint>();
bool isContainer = IsContainerListMember(item);
int categoryCount = 0;
int insertionIndex = members.Count;
int requestedIndex = requestedSlot < 0 ? int.MaxValue : requestedSlot;
for (int i = 0; i < members.Count; i++)
{
if (!_objects.TryGetValue(members[i], out ClientObject? existing)
|| IsContainerListMember(existing) != isContainer)
{
continue;
}
if (categoryCount == requestedIndex)
{
insertionIndex = i;
break;
}
categoryCount++;
insertionIndex = i + 1;
}
members.Insert(insertionIndex, item.ObjectId);
int publishedSlot = requestedSlot < 0 ? categoryCount : requestedSlot;
RenumberContainerCategory(
members,
isContainer,
preservedItemId: item.ObjectId,
preservedSlot: publishedSlot);
}
private void RenumberContainerCategory(
List<uint> members,
bool isContainer,
uint preservedItemId = 0u,
int preservedSlot = -1)
{
int slot = 0;
for (int i = 0; i < members.Count; i++)
{
if (_objects.TryGetValue(members[i], out ClientObject? member)
&& IsContainerListMember(member) == isContainer)
{
member.ContainerSlot = member.ObjectId == preservedItemId
? preservedSlot
: slot;
slot++;
}
}
}
private static bool IsContainerListMember(ClientObject item) =>
item.ContainerTypeHint != 0u
|| (item.Type & ItemType.Container) != 0
|| item.ItemsCapacity > 0;
private void PublishContainerContentsChanges(List<uint>? changed)
{
if (changed is null || changed.Count == 0)