fix(gameplay): reconcile wield ownership and target facing
Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks. Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state. Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration. Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
b26f84cc69
commit
7b7ffcd278
36 changed files with 3884 additions and 761 deletions
|
|
@ -104,13 +104,13 @@ public sealed class GameEventWiringTests
|
|||
[Fact]
|
||||
public void WireAll_WieldObject_RoutesToClientObjectTable()
|
||||
{
|
||||
var (d, items, _, _, _) = MakeAll();
|
||||
const uint player = 0x2000u;
|
||||
var (d, items, _, _, _) = MakeAll(() => player);
|
||||
items.AddOrUpdate(new ClientObject { ObjectId = 0x1000, WeenieClassId = 1 });
|
||||
|
||||
byte[] payload = new byte[12];
|
||||
byte[] payload = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1000);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)EquipMask.MeleeWeapon);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0x2000);
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.WieldObject, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
|
@ -118,7 +118,8 @@ public sealed class GameEventWiringTests
|
|||
var item = items.Get(0x1000);
|
||||
Assert.NotNull(item);
|
||||
Assert.Equal(EquipMask.MeleeWeapon, item!.CurrentlyEquippedLocation);
|
||||
Assert.Equal(0x2000u, item.ContainerId);
|
||||
Assert.Equal(0u, item.ContainerId);
|
||||
Assert.Equal(player, item.WielderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -130,7 +131,7 @@ public sealed class GameEventWiringTests
|
|||
items.MoveItem(0x1500, 0x9999u, newSlot: 0); // start in a pack
|
||||
items.WieldItemOptimistic(0x1500, player, EquipMask.MeleeWeapon); // optimistic wield → snapshot pending
|
||||
var wieldConfirmed = new List<uint>();
|
||||
items.WieldConfirmed += item => wieldConfirmed.Add(item.ObjectId);
|
||||
items.WieldConfirmed += itemId => wieldConfirmed.Add(itemId);
|
||||
|
||||
byte[] payload = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1500);
|
||||
|
|
@ -139,7 +140,8 @@ public sealed class GameEventWiringTests
|
|||
d.Dispatch(env!.Value); // server confirms the wield
|
||||
|
||||
Assert.False(items.RollbackMove(0x1500)); // pending snapshot was cleared by ConfirmMove
|
||||
Assert.Equal(player, items.Get(0x1500)!.ContainerId);
|
||||
Assert.Equal(0u, items.Get(0x1500)!.ContainerId);
|
||||
Assert.Equal(player, items.Get(0x1500)!.WielderId);
|
||||
Assert.Equal(new[] { 0x1500u }, wieldConfirmed);
|
||||
}
|
||||
|
||||
|
|
@ -567,6 +569,7 @@ public sealed class GameEventWiringTests
|
|||
const uint playerGuid = 0x50000001u;
|
||||
const uint packItemGuid = 0x50000A01u;
|
||||
const uint crossbowGuid = 0x50000B01u;
|
||||
const uint ammoGuid = 0x50000B02u;
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
var items = new ClientObjectTable();
|
||||
GameEventWiring.WireAll(
|
||||
|
|
@ -591,10 +594,13 @@ public sealed class GameEventWiringTests
|
|||
w.Write(0u); // spellbook_filters
|
||||
w.Write(1u); // inventory count
|
||||
w.Write(packItemGuid); w.Write(0u);
|
||||
w.Write(1u); // equipped count
|
||||
w.Write(2u); // equipped count
|
||||
w.Write(crossbowGuid);
|
||||
w.Write((uint)EquipMask.MissileWeapon);
|
||||
w.Write(7u); // layering priority
|
||||
w.Write(ammoGuid);
|
||||
w.Write((uint)EquipMask.MissileAmmo);
|
||||
w.Write(8u);
|
||||
|
||||
var envelope = GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.PlayerDescription, sb.ToArray()));
|
||||
|
|
@ -603,17 +609,19 @@ public sealed class GameEventWiringTests
|
|||
ClientObject crossbow = items.Get(crossbowGuid)!;
|
||||
crossbow.Type = ItemType.MissileWeapon;
|
||||
crossbow.CombatUse = 2;
|
||||
var orderedPlayerContents = items.GetContents(playerGuid)
|
||||
.Select(guid => items.Get(guid)!)
|
||||
.ToList();
|
||||
IReadOnlyList<ClientObject> orderedEquipment = items.GetEquippedBy(playerGuid);
|
||||
|
||||
Assert.Equal(new[] { packItemGuid, crossbowGuid }, items.GetContents(playerGuid));
|
||||
Assert.Equal(playerGuid, crossbow.ContainerId);
|
||||
Assert.Equal(new[] { packItemGuid }, items.GetContents(playerGuid));
|
||||
Assert.Equal(
|
||||
new[] { crossbowGuid, ammoGuid },
|
||||
orderedEquipment.Select(item => item.ObjectId));
|
||||
Assert.Equal(0u, crossbow.ContainerId);
|
||||
Assert.Equal(playerGuid, crossbow.WielderId);
|
||||
Assert.Equal(EquipMask.MissileWeapon, crossbow.CurrentlyEquippedLocation);
|
||||
Assert.Equal(7u, crossbow.Priority);
|
||||
Assert.Equal(
|
||||
CombatMode.Missile,
|
||||
CombatInputPlanner.GetDefaultCombatMode(orderedPlayerContents));
|
||||
CombatInputPlanner.GetDefaultCombatMode(orderedEquipment));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -879,8 +887,8 @@ public sealed class GameEventWiringTests
|
|||
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);
|
||||
Assert.Equal(0u, items.Get(0x50000A01u)!.ContainerId);
|
||||
Assert.Equal(0u, items.Get(0x50000A02u)!.ContainerId);
|
||||
Assert.Equal(new[] { 0x50000A01u, 0x50000A02u }, items.GetContents(0x500000C9u));
|
||||
Assert.Equal(0u, items.Get(0x50000A01u)!.ContainerTypeHint);
|
||||
Assert.Equal(1u, items.Get(0x50000A02u)!.ContainerTypeHint);
|
||||
|
|
@ -959,6 +967,37 @@ public sealed class GameEventWiringTests
|
|||
Assert.Equal(1u, items.Get(0x50000B02u)!.ContainerTypeHint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_InventoryPutObjInContainer_ClearsPriorWielder()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint pack = 0x500000C1u;
|
||||
const uint bow = 0x50000B03u;
|
||||
var (d, items, _, _, _) = MakeAll(() => player);
|
||||
items.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = bow,
|
||||
ContainerId = player,
|
||||
WielderId = player,
|
||||
CurrentlyEquippedLocation = EquipMask.MissileWeapon,
|
||||
});
|
||||
|
||||
byte[] payload = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), bow);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), pack);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 1u);
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.InventoryPutObjInContainer,
|
||||
payload));
|
||||
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal(pack, items.Get(bow)!.ContainerId);
|
||||
Assert.Equal(0u, items.Get(bow)!.WielderId);
|
||||
Assert.Equal(EquipMask.None, items.Get(bow)!.CurrentlyEquippedLocation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_InventoryPutObjectIn3D_UnparentsFromContainer()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,16 +41,14 @@ public sealed class AppraiseTests
|
|||
[Fact]
|
||||
public void ParseWieldObject_RoundTrip()
|
||||
{
|
||||
byte[] payload = new byte[12];
|
||||
byte[] payload = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1000u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x00400000u); // MeleeWeapon slot
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0x2000u); // wielder
|
||||
|
||||
var parsed = GameEvents.ParseWieldObject(payload);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x1000u, parsed!.Value.ItemGuid);
|
||||
Assert.Equal(0x00400000u, parsed.Value.EquipLoc);
|
||||
Assert.Equal(0x2000u, parsed.Value.WielderGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -207,7 +207,6 @@ public sealed class GameEventDispatcherTests
|
|||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x50000A01u, parsed!.Value.ItemGuid);
|
||||
Assert.Equal((uint)EquipMask.ChestArmor, parsed.Value.EquipLoc);
|
||||
Assert.Equal(0u, parsed.Value.WielderGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue