Merge branch 'main' into claude/eloquent-hugle-42119e
# Conflicts: # .gitignore
This commit is contained in:
commit
093cdb6d57
38 changed files with 5852 additions and 216 deletions
|
|
@ -882,8 +882,17 @@ public sealed class PlayerMovementController
|
|||
// Falls back to simple Z-snap if transition fails.
|
||||
var resolveResult = _physics.ResolveWithTransition(
|
||||
preIntegratePos, postIntegratePos, CellId,
|
||||
sphereRadius: 0.48f, // human player radius from Setup
|
||||
sphereHeight: 1.2f, // human player height from Setup
|
||||
sphereRadius: 0.48f, // human Setup 0x02000001 sphere radius (dat: 0.480)
|
||||
// #137 window climb (2026-07-06): sphereHeight is the CAPSULE TOP
|
||||
// (InitPath places the head sphere center at height − radius). The
|
||||
// dat human Setup 0x02000001 has Spheres[1].Origin.Z = 1.350
|
||||
// (top 1.830) and Height = 1.835 — retail collides with that
|
||||
// sphere list verbatim (CPhysicsObj::transition 0x00512dc0 →
|
||||
// init_sphere(GetNumSphere, GetSphere, scale)). The old 1.2f put
|
||||
// the head sphere center at 0.72 — the top 0.63 m of the
|
||||
// character had NO collision, letting the player climb into a
|
||||
// 1.3 m window alcove head-through-lintel. Register TS-46.
|
||||
sphereHeight: 1.835f,
|
||||
stepUpHeight: StepUpHeight,
|
||||
stepDownHeight: StepDownHeight, // L.2.3a: from Setup.StepDownHeight
|
||||
isOnGround: _body.OnWalkable,
|
||||
|
|
|
|||
|
|
@ -3714,7 +3714,8 @@ public sealed class GameWindow : IDisposable
|
|||
ownerId: entity.Id,
|
||||
entityPosition: entity.Position,
|
||||
entityRotation: entity.Rotation,
|
||||
isDynamic: true); // #143: server-object lights take the D3D dynamic path (1/d att, range×1.5)
|
||||
isDynamic: true, // #143: server-object lights take the D3D dynamic path (1/d att, range×1.5)
|
||||
cellId: entity.ParentCellId ?? 0u); // A7 #176/#177: scope to the owning cell's visibility
|
||||
foreach (var ls in loaded)
|
||||
_lightingSink.RegisterOwnedLight(ls);
|
||||
}
|
||||
|
|
@ -4193,9 +4194,22 @@ public sealed class GameWindow : IDisposable
|
|||
// 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.
|
||||
// #175 (2026-07-05): BSP part shapes must pose at the motion table's
|
||||
// DEFAULT-STATE frame (the closed pose — what the sequencer renders
|
||||
// for an idle entity and what retail's live CPhysicsPart pose is),
|
||||
// not the Setup's placement frame. The Facility Hub double door
|
||||
// (Setup 0x02000C9D) places its panels AJAR in the placement frame
|
||||
// (yaw −150°/−30°, −0.44 m behind the doorway) while rendering
|
||||
// closed — the user embedded into the visual door on one side and
|
||||
// hit a phantom slab on the other. Null (no motion table / no
|
||||
// cycle / part-count mismatch) falls back to placement frames.
|
||||
var closedPose = MotionTableDefaultPose(
|
||||
spawn.MotionTableId ?? 0u, setup.Parts.Count);
|
||||
|
||||
var raw = AcDream.Core.Physics.ShadowShapeBuilder.FromSetup(
|
||||
setup, entScale,
|
||||
id => _physicsDataCache.GetGfxObj(id)?.BSP?.Root is not null);
|
||||
id => _physicsDataCache.GetGfxObj(id)?.BSP?.Root is not null,
|
||||
partPoseOverride: closedPose);
|
||||
|
||||
// Substitute the real bounding-sphere radius for BSP shapes —
|
||||
// the pure builder's 2.0 placeholder works for typical doors
|
||||
|
|
@ -4284,6 +4298,37 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #175: the motion table's default-state pose (the closed pose for
|
||||
/// doors) — the derivation lives in
|
||||
/// <see cref="AcDream.Core.Physics.Motion.MotionTablePose"/> (Core,
|
||||
/// dat-conformance-tested; the first cut here used a bare-style cycle
|
||||
/// key, always missed, and silently no-oped — the "175 is not fixed"
|
||||
/// report). Returns null → placement-frame fallback.
|
||||
/// </summary>
|
||||
private IReadOnlyList<DatReaderWriter.Types.Frame>? MotionTableDefaultPose(
|
||||
uint motionTableId, int partCount)
|
||||
{
|
||||
if (motionTableId == 0u || partCount == 0 || _dats is null) return null;
|
||||
|
||||
var mt = _dats.Get<DatReaderWriter.DBObjs.MotionTable>(motionTableId);
|
||||
if (mt is null) return null;
|
||||
|
||||
var pose = AcDream.Core.Physics.Motion.MotionTablePose.DefaultStatePartFrames(
|
||||
mt, id => _dats.Get<DatReaderWriter.DBObjs.Animation>(id));
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
string desc = pose is null ? "null->placement-fallback"
|
||||
: System.FormattableString.Invariant(
|
||||
$"part0=({pose[0].Origin.X:F2},{pose[0].Origin.Y:F2},{pose[0].Origin.Z:F2})");
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[shape-pose] mt=0x{motionTableId:X8} parts={partCount} {desc}"));
|
||||
}
|
||||
|
||||
return pose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-W4: one-time per-remote wiring of the animation-dispatch stack —
|
||||
/// the persistent <see cref="RemoteMotion.Sink"/> (ObservedOmega turn
|
||||
|
|
@ -4318,7 +4363,20 @@ public sealed class GameWindow : IDisposable
|
|||
rmForSink.ObservedOmega = System.Numerics.Vector3.Zero,
|
||||
};
|
||||
rm.Motion.DefaultSink = rm.Sink;
|
||||
rm.Motion.RemoveLinkAnimations = ae.Sequencer.RemoveAllLinkAnimations;
|
||||
// #174 (2026-07-05): the RemoveLinkAnimations seam is retail
|
||||
// CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to
|
||||
// CPartArray::HandleEnterWorld 0x00517d70 →
|
||||
// MotionTableManager::HandleEnterWorld 0x0051bdd0: strip the
|
||||
// sequence's link animations AND drain pending_animations
|
||||
// completely (each pop fires MotionDone → CMotionInterp pops its
|
||||
// pending_motions node in lockstep). The previous binding was the
|
||||
// bare sequence strip — every LeaveGround (jump) stripped the
|
||||
// animations that queued manager nodes were counting down on,
|
||||
// orphaning them (NumAnims>0, anims gone) and permanently damming
|
||||
// BOTH queues: MotionsPending() never drained again, so every
|
||||
// armed moveto (ACE's walk-to-door mt-6, the close-range use turn)
|
||||
// starved — the "door only works on a fresh session" bug.
|
||||
rm.Motion.RemoveLinkAnimations = () => ae.Sequencer.Manager.HandleEnterWorld();
|
||||
rm.Motion.InitializeMotionTables = () => ae.Sequencer.Manager.InitializeState();
|
||||
// R3-W5: the per-op zero-tick flush (CPhysicsObj::CheckForCompletedMotions
|
||||
// 0x0050fe30 -> MotionTableManager.CheckForCompletedMotions).
|
||||
|
|
@ -7670,7 +7728,8 @@ public sealed class GameWindow : IDisposable
|
|||
datSetup,
|
||||
ownerId: entity.Id,
|
||||
entityPosition: entity.Position,
|
||||
entityRotation: entity.Rotation);
|
||||
entityRotation: entity.Rotation,
|
||||
cellId: entity.ParentCellId ?? 0u); // A7 #176/#177: scope to the owning cell's visibility
|
||||
foreach (var ls in loaded)
|
||||
_lightingSink.RegisterOwnedLight(ls);
|
||||
}
|
||||
|
|
@ -9350,6 +9409,11 @@ public sealed class GameWindow : IDisposable
|
|||
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||
Camera = camera,
|
||||
CameraWorldPosition = camPos,
|
||||
// A7 #176/#177: once DrawInside has resolved the visible-cell set,
|
||||
// rebuild the point-light pool from ONLY those cells' lights (retail's
|
||||
// per-frame add_*_lights over visible_cell_table). The renderers hold a
|
||||
// reference to the same PointSnapshot list, rebuilt in place here.
|
||||
RebuildScopedLights = visible => Lighting.BuildPointLightSnapshot(camPos, visible),
|
||||
Frustum = frustum,
|
||||
PlayerLandblockId = playerLb,
|
||||
AnimatedEntityIds = animatedIds,
|
||||
|
|
@ -10461,14 +10525,16 @@ public sealed class GameWindow : IDisposable
|
|||
if (rm.CellId != 0 && _physicsEngine.LandblockCount > 0)
|
||||
{
|
||||
// Sphere dims match local-player defaults (human Setup
|
||||
// bounds — ~0.48m radius, ~1.2m height). Good enough for
|
||||
// 0x02000001: sphere radius 0.480, capsule top 1.835 =
|
||||
// Setup.Height; see the #137 TS-46 note at the
|
||||
// PlayerMovementController call). Good enough for
|
||||
// grounded humanoid remotes; can be setup-derived later
|
||||
// if creatures of wildly different sizes need different
|
||||
// collision profiles.
|
||||
var resolveResult = _physicsEngine.ResolveWithTransition(
|
||||
preIntegratePos, postIntegratePos, rm.CellId,
|
||||
sphereRadius: 0.48f,
|
||||
sphereHeight: 1.2f,
|
||||
sphereHeight: 1.835f,
|
||||
stepUpHeight: 0.4f, // L.2.3a: retail human-scale, was 2.0f
|
||||
stepDownHeight: 0.4f, // L.2.3a: retail human-scale, was 0.04f
|
||||
// K-fix9 (2026-04-26): mirror the K-fix7 gate —
|
||||
|
|
@ -10496,6 +10562,55 @@ public sealed class GameWindow : IDisposable
|
|||
if (resolveResult.CellId != 0)
|
||||
rm.CellId = resolveResult.CellId;
|
||||
|
||||
// #173 (2026-07-05): retail CPhysicsObj::handle_all_collisions
|
||||
// (pc:282699-282715) runs after EVERY SetPositionInternal —
|
||||
// remote objects included; a VectorUpdate-launched jump arc
|
||||
// is ordinary object physics in retail. acdream ported the
|
||||
// velocity reflection for the LOCAL player only (L.3a,
|
||||
// PlayerMovementController ~:940), so a remote jumping into
|
||||
// a dungeon ceiling had its POSITION pinned by the sweep
|
||||
// while its +Z velocity kept integrating — the char hovered
|
||||
// at the roof until gravity burned the arc off, landing
|
||||
// late (user report, 0x0007 dungeon). Mirror the local
|
||||
// site exactly:
|
||||
// v_new = v − (1 + elasticity)·dot(v, n)·n
|
||||
// with the AD-25 suppression (bounce only when airborne
|
||||
// before AND after — corridor slides and landings don't
|
||||
// reflect; the landing snap below keeps its
|
||||
// `Velocity.Z <= 0` gate intact). Inelastic movers
|
||||
// (missiles, later) zero out instead.
|
||||
if (resolveResult.CollisionNormalValid)
|
||||
{
|
||||
bool prevOnWalkable = rm.Body.OnWalkable;
|
||||
bool nowOnWalkable = resolveResult.IsOnGround;
|
||||
bool applyBounce = rm.Body.State.HasFlag(
|
||||
AcDream.Core.Physics.PhysicsStateFlags.Sledding)
|
||||
? !(prevOnWalkable && nowOnWalkable)
|
||||
: (!prevOnWalkable && !nowOnWalkable);
|
||||
if (applyBounce)
|
||||
{
|
||||
if (rm.Body.State.HasFlag(
|
||||
AcDream.Core.Physics.PhysicsStateFlags.Inelastic))
|
||||
{
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
var vRem = rm.Body.Velocity;
|
||||
var nRem = resolveResult.CollisionNormal;
|
||||
float dotVN = System.Numerics.Vector3.Dot(vRem, nRem);
|
||||
if (dotVN < 0f)
|
||||
{
|
||||
rm.Body.Velocity =
|
||||
vRem + nRem * (-(dotVN * (rm.Body.Elasticity + 1f)));
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
Console.WriteLine(
|
||||
$"VU.bounce guid=0x{serverGuid:X8} n=({nRem.X:F2},{nRem.Y:F2},{nRem.Z:F2}) vZ {vRem.Z:F2}->{rm.Body.Velocity.Z:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// K-fix15 (2026-04-26): post-resolve landing
|
||||
// detection for airborne remotes. Mirrors
|
||||
// PlayerMovementController's local-player landing
|
||||
|
|
@ -13699,7 +13814,14 @@ public sealed class GameWindow : IDisposable
|
|||
// R3-W4: bind the player interp's retail seams to the player
|
||||
// sequencer — LeaveGround/HitGround strip transition links here
|
||||
// (the K-fix18 replacement).
|
||||
_playerController.Motion.RemoveLinkAnimations = playerSeq.RemoveAllLinkAnimations;
|
||||
// #174 (2026-07-05): the seam is HandleEnterWorld (strip + FULL
|
||||
// queue drain), not the bare sequence strip — see the remote
|
||||
// bind site's comment (retail 0x0050fe20 → 0x00517d70 →
|
||||
// 0x0051bdd0). The bare strip orphaned every pending manager
|
||||
// node on each jump's LeaveGround; the local player's
|
||||
// pending_motions then never drained and every armed moveto
|
||||
// starved (#174: doors unusable after the first jump/run).
|
||||
_playerController.Motion.RemoveLinkAnimations = () => playerSeq.Manager.HandleEnterWorld();
|
||||
_playerController.Motion.InitializeMotionTables =
|
||||
() => playerSeq.Manager.InitializeState();
|
||||
_playerController.Motion.CheckForCompletedMotions =
|
||||
|
|
|
|||
|
|
@ -146,6 +146,12 @@ public sealed class RetailPViewRenderer
|
|||
prepareCells = _lookInPrepareScratch;
|
||||
}
|
||||
|
||||
// A7 #176/#177: scope this frame's point-light pool to the cells actually being
|
||||
// drawn, NOW that the flood has resolved the visible set (retail collects lights
|
||||
// per-frame over visible_cell_table). Must run before the cell/entity draws below
|
||||
// that select from LightManager.PointSnapshot.
|
||||
ctx.RebuildScopedLights?.Invoke(prepareCells);
|
||||
|
||||
_envCells.PrepareRenderBatches(
|
||||
ctx.ViewProjection,
|
||||
ctx.CameraWorldPosition,
|
||||
|
|
@ -1102,6 +1108,16 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
public Action? DrawUnattachedSceneParticles { get; init; }
|
||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
|
||||
/// <summary>A7 #176/#177: rebuild the point-light snapshot scoped to the cells
|
||||
/// this frame actually draws — invoked AFTER the portal flood resolves the visible
|
||||
/// set and BEFORE any cell/entity draw (the faithful port of retail's per-frame
|
||||
/// light collection: <c>CObjCell::add_*_to_global_lights</c> walked over
|
||||
/// <c>CEnvCell::visible_cell_table</c>). The argument is every cell drawn this frame
|
||||
/// (main flood + interior-root look-ins). A cell-less light (viewer fill) is kept
|
||||
/// regardless. Null-safe: outdoor/no-flood callers leave it unset and keep the
|
||||
/// legacy full-pool snapshot.</summary>
|
||||
public Action<IReadOnlySet<uint>>? RebuildScopedLights { get; init; }
|
||||
}
|
||||
|
||||
public sealed class RetailPViewFrameResult
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue