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>
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>
Retail CACQualities::EnchantAttribute (0x00594570), EnchantAttribute2nd
(0x00594670, already ported for #6), and EnchantSkill (0x005947b0) are the
three enchantment-composition functions the Character window's Attributes
and Skills tabs depend on. Primary attributes never reference the vitae
singleton in retail (only Attribute2nd/Skill do) — confirmed directly from
the decompiled function bodies, not assumed.
EnchantmentMath.GetMod gains requiredType/includeVitae parameters (default
to the prior behavior) so a numeric StatMod key collision across domains
(e.g. key=1 is both Strength and MaxHealth) can't leak a buff into the
wrong computation. Spellbook.GetAttributeMod/GetSkillMod and
LocalPlayerState.GetEffectiveAttribute/GetEffectiveSkill/
GetSkillVitaeModifier wire the retail chain through to the panel.
CharacterSheetProvider now reports the effective value as the main number
and CharacterSkill.CurrentLevel is no longer an alias of BaseLevel (this
also activates the previously-dead SkillValueColor buffed/debuffed row
coloring). CharacterStatController's footer-title parenthetical is cited
from gmAttributeUI::DisplaySelectionFooter_Attribute (0x0049d280) and
gmSkillUI::DisplaySelectionFooter_Trained (0x0049b860) +
SkillInfoRegion::GetVitaeModifier (0x004f0fa0): skills show up to two
segments (vitae's own contribution, then the buff-only residual), while
vitae-immune attributes show at most one; no parenthetical when the delta
is zero. The panel now refreshes on Spellbook.EnchantmentsChanged, not only
raw property/attribute updates.
Core goldens cover the user-reported 33% vitae example (303->203, "(-100)"
exactly), buff+vitae composition, and the attribute vitae-immunity finding.
Provider/controller tests cover the full row-click -> footer-title path and
live refresh. Full solution suite passes with zero failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three permanent low-volume probes: installed vitae record (id/type/key/
value) at ReplaceManifest, any vitae record dropped by GetMod's
spell-table prepass (retail's _vitae singleton never runs family
stacking - if this fires, hoist the Bucket-4 branch), and the full
recompute state (base skills, runMod, effective skills, active count).
One instrumented relog decides the break point.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>