diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 8084231a..d3c25c46 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -413,13 +413,7 @@ public sealed class PlayerMovementController stepUpHeight: StepUpHeight, stepDownHeight: 0.04f, // retail default isOnGround: _body.OnWalkable, - body: _body, // persist ContactPlane across frames for slope tracking - // Commit C 2026-04-29 — local player is always IsPlayer. - // The PK/PKLite/Impenetrable bits come from PlayerDescription's - // PlayerKillerStatus property; not yet parsed (non-PK pair → walks - // through other non-PK players, which is retail's default for - // ACE's character creation defaults too). - moverFlags: AcDream.Core.Physics.ObjectInfoState.IsPlayer); + body: _body); // persist ContactPlane across frames for slope tracking // Apply resolved position. _body.Position = resolveResult.Position; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ba3c9781..2b9bfb0e 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2067,18 +2067,6 @@ public sealed class GameWindow : IDisposable // UpdateMotion / UpdatePosition events can reseat this entity by guid. _entitiesByServerGuid[spawn.Guid] = entity; - // Commit B 2026-04-29 — live-entity collision registration. The - // local player is the simulator (its PhysicsBody is the source of - // truth for our own movement); only remotes register as targets. - // Phantom-Setup entities (no CylSpheres / no Spheres / no Radius) - // are deliberately skipped — retail FUN's `FindObjCollisions` - // falls through to OK_TS for any object with no collision - // geometry (acclient_2013_pseudo_c.txt:276917,276987). - if (spawn.Guid != _playerServerGuid) - { - RegisterLiveEntityCollision(entity, setup, spawn, origin); - } - // Phase B.2: capture the server-sent MotionTableId for our own // character so UpdatePlayerAnimation can pass it to GetIdleCycle. // The Setup's DefaultMotionTable is often 0 for human characters; @@ -2296,100 +2284,6 @@ public sealed class GameWindow : IDisposable } } - /// - /// Commit B 2026-04-29 — register a live (server-spawned) entity into - /// the as a single collision body. - /// One entry per entity (in contrast to static scenery's per-CylSphere - /// registration) so RemoveLiveEntityByServerGuid's single - /// Deregister(entity.Id) cleans it up without leaks. - /// - /// - /// Geometry-priority order matches retail - /// (acclient_2013_pseudo_c.txt:276858-276987): CylSpheres > - /// Sphere fallback > Setup.Radius. Phantom Setups (no shape) are - /// rejected — retail's FindObjCollisions falls through to - /// OK_TS in that case. - /// - /// - /// - /// Carries derived from the PWD - /// bitfield (acclient.h:6431-6463) plus IsCreature - /// derived from the inbound ItemType. Commit C consumes these in - /// the PvP exemption block. - /// - /// - private void RegisterLiveEntityCollision( - AcDream.Core.World.WorldEntity entity, - DatReaderWriter.DBObjs.Setup setup, - AcDream.Core.Net.WorldSession.EntitySpawn spawn, - System.Numerics.Vector3 origin) - { - if (spawn.Position is null) return; - - bool hasCyl = setup.CylSpheres.Count > 0; - bool hasSphere = setup.Spheres.Count > 0; - bool hasRadius = setup.Radius > 0.0001f; - - // Retail-faithful phantom skip (acclient_2013_pseudo_c.txt:276917). - if (!hasCyl && !hasSphere && !hasRadius) - return; - - float entScale = spawn.ObjScale ?? 1.0f; - float radius; - float height; - - if (hasCyl) - { - // Pick the largest CylSphere as the body cylinder. Retail - // tests every CylSphere in turn (276891) but for collision - // BLOCKING the largest is sufficient — the player will stop - // at the body's outer radius. - var sph = setup.CylSpheres[0]; - for (int i = 1; i < setup.CylSpheres.Count; i++) - { - if (setup.CylSpheres[i].Radius > sph.Radius) sph = setup.CylSpheres[i]; - } - radius = sph.Radius * entScale; - height = (sph.Height > 0 ? sph.Height : sph.Radius * 4f) * entScale; - } - else if (hasRadius) - { - radius = setup.Radius * entScale; - height = (setup.Height > 0 ? setup.Height : setup.Radius * 2f) * entScale; - } - else - { - // Sphere-only: largest sphere as a Cylinder approximation. - var sph = setup.Spheres[0]; - for (int i = 1; i < setup.Spheres.Count; i++) - { - if (setup.Spheres[i].Radius > sph.Radius) sph = setup.Spheres[i]; - } - radius = sph.Radius * entScale; - height = sph.Radius * 2f * entScale; - } - - if (radius <= 0f) return; - - // Decode PvP / Player / Impenetrable from PWD._bitfield. - // IsCreature comes from the spawn's ItemType (server-known type). - var flags = AcDream.Core.Physics.EntityCollisionFlags.None; - if (spawn.ObjectDescriptionFlags is { } odf) - flags = AcDream.Core.Physics.EntityCollisionFlagsExt.FromPwdBitfield(odf); - if (spawn.ItemType == (uint)AcDream.Core.Items.ItemType.Creature) - flags |= AcDream.Core.Physics.EntityCollisionFlags.IsCreature; - - uint state = spawn.PhysicsState ?? 0u; - - _physicsEngine.ShadowObjects.Register( - entity.Id, entity.SourceGfxObjOrSetupId, - entity.Position, entity.Rotation, radius, - origin.X, origin.Y, spawn.Position.Value.LandblockId, - AcDream.Core.Physics.ShadowCollisionType.Cylinder, - cylHeight: height, scale: 1.0f, - state: state, flags: flags); - } - private bool RemoveLiveEntityByServerGuid(uint serverGuid, bool logDelete) { if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity)) @@ -3086,18 +2980,6 @@ public sealed class GameWindow : IDisposable entity.Position = worldPos; entity.Rotation = rot; - // Commit B 2026-04-29 — keep the shadow registry in sync with - // server-authoritative position so the player's collision broadphase - // tests against the up-to-date target body. Skip the local player - // (its body is the simulator, not a target). Retail does the - // equivalent via SetPosition → change_cell → AddShadowObject - // (acclient_2013_pseudo_c.txt:284276 / 281200 / 282862). - if (update.Guid != _playerServerGuid) - { - _physicsEngine.ShadowObjects.UpdatePosition( - entity.Id, worldPos, rot, origin.X, origin.Y, p.LandblockId); - } - // Track remote-entity motion for stop detection. Only record the // timestamp when position moved MEANINGFULLY (> 0.05m). Updates // that report the same position keep the old Time, so the diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index d574887b..39b30cde 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -84,24 +84,6 @@ public static class CreateObject /// SetupTableId are nullable because their corresponding /// physics-description-flag bits may not be set on every CreateObject. /// - /// - /// - /// (acclient.h:2815) carries flag - /// bits like ETHEREAL_PS=0x4, IGNORE_COLLISIONS_PS=0x10, - /// HAS_PHYSICS_BSP_PS=0x10000 — the bits retail's - /// FindObjCollisions reads to short-circuit ethereal / - /// no-collision entities. Pre-2026-04-29 (Commit A of the live-entity - /// collision port) the parser silently dropped this field. - /// - /// - /// is the PWD._bitfield - /// trailer (acclient.h:6431-6463) — bits include BF_PLAYER (0x8), - /// BF_PLAYER_KILLER (0x20), BF_FREE_PKSTATUS (0x200000), - /// BF_PKLITE_PKSTATUS (0x2000000). Decoded into - /// EntityCollisionFlags at registration time for the PvP - /// exemption gate. - /// - /// public readonly record struct Parsed( uint Guid, ServerPosition? Position, @@ -118,9 +100,7 @@ public static class CreateObject ushort InstanceSequence = 0, ushort TeleportSequence = 0, ushort ServerControlSequence = 0, - ushort ForcePositionSequence = 0, - uint? PhysicsState = null, - uint? ObjectDescriptionFlags = null); + ushort ForcePositionSequence = 0); /// /// The relevant subset of the server-sent MovementData / @@ -280,12 +260,6 @@ public static class CreateObject float? objScale = null; ServerMotionState? motionState = null; uint? motionTableId = null; - // Commit A 2026-04-29 — live-entity collision plumbing. PhysicsState - // (acclient.h:2815) was previously skipped at line ~337; the PWD - // _bitfield (acclient.h:6431-6463) was previously discarded as - // "ObjectDescriptionFlags" at the WeenieHeader trailer. - uint? physicsState = null; - uint? objectDescriptionFlags = null; try { @@ -360,10 +334,7 @@ public static class CreateObject if (body.Length - pos < 8) return null; var physicsFlags = (PhysicsDescriptionFlag)BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; - // PhysicsState (acclient.h:2815). Previously skipped, now - // surfaced for live-entity collision (Commit A 2026-04-29). - physicsState = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); - pos += 4; + pos += 4; // PhysicsState (skip) if ((physicsFlags & PhysicsDescriptionFlag.Movement) != 0) { @@ -492,16 +463,7 @@ public static class CreateObject if (body.Length - pos >= 4) itemType = ReadU32(body, ref pos); if (body.Length - pos >= 4) - { - // ObjectDescriptionFlags = retail PWD._bitfield - // (acclient.h:6431-6463). Carries BF_PLAYER (0x8), - // BF_PLAYER_KILLER (0x20), BF_FREE_PKSTATUS (0x200000), - // BF_PKLITE_PKSTATUS (0x2000000) — the bits that - // acclient_2013_pseudo_c.txt:406898-406918 read for - // IsPK() / IsPKLite() / IsImpenetrable(). Previously - // discarded; now surfaced for the PvP collision rule. - objectDescriptionFlags = ReadU32(body, ref pos); - } + _ = ReadU32(body, ref pos); // ObjectDescriptionFlags AlignTo4(ref pos); } catch { /* truncated name — partial result is still useful */ } @@ -509,15 +471,13 @@ public static class CreateObject return new Parsed(guid, position, setupTableId, animParts, textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId, - instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq, - physicsState, objectDescriptionFlags); + instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq); // Local helper: if we ran out of fields past PhysicsData, still // return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion). Parsed PartialResult() => new( guid, position, setupTableId, animParts, - textureChanges, subPalettes, basePaletteId, objScale, null, null, motionState, motionTableId, - PhysicsState: physicsState, ObjectDescriptionFlags: objectDescriptionFlags); + textureChanges, subPalettes, basePaletteId, objScale, null, null, motionState, motionTableId); } catch { diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 030573b8..885ec634 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -56,14 +56,7 @@ public sealed class WorldSession : IDisposable string? Name, uint? ItemType, CreateObject.ServerMotionState? MotionState, - uint? MotionTableId, - // Commit A 2026-04-29 — live-entity collision plumbing. - // PhysicsState: retail acclient.h:2815 (ETHEREAL_PS=0x4, - // IGNORE_COLLISIONS_PS=0x10, HAS_PHYSICS_BSP_PS=0x10000, ...). - // ObjectDescriptionFlags: retail PWD._bitfield (acclient.h:6431-6463) - // — drives IsPlayer/IsPK/IsPKLite/IsImpenetrable for PvP gating. - uint? PhysicsState = null, - uint? ObjectDescriptionFlags = null); + uint? MotionTableId); /// Fires when the session finishes parsing a CreateObject. public event Action? EntitySpawned; @@ -655,9 +648,7 @@ public sealed class WorldSession : IDisposable parsed.Value.Name, parsed.Value.ItemType, parsed.Value.MotionState, - parsed.Value.MotionTableId, - parsed.Value.PhysicsState, - parsed.Value.ObjectDescriptionFlags)); + parsed.Value.MotionTableId)); } } else if (op == DeleteObject.Opcode) diff --git a/src/AcDream.Core/Physics/CollisionExemption.cs b/src/AcDream.Core/Physics/CollisionExemption.cs deleted file mode 100644 index 89b368d6..00000000 --- a/src/AcDream.Core/Physics/CollisionExemption.cs +++ /dev/null @@ -1,123 +0,0 @@ -namespace AcDream.Core.Physics; - -/// -/// The retail-faithful exemption gate at the top of -/// CPhysicsObj::FindObjCollisions. Decides — based on the moving -/// object's bits and the target's raw -/// PhysicsState + decoded — -/// whether collision against the target should be skipped entirely -/// (return OK_TS) or proceed to broad-phase / shape dispatch. -/// -/// -/// Ported from the named retail decompilation: -/// -/// -/// acclient_2013_pseudo_c.txt:276782 — target -/// ETHEREAL_PS=0x4 & IGNORE_COLLISIONS_PS=0x10: walk through. -/// acclient_2013_pseudo_c.txt:276787 — viewer mover vs -/// creature target: walk through (camera ray ignores creatures). -/// acclient_2013_pseudo_c.txt:276971 — mover with -/// IGNORE_CREATURES (state & 0x400) vs creature target: -/// walk through. -/// acclient_2013_pseudo_c.txt:276807-276839 — PvP rule: -/// -/// If both are players: skip unless target is Impenetrable, -/// or both are PK, or both are PKLite. Mismatched PK status (PK vs -/// non-PK, PK vs PKLite) is exempted — players in different pools -/// pass through each other, matching retail muscle memory. -/// -/// -/// -/// -/// -/// Cross-checked against ACE -/// references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:381-405 -/// (line-for-line C# port of the same logic). Note: ACE adds -/// state.HasFlag(IsImpenetrable) (mover-impenetrable) to the -/// collide list; retail's pseudo-C only checks the target's -/// IsImpenetrable(). acdream follows retail. -/// -/// -public static class CollisionExemption -{ - private const uint ETHEREAL_PS = 0x4u; // acclient.h:2819 - private const uint IGNORE_COLLISIONS_PS = 0x10u; // acclient.h:2821 - - /// - /// Should the moving object skip collision testing against this - /// target entirely? Returns true if exempt (no further - /// shape dispatch). - /// - /// Raw retail PhysicsState bits - /// captured at CreateObject time (ETHEREAL/IGNORE/etc.). - /// Decoded - /// from the target's PWD bitfield - /// plus its ItemType-derived IsCreature bit. - /// The moving object's - /// — typically the local player's - /// IsPlayer + (PK/PKLite/Impenetrable bits if known) flags. - public static bool ShouldSkip(uint targetState, EntityCollisionFlags targetFlags, - ObjectInfoState moverState) - { - // 1. Target ETHEREAL + IGNORE_COLLISIONS → walk through. - // acclient_2013_pseudo_c.txt:276782 — wraps the entire body of - // FindObjCollisions; we hoist it as the first early-out. - if ((targetState & ETHEREAL_PS) != 0 - && (targetState & IGNORE_COLLISIONS_PS) != 0) - { - return true; - } - - // 2. Viewer mover + creature target → walk through. - // acclient_2013_pseudo_c.txt:276787-276790. - bool moverIsViewer = (moverState & ObjectInfoState.IsViewer) != 0; - bool targetIsCreature = (targetFlags & EntityCollisionFlags.IsCreature) != 0; - if (moverIsViewer && targetIsCreature) - return true; - - // 3. IGNORE_CREATURES mover + creature target → walk through. - // acclient_2013_pseudo_c.txt:276971. - bool moverIgnoresCreatures = (moverState & ObjectInfoState.IgnoreCreatures) != 0; - if (moverIgnoresCreatures && targetIsCreature) - return true; - - // 4. PvP exemption block. - // acclient_2013_pseudo_c.txt:276807-276839. - bool moverIsPlayer = (moverState & ObjectInfoState.IsPlayer) != 0; - bool targetIsPlayer = (targetFlags & EntityCollisionFlags.IsPlayer) != 0; - if (moverIsPlayer && targetIsPlayer) - { - // Tentatively exempt (retail `ebp_1 = 1`). Then disqualify - // if any of the COLLIDE conditions hold. - bool collide = false; - - // 4a. Impenetrable target → collide. - // acclient_2013_pseudo_c.txt:276826. - if ((targetFlags & EntityCollisionFlags.IsImpenetrable) != 0) - collide = true; - - // 4b. Both PK → collide. - // acclient_2013_pseudo_c.txt:276832-276836. - if (!collide - && (moverState & ObjectInfoState.IsPK) != 0 - && (targetFlags & EntityCollisionFlags.IsPK) != 0) - { - collide = true; - } - - // 4c. Both PKLite → collide. - // acclient_2013_pseudo_c.txt:276837. - if (!collide - && (moverState & ObjectInfoState.IsPKLite) != 0 - && (targetFlags & EntityCollisionFlags.IsPKLite) != 0) - { - collide = true; - } - - if (!collide) - return true; // exempt — non-PK pair walks through - } - - return false; // proceed to broad-phase + shape dispatch - } -} diff --git a/src/AcDream.Core/Physics/EntityCollisionFlags.cs b/src/AcDream.Core/Physics/EntityCollisionFlags.cs deleted file mode 100644 index 5ee86a85..00000000 --- a/src/AcDream.Core/Physics/EntityCollisionFlags.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace AcDream.Core.Physics; - -/// -/// Per-entity flags driving the retail-faithful PvP / Player / -/// Impenetrable exemption logic in FindObjCollisions. Decoded -/// from PublicWeenieDesc._bitfield at CreateObject time. -/// -/// -/// Bit positions (verified against -/// docs/research/named-retail/acclient_2013_pseudo_c.txt:406898-406918 -/// where ACCWeenieObject::IsPK/IsPKLite/IsImpenetrable read directly -/// from this->pwd._bitfield): -/// -/// -/// BF_PLAYER = 0x8 (bit 3) → -/// BF_PLAYER_KILLER = 0x20 (bit 5) → -/// BF_FREE_PKSTATUS = 0x200000 (bit 21) → -/// BF_PKLITE_PKSTATUS = 0x2000000 (bit 25) → -/// -/// -/// -/// is NOT a PWD bit — retail derives it from -/// PublicWeenieDesc._type matching ITEM_TYPE_CREATURE -/// (acclient.h ITEM_TYPE enum). Set at registration time by callers that -/// already know the item type. -/// -/// -[Flags] -public enum EntityCollisionFlags : byte -{ - None = 0x00, - /// Set when BF_PLAYER (0x8) is set in pwd._bitfield. - IsPlayer = 0x01, - /// Derived from ItemType.Creature on the spawn payload. - IsCreature = 0x02, - /// Set when BF_PLAYER_KILLER (0x20) is set. - IsPK = 0x04, - /// Set when BF_PKLITE_PKSTATUS (0x2000000) is set. - IsPKLite = 0x08, - /// Set when BF_FREE_PKSTATUS (0x200000) is set (a.k.a. "Free" PK status — cannot be PKed). - IsImpenetrable = 0x10, -} - -/// Helpers to convert raw retail bitfields into . -public static class EntityCollisionFlagsExt -{ - /// - /// Decode the player/PK/PKLite/Impenetrable bits from a - /// PublicWeenieDesc._bitfield value (the WeenieHeader trailer - /// field acdream's parser surfaces as ObjectDescriptionFlags). - /// - /// Bit positions per - /// docs/research/named-retail/acclient.h:6431-6463 - /// (PublicWeenieDesc::BitfieldIndex) and - /// acclient_2013_pseudo_c.txt:441868-441890 - /// (PublicWeenieDesc::SetPlayerKillerStatus). - /// - public static EntityCollisionFlags FromPwdBitfield(uint bitfield) - { - var flags = EntityCollisionFlags.None; - if ((bitfield & 0x8u) != 0) flags |= EntityCollisionFlags.IsPlayer; - if ((bitfield & 0x20u) != 0) flags |= EntityCollisionFlags.IsPK; - if ((bitfield & 0x200000u) != 0) flags |= EntityCollisionFlags.IsImpenetrable; - if ((bitfield & 0x2000000u) != 0) flags |= EntityCollisionFlags.IsPKLite; - return flags; - } -} diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 4a934031..bb26c54a 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -388,21 +388,13 @@ public sealed class PhysicsEngine float sphereRadius, float sphereHeight, float stepUpHeight, float stepDownHeight, bool isOnGround, - PhysicsBody? body = null, - ObjectInfoState moverFlags = ObjectInfoState.None) + PhysicsBody? body = null) { var transition = new Transition(); transition.ObjectInfo.StepUpHeight = stepUpHeight; transition.ObjectInfo.StepDownHeight = stepDownHeight; transition.ObjectInfo.StepDown = true; - // Commit C 2026-04-29 — caller-supplied mover flags drive the - // retail PvP exemption block in FindObjCollisions. The local - // player passes IsPlayer (and PK/PKLite/Impenetrable when known - // from PlayerDescription); remote dead-reckoning passes None - // (matches non-player movement, all targets collide). - transition.ObjectInfo.State |= moverFlags; - if (isOnGround) transition.ObjectInfo.State |= ObjectInfoState.Contact | ObjectInfoState.OnWalkable; diff --git a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs index 6b4ea114..e7c29507 100644 --- a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs +++ b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs @@ -19,24 +19,11 @@ public sealed class ShadowObjectRegistry /// /// Register an entity into the cells it overlaps based on world position + radius. - /// - /// - /// The optional + - /// parameters carry retail PhysicsState bits and decoded - /// respectively, so the - /// FindObjCollisions retail-faithful exemption block (PvP rule, - /// ETHEREAL skip, viewer-vs-creature) can short-circuit without an - /// extra lookup. Default state=0 + flags=None preserves - /// the original "static decoration" behavior — the existing 5 - /// landblock-entity registration sites pass nothing. - /// /// public void Register(uint entityId, uint gfxObjId, Vector3 worldPos, Quaternion rotation, float radius, float worldOffsetX, float worldOffsetY, uint landblockId, ShadowCollisionType collisionType = ShadowCollisionType.BSP, - float cylHeight = 0f, float scale = 1.0f, - uint state = 0u, - EntityCollisionFlags flags = EntityCollisionFlags.None) + float cylHeight = 0f, float scale = 1.0f) { Deregister(entityId); @@ -51,8 +38,7 @@ public sealed class ShadowObjectRegistry int minCy = Math.Max(0, (int)((localY - radius) / 24f)); int maxCy = Math.Min(7, (int)((localY + radius) / 24f)); - var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius, - collisionType, cylHeight, scale, state, flags); + var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius, collisionType, cylHeight, scale); var cellIds = new List(); uint lbPrefix = landblockId & 0xFFFF0000u; @@ -76,56 +62,6 @@ public sealed class ShadowObjectRegistry _entityToCells[entityId] = cellIds; } - /// - /// Update an already-registered entity's world position + rotation, - /// preserving its , - /// , and shape parameters. - /// - /// - /// Cheaper than + for - /// the 5–10 Hz UpdatePosition (0xF748) stream the server emits - /// per visible entity: this is the path retail's - /// CPhysicsObj::SetPosition takes (cited at - /// acclient_2013_pseudo_c.txt:284276) — same shape, new cell - /// membership. If the entity isn't already registered, this is a - /// no-op so callers don't have to gate. - /// - /// - public void UpdatePosition(uint entityId, Vector3 worldPos, Quaternion rotation, - float worldOffsetX, float worldOffsetY, uint landblockId) - { - // Find the existing entry (any cell holds a copy with the same - // entity-scoped state + flags + shape). - if (!_entityToCells.TryGetValue(entityId, out var oldCells) || oldCells.Count == 0) - return; - - ShadowEntry? template = null; - foreach (var oldCellId in oldCells) - { - if (_cells.TryGetValue(oldCellId, out var list)) - { - foreach (var e in list) - { - if (e.EntityId == entityId) - { - template = e; - break; - } - } - } - if (template is not null) break; - } - if (template is null) - return; - - // Preserve everything except position + rotation. - var t = template.Value; - Register(entityId, t.GfxObjId, worldPos, rotation, t.Radius, - worldOffsetX, worldOffsetY, landblockId, - t.CollisionType, t.CylHeight, t.Scale, - t.State, t.Flags); - } - /// Remove an entity from all cells it was registered in. public void Deregister(uint entityId) { @@ -267,18 +203,4 @@ public readonly record struct ShadowEntry( float Radius, ShadowCollisionType CollisionType = ShadowCollisionType.BSP, float CylHeight = 0f, - float Scale = 1.0f, - /// - /// Retail PhysicsState bits (acclient.h:2815). Used - /// by FindObjCollisions to honor ETHEREAL_PS=0x4 + - /// IGNORE_COLLISIONS_PS=0x10 short-circuits. Zero for static - /// landblock entities (default behavior matches pre-Commit-A). - /// - uint State = 0u, - /// - /// Decoded player / PK / PKLite / Impenetrable flags driving the - /// retail PvP exemption block in FindObjCollisions. Built - /// from PWD._bitfield at CreateObject time via - /// . - /// - EntityCollisionFlags Flags = EntityCollisionFlags.None); + float Scale = 1.0f); diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 57d6e6d2..62a9c8bf 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -34,13 +34,6 @@ public enum ObjectInfoState : uint IsPlayer = 0x100, EdgeSlide = 0x200, IgnoreCreatures = 0x400, - /// Bits added 2026-04-29 (Commit A live-entity collision): - /// the moving object's PK status drives the player-vs-player - /// exemption block in FindObjCollisions per - /// acclient_2013_pseudo_c.txt:276807-276839. Values match - /// retail's OBJECTINFO::state bits (acclient.h:6190-6194). - IsPK = 0x800, - IsPKLite = 0x1000, } /// @@ -807,17 +800,6 @@ public sealed class Transition if (distToCurr > maxReach) continue; - // Commit C 2026-04-29 — retail exemption block at the top of - // CPhysicsObj::FindObjCollisions - // (acclient_2013_pseudo_c.txt:276782-276839,276971). Skips - // ethereal+ignore-collisions, viewer-vs-creature, - // IGNORE_CREATURES movers, and the player-vs-player PvP - // pass-through (non-PK pair / mismatched PK). Static - // landblock entries register with State=0 and Flags=None, - // so this is a cheap fall-through for them. - if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State)) - continue; - TransitionState result; if (obj.CollisionType == ShadowCollisionType.BSP) diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs index 71bf72db..a7dea334 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -23,63 +23,10 @@ public sealed class CreateObjectTests Assert.Equal((uint)ItemType.Creature, parsed.Value.ItemType); } - // ----------------------------------------------------------------------- - // Commit A of 2026-04-29 live-entity collision port: - // PhysicsState (post-flags u32) + ObjectDescriptionFlags (PWD bitfield) - // must be surfaced for downstream collision registration. - // ----------------------------------------------------------------------- - - [Fact] - public void TryParse_PhysicsState_Parsed() - { - // ETHEREAL_PS = 0x4 + IGNORE_COLLISIONS_PS = 0x10 → 0x14 - byte[] body = BuildMinimalCreateObjectWithWeenieHeader( - guid: 0x50000003u, name: "GhostNpc", - itemType: (uint)ItemType.Creature, - physicsState: 0x14u); - - var parsed = CreateObject.TryParse(body); - - Assert.NotNull(parsed); - Assert.Equal(0x14u, parsed!.Value.PhysicsState); - } - - [Fact] - public void TryParse_ObjectDescriptionFlags_PlayerKillerBitsSurface() - { - // BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20) → a PK player. - byte[] body = BuildMinimalCreateObjectWithWeenieHeader( - guid: 0x50000004u, name: "+PkPlayer", - itemType: (uint)ItemType.Creature, - objectDescriptionFlags: 0x8u | 0x20u); - - var parsed = CreateObject.TryParse(body); - - Assert.NotNull(parsed); - Assert.Equal(0x28u, parsed!.Value.ObjectDescriptionFlags); - } - - [Fact] - public void TryParse_ObjectDescriptionFlags_PkLiteBit() - { - // BF_PLAYER (0x8) | BF_PKLITE_PKSTATUS (0x2000000) → a PK-lite player. - byte[] body = BuildMinimalCreateObjectWithWeenieHeader( - guid: 0x50000005u, name: "+PklPlayer", - itemType: (uint)ItemType.Creature, - objectDescriptionFlags: 0x8u | 0x2000000u); - - var parsed = CreateObject.TryParse(body); - - Assert.NotNull(parsed); - Assert.Equal(0x2000008u, parsed!.Value.ObjectDescriptionFlags); - } - private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, - uint itemType, - uint physicsState = 0, - uint objectDescriptionFlags = 0) + uint itemType) { var bytes = new List(); WriteU32(bytes, CreateObject.Opcode); @@ -91,9 +38,9 @@ public sealed class CreateObjectTests bytes.Add(0); bytes.Add(0); - // PhysicsData: physics flags = 0, then PhysicsState u32, then 9 seq stamps. + // PhysicsData: no flags, empty physics state, then 9 sequence stamps. + WriteU32(bytes, 0); WriteU32(bytes, 0); - WriteU32(bytes, physicsState); for (int i = 0; i < 9; i++) WriteU16(bytes, 0); Align4(bytes); @@ -104,7 +51,7 @@ public sealed class CreateObjectTests WritePackedDword(bytes, 0x1234); // WeenieClassId WritePackedDword(bytes, 0); // IconId via known-type writer WriteU32(bytes, itemType); - WriteU32(bytes, objectDescriptionFlags); + WriteU32(bytes, 0); // ObjectDescriptionFlags Align4(bytes); return bytes.ToArray(); diff --git a/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs b/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs deleted file mode 100644 index b8431658..00000000 --- a/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -using AcDream.Core.Physics; -using Xunit; - -namespace AcDream.Core.Tests.Physics; - -/// -/// Unit tests for — Commit C of the -/// 2026-04-29 live-entity collision port. Covers retail's -/// CPhysicsObj::FindObjCollisions exemption block, ported -/// line-for-line from -/// docs/research/named-retail/acclient_2013_pseudo_c.txt:276782-276839,276971. -/// -/// -/// Behaviour matrix (target / mover columns): -/// -/// -/// MoverTargetSkip? -/// anyETHEREAL+IGNORE_COLLISIONSYES (early-out) -/// IsViewerIsCreatureYES (camera ray-test passes through) -/// IGNORE_CREATURESIsCreatureYES (mover walks through creatures) -/// IsPlayerIsPlayer (no PK)YES (non-PK pair walks through) -/// IsPlayer + IsPKIsPlayer + IsPKNO (PK pair collides) -/// IsPlayer + IsPKLiteIsPlayer + IsPKLiteNO (PKLite pair collides) -/// IsPlayerIsPlayer + IsImpenetrableNO (Impenetrable target always collides) -/// IsPlayer + IsPKIsPlayer (no PK)YES (mismatched PK skip) -/// IsPlayerIsCreature (NPC)NO (player vs NPC always collides) -/// -/// -public class CollisionExemptionTests -{ - private const uint ETHEREAL_PS = 0x4u; - private const uint IGNORE_COLLISIONS_PS = 0x10u; - - [Fact] - public void EtherealAndIgnoreCollisions_AlwaysSkipped() - { - // Target with both bits set is exempted from any mover. - Assert.True(CollisionExemption.ShouldSkip( - targetState: ETHEREAL_PS | IGNORE_COLLISIONS_PS, - targetFlags: EntityCollisionFlags.None, - moverState: ObjectInfoState.IsPlayer)); - } - - [Fact] - public void EtherealOnly_NotSkipped() - { - // Target with ETHEREAL but NOT IGNORE_COLLISIONS does not bail - // out at the first gate — collision proceeds. (Step-down marks - // obstruction_ethereal, but does not exempt.) - Assert.False(CollisionExemption.ShouldSkip( - targetState: ETHEREAL_PS, - targetFlags: EntityCollisionFlags.None, - moverState: ObjectInfoState.IsPlayer)); - } - - [Fact] - public void Viewer_VsCreature_Skipped() - { - // Camera-ray viewer transitions through creatures. - Assert.True(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsCreature, - moverState: ObjectInfoState.IsViewer)); - } - - [Fact] - public void Viewer_VsNonCreature_NotSkipped() - { - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.None, - moverState: ObjectInfoState.IsViewer)); - } - - [Fact] - public void IgnoreCreatures_VsCreature_Skipped() - { - // Per acclient_2013_pseudo_c.txt:276971 — an arrow with - // IGNORE_CREATURES doesn't get blocked by the very monster it's - // tracking towards (until missile_ignore filters its target). - Assert.True(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsCreature, - moverState: ObjectInfoState.IgnoreCreatures)); - } - - [Fact] - public void NonPkPlayer_VsNonPkPlayer_Skipped() - { - // The user-visible payoff: two ordinary players walk through each - // other instead of blocking. - Assert.True(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer, - moverState: ObjectInfoState.IsPlayer)); - } - - [Fact] - public void Pk_VsPk_NotSkipped() - { - // Two PK players collide. - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK, - moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK)); - } - - [Fact] - public void PkLite_VsPkLite_NotSkipped() - { - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPKLite, - moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPKLite)); - } - - [Fact] - public void Pk_VsNonPk_Skipped() - { - // Mismatched PK status: still exempt — only matching pair collides. - Assert.True(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer, - moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK)); - } - - [Fact] - public void Pk_VsPkLite_Skipped() - { - // PK and PKLite are different pools — pair doesn't match. - Assert.True(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPKLite, - moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK)); - } - - [Fact] - public void ImpenetrableTarget_VsAnyPlayer_NotSkipped() - { - // Impenetrable target ("Free" PK status) always collides with - // any player mover — regardless of mover's PK state. - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsImpenetrable, - moverState: ObjectInfoState.IsPlayer)); - } - - [Fact] - public void Player_VsCreature_NotSkipped() - { - // PvP exemption only applies player-on-player. Player vs creature - // (NPC, monster) is the normal blocking case. - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsCreature, - moverState: ObjectInfoState.IsPlayer)); - } - - [Fact] - public void NonPlayerMover_VsPlayer_NotSkipped() - { - // PvP rule requires BOTH to be players. Mover is not a player - // (e.g., dead-reckoned remote NPC) → no exemption applies. - Assert.False(CollisionExemption.ShouldSkip( - targetState: 0u, - targetFlags: EntityCollisionFlags.IsPlayer, - moverState: ObjectInfoState.None)); - } -} diff --git a/tests/AcDream.Core.Tests/Physics/EntityCollisionFlagsTests.cs b/tests/AcDream.Core.Tests/Physics/EntityCollisionFlagsTests.cs deleted file mode 100644 index 9c8cff0f..00000000 --- a/tests/AcDream.Core.Tests/Physics/EntityCollisionFlagsTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using AcDream.Core.Physics; -using Xunit; - -namespace AcDream.Core.Tests.Physics; - -/// -/// Unit tests for — Commit A of the -/// 2026-04-29 live-entity collision port. Verifies retail-faithful -/// conversion from PWD bitfield bits to the boolean collision-decision -/// flags that FindObjCollisions consumes for the PvP exemption. -/// -/// Bit positions confirmed against: -/// -/// docs/research/named-retail/acclient_2013_pseudo_c.txt:406898-406918 -/// (ACCWeenieObject::IsPKLite/IsPK/IsImpenetrable) — the -/// retail client itself reads bits 25 / 5 / 21 of pwd._bitfield. -/// docs/research/named-retail/acclient.h:6431-6463 -/// (PublicWeenieDesc::BitfieldIndex) — names the bits: -/// BF_PLAYER=0x8, BF_PLAYER_KILLER=0x20, -/// BF_FREE_PKSTATUS=0x200000, BF_PKLITE_PKSTATUS=0x2000000. -/// docs/research/named-retail/acclient_2013_pseudo_c.txt:441868-441890 -/// (PublicWeenieDesc::SetPlayerKillerStatus) — confirms -/// the writer maps PKStatusEnum values onto these exact bits. -/// -/// -public class EntityCollisionFlagsTests -{ - [Fact] - public void FromPwdBitfield_AllZeros_NoFlags() - { - Assert.Equal(EntityCollisionFlags.None, EntityCollisionFlagsExt.FromPwdBitfield(0u)); - } - - [Fact] - public void FromPwdBitfield_PlayerBit_SetsIsPlayer() - { - // BF_PLAYER = 0x8 (bit 3) - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x8u); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsPlayer)); - Assert.False(flags.HasFlag(EntityCollisionFlags.IsPK)); - } - - [Fact] - public void FromPwdBitfield_PlayerKillerBit_SetsIsPK() - { - // BF_PLAYER_KILLER = 0x20 (bit 5) - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x20u); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsPK)); - Assert.False(flags.HasFlag(EntityCollisionFlags.IsPKLite)); - } - - [Fact] - public void FromPwdBitfield_PkLiteBit_SetsIsPKLite() - { - // BF_PKLITE_PKSTATUS = 0x2000000 (bit 25) - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x2000000u); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsPKLite)); - Assert.False(flags.HasFlag(EntityCollisionFlags.IsPK)); - } - - [Fact] - public void FromPwdBitfield_FreePkStatusBit_SetsIsImpenetrable() - { - // BF_FREE_PKSTATUS = 0x200000 (bit 21) - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x200000u); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsImpenetrable)); - } - - [Fact] - public void FromPwdBitfield_PlayerAndPK_SetsBoth() - { - // A PK player: BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20) = 0x28 - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x28u); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsPlayer)); - Assert.True(flags.HasFlag(EntityCollisionFlags.IsPK)); - Assert.False(flags.HasFlag(EntityCollisionFlags.IsPKLite)); - Assert.False(flags.HasFlag(EntityCollisionFlags.IsImpenetrable)); - } - - [Fact] - public void FromPwdBitfield_UnrelatedBits_Ignored() - { - // Set BF_OPENABLE (0x1), BF_INSCRIBABLE (0x2), BF_STUCK (0x4) — none - // map to collision flags. A creature spawn might have BF_ATTACKABLE - // (0x10) set; that's ItemType-derived IsCreature, not a PvP flag. - var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x1u | 0x2u | 0x4u | 0x10u); - Assert.Equal(EntityCollisionFlags.None, flags); - } -} diff --git a/tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs b/tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs index 73143d8c..2f2b4636 100644 --- a/tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs +++ b/tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs @@ -177,79 +177,4 @@ public class ShadowObjectRegistryTests var objs = reg.GetObjectsInCell(cellId); Assert.Contains(objs, e => e.EntityId == 99u); } - - // ----------------------------------------------------------------------- - // UpdatePosition — Commit A of 2026-04-29 live-entity collision port - // ----------------------------------------------------------------------- - - [Fact] - public void UpdatePosition_MovedEntity_NewCellOccupied() - { - // Entity starts at local (12, 12) — cell (0,0). - var reg = new ShadowObjectRegistry(); - reg.Register(42u, 0x01000010u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 0.5f, OffX, OffY, LbId); - - // Move to local (60, 12) — cell (2, 0). Same landblock. - reg.UpdatePosition(42u, new Vector3(60f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId); - - // Old cell empty, new cell holds the entity. - Assert.Empty(reg.GetObjectsInCell(LbId | 1u)); // cell (0,0) - var newCell = reg.GetObjectsInCell(LbId | 17u); // cell (2,0) → 2*8+0+1 - Assert.Single(newCell); - Assert.Equal(42u, newCell[0].EntityId); - Assert.Equal(1, reg.TotalRegistered); // not duplicated - } - - [Fact] - public void UpdatePosition_PreservesFlags() - { - // Register with PK flags + PhysicsState; UpdatePosition must keep them. - var reg = new ShadowObjectRegistry(); - reg.Register(50u, 0x01000011u, new Vector3(12f, 12f, 50f), Quaternion.Identity, - 0.5f, OffX, OffY, LbId, - state: 0x4u, // ETHEREAL_PS - flags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK); - - reg.UpdatePosition(50u, new Vector3(60f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId); - - var newCell = reg.GetObjectsInCell(LbId | 17u); - Assert.Single(newCell); - Assert.Equal(0x4u, newCell[0].State); - Assert.Equal(EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK, newCell[0].Flags); - } - - [Fact] - public void UpdatePosition_UnregisteredEntity_NoOp() - { - var reg = new ShadowObjectRegistry(); - // Should not throw, should not register a new entity. - reg.UpdatePosition(99u, new Vector3(12f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId); - Assert.Equal(0, reg.TotalRegistered); - } - - [Fact] - public void Register_WithStateAndFlags_StoredOnEntry() - { - var reg = new ShadowObjectRegistry(); - reg.Register(60u, 0x01000012u, new Vector3(12f, 12f, 50f), Quaternion.Identity, - 0.5f, OffX, OffY, LbId, - state: 0x10u, // IGNORE_COLLISIONS_PS - flags: EntityCollisionFlags.IsImpenetrable); - - var entry = reg.GetObjectsInCell(LbId | 1u)[0]; - Assert.Equal(0x10u, entry.State); - Assert.Equal(EntityCollisionFlags.IsImpenetrable, entry.Flags); - } - - [Fact] - public void Register_DefaultStateAndFlags_AreZeroAndNone() - { - var reg = new ShadowObjectRegistry(); - reg.Register(70u, 0x01000013u, new Vector3(12f, 12f, 50f), Quaternion.Identity, - 0.5f, OffX, OffY, LbId); - - var entry = reg.GetObjectsInCell(LbId | 1u)[0]; - Assert.Equal(0u, entry.State); - Assert.Equal(EntityCollisionFlags.None, entry.Flags); - } }