fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -94,6 +94,40 @@ public sealed class ExternalContainerStateTests
|
|||
Assert.Equal(2, delivered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenedCorpseHistoryMatchesRetailSetAndDeleteLifetime()
|
||||
{
|
||||
var state = new ExternalContainerState();
|
||||
const uint corpse = 0x70000010u;
|
||||
|
||||
Assert.True(state.RequestOpen(corpse, isCorpse: true));
|
||||
Assert.True(state.HasCorpseBeenOpened(corpse));
|
||||
Assert.Equal(1, state.OpenedCorpseCount);
|
||||
|
||||
state.ApplyViewContents(corpse);
|
||||
state.ApplyClose(corpse);
|
||||
Assert.True(state.HasCorpseBeenOpened(corpse));
|
||||
Assert.True(state.SetCorpseDeleted(corpse));
|
||||
Assert.False(state.HasCorpseBeenOpened(corpse));
|
||||
|
||||
state.RequestOpen(corpse, isCorpse: true);
|
||||
Assert.True(state.Reset());
|
||||
Assert.False(state.HasCorpseBeenOpened(corpse));
|
||||
Assert.Equal(0, state.OpenedCorpseCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedGroundObjectRequestStillRecordsCorpseIdentity()
|
||||
{
|
||||
var state = new ExternalContainerState();
|
||||
const uint corpse = 0x70000011u;
|
||||
|
||||
Assert.True(state.RequestOpen(corpse));
|
||||
Assert.False(state.RequestOpen(corpse, isCorpse: true));
|
||||
|
||||
Assert.True(state.HasCorpseBeenOpened(corpse));
|
||||
}
|
||||
|
||||
private static ExternalContainerState Open(uint id)
|
||||
{
|
||||
var state = new ExternalContainerState();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class InventoryContainerPlacementPolicyTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void FullItemCapacityRejectsNewItemButAllowsReorder()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = Player,
|
||||
Name = "Player",
|
||||
ItemsCapacity = 1,
|
||||
ContainersCapacity = 7,
|
||||
});
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 2u });
|
||||
objects.MoveItem(2u, Player, 0);
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 3u });
|
||||
|
||||
Assert.Equal(
|
||||
InventoryContainerPlacementRejection.ItemCapacityFull,
|
||||
InventoryContainerPlacementPolicy.Evaluate(objects, 3u, Player, Player));
|
||||
Assert.Equal(
|
||||
InventoryContainerPlacementRejection.None,
|
||||
InventoryContainerPlacementPolicy.Evaluate(objects, 2u, Player, Player));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainerCycleAndTradeAreRejected()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 10u, Type = ItemType.Container, ItemsCapacity = 24,
|
||||
});
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 11u, Type = ItemType.Container, ItemsCapacity = 24,
|
||||
});
|
||||
objects.MoveItem(11u, 10u, 0);
|
||||
|
||||
Assert.Equal(
|
||||
InventoryContainerPlacementRejection.RecursiveContainment,
|
||||
InventoryContainerPlacementPolicy.Evaluate(objects, 10u, 11u, Player));
|
||||
|
||||
objects.Get(10u)!.TradeState = 1;
|
||||
Assert.Equal(
|
||||
InventoryContainerPlacementRejection.SourceBeingTraded,
|
||||
InventoryContainerPlacementPolicy.Evaluate(objects, 10u, Player, Player));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullMessageMatchesRetailContainerTypeBranches()
|
||||
{
|
||||
var player = new ClientObject { ObjectId = Player, Name = "Backpack" };
|
||||
var bag = new ClientObject { ObjectId = 2u, Name = "Pack" };
|
||||
Assert.Equal(
|
||||
"Backpack is completely full!",
|
||||
InventoryContainerPlacementPolicy.ComposeClientLocal(
|
||||
InventoryContainerPlacementRejection.ItemCapacityFull,
|
||||
null,
|
||||
player,
|
||||
Player));
|
||||
Assert.Equal(
|
||||
"The Pack can fit no more containers!",
|
||||
InventoryContainerPlacementPolicy.ComposeClientLocal(
|
||||
InventoryContainerPlacementRejection.ContainerCapacityFull,
|
||||
null,
|
||||
bag,
|
||||
Player));
|
||||
}
|
||||
}
|
||||
|
|
@ -147,9 +147,53 @@ public sealed class ItemInteractionPolicyTests
|
|||
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);
|
||||
Assert.Equal("You cannot use the item because you are trading it",
|
||||
Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { TradeState = 1 })).Actions).Message);
|
||||
Assert.Equal("You must wield the item to use it",
|
||||
Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { Useability = ItemUseability.Wielded })).Actions).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_targetCompatibilityFailures_useRetailVerbatimMessages()
|
||||
{
|
||||
var source = OwnedDirect() with
|
||||
{
|
||||
Name = "mana stone",
|
||||
Useability = 0x00080008u,
|
||||
TargetType = (uint)ItemType.Misc,
|
||||
};
|
||||
var target = Obj(0x6002) with
|
||||
{
|
||||
Name = "armor",
|
||||
Type = ItemType.Armor,
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
};
|
||||
|
||||
var missing = ItemInteractionPolicy.DecideUse(Use(source) with
|
||||
{
|
||||
UseCurrentSelection = true,
|
||||
});
|
||||
Assert.Equal("Select your target before using the mana stone",
|
||||
Assert.Single(missing.Actions).Message);
|
||||
|
||||
var incompatible = ItemInteractionPolicy.DecideUse(Use(source) with
|
||||
{
|
||||
UseCurrentSelection = true,
|
||||
SelectedTarget = target,
|
||||
});
|
||||
Assert.Equal("Cannot use the mana stone with the armor",
|
||||
Assert.Single(incompatible.Actions).Message);
|
||||
|
||||
var traded = ItemInteractionPolicy.DecideUse(Use(source) with
|
||||
{
|
||||
UseCurrentSelection = true,
|
||||
SelectedTarget = target with { TradeState = 1 },
|
||||
});
|
||||
Assert.Equal("You can't use the mana stone on an item you are trading",
|
||||
Assert.Single(traded.Actions).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -219,7 +263,7 @@ public sealed class ItemInteractionPolicyTests
|
|||
PlayerOnGround = false,
|
||||
});
|
||||
Assert.False(airborne.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.Reject, Assert.Single(airborne.Actions).Kind);
|
||||
Assert.Equal("You cannot do that in mid air", Assert.Single(airborne.Actions).Message);
|
||||
|
||||
var split = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item) with { SplitSize = 4 });
|
||||
Assert.True(split.ReturnValue);
|
||||
|
|
@ -232,8 +276,7 @@ public sealed class ItemInteractionPolicyTests
|
|||
var alreadyWorld = ItemInteractionPolicy.DecidePlacement(
|
||||
PlaceOnGround(item with { IsIn3DView = true }));
|
||||
Assert.False(alreadyWorld.ReturnValue);
|
||||
Assert.Contains("cancelled", Assert.Single(alreadyWorld.Actions).Message,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal("Move cancelled", Assert.Single(alreadyWorld.Actions).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -204,6 +204,28 @@ public sealed class VendorStagingListTests
|
|||
Assert.False(list.TryGet(ItemB, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacePreservesTheSplitPlaceholderPositionAndQuantity()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
list.Add(ItemA, 2);
|
||||
list.Add(ItemB, 9);
|
||||
const uint splitGuid = 0x60000003u;
|
||||
int fired = 0;
|
||||
list.Changed += () => fired++;
|
||||
|
||||
Assert.True(list.Replace(ItemA, splitGuid));
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
new VendorStagingEntry(splitGuid, 2),
|
||||
new VendorStagingEntry(ItemB, 9),
|
||||
},
|
||||
list.Entries);
|
||||
Assert.Equal(1, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearRemovesEveryEntryAndFiresChangedOnce()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue