merge: integrate main (D.2b paperdoll/inventory UI line) into the physics/collision branch
Brings the 134 D.2b UI commits onto the physics/collision development line so main can fast-forward. Today's #149 (BSP-less static collision) + #150 (open doors fully passable) + the full collision/streaming/dense-town-FPS arc meet the paperdoll/inventory work. # Conflicts: # docs/ISSUES.md # docs/architecture/retail-divergence-register.md
This commit is contained in:
commit
01594b4cfd
95 changed files with 17201 additions and 167 deletions
|
|
@ -120,6 +120,25 @@ public sealed class GameEventWiringTests
|
|||
Assert.Equal(0x2000u, item.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_WieldObject_ConfirmsOptimisticWield()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
const uint player = 0x2000u;
|
||||
items.AddOrUpdate(new ClientObject { ObjectId = 0x1500, WeenieClassId = 1 });
|
||||
items.MoveItem(0x1500, 0x9999u, newSlot: 0); // start in a pack
|
||||
items.WieldItemOptimistic(0x1500, player, EquipMask.MeleeWeapon); // optimistic wield → snapshot pending
|
||||
|
||||
byte[] payload = new byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1500);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)EquipMask.MeleeWeapon);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), player);
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.WieldObject, payload));
|
||||
d.Dispatch(env!.Value); // server confirms the wield
|
||||
|
||||
Assert.False(items.RollbackMove(0x1500)); // pending snapshot was cleared by ConfirmMove
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_PopupString_RoutesToChatLog()
|
||||
{
|
||||
|
|
@ -496,4 +515,150 @@ public sealed class GameEventWiringTests
|
|||
Assert.Equal(0x5001u, got![0].ObjectGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_PlayerDescription_UpsertsPlayerPropertiesIntoClientObject()
|
||||
{
|
||||
// PD with a PropertyInt32 table carrying EncumbranceVal(5)=1500. The handler
|
||||
// must land it in the player ClientObject so the burden bar reads the wire value.
|
||||
const uint playerGuid = 0x50000001u;
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
var items = new ClientObjectTable();
|
||||
GameEventWiring.WireAll(dispatcher, items, new CombatState(), new Spellbook(),
|
||||
new ChatLog(), playerGuid: () => playerGuid);
|
||||
|
||||
var sb = new MemoryStream();
|
||||
using var w = new BinaryWriter(sb);
|
||||
w.Write(0x00000001u); // propertyFlags = PropertyInt32
|
||||
w.Write(0x52u); // weenieType
|
||||
// int table: u16 count, u16 buckets, then key/val pairs
|
||||
w.Write((ushort)1); // count
|
||||
w.Write((ushort)8); // buckets (ignored)
|
||||
w.Write(5u); // key = EncumbranceVal
|
||||
w.Write(1500u); // val
|
||||
// vector + has_health (no vector blocks)
|
||||
w.Write(0u); // vectorFlags = None
|
||||
w.Write(0u); // has_health
|
||||
// strict trailer
|
||||
w.Write(0u); // option_flags = None
|
||||
w.Write(0u); // options1
|
||||
w.Write(0u); // legacy hotbar count
|
||||
w.Write(0u); // spellbook_filters
|
||||
w.Write(0u); // inventory count
|
||||
w.Write(0u); // equipped count
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.PlayerDescription, sb.ToArray()));
|
||||
dispatcher.Dispatch(env!.Value);
|
||||
|
||||
var player = items.Get(playerGuid);
|
||||
Assert.NotNull(player);
|
||||
Assert.Equal(1500, player!.Properties.Ints[5]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_ViewContents_RecordsMembershipInClientObjectTable()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
|
||||
// containerGuid 0xC9 + 2 entries
|
||||
byte[] payload = new byte[8 + 2 * 8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), 0x500000C9u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 2u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0x50000A01u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 0u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(16), 0x50000A02u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(20), 1u);
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.ViewContents, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal(0x500000C9u, items.Get(0x50000A01u)!.ContainerId);
|
||||
Assert.Equal(0x500000C9u, items.Get(0x50000A02u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_ViewContents_IsFullReplace_DropsRemovedItems()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
|
||||
// First view of container 0xC9: two items.
|
||||
DispatchViewContents(d, 0x500000C9u, new uint[] { 0x50000A01u, 0x50000A02u });
|
||||
Assert.Equal(2, items.GetContents(0x500000C9u).Count);
|
||||
|
||||
// Second view: only one item — the other was removed server-side. Additive merge would keep both.
|
||||
DispatchViewContents(d, 0x500000C9u, new uint[] { 0x50000A02u });
|
||||
Assert.Equal(new[] { 0x50000A02u }, items.GetContents(0x500000C9u));
|
||||
Assert.Equal(0u, items.Get(0x50000A01u)!.ContainerId);
|
||||
}
|
||||
|
||||
private static void DispatchViewContents(GameEventDispatcher d, uint containerGuid, uint[] itemGuids)
|
||||
{
|
||||
byte[] payload = new byte[8 + itemGuids.Length * 8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), containerGuid);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)itemGuids.Length);
|
||||
for (int i = 0; i < itemGuids.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8 + i * 8), itemGuids[i]);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12 + i * 8), 0u); // containerType
|
||||
}
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.ViewContents, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_InventoryServerSaveFailed_RollsBackOptimisticMove()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
// An item known to be in container 0xC0 at slot 2, then optimistically moved to 0xC1.
|
||||
items.RecordMembership(0x50000B01u, containerId: 0x500000C0u);
|
||||
items.MoveItem(0x50000B01u, 0x500000C0u, 2);
|
||||
items.MoveItemOptimistic(0x50000B01u, 0x500000C1u, 0);
|
||||
Assert.Equal(0x500000C1u, items.Get(0x50000B01u)!.ContainerId); // moved (optimistic)
|
||||
|
||||
// Server bounces it: InventoryServerSaveFailed (itemGuid, weenieError).
|
||||
byte[] payload = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), 0x50000B01u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x1Au); // some WeenieError
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.InventoryServerSaveFailed, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal(0x500000C0u, items.Get(0x50000B01u)!.ContainerId); // snapped back
|
||||
Assert.Equal(2, items.Get(0x50000B01u)!.ContainerSlot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_InventoryPutObjInContainer_ConfirmsOptimisticMove_soNoRollback()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
items.RecordMembership(0x50000B02u, containerId: 0x500000C0u);
|
||||
items.MoveItem(0x50000B02u, 0x500000C0u, 2);
|
||||
items.MoveItemOptimistic(0x50000B02u, 0x500000C1u, 0); // optimistic move (pending snapshot)
|
||||
|
||||
// Server CONFIRMS via InventoryPutObjInContainer (item, container, placement, containerType).
|
||||
byte[] payload = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), 0x50000B02u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x500000C1u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0u); // placement
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 0u); // containerType
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.InventoryPutObjInContainer, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
// ConfirmMove cleared the pending snapshot → a later rollback is a no-op (the move stuck).
|
||||
Assert.False(items.RollbackMove(0x50000B02u));
|
||||
Assert.Equal(0x500000C1u, items.Get(0x50000B02u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_InventoryPutObjectIn3D_UnparentsFromContainer()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
items.RecordMembership(0x50000A01u, containerId: 0x500000C9u);
|
||||
|
||||
byte[] payload = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x50000A01u);
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.InventoryPutObjectIn3D, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal(0u, items.Get(0x50000A01u)!.ContainerId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,15 +56,18 @@ public sealed class AppraiseTests
|
|||
[Fact]
|
||||
public void ParsePutObjInContainer_RoundTrip()
|
||||
{
|
||||
byte[] payload = new byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1001u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x2001u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 3u);
|
||||
// 4 fields (Task 7 fix: containerType added) — 16 bytes required.
|
||||
byte[] payload = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1001u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x2001u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 3u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 1u); // containerType
|
||||
|
||||
var parsed = GameEvents.ParsePutObjInContainer(payload);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x1001u, parsed!.Value.ItemGuid);
|
||||
Assert.Equal(0x2001u, parsed.Value.ContainerGuid);
|
||||
Assert.Equal(3u, parsed.Value.Placement);
|
||||
Assert.Equal(1u, parsed.Value.ContainerType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,42 @@ public sealed class DeleteObjectTests
|
|||
Assert.Equal(0x80000439u, parsed!.Value.Guid);
|
||||
Assert.Equal((ushort)0x1234, parsed.Value.InstanceSequence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression guard: TryParse (0xF747 = true destroy) must always produce
|
||||
/// FromPickup = false. PickupEvent (0xF74A) is a different opcode and is
|
||||
/// the ONLY path that sets FromPickup = true (in WorldSession).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryParse_AlwaysReturnsFromPickupFalse()
|
||||
{
|
||||
Span<byte> body = stackalloc byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, DeleteObject.Opcode);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.Slice(4), 0x80000001u);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.Slice(8), 0x0001);
|
||||
|
||||
var parsed = DeleteObject.TryParse(body);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.False(parsed!.Value.FromPickup,
|
||||
"DeleteObject 0xF747 is a true destroy — FromPickup must be false.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FromPickup = true can be constructed via the record directly (as
|
||||
/// WorldSession does for PickupEvent 0xF74A). Default is false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parsed_DefaultFromPickupIsFalse()
|
||||
{
|
||||
var p = new DeleteObject.Parsed(0x80000001u, 1);
|
||||
Assert.False(p.FromPickup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parsed_FromPickupTrueCanBeSet()
|
||||
{
|
||||
var p = new DeleteObject.Parsed(0x80000001u, 1, FromPickup: true);
|
||||
Assert.True(p.FromPickup);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public sealed class GameEventsInventoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseViewContents_twoEntries_returnsContainerAndItems()
|
||||
{
|
||||
// containerGuid + count(2) + 2×{guid, containerType}
|
||||
var b = new byte[4 + 4 + 2 * 8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), 0x500000C9u); // containerGuid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), 2u); // count
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(8), 0x50000A01u); // guid 1
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(12), 0u); // type 1 (item)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(16), 0x50000A02u);// guid 2
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(20), 1u); // type 2 (container)
|
||||
|
||||
var p = GameEvents.ParseViewContents(b);
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(0x500000C9u, p!.Value.ContainerGuid);
|
||||
Assert.Equal(2, p.Value.Items.Count);
|
||||
Assert.Equal(0x50000A01u, p.Value.Items[0].Guid);
|
||||
Assert.Equal(0u, p.Value.Items[0].ContainerType);
|
||||
Assert.Equal(0x50000A02u, p.Value.Items[1].Guid);
|
||||
Assert.Equal(1u, p.Value.Items[1].ContainerType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseViewContents_zeroCount_returnsEmptyList()
|
||||
{
|
||||
var b = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), 0x500000C9u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), 0u);
|
||||
var p = GameEvents.ParseViewContents(b);
|
||||
Assert.NotNull(p);
|
||||
Assert.Empty(p!.Value.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseViewContents_truncated_returnsNull()
|
||||
=> Assert.Null(GameEvents.ParseViewContents(new byte[4]));
|
||||
|
||||
[Fact]
|
||||
public void ParsePutObjInContainer_readsAllFourFields()
|
||||
{
|
||||
var b = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), 0x50000A01u); // itemGuid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), 0x500000C9u); // containerGuid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(8), 3u); // placement
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(12), 1u); // containerType
|
||||
|
||||
var p = GameEvents.ParsePutObjInContainer(b);
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(0x50000A01u, p!.Value.ItemGuid);
|
||||
Assert.Equal(0x500000C9u, p.Value.ContainerGuid);
|
||||
Assert.Equal(3u, p.Value.Placement);
|
||||
Assert.Equal(1u, p.Value.ContainerType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseInventoryServerSaveFailed_readsGuidAndError()
|
||||
{
|
||||
var b = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), 0x50000A01u); // itemGuid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), 0x0Au); // weenieError
|
||||
|
||||
var p = GameEvents.ParseInventoryServerSaveFailed(b);
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(0x50000A01u, p!.Value.ItemGuid);
|
||||
Assert.Equal(0x0Au, p.Value.WeenieError);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,18 +74,25 @@ public sealed class InventoryActionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAddShortcut_ThreeFields()
|
||||
public void BuildAddShortcut_ItemShortcut_FieldLayout()
|
||||
{
|
||||
byte[] body = InventoryActions.BuildAddShortcut(
|
||||
seq: 1, slotIndex: 0, objectType: 1, targetId: 0x3E1);
|
||||
// ShortCutData = Index(u32), ObjectId(u32), SpellId(u16), Layer(u16). Item → spell/layer 0.
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 0, objectGuid: 0x3E1, spellId: 0, layer: 0);
|
||||
Assert.Equal(24, body.Length);
|
||||
Assert.Equal(InventoryActions.AddShortcutOpcode,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||
Assert.Equal(0u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||
Assert.Equal(1u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
||||
Assert.Equal(0x3E1u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20)));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12))); // index
|
||||
Assert.Equal(0x3E1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16))); // objectGuid
|
||||
Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20))); // spellId
|
||||
Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22))); // layer
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAddShortcut_SpellShortcut_PacksSpellAndLayerAsU16s()
|
||||
{
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 2, objectGuid: 0, spellId: 0x1234, layer: 3);
|
||||
Assert.Equal(0x1234, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20)));
|
||||
Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -106,4 +113,38 @@ public sealed class InventoryActionsTests
|
|||
Assert.Equal(42u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDropItem_layout()
|
||||
{
|
||||
byte[] b = InventoryActions.BuildDropItem(seq: 7, itemGuid: 0x50000A01u);
|
||||
Assert.Equal(16, b.Length);
|
||||
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
|
||||
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
|
||||
Assert.Equal(0x001Bu, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
|
||||
Assert.Equal(0x50000A01u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildGetAndWieldItem_layout()
|
||||
{
|
||||
byte[] b = InventoryActions.BuildGetAndWieldItem(seq: 7, itemGuid: 0x50000A01u, equipMask: 0x02u);
|
||||
Assert.Equal(20, b.Length);
|
||||
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
|
||||
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
|
||||
Assert.Equal(0x001Au, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
|
||||
Assert.Equal(0x50000A01u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
|
||||
Assert.Equal(0x02u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(16)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildNoLongerViewingContents_layout()
|
||||
{
|
||||
byte[] b = InventoryActions.BuildNoLongerViewingContents(seq: 7, containerGuid: 0x500000C9u);
|
||||
Assert.Equal(16, b.Length);
|
||||
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
|
||||
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
|
||||
Assert.Equal(0x0195u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
|
||||
Assert.Equal(0x500000C9u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public sealed class InventoryRemoveObjectTests
|
||||
{
|
||||
private static byte[] Build(uint guid, uint opcode = 0x0024u)
|
||||
{
|
||||
var b = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4), guid);
|
||||
return b;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_valid_returnsGuid()
|
||||
{
|
||||
var p = InventoryRemoveObject.TryParse(Build(0x50000A07u));
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(0x50000A07u, p!.Value.Guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_wrongOpcode_returnsNull()
|
||||
=> Assert.Null(InventoryRemoveObject.TryParse(Build(1, opcode: 0x0023u)));
|
||||
|
||||
[Fact]
|
||||
public void TryParse_truncated_returnsNull()
|
||||
=> Assert.Null(InventoryRemoveObject.TryParse(new byte[7]));
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public sealed class PrivateUpdatePropertyIntTests
|
||||
{
|
||||
// 0x02CD body: opcode(4) + seq(1) + property(4) + value(4) = 13 bytes. NO guid.
|
||||
private static byte[] Build(uint property, int value, byte seq = 1, uint opcode = 0x02CDu)
|
||||
{
|
||||
var b = new byte[13];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode);
|
||||
b[4] = seq;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), property);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(9), value);
|
||||
return b;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_encumbranceVal_returnsPropValue()
|
||||
{
|
||||
var p = PrivateUpdatePropertyInt.TryParse(Build(property: 5u, value: 1500));
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(5u, p!.Value.Property);
|
||||
Assert.Equal(1500, p.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_wrongOpcode_returnsNull()
|
||||
=> Assert.Null(PrivateUpdatePropertyInt.TryParse(Build(5, 1, opcode: 0x02CEu)));
|
||||
|
||||
[Fact]
|
||||
public void TryParse_truncated_returnsNull()
|
||||
=> Assert.Null(PrivateUpdatePropertyInt.TryParse(new byte[12]));
|
||||
}
|
||||
37
tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs
Normal file
37
tests/AcDream.Core.Net.Tests/Messages/SetStackSizeTests.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public sealed class SetStackSizeTests
|
||||
{
|
||||
private static byte[] Build(uint guid, int stackSize, int value, byte seq = 1, uint opcode = 0x0197u)
|
||||
{
|
||||
var b = new byte[17];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode);
|
||||
b[4] = seq;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), guid);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(9), stackSize);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(13), value);
|
||||
return b;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_validStack_returnsGuidStackValue()
|
||||
{
|
||||
var p = SetStackSize.TryParse(Build(0x50000A01u, stackSize: 25, value: 125));
|
||||
Assert.NotNull(p);
|
||||
Assert.Equal(0x50000A01u, p!.Value.Guid);
|
||||
Assert.Equal(25, p.Value.StackSize);
|
||||
Assert.Equal(125, p.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_wrongOpcode_returnsNull()
|
||||
=> Assert.Null(SetStackSize.TryParse(Build(1, 1, 1, opcode: 0x0196u)));
|
||||
|
||||
[Fact]
|
||||
public void TryParse_truncated_returnsNull()
|
||||
=> Assert.Null(SetStackSize.TryParse(new byte[16]));
|
||||
}
|
||||
|
|
@ -7,10 +7,13 @@ namespace AcDream.Core.Net.Tests;
|
|||
/// <summary>
|
||||
/// D.5.4 Task 7 — ObjectTableWiring.
|
||||
///
|
||||
/// Integration test is omitted: WorldSession.EntitySpawned has no internal
|
||||
/// test seam to fire it without a real Tick + packet bytes, so subscription
|
||||
/// correctness is covered by build (type-checks) + the live run. Only the
|
||||
/// pure mapping (ToWeenieData) is unit-tested here.
|
||||
/// WorldSession.EntitySpawned/EntityDeleted have no in-process test seam (the
|
||||
/// ctor opens a UDP socket), so the subscription wiring is tested via the guard
|
||||
/// logic extracted as a local handler — this is the same lambda body that
|
||||
/// ObjectTableWiring.Wire wires to session.EntityDeleted. The retail two-table
|
||||
/// semantics are: PickupEvent (0xF74A) removes from the 3-D object_table but
|
||||
/// KEEPS the weenie in ClientObjectTable; DeleteObject (0xF747) evicts the
|
||||
/// weenie from both. FromPickup = true guards the eviction.
|
||||
/// </summary>
|
||||
public sealed class ObjectTableWiringTests
|
||||
{
|
||||
|
|
@ -94,4 +97,80 @@ public sealed class ObjectTableWiringTests
|
|||
Assert.Equal(100, d.MaxStructure);
|
||||
Assert.Equal(4.5f, d.Workmanship);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// The handler that ObjectTableWiring.Wire subscribes to session.EntityDeleted.
|
||||
// Testing it directly avoids needing a live WorldSession (UDP socket).
|
||||
// -------------------------------------------------------------------------
|
||||
private static Action<DeleteObject.Parsed> MakeEntityDeletedHandler(ClientObjectTable table)
|
||||
=> d => { if (!d.FromPickup) table.Remove(d.Guid); };
|
||||
|
||||
private static WeenieData MinimalWeenie(uint guid) => new(
|
||||
Guid: guid,
|
||||
Name: "Test Item",
|
||||
Type: null,
|
||||
WeenieClassId: 1u,
|
||||
IconId: 0x06000001u,
|
||||
IconOverlayId: 0u,
|
||||
IconUnderlayId: 0u,
|
||||
Effects: 0u,
|
||||
Value: null,
|
||||
StackSize: null,
|
||||
StackSizeMax: null,
|
||||
Burden: null,
|
||||
ContainerId: null,
|
||||
WielderId: null,
|
||||
ValidLocations: null,
|
||||
CurrentWieldedLocation: null,
|
||||
Priority: null,
|
||||
ItemsCapacity: null,
|
||||
ContainersCapacity: null,
|
||||
Structure: null,
|
||||
MaxStructure: null,
|
||||
Workmanship: null);
|
||||
|
||||
/// <summary>
|
||||
/// PickupEvent path: FromPickup = true → weenie is RETAINED in ClientObjectTable.
|
||||
/// The 3-D render entity is removed (EntityDeleted still fires), but the weenie
|
||||
/// data persists so the follow-up InventoryPutObjInContainer can find it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityDeleted_FromPickupTrue_RetainsWeenieInTable()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
var handler = MakeEntityDeletedHandler(table);
|
||||
|
||||
const uint guid = 0x80000100u;
|
||||
table.Ingest(MinimalWeenie(guid));
|
||||
Assert.NotNull(table.Get(guid)); // pre-condition: item is in the table
|
||||
|
||||
// Fire EntityDeleted as WorldSession does for PickupEvent (0xF74A).
|
||||
// The handler itself represents the EntityDeleted subscription — firing it IS the event.
|
||||
handler(new DeleteObject.Parsed(guid, 0, FromPickup: true));
|
||||
|
||||
// Post-condition: weenie still in table (it moved into a container, was NOT destroyed).
|
||||
// EntityDeleted still fires (the handler ran), so the 3-D render layer would remove the WorldEntity.
|
||||
Assert.NotNull(table.Get(guid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DeleteObject path: FromPickup = false → weenie IS evicted from ClientObjectTable.
|
||||
/// Regression guard — true destroys (0xF747) must still clean up the table.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityDeleted_FromPickupFalse_EvictsWeenieFromTable()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
var handler = MakeEntityDeletedHandler(table);
|
||||
|
||||
const uint guid = 0x80000200u;
|
||||
table.Ingest(MinimalWeenie(guid));
|
||||
Assert.NotNull(table.Get(guid)); // pre-condition: item is in the table
|
||||
|
||||
// Fire EntityDeleted as WorldSession does for DeleteObject (0xF747).
|
||||
handler(new DeleteObject.Parsed(guid, 0, FromPickup: false));
|
||||
|
||||
// Post-condition: weenie evicted (truly destroyed).
|
||||
Assert.Null(table.Get(guid));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue