feat(ui): port retail item interaction policy
This commit is contained in:
parent
0cf780478a
commit
a8da4fd05a
20 changed files with 1110 additions and 104 deletions
|
|
@ -387,6 +387,7 @@ public sealed class ItemInteractionControllerTests
|
|||
h.AddContained(0x50000A09u, item => item.Useability = ItemUseability.Contained);
|
||||
|
||||
h.Controller.ActivateItem(0x50000A09u);
|
||||
h.Controller.CompleteUse(0);
|
||||
h.Now += 199;
|
||||
h.Controller.ActivateItem(0x50000A09u);
|
||||
h.Now += 1;
|
||||
|
|
@ -394,4 +395,22 @@ public sealed class ItemInteractionControllerTests
|
|||
|
||||
Assert.Equal(new[] { 0x50000A09u, 0x50000A09u }, h.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseDone_releasesRetailBusyGate()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.AddContained(0x50000A0Au, item => item.Useability = ItemUseability.Contained);
|
||||
|
||||
Assert.True(h.Controller.ActivateItem(0x50000A0Au));
|
||||
h.Now += 200;
|
||||
Assert.False(h.Controller.ActivateItem(0x50000A0Au));
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
h.Controller.CompleteUse(0);
|
||||
h.Now += 200; // rejected busy attempt still advanced retail's throttle timestamp
|
||||
|
||||
Assert.True(h.Controller.ActivateItem(0x50000A0Au));
|
||||
Assert.Equal(2, h.Uses.Count);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,6 +449,27 @@ public sealed class GameEventWiringTests
|
|||
Assert.Equal(1u, items.Get(0x50000A02u)!.ContainerTypeHint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseDone_releasesAppBusyOwnerThroughCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
uint? completedWith = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher,
|
||||
new ClientObjectTable(),
|
||||
new CombatState(),
|
||||
new Spellbook(),
|
||||
new ChatLog(),
|
||||
onUseDone: error => completedWith = error);
|
||||
byte[] payload = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1Du);
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.UseDone, payload));
|
||||
dispatcher.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal(0x1Du, completedWith);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerDescription_ReplacesPlayerPackContents_InRetailManifestOrder()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -172,6 +172,22 @@ public sealed class CreateObjectTests
|
|||
Assert.Equal((uint)ItemType.Creature, parsed!.Value.TargetType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_CombatUseFlag_CapturesByte()
|
||||
{
|
||||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||
guid: 0x5000000Du,
|
||||
name: "Sword",
|
||||
itemType: (uint)ItemType.MeleeWeapon,
|
||||
weenieFlags: 0x00000200u,
|
||||
combatUse: 2);
|
||||
|
||||
var parsed = CreateObject.TryParse(body);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal((byte)2, parsed.Value.CombatUse);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Retail radar: PublicWeenieDesc carries two independently gated bytes.
|
||||
// Absence is distinct from an explicitly transmitted zero because zero is
|
||||
|
|
@ -566,6 +582,7 @@ public sealed class CreateObjectTests
|
|||
float? workmanship = null,
|
||||
byte? radarBlipColor = null,
|
||||
byte? radarBehavior = null,
|
||||
byte? combatUse = null,
|
||||
ushort movementSeq = 0)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
|
|
@ -618,7 +635,7 @@ public sealed class CreateObjectTests
|
|||
}
|
||||
if ((weenieFlags & 0x00080000u) != 0) WriteU32(bytes, targetType ?? 0u); // TargetType u32
|
||||
if ((weenieFlags & 0x00000080u) != 0) WriteU32(bytes, uiEffects); // UiEffects u32
|
||||
if ((weenieFlags & 0x00000200u) != 0) bytes.Add(0); // CombatUse sbyte/1 byte
|
||||
if ((weenieFlags & 0x00000200u) != 0) bytes.Add(combatUse ?? 0); // CombatUse sbyte/1 byte
|
||||
if ((weenieFlags & 0x00000400u) != 0) WriteU16(bytes, structure ?? 0); // Structure u16
|
||||
if ((weenieFlags & 0x00000800u) != 0) WriteU16(bytes, maxStructure ?? 0); // MaxStructure u16
|
||||
if ((weenieFlags & 0x00001000u) != 0) WriteU16(bytes, stackSize ?? 0); // StackSize u16
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ public sealed class ObjectTableWiringTests
|
|||
TargetType = (uint)ItemType.Creature,
|
||||
RadarBlipColor = 4,
|
||||
RadarBehavior = 3,
|
||||
ObjectDescriptionFlags = 0x20800200u,
|
||||
CombatUse = 2,
|
||||
};
|
||||
|
||||
var d = ObjectTableWiring.ToWeenieData(spawn);
|
||||
|
|
@ -104,6 +106,8 @@ public sealed class ObjectTableWiringTests
|
|||
Assert.Equal((uint)ItemType.Creature, d.TargetType);
|
||||
Assert.Equal((byte)4, d.RadarBlipColor);
|
||||
Assert.Equal((byte)3, d.RadarBehavior);
|
||||
Assert.Equal(0x20800200u, d.PublicWeenieBitfield);
|
||||
Assert.Equal((byte)2, d.CombatUse);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
289
tests/AcDream.Core.Tests/Items/ItemInteractionPolicyTests.cs
Normal file
289
tests/AcDream.Core.Tests/Items/ItemInteractionPolicyTests.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class ItemInteractionPolicyTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
private const uint GroundContainer = 0x70000001u;
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_looseItemAndComponentPack_matchRetailPickupBranch()
|
||||
{
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
Determine(Obj(0x6001)));
|
||||
|
||||
var requiresSlot = Obj(0x6002) with
|
||||
{
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
ItemsCapacity = 12,
|
||||
};
|
||||
Assert.Equal(ItemPrimaryUseResult.None, Determine(requiresSlot));
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
Determine(requiresSlot with { IsComponentPack = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_onlyAllowsContainedPickupFromViewedGroundContainer()
|
||||
{
|
||||
var item = Obj(0x6001) with { ContainerId = GroundContainer };
|
||||
|
||||
Assert.Equal(ItemPrimaryUseResult.None,
|
||||
ItemInteractionPolicy.DetermineUseResult(item, Player, 0));
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
ItemInteractionPolicy.DetermineUseResult(item, Player, GroundContainer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_portsAllOwnedPrimaryClassifications()
|
||||
{
|
||||
var owned = Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
};
|
||||
|
||||
Assert.Equal(ItemPrimaryUseResult.WieldRight,
|
||||
Determine(owned with { CombatUse = 1 }));
|
||||
Assert.Equal(ItemPrimaryUseResult.WieldLeft,
|
||||
Determine(owned with { CombatUse = 1, Flags = owned.Flags | PublicWeenieFlags.WieldLeft }));
|
||||
Assert.Equal(ItemPrimaryUseResult.AutoSort,
|
||||
Determine(owned with { ValidLocations = EquipMask.HeadWear }));
|
||||
Assert.Equal(ItemPrimaryUseResult.OpenSalvage,
|
||||
Determine(owned with { Type = ItemType.TinkeringTool }));
|
||||
Assert.Equal(ItemPrimaryUseResult.ItemUse,
|
||||
Determine(owned with { Useability = ItemUseability.Contained }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_nonOwnedGameAndPlayerPaths_areDistinct()
|
||||
{
|
||||
Assert.Equal(ItemPrimaryUseResult.BeginGame,
|
||||
Determine(Obj(0x6001) with
|
||||
{
|
||||
Type = ItemType.Gameboard,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
}));
|
||||
Assert.Equal(ItemPrimaryUseResult.OpenSecureTrade,
|
||||
Determine(Obj(0x6002) with
|
||||
{
|
||||
Flags = PublicWeenieFlags.Player | PublicWeenieFlags.RequiresPackSlot,
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_excludesResultEightFromEarlyClassifiedBound_thenStillWieldsLeft()
|
||||
{
|
||||
var source = Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
CombatUse = 1,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot | PublicWeenieFlags.WieldLeft,
|
||||
};
|
||||
|
||||
var decision = ItemInteractionPolicy.DecideUse(Use(source));
|
||||
|
||||
Assert.Equal(new[] { ItemPolicyActionKind.WieldLeft },
|
||||
decision.Actions.Select(a => a.Kind));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_readyVendorTradeAndWieldedSourceGates_areOrdered()
|
||||
{
|
||||
var direct = OwnedDirect();
|
||||
|
||||
Assert.Empty(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct) with { ReadyForInventoryRequest = false }).Actions);
|
||||
Assert.Empty(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct) with { ActiveVendorId = 0x7001, Source = direct with { ContainerId = 0x7001 } }).Actions);
|
||||
Assert.Equal(ItemPolicyActionKind.Reject,
|
||||
Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { TradeState = 1 })).Actions).Kind);
|
||||
Assert.Contains("wield", Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { Useability = ItemUseability.Wielded })).Actions).Message,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_targetedUse_appliesKindAndLocationGate_thenSendsAndMarksBusy()
|
||||
{
|
||||
var source = OwnedDirect() with
|
||||
{
|
||||
Useability = 0x00080008u,
|
||||
TargetType = (uint)ItemType.Misc,
|
||||
};
|
||||
var target = Obj(0x6002) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Type = ItemType.Misc,
|
||||
};
|
||||
|
||||
var decision = ItemInteractionPolicy.DecideUse(Use(source) with
|
||||
{
|
||||
UseCurrentSelection = true,
|
||||
SelectedTarget = target,
|
||||
});
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
ItemPolicyActionKind.SendUseWithTarget,
|
||||
ItemPolicyActionKind.IncrementBusy,
|
||||
}, decision.Actions.Select(a => a.Kind));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PublicWeenieFlags.PlayerKillerSwitch, ItemPolicyActionKind.ConfirmPlayerKillerSwitch)]
|
||||
[InlineData(PublicWeenieFlags.NonPlayerKillerSwitch, ItemPolicyActionKind.ConfirmNonPlayerKillerSwitch)]
|
||||
[InlineData(PublicWeenieFlags.VolatileRare, ItemPolicyActionKind.ConfirmVolatileRare)]
|
||||
public void UseObject_confirmationFlags_blockWireUse(
|
||||
PublicWeenieFlags flag,
|
||||
ItemPolicyActionKind expected)
|
||||
{
|
||||
var decision = ItemInteractionPolicy.DecideUse(
|
||||
Use(OwnedDirect() with { Flags = flag | PublicWeenieFlags.RequiresPackSlot }));
|
||||
|
||||
Assert.Equal(expected, Assert.Single(decision.Actions).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_vendorAndSecureTrade_actButPreserveRetailFalseReturn()
|
||||
{
|
||||
var item = OwnedDirect() with { StackSize = 10, MaxSplitSize = 10 };
|
||||
var vendor = Obj(0x7001) with { Flags = PublicWeenieFlags.Vendor };
|
||||
var sale = ItemInteractionPolicy.DecidePlacement(Place(item, vendor));
|
||||
Assert.False(sale.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.SellToVendor, Assert.Single(sale.Actions).Kind);
|
||||
|
||||
var player = Obj(0x7002) with { Flags = PublicWeenieFlags.Player };
|
||||
var trade = ItemInteractionPolicy.DecidePlacement(Place(item, player));
|
||||
Assert.False(trade.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.StartSecureTrade, Assert.Single(trade.Actions).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_groundStackAndAirborneMatrix_matchesRetail()
|
||||
{
|
||||
var item = OwnedDirect() with { StackSize = 10, MaxSplitSize = 10 };
|
||||
|
||||
var airborne = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item) with
|
||||
{
|
||||
PlayerOnGround = false,
|
||||
});
|
||||
Assert.False(airborne.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.Reject, Assert.Single(airborne.Actions).Kind);
|
||||
|
||||
var split = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item) with { SplitSize = 4 });
|
||||
Assert.True(split.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.SplitToWorld, Assert.Single(split.Actions).Kind);
|
||||
|
||||
var full = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item));
|
||||
Assert.True(full.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.DropToWorld, Assert.Single(full.Actions).Kind);
|
||||
|
||||
var alreadyWorld = ItemInteractionPolicy.DecidePlacement(
|
||||
PlaceOnGround(item with { IsIn3DView = true }));
|
||||
Assert.False(alreadyWorld.ReturnValue);
|
||||
Assert.Contains("cancelled", Assert.Single(alreadyWorld.Actions).Message,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_containerRequiresOpenableAndCurrentGroundObject()
|
||||
{
|
||||
var item = OwnedDirect();
|
||||
var container = Obj(GroundContainer) with { IsContainer = true };
|
||||
|
||||
var locked = ItemInteractionPolicy.DecidePlacement(Place(item, container) with
|
||||
{
|
||||
GroundObjectId = GroundContainer,
|
||||
});
|
||||
Assert.False(locked.ReturnValue);
|
||||
|
||||
var open = container with { Flags = PublicWeenieFlags.Openable };
|
||||
var closedView = ItemInteractionPolicy.DecidePlacement(Place(item, open));
|
||||
Assert.False(closedView.ReturnValue);
|
||||
|
||||
var placed = ItemInteractionPolicy.DecidePlacement(Place(item, open) with
|
||||
{
|
||||
GroundObjectId = GroundContainer,
|
||||
});
|
||||
Assert.True(placed.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.PlaceInContainer, Assert.Single(placed.Actions).Kind);
|
||||
}
|
||||
|
||||
private static ItemPrimaryUseResult Determine(ItemPolicyObject item)
|
||||
=> ItemInteractionPolicy.DetermineUseResult(item, Player, 0);
|
||||
|
||||
private static ItemPolicyObject OwnedDirect()
|
||||
=> Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
Useability = ItemUseability.Contained,
|
||||
};
|
||||
|
||||
private static ItemPolicyObject Obj(uint id) => new(
|
||||
id,
|
||||
ItemType.Misc,
|
||||
PublicWeenieFlags.None,
|
||||
ContainerId: 0,
|
||||
WielderId: 0,
|
||||
ValidLocations: EquipMask.None,
|
||||
CurrentLocation: EquipMask.None,
|
||||
CombatUse: 0,
|
||||
ItemsCapacity: 0,
|
||||
ContainersCapacity: 0,
|
||||
Useability: 0,
|
||||
TargetType: 0,
|
||||
OwnedByPlayer: false,
|
||||
IsContainer: false,
|
||||
IsComponentPack: false,
|
||||
TradeState: 0,
|
||||
StackSize: 1,
|
||||
MaxSplitSize: 1,
|
||||
IsIn3DView: false);
|
||||
|
||||
private static ItemUsePolicyInput Use(ItemPolicyObject source) => new(
|
||||
source,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
ActiveVendorId: 0,
|
||||
BypassClassification: false,
|
||||
UseCurrentSelection: false,
|
||||
SelectedTarget: null,
|
||||
ConfirmVolatileRareUses: true,
|
||||
InNonCombatMode: false);
|
||||
|
||||
private static ItemPlacementPolicyInput Place(
|
||||
ItemPolicyObject item,
|
||||
ItemPolicyObject target) => new(
|
||||
item,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
TargetId: target.Id,
|
||||
Target: target,
|
||||
AllowGroundFallback: false,
|
||||
MergeAccepted: false,
|
||||
DragOnPlayerOpensSecureTrade: true,
|
||||
PlayerOnGround: true,
|
||||
SplitSize: item.StackSize);
|
||||
|
||||
private static ItemPlacementPolicyInput PlaceOnGround(ItemPolicyObject item) => new(
|
||||
item,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
TargetId: 0,
|
||||
Target: null,
|
||||
AllowGroundFallback: true,
|
||||
MergeAccepted: false,
|
||||
DragOnPlayerOpensSecureTrade: true,
|
||||
PlayerOnGround: true,
|
||||
SplitSize: item.StackSize);
|
||||
}
|
||||
|
|
@ -27,4 +27,16 @@ public sealed class ItemUseabilityTests
|
|||
{
|
||||
Assert.False(ItemUseability.IsDirectUseable(ItemUseability.No));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeastLimitedSourceUse_matchesRetailPriorityAndIgnoresObjSelf()
|
||||
{
|
||||
Assert.Equal(ItemUseability.Remote,
|
||||
ItemUseability.LeastLimitedSourceUse(
|
||||
ItemUseability.Remote | ItemUseability.Contained | ItemUseability.Wielded));
|
||||
Assert.Equal(ItemUseability.Wielded,
|
||||
ItemUseability.LeastLimitedSourceUse(ItemUseability.Wielded | ItemUseability.Self));
|
||||
Assert.Equal(ItemUseability.Undef,
|
||||
ItemUseability.LeastLimitedSourceUse(ItemUseability.ObjSelf));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue