fix #137 (window climb): the player's collision capsule topped out at 1.2m — head sphere 0.63m too low
The 'run into the last corridor window and pop up through its roof' report: the live callers passed sphereHeight: 1.2f into SpherePath.InitPath, whose head-sphere formula (height - radius) put the head sphere center at 0.72 - the capsule top at 1.2m. The top 0.63m of a 1.83m character had NO collision, so at the corridor-end window alcove (0x8A020179 -> 0x8A02017E: 0.70m sill face, 1.3m opening, sloped funnel behind) the step-up's placement never saw the head overlapping the lintel solids and let the player climb in head-through-roof. Dat truth (HumanSetup_CollisionSpheres_DatTruth): Setup 0x02000001 spheres = (0,0,0.475) r=0.48 and (0,0,1.350) r=0.48 - capsule top 1.83 = Setup.Height 1.835. Retail collides with that sphere list verbatim (CPhysicsObj::transition 0x00512dc0 -> init_sphere(GetNumSphere, GetSphere, m_scale)). Fix: PlayerMovementController + the GameWindow remote resolve now pass sphereHeight: 1.835f (capsule top; head center 1.355 vs dat 1.350). InitPath unchanged - captured-input replay fixtures (recorded 1.2 inputs) stay byte-identical. Register TS-46: the (radius, capsule-top) scalar approximation of the Setup sphere list (5mm foot/head offsets; remotes use human dims) with the retire path (plumb the sphere list). Pins: WindowOpening_HeadCannotFit_EntryBlocked (22-frame walked approach wall-slides at the sill, never enters 0x8A02017E) + WindowAlcove_RaisedPlacement_HeadInLintelSolid_Collides (Path-1 placement rejects the raised head in the lintel solids) + the WindowShaft_FullPolyDump / HumanSetup dat inspections. Suites: Core 2562 / App 713 / UI 425 / Net 385, 0 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1494f10cb5
commit
aa96d7ad77
6 changed files with 285 additions and 13 deletions
|
|
@ -29,6 +29,8 @@ public class Issue137CorridorSeamInspectionTests
|
|||
[InlineData(0x8A02016Eu)]
|
||||
[InlineData(0x8A02017Au)]
|
||||
[InlineData(0x8A02011Eu)] // the under-floor room the corridor's floor-portals lead to
|
||||
[InlineData(0x8A020179u)] // the ramp corridor cell with the window (the #137 window-climb repro)
|
||||
[InlineData(0x8A02017Eu)] // the cell beyond the window the player climbed into
|
||||
public void CorridorCell_PhysicsPolysAndPortals_DatInspection(uint envCellId)
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
|
|
@ -177,6 +179,94 @@ public class Issue137CorridorSeamInspectionTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #137 window climb: the dat truth for the player's collision spheres.
|
||||
/// Our InitPath places the head sphere at (sphereHeight − radius) = 0.72
|
||||
/// (capsule top 1.2 m); retail collides with the Setup's SPHERE LIST
|
||||
/// verbatim (CPhysicsObj::transition → init_sphere(GetNumSphere,
|
||||
/// GetSphere, scale)). Print human Setup 0x02000001's spheres.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HumanSetup_CollisionSpheres_DatTruth()
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
||||
"Documents", "Asheron's Call");
|
||||
if (!Directory.Exists(datDir))
|
||||
{
|
||||
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var setup = dats.Get<DatReaderWriter.DBObjs.Setup>(0x02000001u);
|
||||
Assert.NotNull(setup);
|
||||
|
||||
_out.WriteLine($"Setup 0x02000001: Height={setup!.Height:F3} Radius={setup.Radius:F3} " +
|
||||
$"StepUp={setup.StepUpHeight:F3} StepDown={setup.StepDownHeight:F3}");
|
||||
_out.WriteLine($"Spheres ({setup.Spheres.Count}):");
|
||||
foreach (var s in setup.Spheres)
|
||||
_out.WriteLine($" origin=({s.Origin.X:F3},{s.Origin.Y:F3},{s.Origin.Z:F3}) r={s.Radius:F3}");
|
||||
_out.WriteLine($"CylSpheres ({setup.CylSpheres.Count}):");
|
||||
foreach (var c in setup.CylSpheres)
|
||||
_out.WriteLine($" origin=({c.Origin.X:F3},{c.Origin.Y:F3},{c.Origin.Z:F3}) r={c.Radius:F3} h={c.Height:F3}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #137 window-climb geometry (2026-07-06): full world-space vertex dump
|
||||
/// of the shaft cell 0x8A02017E (all physics polys) and 0x8A020179's
|
||||
/// south-wall family — the opening's lintel/ceiling spans decide where
|
||||
/// retail blocks the head.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WindowShaft_FullPolyDump()
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
||||
"Documents", "Asheron's Call");
|
||||
if (!Directory.Exists(datDir))
|
||||
{
|
||||
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
foreach (var cellId in new[] { 0x8A02017Eu, 0x8A020179u })
|
||||
{
|
||||
var envCell = dats.Get<EnvCell>(cellId);
|
||||
Assert.NotNull(envCell);
|
||||
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell!.EnvironmentId);
|
||||
Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cs));
|
||||
|
||||
var rot = new System.Numerics.Quaternion(
|
||||
envCell.Position.Orientation.X, envCell.Position.Orientation.Y,
|
||||
envCell.Position.Orientation.Z, envCell.Position.Orientation.W);
|
||||
var world = System.Numerics.Matrix4x4.CreateFromQuaternion(rot)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(
|
||||
envCell.Position.Origin.X, envCell.Position.Origin.Y, envCell.Position.Origin.Z);
|
||||
|
||||
_out.WriteLine($"=== 0x{cellId:X8} full physics polys (world verts) ===");
|
||||
foreach (var (id, poly) in cs!.PhysicsPolygons)
|
||||
{
|
||||
var verts = poly.VertexIds;
|
||||
if (verts.Count < 3) continue;
|
||||
var w = new System.Collections.Generic.List<System.Numerics.Vector3>();
|
||||
foreach (var vid in verts)
|
||||
if (cs.VertexArray.Vertices.TryGetValue((ushort)vid, out var v))
|
||||
w.Add(System.Numerics.Vector3.Transform(v.Origin, world));
|
||||
var n = System.Numerics.Vector3.Normalize(
|
||||
System.Numerics.Vector3.Cross(w[1] - w[0], w[2] - w[0]));
|
||||
|
||||
// 017E: everything. 0179: south-wall family + ceilings only.
|
||||
if (cellId == 0x8A020179u && MathF.Abs(n.Y) < 0.3f && n.Z > -0.3f) continue;
|
||||
|
||||
var vs = string.Join(" ", w.ConvertAll(p => $"({p.X:F2},{p.Y:F2},{p.Z:F2})"));
|
||||
_out.WriteLine($" poly {id}: n=({n.X:F2},{n.Y:F2},{n.Z:F2}) verts={vs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mechanism-1 re-characterization (2026-07-06): the live hit normal
|
||||
/// (−1.00, 0.03, −0.03) at world (85.253, −39.776, −5.992) matches NO
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue