fix #266: retail run-rate 800 branch is exact-equality sentinel, not a cap
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>
This commit is contained in:
parent
bd3ade625f
commit
61e959169b
5 changed files with 85 additions and 28 deletions
|
|
@ -156,22 +156,30 @@ first, don't guess the format string.
|
|||
|
||||
## #266 — Local player faster than a comparable retail character
|
||||
|
||||
**Status:** OPEN — needs a controlled comparison capture
|
||||
**Status:** CLOSED 2026-07-30 — root cause: ACE-inherited `>= 800` misread
|
||||
of retail's exact-equality run-rate sentinel
|
||||
**Severity:** HIGH (matrix live gate 2026-07-30; core speed parity)
|
||||
**Component:** movement / stat chain (Campaign P P1) / outbound speed
|
||||
**Component:** movement / `MovementSystem.GetRunRate`
|
||||
|
||||
**Symptom (user report):** side-by-side, the acdream character runs
|
||||
visibly faster than a character on retail. Confounds to rule out first:
|
||||
(a) unequal run skills/vitae between the two characters; (b) whether the
|
||||
comparison retail char had buffs. Diagnosis plan: log both chars' run
|
||||
skill + vitae + computed runRate; capture our GetRunRate inputs
|
||||
(effective skill after EnchantSkill, burden) at the moment of comparison;
|
||||
verify ACE's echoed ForwardSpeed matches what we apply locally
|
||||
(ApplyServerRunRate) and what retail applies for the same skill. Suspect
|
||||
list: effective-skill vitae application on the LOCAL run path,
|
||||
run-rate-cap difference, or an uneven comparison.
|
||||
**Root cause:** retail `MovementSystem::GetRunRate` (0x006b0950) returns
|
||||
18/4 = 4.5 ONLY when runSkill == 800 EXACTLY (byte-decoded `fcom [800f];
|
||||
test ah, 0x44; jp` — the C2/C3 parity equality idiom; <, >, and unordered
|
||||
all take the general formula). ACE misread the same x87 mush as
|
||||
`>= 800` ("max run speed?") and our P1 port inherited it via the
|
||||
ACE cross-reference. Every maxed character therefore ran a flat 4.5
|
||||
(retail-true ~3.70) — ~21% too fast and completely vitae-independent,
|
||||
because both the vitae-reduced and unreduced skill sat above 800. The
|
||||
controlled comparison (33%-vitae +Acdream vs 5%-vitae +Je, both maxed)
|
||||
showed acdream faster while retail runs them within ~0.4% — exactly the
|
||||
formula's prediction. The vitae/enchantment chain itself was verified
|
||||
intact end-to-end via [stat-chain] live capture (vitae 0.67 installed →
|
||||
eff run 10200 → applied to controller).
|
||||
|
||||
---
|
||||
**Fix:** `==` restores the general formula for all non-800 skills;
|
||||
golden tests pin 799/800/801 straddle + the maxed-skill vitae
|
||||
differential; `docs/research/2026-07-30-stat-coupled-movement-pseudocode.md`
|
||||
§6 corrected with the full byte decode and an explicit "do not re-import
|
||||
ACE's >= reading" warning.
|
||||
|
||||
## #265 — Steep-slope response set: uphill-jump bounce, roof slides lost, edge wedge (TS-4 removal fallout — REVERTED)
|
||||
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ AP-127):
|
|||
|
||||
`GetRunRate` (0x006b0950) and the `arg3!=0` (PK) branch of `JumpStaminaCost`
|
||||
(0x006b0a40) have their GENERAL-CASE arithmetic entirely dropped by BN (only
|
||||
the `EncumbranceSystem::LoadMod`/`800`-skill-cap calls and the `arg3==0`
|
||||
the `EncumbranceSystem::LoadMod`/`800`-skill-compare calls and the `arg3==0`
|
||||
ceil expression survive uncollapsed — the same information-loss class as the
|
||||
x87 mush, just total rather than partial). **ACE is the cross-reference
|
||||
tiebreaker for those two spots** (`references/ACE/Source/ACE.Server/Physics/
|
||||
|
|
@ -213,13 +213,26 @@ citation style (`PlayerWeenie.cs`'s pre-P1 doc comments already said
|
|||
"decompiled + ACE MovementSystem" for these two formulas — nothing new here,
|
||||
just now with a named-decomp address alongside):
|
||||
|
||||
- `GetRunRate(load, skill, scaling) = skill>=800 ? 18/4 : ((LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling) / 4` —
|
||||
matches acdream's pre-existing `PlayerWeenie.GetRunRate` exactly (which
|
||||
hardcoded `scaling=1`); the true retail signature carries a 3rd
|
||||
`scaling` arg (confirmed by the decomp's own function signature), and
|
||||
every known call site (`InqMaxRunRate`, `InqRunRate`) passes `1f` — so
|
||||
porting the full signature is free (no behavior change), just closer to
|
||||
the retail surface for future callers.
|
||||
- `GetRunRate(load, skill, scaling) = skill==800 ? 18/4 : ((LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling) / 4` —
|
||||
**§12c correction (#266, 2026-07-30): the 800 branch is EXACT EQUALITY,
|
||||
not `>=`.** Raw byte decode of 0x006b0950 (PDB-paired binary):
|
||||
`fild skill; fcom [0x00803b94 = 800f]; fnstsw ax; test ah, 0x44; jp
|
||||
0x6b097f` — the C2/C3 parity idiom in which `jp` (general path) fires
|
||||
for `<`, `>`, AND unordered; the `fld [18f]; fdiv [4f]; ret` fall-through
|
||||
executes only when C3=1/C2=0, i.e. skill == 800 exactly. The general
|
||||
path decodes instruction-by-instruction to
|
||||
`(LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling / 4`
|
||||
(constants 200f @0x00803b8c, 11f @0x00803b88, 4f @0x007c6174, /scaling
|
||||
from `[esp+0xc]`, final /4f @0x00803b80). **ACE's `>= 800` "max run
|
||||
speed?" reading is a misread of the same mush and must not be used as a
|
||||
tiebreaker here** — it flat-lined every maxed character at 4.5 (retail
|
||||
general formula gives ~3.70) and erased the vitae speed differential
|
||||
(#266: 33%-vitae +Acdream visibly outran 5%-vitae +Je in acdream while
|
||||
retail runs them within ~0.4%). `InqMaxRunRate`'s skill=9999 probe gets
|
||||
the general formula (~3.6961), not 4.5. The true retail signature
|
||||
carries a 3rd `scaling` arg (confirmed by the decomp's own function
|
||||
signature), and every known call site (`InqMaxRunRate`, `InqRunRate`)
|
||||
passes `1f`.
|
||||
- `GetJumpHeight(load, skill, extent, scaling)` — BN's extent-clamp
|
||||
micro-branch (pc 006b09b0-006b09ca) is the SAME x87-mush pattern as §3;
|
||||
ACE's `Math.Clamp(extent, 0, 1)` is the tiebreaker (matches the EXISTING
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue