fix(physics): TS-23 - plumb real PK/PKLite/Impenetrable mover flags
Campaign P Slice P3 item 3. The wire parse (CreateObject's PublicWeenieDesc._bitfield), the decode (EntityCollisionFlagsExt. FromPwdBitfield), the per-GUID storage (ClientObjectTable. PublicWeenieBitfield), and the exemption logic (CollisionExemption. ShouldSkip) all already existed and were already correct -- every mover-flags call site just fed a GUID-prefix IsPlayer heuristic instead of the real per-entity PK/PKLite/Impenetrable state (retail OBJECTINFO::init 0x0050cf30 state |= 0x80/0x800/0x1000). Port: - EntityCollisionFlagsExt.ToMoverState translates the decoded PWD bit-space into the ObjectInfoState bit-space FindObjCollisions actually reads -- two different numberings that must not be confused. Deliberately does not translate IsPlayer (every call site already derives that correctly from its own GUID heuristic per #184 Slice 2b). - EntityCollisionFlagsExt.ResolveMoverPvpState is the one shared ClientObjectTable-backed lookup (guid -> ObjectInfoState), replacing what would otherwise have been three separate inline copies across GameWindow/LivePresentationComposition/RemoteTeleportController. - Threaded as a new optional moverPvpState parameter through RuntimeRemotePhysicsUpdater.Tick/TickHidden and RuntimeOrdinaryPhysicsUpdater.TryBegin (default None preserves every pre-P3 caller unchanged), and as PlayerMovementController.OwnPvpFlags for the local player's own two resolve call sites. - TS-23 section 12b: PlayerWeenie.JumpStaminaCost's pk parameter now reads the real PlayerKillerStatus(0x86)/LastPkAttackTimestamp(0x91) pair against retail's 20-second recency window (pkStatus in {4, 0x40} && (timestamp + 20.0) >= now), replacing the P1 hardcoded false. RuntimeMovementSkillState/Snapshot and LiveSessionEventRouter.RecomputePvpStatus push both the PWD bitfield and the PlayerKillerStatus pair reactively, riding the SAME ClientObject event triggers RecomputeBurden already uses. - A conformance test caught a genuine precision bug in the first PK-timer clock choice: DateTimeOffset.UtcNow's Unix-epoch seconds (~1.7 billion) loses ~128 seconds of precision in a 32-bit float, silently swallowing the entire 20-second window. Switched to Environment.TickCount64 (small, monotonic magnitude) -- also the more retail-plausible basis, since LastPkAttackTimestamp is itself a wire PropertyFloat and retail's Timer::cur_time is almost certainly a process/session-relative counter for the same precision reason, not an absolute epoch. Non-PK invariant (the acceptance criterion): an entity with no ClientObjectTable row, or a row whose PublicWeenieBitfield is null or 0, resolves to ObjectInfoState.None -- a no-op OR into moverFlags, bit-identical to every pre-P3 caller's hardcoded value. A dedicated test drives two real ClientObjectTable rows through CollisionExemption.ShouldSkip and confirms PK-vs-PK collides while PK-vs-non-PK and non-PK-vs-non-PK both stay exempt (walk through). Register: TS-23 retired (both the collision-flags and PK-timer halves); the stale "M2 combat must land TS-23" phase-gate note removed. dotnet build + dotnet test (Core.Tests 4008/2 skip, Runtime.Tests 425/0, App.Tests 3968/3 skip, complete solution build) all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8b5425498c
commit
bb7b899bfe
17 changed files with 702 additions and 53 deletions
|
|
@ -57,7 +57,10 @@ internal sealed class RuntimeOrdinaryPhysicsUpdater
|
|||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
float stepDownHeight = 0.4f,
|
||||
// TS-23 (2026-07-30): see RuntimeRemotePhysicsUpdater.Tick's
|
||||
// identical parameter.
|
||||
ObjectInfoState moverPvpState = ObjectInfoState.None)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rootFrame);
|
||||
|
|
@ -142,9 +145,12 @@ internal sealed class RuntimeOrdinaryPhysicsUpdater
|
|||
stepDownHeight: stepDownHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
isOnGround: previousOnWalkable,
|
||||
body: body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
// TS-23: moverPvpState is a no-op OR (None) for every
|
||||
// non-PK ordinary mover.
|
||||
moverFlags: (IsPlayerGuid(record.ServerGuid)
|
||||
? ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide
|
||||
: ObjectInfoState.EdgeSlide,
|
||||
: ObjectInfoState.EdgeSlide)
|
||||
| moverPvpState,
|
||||
movingEntityId: movingEntityId,
|
||||
// TS-46: the Setup's own sphere list, scaled by ObjScale.
|
||||
// Empty falls back to the radius/height reconstruction above.
|
||||
|
|
|
|||
|
|
@ -83,7 +83,14 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
float stepDownHeight = 0.4f,
|
||||
// TS-23 (2026-07-30): the remote's own PK/PKLite/Impenetrable
|
||||
// ObjectInfoState bits (LiveEntityMotionRuntimeController's
|
||||
// ClientObjectTable-backed lookup, translated via
|
||||
// EntityCollisionFlagsExt.ToMoverState). Default None is a no-op OR
|
||||
// for every non-PK remote — bit-identical to the pre-P3 value.
|
||||
AcDream.Core.Physics.ObjectInfoState moverPvpState =
|
||||
AcDream.Core.Physics.ObjectInfoState.None)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rm);
|
||||
|
|
@ -382,15 +389,17 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
// non-PK player pair). Without IsPlayer the mover would de-overlap
|
||||
// two players — MORE solid than retail (you can stand inside another
|
||||
// non-PK player in AC). Players still COLLIDE with monsters (target
|
||||
// not IsPlayer → no exemption) + terrain + walls. PK/PKLite/
|
||||
// Impenetrable are NOT plumbed onto the remote mover yet, so a PK
|
||||
// pair walks through where retail collides — the SAME M1.5 gap the
|
||||
// local player carries (see TS-23; PlayerDescription PK status
|
||||
// unparsed).
|
||||
moverFlags: IsPlayerGuid(serverGuid)
|
||||
// not IsPlayer → no exemption) + terrain + walls. TS-23
|
||||
// (2026-07-30): moverPvpState carries the remote's real
|
||||
// PK/PKLite/Impenetrable bits (default None — a no-op OR
|
||||
// for every non-PK remote, bit-identical to the pre-P3
|
||||
// value); a PK-vs-PK pair now collides exactly like
|
||||
// retail instead of always walking through.
|
||||
moverFlags: (IsPlayerGuid(serverGuid)
|
||||
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
|
||||
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
|
||||
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
|
||||
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide)
|
||||
| moverPvpState,
|
||||
// Fix #42 (2026-05-05): skip the moving remote's
|
||||
// own ShadowEntry. _animatedEntities is keyed by
|
||||
// entity.Id so kv.Key matches the EntityId the
|
||||
|
|
@ -629,12 +638,14 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
System.Func<RuntimeRemotePhysicsSnapshot, bool>?
|
||||
acknowledgeProjection = null,
|
||||
System.Func<bool>? externalOwnerValid = null,
|
||||
// TS-46 (2026-07-30): see the visible Tick's identical parameters.
|
||||
// TS-46/TS-23 (2026-07-30): see the visible Tick's identical parameters.
|
||||
System.Collections.Immutable.ImmutableArray<AcDream.Core.Physics.FlatCollisionSphere>
|
||||
sphereList = default,
|
||||
float sphereScale = 1f,
|
||||
float stepUpHeight = 0.4f,
|
||||
float stepDownHeight = 0.4f)
|
||||
float stepDownHeight = 0.4f,
|
||||
AcDream.Core.Physics.ObjectInfoState moverPvpState =
|
||||
AcDream.Core.Physics.ObjectInfoState.None)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(rm);
|
||||
|
|
@ -709,10 +720,13 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
stepDownHeight: stepDownHeight, // TS-46: Setup-derived, was a 0.4f literal
|
||||
isOnGround: previousOnWalkable,
|
||||
body: rm.Body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
// TS-23: moverPvpState is a no-op OR (None) for every
|
||||
// non-PK remote.
|
||||
moverFlags: (IsPlayerGuid(record.ServerGuid)
|
||||
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
|
||||
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
|
||||
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
|
||||
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide)
|
||||
| moverPvpState,
|
||||
movingEntityId: localEntityId,
|
||||
// TS-46: the Setup's own sphere list, scaled by ObjScale.
|
||||
// Empty falls back to the radius/height reconstruction above.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue