feat(phys): A6.P4 Task 7 — RegisterLiveEntityCollision uses ShadowShapeBuilder + RegisterMultiPart
Closes the M1.5 "doors don't block in production" bug (alongside the foundation fix at3b7dc46). Server-spawned entities (doors, NPCs, chests, items) now register one ShadowEntry per collision shape — matching retail's CPhysicsObj-with-CPartArray model (acclient_2013_pseudo_c.txt:286236) — instead of one Cylinder approximation per entity. Before: RegisterLiveEntityCollision picked ONE shape via a CylSphere → Radius → Sphere cascade, registered as a single Cylinder. Doors got a 14 cm × 20 cm cylinder from setup.Radius — far too narrow to span the doorway gap. Players could walk through closed doors. After: - ShadowShapeBuilder.FromSetup emits N shapes: • one Cylinder per CylSphere • one Cylinder per Sphere (only when no CylSpheres — retail convention) • one BSP shape per Part with a non-null PhysicsBSP - Caller substitutes the real BoundingSphere.Radius from PhysicsDataCache for BSP shapes (pure builder's 2.0 placeholder is tightened to the actual cached value). - setup.Radius fallback preserved: if the builder produces zero shapes but Radius > 0, register a Radius-based Cylinder so simple decorative props don't silently lose collision. - ShadowObjects.RegisterMultiPart adds N rows, all sharing entity.Id so the existing UpdatePhysicsState (ETHEREAL flip on door Use) propagates to every part without changes. Door 0x020019FF (Holtburg cottage) now registers: - Cylinder r=0.10 h=0.20 (from the single Sphere) - BSP from Part 0 = GfxObj 0x010044B5, the 6-face 1.925 m × 0.261 m × 2.490 m two-sided slab confirmed by DoorSetupGfxObjInspectionTests Parts 1 + 2 (GfxObj 0x010044B6, the visual leaves) are visual-only in the dat by retail design and correctly skipped. Test impact: 53/53 pass in the shape / registry / door / cellar-replay scope. App-layer 41/41 pass. Visual verification needed: launch the client, walk into a closed Holtburg cottage door from outside (dead center AND ~50 cm off-center), then walk into it from inside. Door should block all three approaches. Use the door (click + Use) → door swings open → walking through passes (ETHEREAL flip via existing SetState path). Foundation fix dependency:3b7dc46fix(phys): GetNearbyObjects dedup-by-entityId silently drops multi-part shadows Without3b7dc46in place, the BSP shape registered here would be dropped by GetNearbyObjects's dedup. They land together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b7dc46219
commit
ca9341c2cb
1 changed files with 68 additions and 35 deletions
|
|
@ -3090,41 +3090,60 @@ public sealed class GameWindow : IDisposable
|
|||
return;
|
||||
|
||||
float entScale = spawn.ObjScale ?? 1.0f;
|
||||
float radius;
|
||||
float height;
|
||||
|
||||
if (hasCyl)
|
||||
// A6.P4 door fix (2026-05-24): build the multi-part shape list.
|
||||
// ShadowShapeBuilder emits one entry per CylSphere, one per Sphere
|
||||
// (only when no CylSpheres), and one per Part with a non-null
|
||||
// PhysicsBSP. Retail-faithful per CTransition::find_obj_collisions
|
||||
// → CPartArray::FindObjCollisions
|
||||
// (acclient_2013_pseudo_c.txt:286236). Pre-fix doors registered
|
||||
// ONE small Cylinder via setup.Radius / Sphere — too narrow to
|
||||
// span the doorway gap, so the player could walk through. With
|
||||
// this change the door also registers the part-0 BSP slab
|
||||
// (1.9 × 0.26 × 2.5 m) that retail uses for the real block.
|
||||
var raw = AcDream.Core.Physics.ShadowShapeBuilder.FromSetup(
|
||||
setup, entScale,
|
||||
id => _physicsDataCache.GetGfxObj(id)?.BSP?.Root is not null);
|
||||
|
||||
// Substitute the real bounding-sphere radius for BSP shapes —
|
||||
// the pure builder's 2.0 placeholder works for typical doors
|
||||
// (BS radius ≈ 1.975 m) but is loose for larger entities and
|
||||
// tight for smaller ones. Mirrors the landblock-static path's
|
||||
// pattern of pulling the real radius from PhysicsDataCache.
|
||||
var shapes = new List<AcDream.Core.Physics.ShadowShape>(raw.Count);
|
||||
foreach (var s in raw)
|
||||
{
|
||||
// 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 (s.CollisionType == AcDream.Core.Physics.ShadowCollisionType.BSP)
|
||||
{
|
||||
if (setup.CylSpheres[i].Radius > sph.Radius) sph = setup.CylSpheres[i];
|
||||
var phys = _physicsDataCache.GetGfxObj(s.GfxObjId);
|
||||
float bspR = (phys?.BoundingSphere?.Radius ?? 2f) * entScale;
|
||||
shapes.Add(s with { Radius = bspR });
|
||||
}
|
||||
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++)
|
||||
else
|
||||
{
|
||||
if (setup.Spheres[i].Radius > sph.Radius) sph = setup.Spheres[i];
|
||||
shapes.Add(s);
|
||||
}
|
||||
radius = sph.Radius * entScale;
|
||||
height = sph.Radius * 2f * entScale;
|
||||
}
|
||||
|
||||
if (radius <= 0f) return;
|
||||
// setup.Radius fallback: the builder doesn't emit a Radius-only
|
||||
// shape (it only walks CylSpheres / Spheres / Parts). For entities
|
||||
// with no CylSpheres / Spheres / BSP-bearing Parts but a non-zero
|
||||
// Radius (rare — simple decorative props), preserve the prior
|
||||
// behavior of registering a setup.Radius cylinder so we don't
|
||||
// silently regress those entities' collision.
|
||||
if (shapes.Count == 0 && hasRadius)
|
||||
{
|
||||
shapes.Add(new AcDream.Core.Physics.ShadowShape(
|
||||
GfxObjId: 0u,
|
||||
LocalPosition: System.Numerics.Vector3.Zero,
|
||||
LocalRotation: System.Numerics.Quaternion.Identity,
|
||||
Scale: entScale,
|
||||
CollisionType: AcDream.Core.Physics.ShadowCollisionType.Cylinder,
|
||||
Radius: setup.Radius * entScale,
|
||||
CylHeight: (setup.Height > 0 ? setup.Height : setup.Radius * 2f) * entScale));
|
||||
}
|
||||
|
||||
if (shapes.Count == 0) return;
|
||||
|
||||
// Decode PvP / Player / Impenetrable from PWD._bitfield.
|
||||
// IsCreature comes from the spawn's ItemType (server-known type).
|
||||
|
|
@ -3136,17 +3155,31 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
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);
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entityId: entity.Id,
|
||||
entityWorldPos: entity.Position,
|
||||
entityWorldRot: entity.Rotation,
|
||||
shapes: shapes,
|
||||
state: state,
|
||||
flags: flags,
|
||||
worldOffsetX: origin.X,
|
||||
worldOffsetY: origin.Y,
|
||||
landblockId: spawn.Position.Value.LandblockId);
|
||||
|
||||
// L.2d slice 1 (2026-05-13): [entity-source] greppable from [resolve-bldg].
|
||||
// Per-shape detail appears in [resolve-bldg] when collisions fire;
|
||||
// this entity-level line keeps the spawn-time identification.
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||
{
|
||||
int nCyl = 0, nBsp = 0;
|
||||
foreach (var s in shapes)
|
||||
{
|
||||
if (s.CollisionType == AcDream.Core.Physics.ShadowCollisionType.Cylinder) nCyl++;
|
||||
else nBsp++;
|
||||
}
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{entity.SourceGfxObjOrSetupId:X8} lb=0x{spawn.Position.Value.LandblockId:X8} type=Cylinder note=server-spawn-root state=0x{state:X8} flags={flags}"));
|
||||
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{entity.SourceGfxObjOrSetupId:X8} lb=0x{spawn.Position.Value.LandblockId:X8} shapes=cyl{nCyl}+bsp{nBsp} note=server-spawn-root state=0x{state:X8} flags={flags}"));
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveLiveEntityByServerGuid(uint serverGuid, bool logDelete)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue