feat(physics): live-entity collision registration (Commit B)
NPCs / monsters / other players now register into ShadowObjectRegistry as collision targets. The local player walks into them and stops at the body cylinder, instead of passing through. GameWindow.OnLiveEntitySpawnedLocked: after the WorldEntity is built and stored in `_entitiesByServerGuid`, call the new RegisterLiveEntityCollision helper for any non-self entity. The helper honors retail's geometry-priority order (acclient_2013_pseudo_c.txt: 276858-276987) — CylSpheres > Setup.Radius > Sphere fallback — and applies the retail phantom-Setup skip (no CylSpheres / no Spheres / zero Radius → walk-through, matching FUN_FindObjCollisions's OK_TS fallthrough at :276917). GameWindow.OnLivePositionUpdated: after the entity's render pos/rot are set to server truth, push the same coordinates into the registry via ShadowObjectRegistry.UpdatePosition (the cheap preserve-shape-and-flags path Commit A added). Mirrors retail's SetPosition → change_cell → AddShadowObject chain ( acclient_2013_pseudo_c.txt:284276 / 281200 / 282862). The local player's own server guid is filtered out at both registration and update — its PhysicsBody is the simulator (the source of truth for our collisions), not a collision target. The decoded EntityCollisionFlags + raw PhysicsState bits are stored on each ShadowEntry but NOT YET CONSULTED by the collision resolver — Commit C is where the PvP exemption block lands. Practical effect of THIS commit: every visible body, including non-PK other players, blocks the local player. Two non-PK players currently can't pass through each other; that's the rule Commit C reverts to retail. No new unit tests in this commit (Commit A's ShadowObjectRegistry + EntityCollisionFlags suite covers the new field plumbing). Verification is live: at Holtburg the +Acdream test character should stop on contact with NPCs / vendors. Phantom decorations (small plants, grass) continue to pass through (L-fix3 phantom skip extends naturally to the live path via the same Setup-shape gate). dotnet build green, dotnet test 1454 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffefc6977f
commit
46ca3ba26b
1 changed files with 118 additions and 0 deletions
|
|
@ -2067,6 +2067,18 @@ 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;
|
||||
|
|
@ -2284,6 +2296,100 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit B 2026-04-29 — register a live (server-spawned) entity into
|
||||
/// the <see cref="ShadowObjectRegistry"/> as a single collision body.
|
||||
/// One entry per entity (in contrast to static scenery's per-CylSphere
|
||||
/// registration) so <c>RemoveLiveEntityByServerGuid</c>'s single
|
||||
/// <c>Deregister(entity.Id)</c> cleans it up without leaks.
|
||||
///
|
||||
/// <para>
|
||||
/// Geometry-priority order matches retail
|
||||
/// (<c>acclient_2013_pseudo_c.txt:276858-276987</c>): CylSpheres >
|
||||
/// Sphere fallback > Setup.Radius. Phantom Setups (no shape) are
|
||||
/// rejected — retail's <c>FindObjCollisions</c> falls through to
|
||||
/// OK_TS in that case.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Carries <see cref="EntityCollisionFlags"/> derived from the PWD
|
||||
/// bitfield (<c>acclient.h:6431-6463</c>) plus <c>IsCreature</c>
|
||||
/// derived from the inbound ItemType. Commit C consumes these in
|
||||
/// the PvP exemption block.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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))
|
||||
|
|
@ -2980,6 +3086,18 @@ 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue