feat(net): port retail physics spawn and event timestamps
Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client. Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
d53fe30ffe
commit
8a5d77f7f4
50 changed files with 3809 additions and 649 deletions
71
tests/AcDream.App.Tests/UI/AutoWieldGenerationTests.cs
Normal file
71
tests/AcDream.App.Tests/UI/AutoWieldGenerationTests.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class AutoWieldGenerationTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void GenerationReplacementCancelsPendingWeaponSwitch()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
var blocker = new ClientObject
|
||||
{
|
||||
ObjectId = 0x60000001u,
|
||||
WielderId = Player,
|
||||
ValidLocations = EquipMask.MeleeWeapon,
|
||||
};
|
||||
objects.AddOrUpdate(blocker);
|
||||
objects.MoveItem(
|
||||
blocker.ObjectId,
|
||||
Player,
|
||||
newSlot: -1,
|
||||
newEquipLocation: EquipMask.MeleeWeapon);
|
||||
var requested = new ClientObject
|
||||
{
|
||||
ObjectId = 0x60000002u,
|
||||
ContainerId = Player,
|
||||
ValidLocations = EquipMask.MeleeWeapon,
|
||||
};
|
||||
objects.AddOrUpdate(requested);
|
||||
|
||||
using var controller = new AutoWieldController(
|
||||
objects,
|
||||
() => Player,
|
||||
sendWield: null,
|
||||
sendPutItemInContainer: (_, _, _) => { },
|
||||
toast: null);
|
||||
Assert.True(controller.TryWield(requested));
|
||||
Assert.True(controller.IsBusy);
|
||||
|
||||
objects.ReplaceGeneration(WorldReplacement(blocker.ObjectId), generation: 2);
|
||||
|
||||
Assert.False(controller.IsBusy);
|
||||
}
|
||||
|
||||
private static WeenieData WorldReplacement(uint guid) => new(
|
||||
Guid: guid,
|
||||
Name: "replacement",
|
||||
Type: ItemType.Misc,
|
||||
WeenieClassId: 1,
|
||||
IconId: 0,
|
||||
IconOverlayId: 0,
|
||||
IconUnderlayId: 0,
|
||||
Effects: 0,
|
||||
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);
|
||||
}
|
||||
|
|
@ -125,6 +125,40 @@ public class InventoryControllerTests
|
|||
Assert.Equal(0u, containers.GetItem(1)!.ItemId); // padded empty frame
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionTableClearRebuildsInventoryWithoutOldObjects()
|
||||
{
|
||||
var (layout, grid, containers, _, _, _, _, _) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedContained(objects, 0xAu, Player, slot: 0);
|
||||
SeedBag(objects, 0xCu, slot: 1);
|
||||
Bind(layout, objects);
|
||||
Assert.Equal(0xAu, grid.GetItem(0)!.ItemId);
|
||||
Assert.Equal(0xCu, containers.GetItem(0)!.ItemId);
|
||||
|
||||
objects.Clear();
|
||||
|
||||
Assert.Equal(0u, grid.GetItem(0)!.ItemId);
|
||||
Assert.Equal(0u, containers.GetItem(0)!.ItemId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerationReplacementRemovesOldOwnedProjectionAndSelection()
|
||||
{
|
||||
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedContained(objects, 0xAu, Player, slot: 0);
|
||||
var selection = new SelectionState();
|
||||
selection.Select(0xAu, SelectionChangeSource.Inventory);
|
||||
Bind(layout, objects, selection: selection);
|
||||
Assert.Equal(0xAu, grid.GetItem(0)!.ItemId);
|
||||
|
||||
objects.ReplaceGeneration(WorldReplacement(0xAu), generation: 2);
|
||||
|
||||
Assert.Equal(0u, grid.GetItem(0)!.ItemId);
|
||||
Assert.Null(selection.SelectedObjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Populate_uses_manifest_container_hint_before_create_object_details()
|
||||
{
|
||||
|
|
@ -557,6 +591,30 @@ public class InventoryControllerTests
|
|||
|
||||
private static ItemDragPayload Payload(uint obj) => new(obj, ItemDragSource.Inventory, 0, new UiItemSlot());
|
||||
|
||||
private static WeenieData WorldReplacement(uint guid) => new(
|
||||
Guid: guid,
|
||||
Name: "replacement",
|
||||
Type: ItemType.Misc,
|
||||
WeenieClassId: 1,
|
||||
IconId: 0,
|
||||
IconOverlayId: 0,
|
||||
IconUnderlayId: 0,
|
||||
Effects: 0,
|
||||
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);
|
||||
|
||||
[Fact]
|
||||
public void Drop_onOccupiedGridCell_insertsBefore_andMovesLocally()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,6 +64,30 @@ public class PaperdollControllerTests
|
|||
t.MoveItem(guid, Pack, newSlot: 0);
|
||||
}
|
||||
|
||||
private static WeenieData WorldReplacement(uint guid) => new(
|
||||
Guid: guid,
|
||||
Name: "replacement",
|
||||
Type: ItemType.Misc,
|
||||
WeenieClassId: 1,
|
||||
IconId: 0,
|
||||
IconOverlayId: 0,
|
||||
IconUnderlayId: 0,
|
||||
Effects: 0,
|
||||
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);
|
||||
|
||||
[Fact]
|
||||
public void Populate_shows_equipped_item_in_its_slot()
|
||||
{
|
||||
|
|
@ -75,6 +99,41 @@ public class PaperdollControllerTests
|
|||
Assert.Equal(0u, lists[ShieldSlot].Cell.ItemId); // shield slot empty
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionTableClearRemovesOldEquipmentAndLocksAetheriaSlots()
|
||||
{
|
||||
var (layout, lists) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedEquipped(objects, 0xA01u, EquipMask.HeadWear);
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Player });
|
||||
objects.UpdateIntProperty(
|
||||
Player,
|
||||
AetheriaUnlocks.PropertyId,
|
||||
(int)AetheriaUnlockState.Blue);
|
||||
Bind(layout, objects);
|
||||
Assert.Equal(0xA01u, lists[HeadSlot].Cell.ItemId);
|
||||
Assert.True(lists[BlueAetheriaSlot].Visible);
|
||||
|
||||
objects.Clear();
|
||||
|
||||
Assert.Equal(0u, lists[HeadSlot].Cell.ItemId);
|
||||
Assert.False(lists[BlueAetheriaSlot].Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerationReplacementRemovesOldEquippedProjection()
|
||||
{
|
||||
var (layout, lists) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedEquipped(objects, 0xA01u, EquipMask.HeadWear);
|
||||
Bind(layout, objects);
|
||||
Assert.Equal(0xA01u, lists[HeadSlot].Cell.ItemId);
|
||||
|
||||
objects.ReplaceGeneration(WorldReplacement(0xA01u), generation: 2);
|
||||
|
||||
Assert.Equal(0u, lists[HeadSlot].Cell.ItemId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClickEquippedItem_updatesSharedSelection()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,6 +115,34 @@ public class ToolbarControllerTests
|
|||
Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // others empty
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionTableClearRemovesResolvedShortcutPresentation()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x5001u,
|
||||
WeenieClassId = 1u,
|
||||
IconId = 0x06001234u,
|
||||
});
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{
|
||||
new(Index: 0, ObjectId: 0x5001u, SpellId: 0),
|
||||
};
|
||||
ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => shortcuts,
|
||||
iconIds: (_, _, _, _, _) => 0x77u,
|
||||
useItem: _ => { });
|
||||
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
||||
|
||||
repo.Clear();
|
||||
|
||||
Assert.Equal(0u, slots[Row1[0]].Cell.ItemId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredRebind_whenItemArrivesLate()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue