fix(physics #145): Slice 3 — carried-anchor membership; closes the far-town cascade
The outdoor membership pick derived the landblock origin from the terrain registry, which returns (0,0) for an UNSTREAMED neighbour — so a fresh far-town teleport at a landblock edge marched the cell id one block per physics tick (the cascade; the 17410 ACE rejects is its wire artifact). Fix: thread the CARRIED cell-relative frame anchor (body.Position - body.CellPosition.Frame.Origin) into the pick via SpherePath.CarriedBlockOrigin. That anchor IS the true landblock world origin, correct even for an unstreamed neighbour, so the pick re-derives the SAME (consistent) cell and never marches. - CellTransit.FindCellSet/BuildCellSetAndPickContaining: Vector3? carriedBlockOrigin (null default = legacy TryGetTerrainOrigin → every existing caller/test untouched). - PhysicsEngine.ResolveWithTransition: set the anchor from a SEEDED OUTDOOR body whose carried landblock matches the resolve cell (else null → legacy). - PlayerMovementController.SetPosition: 3-arg overload seeds CellPosition from the wire's (cell, local) via SnapToCell; 2-arg delegates with cellLocal=pos (anchor (0,0,0) == legacy → zero test churn). - GameWindow.CellLocalForSeed: the placement seam (_liveCenter used ONCE here to derive the cell-local; physics carries it forward without _liveCenter). Regression: TeleportFarTownRunawayTests (south + east edge, unstreamed neighbour). Core 1529 / App 480, zero regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
865aae8876
commit
6349ba49aa
6 changed files with 147 additions and 13 deletions
|
|
@ -677,7 +677,8 @@ public static class CellTransit
|
|||
Vector3 worldSphereCenter,
|
||||
float sphereRadius,
|
||||
uint currentCellId,
|
||||
out IReadOnlyCollection<uint> cellSet)
|
||||
out IReadOnlyCollection<uint> cellSet,
|
||||
Vector3? carriedBlockOrigin = null)
|
||||
{
|
||||
var spheres = new[]
|
||||
{
|
||||
|
|
@ -688,7 +689,7 @@ public static class CellTransit
|
|||
},
|
||||
};
|
||||
|
||||
return FindCellSet(cache, spheres, spheres.Length, currentCellId, out cellSet);
|
||||
return FindCellSet(cache, spheres, spheres.Length, currentCellId, out cellSet, carriedBlockOrigin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -701,11 +702,12 @@ public static class CellTransit
|
|||
IReadOnlyList<Sphere> worldSpheres,
|
||||
int numSpheres,
|
||||
uint currentCellId,
|
||||
out IReadOnlyCollection<uint> cellSet)
|
||||
out IReadOnlyCollection<uint> cellSet,
|
||||
Vector3? carriedBlockOrigin = null)
|
||||
{
|
||||
var containing = BuildCellSetAndPickContaining(
|
||||
cache, worldSpheres, numSpheres, currentCellId,
|
||||
out var candidates);
|
||||
carriedBlockOrigin, out var candidates);
|
||||
cellSet = candidates;
|
||||
return containing;
|
||||
}
|
||||
|
|
@ -715,6 +717,7 @@ public static class CellTransit
|
|||
IReadOnlyList<Sphere> worldSpheres,
|
||||
int numSpheres,
|
||||
uint currentCellId,
|
||||
Vector3? carriedBlockOrigin,
|
||||
out CellArray candidates)
|
||||
{
|
||||
// Ordered, deduped candidate array — retail CELLARRAY (add_cell @701036).
|
||||
|
|
@ -729,11 +732,21 @@ public static class CellTransit
|
|||
float sphereRadius = worldSpheres[0].Radius;
|
||||
uint currentLow = currentCellId & 0xFFFFu;
|
||||
|
||||
// #106: the current block's world origin converts the world-frame sphere
|
||||
// coords into retail's block-local frame for the LandDefs lcoord math.
|
||||
// Unregistered terrain (tests; pre-stream) falls back to Vector3.Zero —
|
||||
// the legacy anchor-frame assumption (world frame == block-local frame).
|
||||
cache.CellGraph.TryGetTerrainOrigin(currentCellId, out var blockOrigin);
|
||||
// #145: the carried cell-relative frame supplies the TRUE landblock world
|
||||
// origin (body.Position - body.CellPosition.Frame.Origin) — correct even
|
||||
// for an UNSTREAMED neighbour, where TryGetTerrainOrigin returns (0,0) and
|
||||
// the pick marches the cell id one block per tick (the far-town cascade).
|
||||
// Falls back to the terrain registry for unseeded movers (NPCs/tests) and
|
||||
// indoor seeds. The caller guarantees the anchor's landblock == currentCellId's
|
||||
// (PhysicsEngine passes body.CellPosition.ObjCellId as the cell when seeded).
|
||||
//
|
||||
// #106: blockOrigin converts the world-frame sphere coords into retail's
|
||||
// block-local frame for the LandDefs lcoord math.
|
||||
Vector3 blockOrigin;
|
||||
if (carriedBlockOrigin is { } carriedAnchor)
|
||||
blockOrigin = carriedAnchor;
|
||||
else
|
||||
cache.CellGraph.TryGetTerrainOrigin(currentCellId, out blockOrigin);
|
||||
|
||||
// #112 rider: outdoor candidates may win the pick only when retail would
|
||||
// have admitted them — outdoor seeds always; indoor seeds only when a
|
||||
|
|
|
|||
|
|
@ -974,6 +974,22 @@ public sealed class PhysicsEngine
|
|||
|
||||
transition.SpherePath.InitPath(currentPos, targetPos, cellId, sphereRadius, sphereHeight);
|
||||
|
||||
// #145: supply the carried cell-relative frame anchor to the outdoor
|
||||
// membership pick. body.Position - body.CellPosition.Frame.Origin is the TRUE
|
||||
// landblock world origin, correct even for an UNSTREAMED neighbour — replacing
|
||||
// the terrain-registry origin that returns (0,0) and marches the cell id one
|
||||
// landblock per tick (the #145 far-town cascade). Engaged only for a SEEDED
|
||||
// OUTDOOR body whose carried landblock matches the resolve cell (the controller
|
||||
// passes body.CellPosition.ObjCellId for the outdoor case, so they agree);
|
||||
// null otherwise → legacy TryGetTerrainOrigin for NPCs/tests/indoor.
|
||||
transition.SpherePath.CarriedBlockOrigin =
|
||||
body is not null
|
||||
&& (cellId & 0xFFFFu) is >= 1u and <= 0x40u // resolve cell is an outdoor landcell
|
||||
&& (body.CellPosition.ObjCellId & 0xFFFFu) is >= 1u and <= 0x40u // carried cell is outdoor (seeded)
|
||||
&& (cellId >> 16) == (body.CellPosition.ObjCellId >> 16) // same landblock → anchor consistent
|
||||
? body.Position - body.CellPosition.Frame.Origin
|
||||
: null;
|
||||
|
||||
if (isOnGround && body is not null
|
||||
&& body.WalkablePolygonValid
|
||||
&& body.WalkableVertices is { Length: >= 3 })
|
||||
|
|
|
|||
|
|
@ -335,6 +335,15 @@ public sealed class SpherePath
|
|||
public uint CurCellId;
|
||||
public uint CheckCellId;
|
||||
|
||||
// #145: the carried cell-relative frame anchor (body.Position -
|
||||
// body.CellPosition.Frame.Origin) — the TRUE landblock world origin, set at
|
||||
// resolve entry from a SEEDED OUTDOOR player body. Threaded into the outdoor
|
||||
// membership pick (CellTransit.FindCellSet) so it no longer derives the origin
|
||||
// from the terrain registry (which returns (0,0) for an unstreamed neighbour →
|
||||
// the far-town cell-march cascade). Null for unseeded movers (NPCs/tests) and
|
||||
// indoor seeds → legacy TryGetTerrainOrigin.
|
||||
public Vector3? CarriedBlockOrigin;
|
||||
|
||||
// Per-step offset
|
||||
public Vector3 GlobalOffset;
|
||||
|
||||
|
|
@ -2289,7 +2298,8 @@ public sealed class Transition
|
|||
sp.HitsInteriorCell = false;
|
||||
|
||||
uint containingCellId = CellTransit.FindCellSet(
|
||||
engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out var cellSet);
|
||||
engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out var cellSet,
|
||||
sp.CarriedBlockOrigin);
|
||||
LogIssue98CellSetSummary(engine, containingCellId, cellSet, footCenter, sphereRadius);
|
||||
|
||||
if ((sp.CheckCellId & 0xFFFFu) >= 0x0100u
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue