j134 fix: tracker must take the mover's switch per-call (instance flag was silently off on the object-literal construction path) + apply the correction on the reading that carries the server's turn-N+1 energy change (two-slot buffer); live-verified alignment

This commit is contained in:
2026-09-26 12:56:26 +02:00
parent 6ad5d99922
commit 882733819b
8 changed files with 133 additions and 79 deletions
+40 -24
View File
@@ -1,4 +1,4 @@
## Unit checks for the j133 STRAFE fire-detection fix (`TR_STRAFE_FIRE_FIX`).
## Unit checks for the j133/j134 fire-detection fix (`TR_STRAFE_FIRE_FIX`).
##
## NO battle, NO Java, NO server. Run with:
## nim c -r --nimcache:/tmp/nc_j133 --path:common_libs \
@@ -17,6 +17,16 @@
## The fix undoes both from the events (`noteEnemyBulletHit` / `noteDamageDealt`)
## and splits any still-too-large delta instead of dropping it. These checks pin
## the four behaviours plus the OFF-switch parity with the shipped detector.
##
## ── The one-SCAN delay (j134, MEASURED LIVE) ────────────────────────────────
## The correction is NOT applied to the immediately-next reading. The server
## emits the hit event on turn N but applies the energy change to turn N+1's
## reading (live evidence: `EV hit getTurn=67` -> the `raw=-5.803` gain appears
## in the `getTurn=68` reading). Because `note*` runs during `go()`, AFTER that
## turn's `endScan`, the tracker keeps a one-slot double buffer so the
## correction lands on the reading that carries the change. A unit test must
## therefore feed: (1) a scan to rotate the event in, then (2) the reading whose
## raw shows the effect. `advance` below does exactly that.
import std/[math, os]
import gun_harness/gun_interface
@@ -44,13 +54,10 @@ proc freshModule(): StrafeModule =
# ── 1. plain fire (unchanged behaviour) ───────────────────────────────────────
block:
let m = freshModule()
var mm = m
var mm = freshModule()
mm.detectFires(ws(100.0))
mm.detectFires(ws(97.95)) # fired 2.05? use a clean power
# second reading: 100 -> 97.95 is a 2.05 drop, in range -> one wave
check "plain fire creates exactly one wave",
mm.bullets.len == 1
mm.detectFires(ws(97.95)) # 2.05 drop, in range -> one wave
check "plain fire creates exactly one wave", mm.bullets.len == 1
check "plain fire wave power == drop",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.05) < 1e-9
@@ -58,12 +65,12 @@ block:
block:
# Enemy at 100 fires 0.35 while an EARLIER 0.45 bullet of its own hits us
# (+3*0.45 = +1.35). Net energy = 100 - 0.35 + 1.35 = 101.0 -> raw drop = -1.0.
let m = freshModule()
var mm = m
mm.noteEnemyBulletHit(0.45)
mm.detectFires(ws(101.0))
check "masked fire: WITH fix a wave is created",
mm.bullets.len == 1
var mm = freshModule()
mm.detectFires(ws(100.0)) # establish prev = 100
mm.noteEnemyBulletHit(0.45) # event noted (staged)
mm.detectFires(ws(100.0)) # rotates the staged correction in
mm.detectFires(ws(101.0)) # the reading carrying the change
check "masked fire: WITH fix a wave is created", mm.bullets.len == 1
check "masked fire: WITH fix the power is recovered",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 0.35) < 1e-9
@@ -71,24 +78,21 @@ block:
block:
# Enemy at 50 fires 2.0 and our bullet hits it for 1.8 -> energy 46.2,
# raw drop 3.8 > 3.01 (shipped detector rejects it outright).
let m = freshModule()
var mm = m
mm.prevEnergySet(1, 50.0) # last reading before the contested tick
var mm = freshModule()
mm.prevEnergySet(1, 50.0)
mm.noteDamageDealt(1.8)
mm.detectFires(ws(46.2))
check "inflated drop: WITH fix a wave is created",
mm.bullets.len == 1
mm.detectFires(ws(50.0)) # rotates the staged correction in
mm.detectFires(ws(46.2)) # raw 3.8 - 1.8 = 2.0 -> wave
check "inflated drop: WITH fix a wave is created", mm.bullets.len == 1
check "inflated drop: WITH fix the power is recovered",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.0) < 1e-9
# ── 4. never silently drop a too-large delta (unobserved contamination) ───────
block:
let m = freshModule()
var mm = m
var mm = freshModule()
mm.detectFires(ws(100.0))
mm.detectFires(ws(93.0)) # 7.0 with no events -> split, not dropped
check "too-large drop is SPLIT, never dropped",
mm.bullets.len == 3
check "too-large drop is SPLIT, never dropped", mm.bullets.len == 3
check "split waves all within the power cap",
mm.bullets.len == 3 and mm.bullets[0].power <= 3.0 and mm.bullets[1].power <= 3.0
check "split waves sum back to the drop",
@@ -98,15 +102,18 @@ block:
block:
putEnv("TR_STRAFE_FIRE_FIX", "0")
loadStrafeEnv()
# (a) masked fire: shipped detector sees drop = -1.0 -> NO wave.
# (a) masked fire: shipped detector sees raw = -1.0 -> NO wave.
var a = freshModule()
a.detectFires(ws(100.0))
a.noteEnemyBulletHit(0.45) # must be a no-op when the fix is off
a.detectFires(ws(100.0))
a.detectFires(ws(101.0))
check "OFF: masked fire is NOT detected (shipped behaviour)", a.bullets.len == 0
# (b) inflated drop: shipped detector sees 3.8 -> rejected -> NO wave.
var b = freshModule()
b.prevEnergySet(1, 50.0)
b.noteDamageDealt(1.8)
b.detectFires(ws(50.0))
b.detectFires(ws(46.2))
check "OFF: inflated drop is rejected (shipped behaviour)", b.bullets.len == 0
# (c) in-range drop is still caught exactly once.
@@ -121,9 +128,18 @@ block:
# ── 6. default is ON ─────────────────────────────────────────────────────────
block:
delEnv("TR_STRAFE_FIRE_FIX")
delEnv("TR_FIRE_FIX")
loadStrafeEnv()
check "default TR_STRAFE_FIRE_FIX is ON", StrafeFireFix
# ── 7. the global TR_FIRE_FIX also gates STRAFE ──────────────────────────────
block:
putEnv("TR_FIRE_FIX", "0")
loadStrafeEnv()
check "TR_FIRE_FIX=0 disables STRAFE too", not StrafeFireFix
delEnv("TR_FIRE_FIX")
loadStrafeEnv()
if failures == 0:
echo "\nAll STRAFE fire-detection fix checks passed."
else: