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
40
tests/AcDream.Core.Tests/Items/BurdenMathTests.cs
Normal file
40
tests/AcDream.Core.Tests/Items/BurdenMathTests.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public class BurdenMathTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(100, 0, 15000)] // base: str*150
|
||||
[InlineData(100, 3, 24000)] // +clamp(3*30,0,150)=90 -> +90*100
|
||||
[InlineData(100, 10, 30000)] // 10*30=300 clamped to 150 -> +150*100
|
||||
[InlineData(0, 5, 0)] // str<=0 -> 0
|
||||
public void EncumbranceCapacity_matches_retail(int str, int aug, int expected)
|
||||
=> Assert.Equal(expected, BurdenMath.EncumbranceCapacity(str, aug));
|
||||
|
||||
[Theory]
|
||||
[InlineData(15000, 7500, 0.5f)]
|
||||
[InlineData(15000, 0, 0f)]
|
||||
[InlineData(0, 100, 0f)] // cap<=0 guard
|
||||
public void LoadRatio_is_burden_over_capacity(int cap, int burden, float expected)
|
||||
=> Assert.Equal(expected, BurdenMath.LoadRatio(cap, burden), 4);
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 0f)]
|
||||
[InlineData(0.5f, 0.16667f)]
|
||||
[InlineData(1.0f, 0.33333f)]
|
||||
[InlineData(3.0f, 1.0f)] // full at 300%
|
||||
[InlineData(4.0f, 1.0f)] // clamped
|
||||
public void LoadToFill_is_third_clamped(float load, float expected)
|
||||
=> Assert.Equal(expected, BurdenMath.LoadToFill(load), 4);
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 0)]
|
||||
[InlineData(0.5f, 50)]
|
||||
[InlineData(1.0f, 100)]
|
||||
[InlineData(3.0f, 300)] // 300% = full
|
||||
[InlineData(4.0f, 300)] // SATURATES at 300% (computed from the clamped fill, decomp 176544-176576)
|
||||
public void LoadToPercent_saturates_at_300(float load, int expected)
|
||||
=> Assert.Equal(expected, BurdenMath.LoadToPercent(load));
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public class ClientObjectTableBurdenTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void SumCarriedBurden_sums_pack_sidebag_and_wielded_but_not_unrelated()
|
||||
{
|
||||
var t = new ClientObjectTable();
|
||||
// loose pack item
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0xA, ContainerId = Player, Burden = 100 });
|
||||
// side bag in pack
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0xB, ContainerId = Player, Burden = 50,
|
||||
Type = ItemType.Container });
|
||||
// item inside the side bag (2-deep)
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0xC, ContainerId = 0xB, Burden = 30 });
|
||||
// wielded (ContainerId 0, WielderId = player)
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0xD, WielderId = Player, Burden = 200 });
|
||||
// unrelated object in the world
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0xE, ContainerId = 0, Burden = 999 });
|
||||
|
||||
Assert.Equal(380, t.SumCarriedBurden(Player)); // 100+50+30+200
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumCarriedBurden_unknown_owner_is_zero()
|
||||
=> Assert.Equal(0, new ClientObjectTable().SumCarriedBurden(Player));
|
||||
}
|
||||
|
|
@ -319,6 +319,126 @@ public sealed class ClientObjectTableTests
|
|||
Assert.True((o.Type & ItemType.Creature) != 0); // LiveItemType(guid) & Creature drives creature targeting
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceContents_RecordsAllInListOrder()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u, 0xA03u });
|
||||
Assert.Equal(new[] { 0xA01u, 0xA02u, 0xA03u }, table.GetContents(0xC9u));
|
||||
Assert.Equal(0xC9u, table.Get(0xA01u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceContents_SecondSmallerList_DetachesDropped() // the full-REPLACE semantics (vs the old additive merge)
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u, 0xA03u });
|
||||
table.ReplaceContents(0xC9u, new uint[] { 0xA02u }); // A01 + A03 removed server-side
|
||||
Assert.Equal(new[] { 0xA02u }, table.GetContents(0xC9u));
|
||||
Assert.Equal(0u, table.Get(0xA01u)!.ContainerId); // detached, not lingering
|
||||
Assert.Equal(0u, table.Get(0xA03u)!.ContainerId);
|
||||
Assert.Equal(0xC9u, table.Get(0xA02u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceContents_ReordersToNewListOrder()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.ReplaceContents(0xC9u, new uint[] { 0xA01u, 0xA02u });
|
||||
table.ReplaceContents(0xC9u, new uint[] { 0xA02u, 0xA01u });
|
||||
Assert.Equal(new[] { 0xA02u, 0xA01u }, table.GetContents(0xC9u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceContents_ZeroContainer_NoOp()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.ReplaceContents(0u, new uint[] { 0xA01u });
|
||||
Assert.Empty(table.GetContents(0u));
|
||||
Assert.Null(table.Get(0xA01u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_movesAndRemembersPreMove()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x700u, container: 0xC0u)); table.MoveItem(0x700u, 0xC0u, newSlot: 3);
|
||||
table.MoveItemOptimistic(0x700u, 0xC1u, 0);
|
||||
Assert.Equal(0xC1u, table.Get(0x700u)!.ContainerId); // moved now (instant)
|
||||
Assert.True(table.RollbackMove(0x700u)); // pre-move was remembered
|
||||
Assert.Equal(0xC0u, table.Get(0x700u)!.ContainerId); // snapped back to the original container
|
||||
Assert.Equal(3, table.Get(0x700u)!.ContainerSlot); // and slot
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfirmMove_clearsPending_soRollbackIsNoOp()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x701u, container: 0xC0u));
|
||||
table.MoveItemOptimistic(0x701u, 0xC1u, 0);
|
||||
table.ConfirmMove(0x701u);
|
||||
Assert.False(table.RollbackMove(0x701u)); // nothing pending → no rollback
|
||||
Assert.Equal(0xC1u, table.Get(0x701u)!.ContainerId); // stays at the confirmed container
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_twice_keepsTheOriginalPreMove()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x702u, container: 0xC0u));
|
||||
table.MoveItemOptimistic(0x702u, 0xC1u, 0);
|
||||
table.MoveItemOptimistic(0x702u, 0xC2u, 0); // a second move before any confirm
|
||||
table.RollbackMove(0x702u);
|
||||
Assert.Equal(0xC0u, table.Get(0x702u)!.ContainerId); // rolls back to the FIRST pre-move
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollbackMove_unknownItem_false()
|
||||
=> Assert.False(new ClientObjectTable().RollbackMove(0xDEADu));
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_twice_oneConfirm_stillRollsBack() // I1: an early confirm must not strand the item
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x910u, container: 0xC0u)); table.MoveItem(0x910u, 0xC0u, 5);
|
||||
table.MoveItemOptimistic(0x910u, 0xC1u, 0); // move 1 (outstanding 1, snapshot C0/5)
|
||||
table.MoveItemOptimistic(0x910u, 0xC2u, 0); // move 2 (outstanding 2)
|
||||
table.ConfirmMove(0x910u); // server confirms move 1 → outstanding 1, STILL pending
|
||||
Assert.True(table.RollbackMove(0x910u)); // move 2 rejected → can still roll back (not stranded at C2)
|
||||
Assert.Equal(0xC0u, table.Get(0x910u)!.ContainerId);
|
||||
Assert.Equal(5, table.Get(0x910u)!.ContainerSlot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_dropsPendingMoves_soRecycledGuidIsntMisRolledBack() // I2: pending must not survive a table flush
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x911u, container: 0xC0u));
|
||||
table.MoveItemOptimistic(0x911u, 0xC1u, 0); // pending snapshot for 0x911
|
||||
table.Clear(); // teleport/logoff flushes the table
|
||||
|
||||
table.Ingest(FullWeenie(0x911u, container: 0xC5u)); // a recycled guid reappears fresh
|
||||
Assert.False(table.RollbackMove(0x911u)); // no stale pending → no mis-rollback
|
||||
Assert.Equal(0xC5u, table.Get(0x911u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveItemOptimistic_insertBefore_shiftsOthers_keepsGaplessOrder() // the "items change position" bug
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
table.Ingest(FullWeenie(0x920u, container: 0xC0u)); table.MoveItem(0x920u, 0xC0u, 0);
|
||||
table.Ingest(FullWeenie(0x921u, container: 0xC0u)); table.MoveItem(0x921u, 0xC0u, 1);
|
||||
table.Ingest(FullWeenie(0x922u, container: 0xC0u)); table.MoveItem(0x922u, 0xC0u, 2);
|
||||
|
||||
// Move 0x922 (index 2) to index 0 → order [0x922, 0x920, 0x921], slots renumbered 0,1,2 (no ties,
|
||||
// no reshuffle). A raw slot-set would leave 0x922 + 0x920 both at slot 0 → unstable sort.
|
||||
table.MoveItemOptimistic(0x922u, 0xC0u, 0);
|
||||
Assert.Equal(new[] { 0x922u, 0x920u, 0x921u }, table.GetContents(0xC0u));
|
||||
Assert.Equal(0, table.Get(0x922u)!.ContainerSlot);
|
||||
Assert.Equal(1, table.Get(0x920u)!.ContainerSlot);
|
||||
Assert.Equal(2, table.Get(0x921u)!.ContainerSlot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainerIndex_SlotChange_ResortsInPlace() // guards the Reindex same-container early-out
|
||||
{
|
||||
|
|
@ -333,4 +453,67 @@ public sealed class ClientObjectTableTests
|
|||
Assert.Equal(new[] { 0x541u, 0x540u }, table.GetContents(0xC4u));
|
||||
Assert.Equal(2, table.GetContents(0xC4u).Count); // no duplicate from the same-container move
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WieldItemOptimistic_equipsInstantly_setsContainerAndEquip()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint player = 0x50000001u, pack = 0x40000005u;
|
||||
table.AddOrUpdate(new ClientObject { ObjectId = 0x940u });
|
||||
table.MoveItem(0x940u, pack, newSlot: 2); // a pack item (WielderId stays 0)
|
||||
table.WieldItemOptimistic(0x940u, player, EquipMask.HeadWear);
|
||||
var o = table.Get(0x940u)!;
|
||||
Assert.Equal(EquipMask.HeadWear, o.CurrentlyEquippedLocation); // equipped now (instant)
|
||||
Assert.Equal(player, o.ContainerId); // contained-by-wielder (matches the WieldObject 0x0023 confirm)
|
||||
Assert.Equal(0u, o.WielderId); // optimistic wield does NOT write WielderId (ContainerId-based model)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WieldItemOptimistic_rollback_unequips_andReturnsToPack()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint player = 0x50000001u, pack = 0x40000005u;
|
||||
table.AddOrUpdate(new ClientObject { ObjectId = 0x941u });
|
||||
table.MoveItem(0x941u, pack, newSlot: 2);
|
||||
table.WieldItemOptimistic(0x941u, player, EquipMask.HeadWear);
|
||||
Assert.True(table.RollbackMove(0x941u)); // server rejected the wield
|
||||
var o = table.Get(0x941u)!;
|
||||
Assert.Equal(EquipMask.None, o.CurrentlyEquippedLocation); // un-equipped
|
||||
Assert.Equal(pack, o.ContainerId); // back in the pack
|
||||
Assert.Equal(2, o.ContainerSlot); // at its original slot
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollbackMove_restoresPreMoveEquipLocation() // unwield-reject must snap back to EQUIPPED
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint player = 0x50000001u;
|
||||
table.AddOrUpdate(new ClientObject { ObjectId = 0x942u });
|
||||
table.MoveItem(0x942u, player, newSlot: -1, newEquipLocation: EquipMask.Shield); // equipped
|
||||
table.MoveItemOptimistic(0x942u, player, 0); // optimistic UNWIELD into the pack (clears equip)
|
||||
Assert.Equal(EquipMask.None, table.Get(0x942u)!.CurrentlyEquippedLocation);
|
||||
Assert.True(table.RollbackMove(0x942u)); // server rejected the unwield
|
||||
Assert.Equal(EquipMask.Shield, table.Get(0x942u)!.CurrentlyEquippedLocation); // restored to equipped
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WieldItemOptimistic_unknownItem_false()
|
||||
=> Assert.False(new ClientObjectTable().WieldItemOptimistic(0xDEADu, 0x1u, EquipMask.HeadWear));
|
||||
|
||||
[Fact]
|
||||
public void WieldThenMove_oneConfirm_rollsBackToPreWield() // outstanding-count across wield+move (shared RecordPending)
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
const uint player = 0x50000001u, pack = 0x40000005u, pack2 = 0x40000006u;
|
||||
table.AddOrUpdate(new ClientObject { ObjectId = 0x950u });
|
||||
table.MoveItem(0x950u, pack, newSlot: 2); // pre-wield: pack slot 2
|
||||
table.WieldItemOptimistic(0x950u, player, EquipMask.HeadWear); // outstanding 1, snapshot (pack, 2, None)
|
||||
table.MoveItemOptimistic(0x950u, pack2, 0); // outstanding 2 (same snapshot)
|
||||
table.ConfirmMove(0x950u); // confirm the first → outstanding 1, still pending
|
||||
Assert.True(table.RollbackMove(0x950u)); // the second rejected → roll back
|
||||
var o = table.Get(0x950u)!;
|
||||
Assert.Equal(pack, o.ContainerId); // to the ORIGINAL pre-wield container
|
||||
Assert.Equal(2, o.ContainerSlot); // and slot
|
||||
Assert.Equal(EquipMask.None, o.CurrentlyEquippedLocation); // pre-wield was un-equipped
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class ClientObjectTableUpdateTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpsertProperties_unknownObject_createsThenMerges()
|
||||
{
|
||||
var t = new ClientObjectTable();
|
||||
bool added = false;
|
||||
t.ObjectAdded += _ => added = true;
|
||||
|
||||
var bundle = new PropertyBundle();
|
||||
bundle.Ints[5] = 1234; // EncumbranceVal
|
||||
|
||||
t.UpsertProperties(0x50000001u, bundle);
|
||||
|
||||
var o = t.Get(0x50000001u);
|
||||
Assert.NotNull(o);
|
||||
Assert.Equal(1234, o!.Properties.Ints[5]);
|
||||
Assert.True(added);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertProperties_existingObject_mergesAndFiresUpdated()
|
||||
{
|
||||
var t = new ClientObjectTable();
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0x50000001u });
|
||||
bool updated = false;
|
||||
t.ObjectUpdated += _ => updated = true;
|
||||
|
||||
var bundle = new PropertyBundle();
|
||||
bundle.Ints[5] = 99;
|
||||
t.UpsertProperties(0x50000001u, bundle);
|
||||
|
||||
Assert.Equal(99, t.Get(0x50000001u)!.Properties.Ints[5]);
|
||||
Assert.True(updated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStackSize_knownObject_setsFieldsAndFiresUpdated()
|
||||
{
|
||||
var t = new ClientObjectTable();
|
||||
t.AddOrUpdate(new ClientObject { ObjectId = 0x600u, StackSize = 1, Value = 5 });
|
||||
bool updated = false;
|
||||
t.ObjectUpdated += _ => updated = true;
|
||||
|
||||
bool ok = t.UpdateStackSize(0x600u, stackSize: 25, value: 125);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(25, t.Get(0x600u)!.StackSize);
|
||||
Assert.Equal(125, t.Get(0x600u)!.Value);
|
||||
Assert.True(updated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStackSize_unknownObject_returnsFalse()
|
||||
=> Assert.False(new ClientObjectTable().UpdateStackSize(0xDEADu, 1, 1));
|
||||
}
|
||||
54
tests/AcDream.Core.Tests/Items/EquipMaskTests.cs
Normal file
54
tests/AcDream.Core.Tests/Items/EquipMaskTests.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Pins every EquipMask member to the verbatim retail INVENTORY_LOC value
|
||||
/// (docs/research/named-retail/acclient.h:3193). The wire delivers these exact bits
|
||||
/// (ACE EquipMask == retail INVENTORY_LOC); any drift desyncs the paperdoll element-id→mask
|
||||
/// map and the GetAndWieldItem wire. This test is the anti-regression lock.
|
||||
/// </summary>
|
||||
public sealed class EquipMaskTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0x00000000u, EquipMask.None)]
|
||||
[InlineData(0x00000001u, EquipMask.HeadWear)]
|
||||
[InlineData(0x00000002u, EquipMask.ChestWear)]
|
||||
[InlineData(0x00000004u, EquipMask.AbdomenWear)]
|
||||
[InlineData(0x00000008u, EquipMask.UpperArmWear)]
|
||||
[InlineData(0x00000010u, EquipMask.LowerArmWear)]
|
||||
[InlineData(0x00000020u, EquipMask.HandWear)]
|
||||
[InlineData(0x00000040u, EquipMask.UpperLegWear)]
|
||||
[InlineData(0x00000080u, EquipMask.LowerLegWear)]
|
||||
[InlineData(0x00000100u, EquipMask.FootWear)]
|
||||
[InlineData(0x00000200u, EquipMask.ChestArmor)]
|
||||
[InlineData(0x00000400u, EquipMask.AbdomenArmor)]
|
||||
[InlineData(0x00000800u, EquipMask.UpperArmArmor)]
|
||||
[InlineData(0x00001000u, EquipMask.LowerArmArmor)]
|
||||
[InlineData(0x00002000u, EquipMask.UpperLegArmor)]
|
||||
[InlineData(0x00004000u, EquipMask.LowerLegArmor)]
|
||||
[InlineData(0x00008000u, EquipMask.NeckWear)]
|
||||
[InlineData(0x00010000u, EquipMask.WristWearLeft)]
|
||||
[InlineData(0x00020000u, EquipMask.WristWearRight)]
|
||||
[InlineData(0x00040000u, EquipMask.FingerWearLeft)]
|
||||
[InlineData(0x00080000u, EquipMask.FingerWearRight)]
|
||||
[InlineData(0x00100000u, EquipMask.MeleeWeapon)]
|
||||
[InlineData(0x00200000u, EquipMask.Shield)]
|
||||
[InlineData(0x00400000u, EquipMask.MissileWeapon)]
|
||||
[InlineData(0x00800000u, EquipMask.MissileAmmo)]
|
||||
[InlineData(0x01000000u, EquipMask.Held)]
|
||||
[InlineData(0x02000000u, EquipMask.TwoHanded)]
|
||||
[InlineData(0x04000000u, EquipMask.TrinketOne)]
|
||||
[InlineData(0x08000000u, EquipMask.Cloak)]
|
||||
[InlineData(0x10000000u, EquipMask.SigilOne)]
|
||||
[InlineData(0x20000000u, EquipMask.SigilTwo)]
|
||||
[InlineData(0x40000000u, EquipMask.SigilThree)]
|
||||
public void Member_has_canonical_retail_value(uint expected, EquipMask member)
|
||||
=> Assert.Equal(expected, (uint)member);
|
||||
|
||||
[Fact]
|
||||
public void Weapon_ready_slot_composite_is_0x3500000() // acclient.h:3235 WEAPON_READY_SLOT_LOC
|
||||
=> Assert.Equal(0x3500000u,
|
||||
(uint)(EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded));
|
||||
}
|
||||
39
tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs
Normal file
39
tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public class ShortcutStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Load_mapsSlotToObjId_skipsEmptyAndOutOfRange()
|
||||
{
|
||||
var store = new ShortcutStore();
|
||||
store.Load(new (int, uint)[]
|
||||
{
|
||||
(0, 0x5001u),
|
||||
(3, 0x5002u),
|
||||
(5, 0u), // empty/spell → skipped
|
||||
(99, 0x5003u), // out of range → skipped
|
||||
});
|
||||
Assert.Equal(0x5001u, store.Get(0));
|
||||
Assert.Equal(0x5002u, store.Get(3));
|
||||
Assert.Equal(0u, store.Get(5));
|
||||
Assert.True(store.IsEmpty(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetRemoveGet_roundtrip_andBoundsSafe()
|
||||
{
|
||||
var store = new ShortcutStore();
|
||||
store.Set(4, 0xABCDu);
|
||||
Assert.Equal(0xABCDu, store.Get(4));
|
||||
Assert.False(store.IsEmpty(4));
|
||||
store.Remove(4);
|
||||
Assert.True(store.IsEmpty(4));
|
||||
Assert.Equal(0u, store.Get(-1));
|
||||
Assert.Equal(0u, store.Get(18));
|
||||
store.Set(18, 1u); // no-op, no throw
|
||||
store.Remove(-1); // no-op, no throw
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue