User-verified: casting fixed (exhaustion-edge gate) and monster attack
animations restored (spawn settle placement + lost-cell retry). Final
session evidence: 14/15 spawn settles grounded; Falling-refusal spam
collapsed 2,954 -> 15 transient pre-settle lines.
Strips the [UM-ACT]/[MT-FAIL]/[SPAWN-PLACE]/[remote-edge] probes, the
MotionInterpreter.DiagnosticGuid plumbing, and the two throwaway probe
tests (motion-table attack sweep, vitae color dump - both findings are
recorded in ISSUES/research). Complete Release suite: 10,030 passed /
5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The [SPAWN-PLACE] probe showed placements succeeding with contact=False:
find_placement_pos validates the spot but the sphere sits a few cm above
the floor with no touch. Retail gains spawn contact from the FIRST
GRAVITY FRAME (every CPhysicsObj simulates, falls, touches); our
stationary remotes never run a physics frame. The seed now compresses
that settle: a short downward ResolveWithTransition from the server
position snaps the body onto the floor, and its touch grants the contact
plane + CONTACT/ON_WALKABLE via the verbatim commit. No floor within
reach = stays airborne, exactly like retail's fall.
The retry predicate now watches the CONTACT transient (the flag
contact_allows_move reads) instead of ContactPlaneValid - a DR-tick
writeback can set plane DATA from last-known state without real contact,
which is why the heavy attackers in the retry-session log got exactly
one placement attempt and then 250+ refused Falling dispatches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The creation-only spawn placement could no-op or fail during the login
flood (cell id not yet hydrated / streaming collision not resident) with
nothing retrying - monsters created in that window stayed
airborne-flagged forever and their action animations remained refused.
Retail's answer to 'object addressed before its cell exists' is the
CObjectMaint lost-cell list (GotoLostCell): park, re-place when the cell
is available. The UM dispatch path now retries SeedRemoteSpawnPlacement
while the body has never been successfully placed (no contact AND no
stored plane); one success ends the retries.
Probes: [SPAWN-PLACE] logs each placement outcome (guid/cell/ok/
contact/walkable); [UM-ACT]/[MT-FAIL] now carry the owning guid via
MotionInterpreter.DiagnosticGuid (probe-identity-attribution lesson)
plus the body's live contact/walkable flags.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The [MT-FAIL] probe caught combat-stance monsters constantly failing to
dispatch 0x40000015 (Falling): their bodies were airborne-flagged while
standing. contact_allows_move (0x00528dd0) requires Contact+OnWalkable
and silently refuses every action animation for an airborne mover - a
spawned-standing monster's swings never played until it first moved.
Retail never has this state: CreateObject spawns run the placement
transition (CPhysicsObj::SetPosition -> SetPositionInternal 0x00515330),
which establishes CONTACT/ON_WALKABLE from the floor at spawn. Our
remote creation seeded a raw position with no placement.
SeedRemoteSpawnPlacement mirrors RemoteTeleportPlacement: engine
placement resolve (Setup-derived cylinder, TS-46) + the verbatim
CommitSetPositionTransition, wired at BOTH RemoteMotion creation sites
(UM-triggered creation - so a first-ever-UM attack animates in the same
packet - and ordinary first-UP creation). Unplaceable results leave the
body airborne exactly like a failed retail placement.
Also adds the [UM-ACT] (wire action items + stamp-gate verdict) and
[MT-FAIL] (refused animation dispatches) probes, riding
ACDREAM_DUMP_MOTION=1, which are what convicted the body state.
Complete Release suite: 10,032 passed / 5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail calls CPhysicsObj::report_exhaustion from exactly one site -
CommandInterpreter::HandleExhaustion (0x006b3c70), a notification handler
for the stamina-exhaustion EVENT. Campaign P P1 wired it to every
movement-stats application instead (every stamina regen/drain tick), and
each call re-dispatches the current movement state through the animation
sink - truncating any in-flight action animation. The diagnostic session
log shows 490 spurious casting-stance re-queues in one short session:
'sometimes stuck in spell animations' was every stamina tick that
collided with a cast gesture's play window.
The re-apply now fires only when the exhausted state (stamina == 0)
transitions, matching retail's event semantics. Stats still reach
PlayerWeenie immediately via RuntimeMovementSkillProjection.ApplyTo.
Also adds the [remote-edge] probe (rides ACDREAM_DUMP_MOTION=1): one
line per remote HitGround/LeaveGround - each such edge drains the
mover's pending action animations (retail HandleEnterWorld), the
working theory for intermittently missing monster attack swings.
Complete Release suite: 10,026 passed / 5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail jump landings BOUNCE: the floor touch records both a contact plane
(grounding) AND a collision normal (collided_with_environment), and
handle_all_collisions reflects the unmodified impact velocity off it at
5% elasticity (v += -(v.n)(elasticity+1).n, DEFAULT_ELASTICITY 0.05
@0x007c6a7c). Our transition already recorded both facts; the bounce was
suppressed by the AD-25 adaptation stack in the per-tick commit: a
Velocity.Z<=0 landing gate (needed because the resolver glued ascending
movers to the ground) plus a landing Velocity.Z=0 hand-zero whose stated
purpose was making the reflect a no-op. Downhill glided instead of
bouncing, flat-ground landings had no pop, and uphill jumps flapped
between grounded/airborne against the animation machine.
Three retail mechanisms replace the stack:
- check_contact (0x0050f5b0) seeding in ResolveWithTransition: a body in
CONTACT seeds the transition's contact only while v.contactPlane.N <=
0.0002; moving away seeds the last-known plane alone (get_object_info
0x00511cc0). Ascending jumps therefore run contact-free (ballistic, no
glue) - the gate's reason-for-being is gone. The plane requirement is
strict: Contact-without-plane is unrepresentable in retail.
- SetPositionInternal-shaped commit (0x00515330, byte-read end-to-end,
velocity-sign-FREE): contact purely from the transition's contact
plane, HitGround on the airborne->walkable edge, HandleAllCollisions
with unmodified impact velocity. Whole commit gated on Ok &&
candidateMoved (retail pc:283657 skips SetPositionInternal entirely
when the candidate did not move) - a standing body's contact state is
never re-derived, which is what keeps rest bit-stable (AD-41 updated).
- Byte decodes: gate override state&0x800000=Sledding, zero branch
state&0x20000=Inelastic, reflect strictly dot<0 - our port already had
all three correct.
Settle: real landings (>=0.25 m/s) bounce and decay geometrically;
smaller impacts are consumed by retail's unconditional small-velocity
zero, so standing never micro-bounces. Re-baselines documented in place:
landing-survival pin measures decay post-settle; LiveCompare_Tick0/376
pin the new IsOnGround=false on zero-move ticks (captured true was the
retired seed echo; tick 376's captured body carries an 11.8 m/s grounded
velocity from the deleted get_state_velocity-overwrite era); de-overlap
fixture now carries the plane real grounded bodies always have. New
pins: LandingBounceSeedingTests (ascent no-seed, rest keeps contact,
strict plane, slope 5% reversal + tangential preservation, Sledding
override).
Investigation + implementation record:
docs/research/2026-07-30-landing-bounce-family.md. Complete Release
suite: 10,031 passed / 5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/ISSUES.md: #265 and #166 updated with the root cause and fix from
the prior two commits; closure of both pends the user's visual-gate
acceptance. #265 also records the confirmed-separate uphill-bounce
finding (AD-25, byte-exact retail, out of scope). #166 records that the
Campaign P visual-matrix recheck it was waiting on DID happen and found
the glide/bounce still missing even with AD-25/AP-7/AD-55/TS-4 all
landed - that negative result is what triggered the #265 capture bisect
and this fix.
docs/architecture/retail-divergence-register.md: AP-7's retirement note
corrected. The row's original claim ("no horizontal velocity to hammer")
undersold the gap - calc_friction was structurally unreachable with
meaningful data on any grounded path, not just inert on the root-motion
path. No new row filed: this change ports retail's mechanism faithfully
and does not introduce a new deviation.
docs/research/2026-07-30-265-capture-bisect.md: full "as-fixed" addendum
(new section 9) recording the implementation - the fix mechanism, fixture
results (freeze reproduced under the old model, slide+decay proven under
the new one), the downhill-direction derivation for the synthetic decay
case, the two separate mechanisms found while building the Runtime tests
(LeaveGround's edge-timing recompute, AP-77's no-sink fallback), the
uphill-bounce orthogonality proof, and final test totals.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two new PlayerMovementController-level tests, exercising the real
production controller (not just the Core-level composed model in the
prior commit):
- Update_AnimationRootMotion_WalkSpeedUnaffectedByResidualVelocityFix:
ordinary root-motion walking (no fall/collision in flight) advances by
exactly the authored per-tick delta for 30 ticks with BodyVelocity
staying exactly zero throughout - the fix is a complete no-op for the
common case, pinning the L.3c hazard
(claude-memory/project_physics_collision_digest.md's DO-NOT-RETRY
table) at the Runtime level alongside the existing Core-level
GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests pin
(unmodified, still green).
- Update_RunningJumpLandsOnFlatGround_ResidualVelocitySurvivesAndDecays_NotFrozen:
a real charged running jump lands on flat ground and its residual
horizontal speed survives the first post-landing tick, then measurably
decays (dot(velocity, (0,0,1)) ~ 0 < 0.25, so friction engages here,
unlike the sloped roof capture where it doesn't).
Building the second test surfaced two genuinely separate, already-
correctly-scoped mechanisms unrelated to #265/#166, requiring no
production change: MotionInterpreter.LeaveGround (CMotionInterp::
LeaveGround 0x00528b00, R3-W4/J7/J8) recomputes velocity from the
CURRENT interpreted command on the grounded->airborne edge tick, so the
test holds Forward for one extra tick before releasing it; and
MotionInterpreter.ApplyCurrentMovementInterpreted's AP-77 "animation-less
/headless movement fallback" (already correctly scoped in the
divergence register) independently rewrites grounded velocity when no
DefaultSink is wired, so the test wires a minimal
FakeAnimationDispatchSink to match production's always-wired sink.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Capture bisect (docs/research/2026-07-30-265-capture-bisect.md, mined
from artifacts/matrix-session2-resolve.jsonl records 3415-3434) traced
#265's lost roof slides / permanent landing freeze and #166's missing
downhill sled to a pre-existing (2026-07-20, ten days before Campaign P
- not a regression) mechanism in PlayerMovementController.cs's grounded
quantum block: it hand-zeroed Velocity.X/Y to exactly zero every tick
once OnWalkable whenever animation root motion drives the walk (the
production graphical local-player path), discarding any residual
horizontal momentum a fall left on the body before calc_friction
(AP-7/AD-55, already correctly ported) or PhysicsBody.
UpdatePhysicsInternal's Euler integrator ever got a chance to act on it.
Two changes:
1. PhysicsEngine.cs now syncs body.GroundNormal (the vector
calc_friction dots velocity against, per retail
CPhysicsObj::calc_friction 0x0050ee70's `contact_plane.Normal` read)
from the committed ContactPlane.Normal at the same commit point that
already publishes ContactPlane. GroundNormal had zero production
writers before this and silently defaulted to Vector3.UnitZ forever
- even surviving velocity would have been tested against a fake
flat-ground normal on any real slope. Core-level, so player, remote,
ordinary, and projectile movers all benefit uniformly.
2. PlayerMovementController.cs's grounded block no longer reconstructs
Velocity at all for the animation-root-motion case (only the
headless/test-controller get_state_velocity fallback still does,
unchanged). Root motion continues to fully own commanded locomotion;
this only stops destroying whatever Velocity already holds, letting
it compose with root motion through the same ResolveWithTransition
sweep exactly as retail's CPhysicsObj::UpdatePositionInternal
composes both channels.
Symptom (a), the uphill-jump bounce, traces to a SEPARATE, byte-exact
(re-verified against acclient_2013_pseudo_c.txt:282647-282760),
already-closed retail mechanism (AD-25, PhysicsObjUpdate.
HandleAllCollisions's shouldReflect gate) - confirmed orthogonal to this
fix, not addressed here (see the research doc's as-fixed addendum §9.5).
Issue265SteepSlopeCaptureBisectTests.cs gains a composed harness
(ReplayRealRoofLandingComposed) mirroring PlayerMovementController.cs's
per-tick composition against Core types only, proving: the old model
reproduces the mined freeze exactly; the new model survives the landing
and slides continuously (the real captured geometry glides at constant
velocity per retail's own dot>=0.25 early-return - AP-7); a synthetic
dot<0.25 case shows genuine exponential decay via calc_friction; and a
synthetic uphill-bounce case proves the fix changes nothing about
HandleAllCollisions's reflection decision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raw byte decode of MovementSystem::GetRunRate (0x006b0950, PDB-paired
binary): fild skill; fcom [800f]; fnstsw; test ah, 0x44; jp general —
the C2/C3 parity idiom whose 18/4 fall-through executes ONLY at
skill == 800 exactly. ACE read this as >= 800 ('max run speed?') and
Campaign P P1 inherited that misread when BN dropped the arithmetic,
flat-lining every maxed character at 4.5 (retail-true ~3.70, +21%) and
erasing the vitae differential (both 10200 and 15225 sat above 800).
The [stat-chain] live capture proved the enchant chain correct end to
end (vitae 0.67 -> eff run 10200 -> controller), isolating the formula.
General path byte-verified: (loadMod*(skill/(skill+200)*11)+4)/scaling/4.
InqMaxRunRate's skill=9999 probe gets ~3.6961, not 4.5.
Golden tests pin the 799/800/801 straddle and the maxed-skill vitae
differential; pseudocode doc §6 carries the decode plus a do-not-
reimport-ACE warning. Complete Release suite: 10,025 passed / 5 skips.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>