weapons batch VERIFIED on the final tree + last two gaps closed: BYSTANDER SWEEP landed (guided damage rounds test the flight segment vs every mech, detonate on interposers like the binary's world sweep @0042291c -- 11 legitimate strikes in the sweep bench, zero self/phantom/fuze-race hits per forensics) + COAST GRAVITY (-6.5 after burnout). Comprehensive 2-node weapons_sweep bench PASS, all families: ENERGY 184 named FIRED / 79 hits; AC 17 type=1 hits ALL burst=1 + 84 ballistic pushes ALL dmg=0 (hitscan live-proven multi-node on final code); MISSILE 593 type=2 hits, 576 salvo bursts (4.0 fuze + drag speeds work); #174 guard worst-repeat=1 across 13 lives; detector SELF-TESTED against synthetic pre-fix shapes before any verdict trusted. Rig lessons -> test-harness.md: BT_FIRE_AT_ICON designates BUILDINGS (use BT_GOTO=enemy + throttled BT_AUTOFIRE/BT_AF_MISSILE, the kd pattern); mad2 'Zanin Neko' carries AFC50+ERPPC+LRM15+SRM6x2+ERMLaser -- all families on one engaging chassis; -net 1501 -> pilot :1502 mapping
This commit is contained in:
@@ -234,3 +234,29 @@ and asserts [crit] DESTROYED + [techstat] X + zero named FIRED after + REFUSED>0
|
||||
fires; P2 partial-crits to 0.5 and asserts the weapon KEEPS firing (freeze regression, the #164
|
||||
upstream writer). LESSON (gotcha SS30): BT_KILL_SUBSYS writes the cells directly and can only
|
||||
validate READERS -- state benches must drive the real writer path.
|
||||
|
||||
## weapons_sweep (night16) -- the all-families weapons bench (#171/#174/lane-12, PASS 2026-08-13)
|
||||
`scratchpad/night16/weapons_sweep.sh` + `weapons_check.py`: ONE 2-node bench proving every
|
||||
weapons family on one tree. Rig: **A=mad2 "Zanin Neko"** (AFC50 + ERPPC + LRM15 + SRM6 x2 +
|
||||
ERMLaser x2 -- all three families on one chassis, the AC carrier per L4GAUGE.CFG) vs **B=madcat**
|
||||
(AFC100 + LRM15 x2 + ER lasers), grass/day, `BT_GOTO=enemy` + throttled BT_AUTOFIRE +
|
||||
BT_AF_MISSILE (periods 4/7), 300s, env `BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG BT_AMMO_LOG`.
|
||||
Asserts per family: energy named [emitter] FIRED + type-3/4 dmghits; AC type=1 dmghits ALL
|
||||
burst=1 + ballistic (guided=0) PUSHes ALL dmg=0 (#171 hitscan; damage DETs reconciled to guided
|
||||
pushes per log); missiles [ammo] FIRED + IMPACT dmg>0 with burst>1 + type=2 dmghits (contact
|
||||
through the 4.0u fuze at drag-governed speeds); #174 cascade-per-life<=1; bystander receipts
|
||||
individually classified; hard rig floors. `--selftest` feeds the detector pre-#171/pre-#174
|
||||
shapes + bystander false-positive shapes and requires each to FAIL (run first, gated).
|
||||
Rig lessons [T2]:
|
||||
- **avatar-vs-avatar forms NO locks** (three prior runs, 0 dmghits even from lasers) while the
|
||||
madcat family fights fine -- dodge with madcat-family chassis; `mad2` is the AC carrier.
|
||||
- Node->pilot-page mapping: `-net 1501` claims pilot `127.0.0.1:1502`, `-net 1601` claims
|
||||
`:1602` (net port + 1) -- sed each pilot page's vehicle= line EXACTLY for mixed pairings.
|
||||
- **BYSTANDER receipts legitimately fire in a 2-mech fight**: the production aim ray designates
|
||||
SCENERY constantly (the ac_bench BT_FIRE_AT_ICON lesson generalizes to the real pick), and a
|
||||
mech crossing a scenery-locked round's path detonates it -- the binary world-sweep shape
|
||||
(FUN_0042291c via @004bef78 [T1]). All 11 receipts of the PASS run were true positives
|
||||
(struck = the other live mech, aim 146-1016u away, one paired IMPACT each;
|
||||
`scratchpad/night16/bys_forensics.py`). A zero-receipt assertion is miscalibrated; assert
|
||||
the false-positive conditions instead (self-strike / phantom id / det-at-lock-aim / missing
|
||||
paired delivery / print-cap 24 reached).
|
||||
|
||||
+160
-13
@@ -1848,10 +1848,10 @@ static void
|
||||
// (sim: binary 492 u/s at 800u vs undamped 695; port t+15% to 800u).
|
||||
// Drag runs during burn AND coast (a burned-out round DECELERATES,
|
||||
// v(t)=v0/(1+COD*v0*t)); thruster rounds only -- the AC round is the
|
||||
// 0xBCD renderer tracer, a cosmetic with no Mover physics. Still
|
||||
// unported [T3 minor]: coast gravity (-6.5 world-Y, @4bef78
|
||||
// else-branch) and the binary's burnout end to velocity-slaving --
|
||||
// both live beyond practical impact ranges (see WEAPONS_DRIFT_AUDIT
|
||||
// 0xBCD renderer tracer, a cosmetic with no Mover physics. Coast
|
||||
// gravity ported below (audit close 2026-08-13). Still unported
|
||||
// [T3 minor]: the binary's burnout end to velocity-slaving (lives
|
||||
// beyond practical impact ranges -- see WEAPONS_DRIFT_AUDIT
|
||||
// 'VELOCITY/DT INTEGRATION').
|
||||
if (p.accel > 0.0f)
|
||||
{
|
||||
@@ -1867,6 +1867,29 @@ static void
|
||||
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
|
||||
}
|
||||
p.speed = ns;
|
||||
// COAST GRAVITY (WEAPONS_DRIFT_AUDIT dt-table row closed
|
||||
// 2026-08-13). The binary applies the environment gravity
|
||||
// (default 6.5, FUN_00421e2c `vy -= g`) ONLY while COASTING --
|
||||
// the @4bef78 else-branch; during burn the thruster re-adds
|
||||
// thrust with NO gravity term onto the frame-zeroed accumulator
|
||||
// [T1, decomp-reference flight-model bullet]. It is a fresh
|
||||
// per-frame ACCELERATION (not an accumulate), so `vy -= 6.5*dt`
|
||||
// is the faithful integration at any frame rate. coast_dt is
|
||||
// this frame's non-burning remainder (exact at the burnout
|
||||
// boundary). Applies to EVERY thruster round, lock or no lock
|
||||
// -- it is Mover physics, not seeker logic (a lock-dropped
|
||||
// round coasts ballistic and now DROPS instead of flying level).
|
||||
Scalar coast_dt = dt - burn_dt;
|
||||
if (p.burnLeft <= 0.0f && coast_dt > 0.0f)
|
||||
{
|
||||
p.vel.y -= 6.5f * coast_dt; // env gravityConstant default [T1]
|
||||
Scalar sv = (Scalar)sqrtf((float)(p.vel.x*p.vel.x
|
||||
+ p.vel.y*p.vel.y + p.vel.z*p.vel.z));
|
||||
if (sv > 0.01f)
|
||||
p.speed = sv; // keep speed == |vel| (the step
|
||||
// length + normalizations below
|
||||
// divide by p.speed)
|
||||
}
|
||||
}
|
||||
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
|
||||
p.age += dt;
|
||||
@@ -1955,24 +1978,120 @@ static void
|
||||
// detonation being flung past the target at high speed.
|
||||
Point3D hitPos = p.pos;
|
||||
Scalar contactD2;
|
||||
Scalar tTgt = 0.0f; // target's nearest-approach segment param
|
||||
// (the bystander sweep compares against it)
|
||||
{
|
||||
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y, sz = p.pos.z - prev.z;
|
||||
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
|
||||
Scalar t = 0.0f;
|
||||
if (seg2 > 1.0e-6f)
|
||||
{
|
||||
const Scalar wx = p.targetPos.x - prev.x,
|
||||
wy = p.targetPos.y - prev.y,
|
||||
wz = p.targetPos.z - prev.z;
|
||||
t = (wx*sx + wy*sy + wz*sz) / seg2;
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
tTgt = (wx*sx + wy*sy + wz*sz) / seg2;
|
||||
if (tTgt < 0.0f) tTgt = 0.0f; else if (tTgt > 1.0f) tTgt = 1.0f;
|
||||
}
|
||||
hitPos.x = prev.x + t*sx; hitPos.y = prev.y + t*sy; hitPos.z = prev.z + t*sz;
|
||||
hitPos.x = prev.x + tTgt*sx; hitPos.y = prev.y + tTgt*sy; hitPos.z = prev.z + tTgt*sz;
|
||||
const Scalar cx = p.targetPos.x - hitPos.x,
|
||||
cy = p.targetPos.y - hitPos.y,
|
||||
cz = p.targetPos.z - hitPos.z;
|
||||
contactD2 = cx*cx + cy*cy + cz*cz;
|
||||
}
|
||||
// BYSTANDER SWEEP (WEAPONS_DRIFT_AUDIT lane 12, closed 2026-08-13).
|
||||
// The binary's per-tick contact test is a WORLD sweep (FUN_0042291c,
|
||||
// called from Missile::MoveAndCollide @004bef78): every solid in the
|
||||
// flight path can detonate the round -- including a third mech between
|
||||
// shooter and target [T1]. The port pool tested only the locked
|
||||
// target sphere + the terrain ray, so a mech standing in someone
|
||||
// else's crossfire was flown through (splash alone could touch it).
|
||||
// Port shape [T3 -- radius approximation of the real solid sweep]:
|
||||
// nearest-approach of this frame's flight segment vs a VERTICAL
|
||||
// CAPSULE per other registered mech. The capsule is derived from the
|
||||
// mech's OWN collision template (Mover::GetCollisionTemplate, the same
|
||||
// BoxedSolid whose maxY is CylinderReferenceHeight): axis = mech
|
||||
// origin up template minY..maxY, radius = the template's larger
|
||||
// horizontal half-extent (the axis ignores the template's near-zero
|
||||
// horizontal center offset, so no yaw rotation is needed). Fallback
|
||||
// when the template is unresolved: 3.5u x 14u [T3 fixed constants,
|
||||
// fielded-mech scale -- CylinderReferenceHeight ~14]. Wrecks stay in
|
||||
// the sweep: the binary detonates on ANY solid, and the victim-side
|
||||
// zone-state guard (#174) keeps a dead zone from cascading or scoring.
|
||||
// Cheap by construction: damage-carrying GUIDED rounds only (= the one
|
||||
// salvo lead; the N-1 visual tracers are damage-0), per-mech
|
||||
// distance cull, shooter excluded by the registry helper, locked
|
||||
// target excluded (its own 4.0u fuze above is the authentic test).
|
||||
Entity *bysMech = 0;
|
||||
Scalar bysT = 2.0f;
|
||||
Point3D bysHit = p.pos;
|
||||
if (p.guided && p.damage > 0.0f)
|
||||
{
|
||||
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
|
||||
Entity *cand[32];
|
||||
const int nc = BTGetTargetCandidates(p.shooter, cand, 32); // excludes the shooter
|
||||
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y,
|
||||
sz = p.pos.z - prev.z;
|
||||
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
|
||||
const Scalar step = p.speed * dt;
|
||||
for (int ci = 0; ci < nc && seg2 > 1.0e-6f; ++ci)
|
||||
{
|
||||
Entity *e = cand[ci];
|
||||
if (e == 0 || e == p.target || !BTIsRegisteredMech(e))
|
||||
continue;
|
||||
Mech *m = (Mech *)e;
|
||||
Point3D mp = m->localOrigin.linearPosition;
|
||||
// distance cull: farther than this frame's step + the largest
|
||||
// plausible capsule reach -> untouchable this frame
|
||||
const Scalar cdx = mp.x - prev.x, cdz = mp.z - prev.z;
|
||||
const Scalar cull = step + 24.0f;
|
||||
if (cdx*cdx + cdz*cdz > cull*cull)
|
||||
continue;
|
||||
Scalar rr, cy0, cy1;
|
||||
BoxedSolid *tmpl = m->GetCollisionTemplate();
|
||||
if (tmpl != 0 && tmpl->maxY > tmpl->minY)
|
||||
{
|
||||
const Scalar hx = 0.5f * (tmpl->maxX - tmpl->minX);
|
||||
const Scalar hz = 0.5f * (tmpl->maxZ - tmpl->minZ);
|
||||
rr = (hx > hz) ? hx : hz;
|
||||
cy0 = mp.y + tmpl->minY;
|
||||
cy1 = mp.y + tmpl->maxY;
|
||||
}
|
||||
else
|
||||
{
|
||||
rr = 3.5f; cy0 = mp.y; cy1 = mp.y + 14.0f; // [T3] fixed fallback
|
||||
}
|
||||
if (!(rr > 0.5f)) rr = 0.5f; // degenerate/garbage template
|
||||
if (rr > 10.0f) rr = 10.0f; // guards (NaN falls to 0.5)
|
||||
// closest points between the flight segment prev+t*(sx,sy,sz)
|
||||
// and the vertical axis segment (mp.x, cy0..cy1, mp.z) --
|
||||
// standard segment-segment closest point (RTCD 5.1.9 with
|
||||
// d2 = (0,h,0))
|
||||
const Scalar h = cy1 - cy0;
|
||||
const Scalar rx = prev.x - mp.x, ry = prev.y - cy0,
|
||||
rz = prev.z - mp.z;
|
||||
const Scalar ee = h*h;
|
||||
const Scalar f = h*ry;
|
||||
const Scalar c = sx*rx + sy*ry + sz*rz;
|
||||
const Scalar b = sy*h;
|
||||
const Scalar den = seg2*ee - b*b;
|
||||
Scalar t = (den > 1.0e-6f) ? (b*f - c*ee) / den : 0.0f;
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
Scalar s = (ee > 1.0e-6f) ? (b*t + f) / ee : 0.0f;
|
||||
if (s < 0.0f) { s = 0.0f; t = -c / seg2; }
|
||||
else if (s > 1.0f) { s = 1.0f; t = (b - c) / seg2; }
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
const Scalar qx = prev.x + t*sx - mp.x;
|
||||
const Scalar qy = prev.y + t*sy - (cy0 + s*h);
|
||||
const Scalar qz = prev.z + t*sz - mp.z;
|
||||
if (qx*qx + qy*qy + qz*qz < rr*rr && t < bysT)
|
||||
{
|
||||
bysT = t;
|
||||
bysMech = e;
|
||||
bysHit.x = prev.x + t*sx;
|
||||
bysHit.y = prev.y + t*sy;
|
||||
bysHit.z = prev.z + t*sz;
|
||||
}
|
||||
}
|
||||
}
|
||||
// #171 FUZE RADIUS: a GUIDED damage round detonates at the binary's
|
||||
// proximity fuze _DAT_004bf5a4 = 4.0 (seeker rangeToTarget +0x10C <
|
||||
// 4.0, @004bef78 -- the SAME constant the old steering comment
|
||||
@@ -1981,7 +2100,32 @@ static void
|
||||
// AC tracers, cluster visuals) keep the wider 10u so their burst
|
||||
// visuals still read at speed.
|
||||
const Scalar fuzeR = (p.guided && p.damage > 0.0f) ? 4.0f : 10.0f;
|
||||
const int contact = (contactD2 < fuzeR * fuzeR);
|
||||
int contact = (contactD2 < fuzeR * fuzeR);
|
||||
Entity *victim = p.target;
|
||||
if (bysMech != 0 && (!contact || bysT < tTgt))
|
||||
{
|
||||
// a third mech sits EARLIER on this frame's flight segment than
|
||||
// the locked target's fuze point -- it takes the round (the
|
||||
// binary's sweep detonates on the FIRST solid in the path). The
|
||||
// struck mech becomes the DIRECT victim; the salvo cluster roll
|
||||
// and the splash package below run unchanged against it.
|
||||
contact = 1;
|
||||
victim = bysMech;
|
||||
hitPos = bysHit;
|
||||
static int s_bysPrints = 0; // capped receipt (field forensics)
|
||||
if (s_bysPrints < 24)
|
||||
{
|
||||
++s_bysPrints;
|
||||
DEBUG_STREAM << "[projectile] BYSTANDER id="
|
||||
<< (int)bysMech->GetEntityID()
|
||||
<< " t=" << bysT
|
||||
<< " at(" << bysHit.x << "," << bysHit.y << "," << bysHit.z
|
||||
<< ") dmg=" << p.damage
|
||||
<< " lockedTgt=" << (void *)p.target
|
||||
<< ((s_bysPrints == 24) ? " (receipt cap reached)" : "")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
||||
{
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
@@ -2006,10 +2150,13 @@ static void
|
||||
<< "," << p.targetPos.z << ")" << std::endl;
|
||||
if (p.salvoLead) // ONE Explosion per salvo (@004bcc60)
|
||||
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
|
||||
Entity *tgt = p.target;
|
||||
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||
// from the shooter's 0x388 slot (the picked victim; any peer mech in
|
||||
// MP, task #46). A replicant target reroutes cross-pod via Dispatch.
|
||||
Entity *tgt = victim;
|
||||
// Deliver to the struck mech -- normally the launcher's locked
|
||||
// target (the shooter's 0x388 slot; any peer mech in MP, task #46),
|
||||
// or the BYSTANDER the sweep above found first on the flight
|
||||
// segment (lane 12: it becomes the direct victim, exactly as if it
|
||||
// had been the lock). A replicant victim reroutes cross-pod via
|
||||
// Dispatch.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python
|
||||
# Forensics for the 11 [projectile] BYSTANDER receipts of the weapons_sweep
|
||||
# run: classify every DET lock pointer by aim-point motion (a mech lock TRACKS
|
||||
# the walking enemy; a scenery/wreck lock is stationary), then test each
|
||||
# receipt: struck id vs the shooter's own id, det-to-aim distance, and the
|
||||
# paired IMPACT delivery.
|
||||
import io
|
||||
import math
|
||||
import re
|
||||
|
||||
RX_DET = re.compile(r"\[projectile\] DET at\(([^)]*)\) dmg=([\d.eE+\-]+) "
|
||||
r"lead=(\d+) tgt=(\S+) aim\(([^)]*)\)")
|
||||
RX_BYS = re.compile(r"\[projectile\] BYSTANDER id=(\d+) t=(\S+) at\(([^)]*)\) "
|
||||
r"dmg=([\d.eE+\-]+) lockedTgt=(\S+)")
|
||||
RX_RST = re.compile(r"\[respawn\] Mech::Reset (\d+):(\d+) ")
|
||||
|
||||
|
||||
def p3(s):
|
||||
a = [float(x) for x in s.split(",")]
|
||||
return a
|
||||
|
||||
|
||||
def dist(a, b):
|
||||
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||
|
||||
|
||||
for fn in ("ws_a.log", "ws_b.log"):
|
||||
lines = io.open("/c/git/bt411/content/" + fn.replace("/", ""),
|
||||
encoding="latin-1", errors="replace").read().splitlines() \
|
||||
if False else io.open("C:/git/bt411/content/" + fn,
|
||||
encoding="latin-1", errors="replace").read().splitlines()
|
||||
own = None
|
||||
dets = {} # tgt ptr -> list of (lineno, aim)
|
||||
bys = [] # (lineno, id, at, dmg, lockedTgt)
|
||||
for i, l in enumerate(lines):
|
||||
m = RX_DET.search(l)
|
||||
if m and m.group(4) != "0x0" and float(m.group(2)) > 0.0:
|
||||
dets.setdefault(m.group(4), []).append((i, p3(m.group(5))))
|
||||
continue
|
||||
m = RX_BYS.search(l)
|
||||
if m:
|
||||
bys.append((i, int(m.group(1)), p3(m.group(3)),
|
||||
float(m.group(4)), m.group(5)))
|
||||
continue
|
||||
m = RX_RST.search(l)
|
||||
if m and own is None:
|
||||
own = int(m.group(2))
|
||||
print("=== %s (own mech entity=%s) ===" % (fn, own))
|
||||
# classify pointers: max pairwise aim distance within a 1500-line window
|
||||
cls = {}
|
||||
for ptr, lst in sorted(dets.items(), key=lambda kv: -len(kv[1])):
|
||||
move = 0.0
|
||||
for j in range(1, len(lst)):
|
||||
if lst[j][0] - lst[j - 1][0] <= 1500:
|
||||
d = dist(lst[j][1], lst[j - 1][1])
|
||||
if d > move:
|
||||
move = d
|
||||
cls[ptr] = "MECH-TRACKING" if move > 60.0 else "STATIONARY"
|
||||
print(" lock %s damage-DETs=%3d max local aim motion=%7.1fu -> %s"
|
||||
% (ptr, len(lst), move, cls[ptr]))
|
||||
ok = True
|
||||
for (i, mid, at, dmg, ptr) in bys:
|
||||
# find this receipt's own DET (next damage DET line)
|
||||
aim = None
|
||||
imp = False
|
||||
for j in range(i, min(i + 4, len(lines))):
|
||||
m = RX_DET.search(lines[j])
|
||||
if m and aim is None:
|
||||
aim = p3(m.group(5))
|
||||
if "[projectile] IMPACT damage=" in lines[j]:
|
||||
imp = True
|
||||
d_aim = dist(at, aim) if aim else -1.0
|
||||
verdict = []
|
||||
if mid == own:
|
||||
verdict.append("SELF-STRIKE (geometry bug)")
|
||||
if cls.get(ptr) == "MECH-TRACKING":
|
||||
verdict.append("LOCK WAS THE MECH (exclusion failed)")
|
||||
if aim is not None and d_aim < 30.0:
|
||||
verdict.append("det at the lock's own aim point")
|
||||
if not imp:
|
||||
verdict.append("no paired IMPACT delivery")
|
||||
tag = "TRUE-POSITIVE" if not verdict else "FALSE-POSITIVE: " + "; ".join(verdict)
|
||||
if verdict:
|
||||
ok = False
|
||||
print(" bys@%-6d struck=%d dmg=%g lock=%s(%s) det-to-aim=%.0fu imp=%d %s"
|
||||
% (i + 1, mid, dmg, ptr, cls.get(ptr, "?"), d_aim, imp, tag))
|
||||
print(" %s: %s" % (fn, "all receipts TRUE positives" if ok
|
||||
else "FALSE POSITIVES PRESENT"))
|
||||
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/env python
|
||||
# weapons_sweep checker -- ONE comprehensive per-family PASS/FAIL adjudicator
|
||||
# for scratchpad/night16/weapons_sweep.sh (2-node mad2-vs-madcat fight).
|
||||
#
|
||||
# Families / assertions (env on both nodes: BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG
|
||||
# BT_AMMO_LOG):
|
||||
# A1 ENERGY : named [emitter] FIRED lines present AND laser/PPC [dmghit]
|
||||
# (type 3/4) land on the victim.
|
||||
# A2 AC : type=1 [dmghit] present, ALL of them burst=1 (single panel,
|
||||
# trigger-time hitscan); ZERO damage-carrying ballistic rounds in
|
||||
# the pool ([projectile] PUSH guided=0 must all be dmg=0 -- the
|
||||
# #171 restoration: the visible round is the cosmetic 0xBCD
|
||||
# tracer); every damage-carrying DET is reconciled to a guided
|
||||
# (missile) push.
|
||||
# A3 MISSILE: [ammo] LRM/SRM FIRED receipts, [projectile] IMPACT dmg>0
|
||||
# deliveries with burst>1 salvo bursts, type=2 [dmghit]s landing
|
||||
# (burst>1 present) -- contact through the 4.0u fuze at
|
||||
# drag-governed speeds; plus >=1 death or zone cascade (vitality).
|
||||
# A4 NO-REGR: no zone cascades twice within one life per log (the #174
|
||||
# guard); every [projectile] BYSTANDER receipt in the 2-mech
|
||||
# fight is a TRUE positive of the lane-12 sweep.
|
||||
# CALIBRATION NOTE (2026-08-13 run): the original "zero receipts
|
||||
# in a 2-mech fight" operationalization assumed a lock is always
|
||||
# the enemy mech. The run disproved the ASSUMPTION, not the
|
||||
# code: the production aim ray designates SCENERY constantly
|
||||
# (the ac_bench lesson), and rounds locked onto a structure DO
|
||||
# fly past the other mech -- the binary's world sweep detonates
|
||||
# on ANY solid in the path (FUN_0042291c via @004bef78 [T1]), so
|
||||
# a receipt there is the sweep WORKING. Forensics on all 11
|
||||
# receipts of the calibration run (bys_forensics.py): struck ==
|
||||
# the other live mech every time, aim point 146-1016u away from
|
||||
# the strike (the round was flying elsewhere), one paired IMPACT
|
||||
# delivery each. The check now asserts the actual false-positive
|
||||
# conditions per receipt:
|
||||
# - struck id == the shooter's OWN mech -> self-strike bug
|
||||
# - struck id not a known player mech -> phantom strike
|
||||
# - no DET/IMPACT pairing -> lost/duplicated delivery
|
||||
# - strike within 30u of the round's aim -> the sweep raced the
|
||||
# authentic 4.0u fuze at the lock itself (exclusion suspect)
|
||||
# and flags a hit receipt-print cap (24) as unmeasurable.
|
||||
# A5 FLOORS : rig-sanity minimums so a quiet rig cannot PASS.
|
||||
#
|
||||
# --selftest: prove the detector CAN fail (the week's hard rule) by feeding it
|
||||
# synthetic pre-fix log shapes (pre-#171 damage-carrying ballistic round,
|
||||
# pre-#174 double cascade, a bystander receipt, and an empty/quiet rig) and
|
||||
# requiring each to FAIL the matching assertion.
|
||||
import io
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
|
||||
RX_DMGHIT = re.compile(r"\[dmghit\] mech=(\S+) zone=(-?\d+) vital=(\d+) "
|
||||
r"type=(\d+) amt=([\d.eE+\-]+) burst=(\d+)")
|
||||
RX_PUSH = re.compile(r"\[projectile\] PUSH target=(\S+) len=(\S+) speed=(\S+) "
|
||||
r"dmg=([\d.eE+\-]+) guided=(\d+)")
|
||||
RX_DET = re.compile(r"\[projectile\] DET .*?dmg=([\d.eE+\-]+) lead=(\d+)")
|
||||
RX_IMPACT = re.compile(r"\[projectile\] IMPACT damage=([\d.eE+\-]+) .*?burst=(\d+)")
|
||||
RX_EMIT = re.compile(r"\[emitter\] FIRED '([^']+)'")
|
||||
RX_AMMO = re.compile(r"\[ammo\] (\S+) FIRED, rounds left=")
|
||||
RX_CASC = re.compile(r"\[cascade\] zone (\d+) DESTROYED")
|
||||
RX_BYS = re.compile(r"\[projectile\] BYSTANDER id=(\d+) t=(\S+) at\(([^)]*)\) "
|
||||
r"dmg=([\d.eE+\-]+) lockedTgt=(\S+)")
|
||||
RX_DETAIM = re.compile(r"\[projectile\] DET at\(([^)]*)\).*?aim\(([^)]*)\)")
|
||||
RX_RESET = re.compile(r"Mech::Reset (\d+):(\d+)")
|
||||
RX_DEATH = re.compile(r"\[death\] VehicleDead")
|
||||
|
||||
# rig-sanity floors (A5) -- calibrated against the proven kd_bench/cascade_bench
|
||||
# engagement levels (worker's 180s single-shooter evidence run: 200 dmghits).
|
||||
FLOOR_DMGHITS_TOTAL = 40
|
||||
FLOOR_ENERGY_HITS = 5 # type 3+4 dmghits
|
||||
FLOOR_AC_HITS = 3 # type=1 dmghits
|
||||
FLOOR_MISSILE_HITS = 3 # type=2 dmghits
|
||||
FLOOR_MISSILE_BURSTS = 2 # IMPACT dmg>0 burst>1 deliveries
|
||||
FLOOR_EMITTER_FIRED = 10
|
||||
FLOOR_VITALITY = 1 # deaths + cascades
|
||||
|
||||
|
||||
def _p3(s):
|
||||
try:
|
||||
v = [float(x) for x in s.split(",")]
|
||||
return v if len(v) == 3 else None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _dist(a, b):
|
||||
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||
|
||||
|
||||
def parse(lines):
|
||||
lines = list(lines)
|
||||
d = {
|
||||
"dmghit": [], # (mech, zone, vital, type, amt, burst)
|
||||
"push": [], # (dmg, guided)
|
||||
"det": [], # (dmg, lead)
|
||||
"impact": [], # (dmg, burst)
|
||||
"emitter": {}, # name -> count
|
||||
"ammo": {}, # name -> count
|
||||
"bystander": 0,
|
||||
"bys_recs": [], # (line, struckid, at, dmg, lockptr, aim|None, paired)
|
||||
"bys_capped": 0, # the 24-receipt print cap was reached
|
||||
"own": None, # this node's own mech entity id (first Reset h:ID)
|
||||
"mechids": set(), # every player-mech entity id seen in Reset lines
|
||||
"deaths": 0,
|
||||
"resets": 0,
|
||||
"casc_total": 0,
|
||||
"casc_worst": {}, # zone -> worst per-life repeat count
|
||||
}
|
||||
life = {}
|
||||
for i, l in enumerate(lines):
|
||||
m = RX_DMGHIT.search(l)
|
||||
if m:
|
||||
d["dmghit"].append((m.group(1), int(m.group(2)), int(m.group(3)),
|
||||
int(m.group(4)), float(m.group(5)), int(m.group(6))))
|
||||
continue
|
||||
m = RX_PUSH.search(l)
|
||||
if m:
|
||||
d["push"].append((float(m.group(4)), int(m.group(5))))
|
||||
continue
|
||||
m = RX_BYS.search(l)
|
||||
if m:
|
||||
struck = int(m.group(1))
|
||||
at = _p3(m.group(3))
|
||||
if "receipt cap reached" in l:
|
||||
d["bys_capped"] = 1
|
||||
# the receipt's own detonation follows within a few lines: DET
|
||||
# (carries the round's aim) then the mech delivery IMPACT
|
||||
aim = None
|
||||
paired = False
|
||||
for j in range(i + 1, min(i + 5, len(lines))):
|
||||
dm = RX_DETAIM.search(lines[j])
|
||||
if dm and aim is None:
|
||||
aim = _p3(dm.group(2))
|
||||
if "[projectile] IMPACT damage=" in lines[j]:
|
||||
paired = True
|
||||
break
|
||||
d["bys_recs"].append((i + 1, struck, at, float(m.group(4)),
|
||||
m.group(5), aim, paired))
|
||||
d["bystander"] += 1
|
||||
continue
|
||||
m = RX_DET.search(l)
|
||||
if m:
|
||||
d["det"].append((float(m.group(1)), int(m.group(2))))
|
||||
continue
|
||||
m = RX_IMPACT.search(l)
|
||||
if m:
|
||||
d["impact"].append((float(m.group(1)), int(m.group(2))))
|
||||
continue
|
||||
m = RX_EMIT.search(l)
|
||||
if m:
|
||||
d["emitter"][m.group(1)] = d["emitter"].get(m.group(1), 0) + 1
|
||||
continue
|
||||
m = RX_AMMO.search(l)
|
||||
if m:
|
||||
d["ammo"][m.group(1)] = d["ammo"].get(m.group(1), 0) + 1
|
||||
continue
|
||||
m = RX_CASC.search(l)
|
||||
if m:
|
||||
z = int(m.group(1))
|
||||
life[z] = life.get(z, 0) + 1
|
||||
if life[z] > d["casc_worst"].get(z, 0):
|
||||
d["casc_worst"][z] = life[z]
|
||||
d["casc_total"] += 1
|
||||
continue
|
||||
if "Mech::Reset" in l:
|
||||
life = {}
|
||||
d["resets"] += 1
|
||||
m = RX_RESET.search(l)
|
||||
if m:
|
||||
mid = int(m.group(2))
|
||||
d["mechids"].add(mid)
|
||||
if d["own"] is None:
|
||||
d["own"] = mid
|
||||
continue
|
||||
if RX_DEATH.search(l):
|
||||
d["deaths"] += 1
|
||||
return d
|
||||
|
||||
|
||||
def adjudicate(logs, verbose=True):
|
||||
"""logs: {name: parsed-dict}. Returns (fails, table_rows)."""
|
||||
fails = []
|
||||
|
||||
def agg(key):
|
||||
out = []
|
||||
for nm in logs:
|
||||
out.extend(logs[nm][key])
|
||||
return out
|
||||
|
||||
hits = agg("dmghit")
|
||||
push = agg("push")
|
||||
det = agg("det")
|
||||
impact = agg("impact")
|
||||
emitter = {}
|
||||
ammo = {}
|
||||
for nm in logs:
|
||||
for k, v in logs[nm]["emitter"].items():
|
||||
emitter[k] = emitter.get(k, 0) + v
|
||||
for k, v in logs[nm]["ammo"].items():
|
||||
ammo[k] = ammo.get(k, 0) + v
|
||||
bystander = sum(logs[nm]["bystander"] for nm in logs)
|
||||
deaths = sum(logs[nm]["deaths"] for nm in logs)
|
||||
resets = sum(logs[nm]["resets"] for nm in logs)
|
||||
cascades = sum(logs[nm]["casc_total"] for nm in logs)
|
||||
|
||||
h_energy = [h for h in hits if h[3] in (3, 4)]
|
||||
h_laser = [h for h in hits if h[3] == 3]
|
||||
h_ppc = [h for h in hits if h[3] == 4]
|
||||
h_ball = [h for h in hits if h[3] == 1]
|
||||
h_missile = [h for h in hits if h[3] == 2]
|
||||
h_mis_multi = [h for h in h_missile if h[5] > 1]
|
||||
ball_multi = [h for h in h_ball if h[5] != 1]
|
||||
|
||||
push_ballistic = [p for p in push if p[1] == 0]
|
||||
push_ballistic_dmg = [p for p in push_ballistic if p[0] != 0.0]
|
||||
push_guided_dmg = [p for p in push if p[1] == 1 and p[0] > 0.0]
|
||||
det_dmg = [x for x in det if x[0] != 0.0]
|
||||
imp_dmg = [x for x in impact if x[0] > 0.0]
|
||||
imp_multi = [x for x in imp_dmg if x[1] > 1]
|
||||
|
||||
ammo_ac = {k: v for k, v in ammo.items() if k.upper().startswith("AFC")}
|
||||
ammo_mis = {k: v for k, v in ammo.items()
|
||||
if k.upper().startswith(("LRM", "SRM", "STRK", "STREAK", "NARC", "NRK"))}
|
||||
|
||||
# ---- A1 ENERGY ----
|
||||
a1 = []
|
||||
if not emitter:
|
||||
a1.append("no named [emitter] FIRED lines")
|
||||
if len(h_energy) < FLOOR_ENERGY_HITS:
|
||||
a1.append("laser/PPC dmghits %d < floor %d" % (len(h_energy), FLOOR_ENERGY_HITS))
|
||||
if not h_laser:
|
||||
a1.append("no type=3 laser dmghit landed")
|
||||
if sum(emitter.values()) < FLOOR_EMITTER_FIRED:
|
||||
a1.append("emitter FIRED total %d < floor %d"
|
||||
% (sum(emitter.values()), FLOOR_EMITTER_FIRED))
|
||||
|
||||
# ---- A2 AC HITSCAN (#171) ----
|
||||
a2 = []
|
||||
if len(h_ball) < FLOOR_AC_HITS:
|
||||
a2.append("type=1 dmghits %d < floor %d" % (len(h_ball), FLOOR_AC_HITS))
|
||||
if ball_multi:
|
||||
a2.append("%d type=1 dmghit(s) with burst!=1 (multi-panel ballistic -- "
|
||||
"not the single-panel hitscan)" % len(ball_multi))
|
||||
if push_ballistic_dmg:
|
||||
a2.append("%d ballistic PUSH(es) with dmg!=0 -- a damage-carrying "
|
||||
"ballistic round entered the pool (pre-#171 shape)"
|
||||
% len(push_ballistic_dmg))
|
||||
if not push_ballistic:
|
||||
a2.append("no ballistic (guided=0) PUSH at all -- AC never cycled")
|
||||
if not ammo_ac:
|
||||
a2.append("no [ammo] AFC* FIRED receipt -- no autocannon cycled a round")
|
||||
# every damage-carrying DET must be attributable to a guided (missile) push,
|
||||
# per log (pool is node-local)
|
||||
for nm in logs:
|
||||
nd = len([x for x in logs[nm]["det"] if x[0] != 0.0])
|
||||
ng = len([p for p in logs[nm]["push"] if p[1] == 1 and p[0] > 0.0])
|
||||
if nd > ng:
|
||||
a2.append("%s: %d damage DETs > %d guided damage PUSHes -- an "
|
||||
"unguided round detonated carrying damage" % (nm, nd, ng))
|
||||
|
||||
# ---- A3 MISSILES ----
|
||||
a3 = []
|
||||
if not ammo_mis:
|
||||
a3.append("no [ammo] LRM/SRM FIRED receipt")
|
||||
if len(imp_dmg) < FLOOR_MISSILE_HITS:
|
||||
a3.append("missile IMPACT deliveries %d < floor %d"
|
||||
% (len(imp_dmg), FLOOR_MISSILE_HITS))
|
||||
if len(imp_multi) < FLOOR_MISSILE_BURSTS:
|
||||
a3.append("salvo bursts (IMPACT dmg>0 burst>1) %d < floor %d"
|
||||
% (len(imp_multi), FLOOR_MISSILE_BURSTS))
|
||||
if len(h_missile) < FLOOR_MISSILE_HITS:
|
||||
a3.append("type=2 dmghits %d < floor %d" % (len(h_missile), FLOOR_MISSILE_HITS))
|
||||
if not h_mis_multi:
|
||||
a3.append("no type=2 dmghit with burst>1 (no salvo cluster landed)")
|
||||
if deaths + resets + cascades < FLOOR_VITALITY:
|
||||
a3.append("rig vitality: deaths+resets+cascades = %d < %d"
|
||||
% (deaths + resets + cascades, FLOOR_VITALITY))
|
||||
|
||||
# ---- A4 NO REGRESSION ----
|
||||
a4 = []
|
||||
for nm in logs:
|
||||
dup = {z: c for z, c in logs[nm]["casc_worst"].items() if c > 1}
|
||||
if dup:
|
||||
a4.append("%s re-descended zones %s within one life (#174 ALIVE)"
|
||||
% (nm, dup))
|
||||
# BYSTANDER receipts: every receipt must be a TRUE positive (see the
|
||||
# calibration note in the header -- rounds locked onto scenery legitimately
|
||||
# detonate on the other mech crossing the flight path; the binary's world
|
||||
# sweep fires on ANY solid [T1]). False-positive conditions per receipt:
|
||||
all_mechids = set()
|
||||
for nm in logs:
|
||||
all_mechids |= logs[nm]["mechids"]
|
||||
bys_true = 0
|
||||
for nm in logs:
|
||||
own = logs[nm]["own"]
|
||||
if logs[nm]["bys_capped"]:
|
||||
a4.append("%s: BYSTANDER print cap (24) reached -- receipt count "
|
||||
"unmeasurable, rerun a shorter window" % nm)
|
||||
for (ln, struck, at, bdmg, lockptr, aim, paired) in logs[nm]["bys_recs"]:
|
||||
bad = []
|
||||
if own is not None and struck == own:
|
||||
bad.append("SELF-STRIKE (sweep hit the shooter's own mech)")
|
||||
if all_mechids and struck not in all_mechids:
|
||||
bad.append("phantom strike: id %d is no known player mech"
|
||||
% struck)
|
||||
if aim is None or not paired:
|
||||
bad.append("no paired DET/IMPACT delivery (lost or duplicated "
|
||||
"round)")
|
||||
elif at is not None and _dist(at, aim) < 30.0:
|
||||
bad.append("strike within %.0fu of the round's own aim -- the "
|
||||
"sweep raced the 4.0u fuze at the lock itself "
|
||||
"(target-exclusion suspect)" % _dist(at, aim))
|
||||
if bad:
|
||||
a4.append("%s:%d BYSTANDER FALSE POSITIVE: %s"
|
||||
% (nm, ln, "; ".join(bad)))
|
||||
else:
|
||||
bys_true += 1
|
||||
|
||||
# ---- A5 FLOORS ----
|
||||
a5 = []
|
||||
if len(hits) < FLOOR_DMGHITS_TOTAL:
|
||||
a5.append("total dmghits %d < floor %d (quiet rig proves nothing)"
|
||||
% (len(hits), FLOOR_DMGHITS_TOTAL))
|
||||
|
||||
rows = [
|
||||
("ENERGY", a1, "emitterFIRED=%d(names=%d) hits t3=%d t4=%d"
|
||||
% (sum(emitter.values()), len(emitter),
|
||||
len(h_laser), len(h_ppc))),
|
||||
("AC", a2, "acFIRED=%d hits t1=%d (burst!=1: %d) ballPUSH=%d "
|
||||
"(dmg!=0: %d)"
|
||||
% (sum(ammo_ac.values()), len(h_ball), len(ball_multi),
|
||||
len(push_ballistic), len(push_ballistic_dmg))),
|
||||
("MISSILE", a3, "misFIRED=%d IMPACTdmg=%d(burst>1:%d) hits t2=%d"
|
||||
"(burst>1:%d) FIZZ n/a"
|
||||
% (sum(ammo_mis.values()), len(imp_dmg), len(imp_multi),
|
||||
len(h_missile), len(h_mis_multi))),
|
||||
("NO-REGR", a4, "cascades=%d worstRepeat=%d bystander=%d "
|
||||
"(true-positive crossfire=%d, false=%d)"
|
||||
% (cascades,
|
||||
max([c for nm in logs
|
||||
for c in logs[nm]["casc_worst"].values()] or [0]),
|
||||
bystander, bys_true, bystander - bys_true)),
|
||||
("RIG", a5, "dmghits=%d deaths=%d resets=%d DETdmg>0=%d "
|
||||
"guidedDmgPUSH=%d"
|
||||
% (len(hits), deaths, resets, len(det_dmg),
|
||||
len(push_guided_dmg))),
|
||||
]
|
||||
for fam, fl, _ in rows:
|
||||
fails.extend("%s: %s" % (fam, f) for f in fl)
|
||||
|
||||
if verbose:
|
||||
print("=== WEAPONS SWEEP -- per-family verdict ===")
|
||||
for fam, fl, info in rows:
|
||||
print("%-8s %-4s %s" % (fam, "PASS" if not fl else "FAIL", info))
|
||||
for f in fl:
|
||||
print(" - %s" % f)
|
||||
print("weapon receipts (named, both logs):")
|
||||
for k in sorted(set(list(emitter) + list(ammo))):
|
||||
n = emitter.get(k, 0) + ammo.get(k, 0)
|
||||
src = "emitter" if k in emitter else "ammo"
|
||||
print(" %-16s %4d (%s)" % (k, n, src))
|
||||
for nm in logs:
|
||||
print("%s: dmghits=%d pushes=%d dets=%d impacts=%d casc=%d "
|
||||
"resets=%d deaths=%d bys=%d"
|
||||
% (nm, len(logs[nm]["dmghit"]), len(logs[nm]["push"]),
|
||||
len(logs[nm]["det"]), len(logs[nm]["impact"]),
|
||||
logs[nm]["casc_total"], logs[nm]["resets"],
|
||||
logs[nm]["deaths"], logs[nm]["bystander"]))
|
||||
print("RESULT:", "PASS" if not fails else "FAIL")
|
||||
if fails:
|
||||
for f in fails:
|
||||
print(" FAIL:", f)
|
||||
return fails
|
||||
|
||||
|
||||
def load(fn):
|
||||
return parse(io.open(fn, encoding="latin-1", errors="replace").read().splitlines())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --selftest: the detector must FAIL on pre-fix log shapes (negative controls)
|
||||
# ---------------------------------------------------------------------------
|
||||
HEALTHY = [
|
||||
# energy: named emitters + laser/PPC hits
|
||||
] + [
|
||||
"[emitter] FIRED 'ERMLaser_1' damage=8 heat=2" for _ in range(6)
|
||||
] + [
|
||||
"[emitter] FIRED 'ERPPC' damage=15 heat=6" for _ in range(6)
|
||||
] + [
|
||||
"[dmghit] mech=42 zone=%d vital=0 type=3 amt=8 burst=1 lvl 0->0.1" % (i % 5)
|
||||
for i in range(8)
|
||||
] + [
|
||||
"[dmghit] mech=42 zone=2 vital=0 type=4 amt=15 burst=1 lvl 0->0.2",
|
||||
] + [
|
||||
# AC: named ammo fire, cosmetic tracer pushes, hitscan type=1 burst=1 hits
|
||||
"[ammo] AFC50 FIRED, rounds left=93" for _ in range(4)
|
||||
] + [
|
||||
"[projectile] PUSH target=0x0 len=400 speed=250 dmg=0 guided=0 ttl=5 "
|
||||
"mz=(1,12,1) relY=12 lv=(fallback)" for _ in range(4)
|
||||
] + [
|
||||
"[projectile] DET at(9,2,3) dmg=0 lead=1 tgt=0x0 aim(9,2,3)" for _ in range(4)
|
||||
] + [
|
||||
"[dmghit] mech=42 zone=%d vital=0 type=1 amt=12.5 burst=1 lvl 0->0.1" % (i % 4)
|
||||
for i in range(6)
|
||||
] + [
|
||||
# missiles: guided pushes (1 lead + visuals), IMPACT bursts, type=2 hits
|
||||
"[ammo] LRM15 FIRED, rounds left=110" for _ in range(4)
|
||||
] + [
|
||||
"[projectile] PUSH target=0x5a len=300 speed=180 dmg=52.5 guided=1 ttl=13 "
|
||||
"mz=(1,14,1) relY=14 lv=(auth)" for _ in range(6)
|
||||
] + [
|
||||
"[projectile] DET at(4,5,6) dmg=52.5 lead=1 tgt=0x5a aim(4,5,6)" for _ in range(6)
|
||||
] + [
|
||||
"[projectile] IMPACT damage=52.5 subsys=21 v=540 burnLeft=0 burst=9 "
|
||||
"(direct dispatch) (zone cyl-resolved)" for _ in range(6)
|
||||
] + [
|
||||
"[dmghit] mech=42 zone=%d vital=0 type=2 amt=3.5 burst=%d lvl 0->0.3"
|
||||
% (i % 6, 4 + (i % 8)) for i in range(24)
|
||||
] + [
|
||||
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
|
||||
"[respawn] Mech::Reset 3:42 healed+moved to (0,0,0) alive=1 zones=22 subsys=34",
|
||||
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
|
||||
"[death] VehicleDead(-1) dispatched to the owning player",
|
||||
# the peer's mech id (a second life witness so 43 is a known player mech)
|
||||
"[respawn] Mech::Reset 2:43 healed+moved to (9,0,9) alive=1 zones=22 subsys=32",
|
||||
]
|
||||
|
||||
# a TRUE-positive bystander receipt: struck the OTHER mech (43 != own 42),
|
||||
# far from the round's own aim, one paired delivery -- must NOT fail A4.
|
||||
BYS_TRUE = [
|
||||
"[projectile] BYSTANDER id=43 t=0.7 at(100,10,100) dmg=5 lockedTgt=0BADF00D",
|
||||
"[projectile] DET at(100,10,100) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
||||
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
||||
"(direct dispatch) (zone cyl-resolved)",
|
||||
]
|
||||
|
||||
|
||||
def selftest():
|
||||
ok = True
|
||||
|
||||
def expect(name, lines_a, want_frag):
|
||||
nonlocal ok
|
||||
logs = {"fx_a.log": parse(lines_a), "fx_b.log": parse(HEALTHY)}
|
||||
fails = adjudicate(logs, verbose=False)
|
||||
hit = any(want_frag in f for f in fails)
|
||||
print(" selftest %-28s %s" % (name, "DETECTED" if hit else "** MISSED **"))
|
||||
if not hit:
|
||||
ok = False
|
||||
for f in fails:
|
||||
print(" got:", f)
|
||||
|
||||
print("=== DETECTOR SELF-TEST (negative controls; each MUST fail) ===")
|
||||
# 1. pre-#171: a damage-carrying ballistic round + multi-panel ballistic hit
|
||||
fx = list(HEALTHY) + [
|
||||
"[projectile] PUSH target=0x5a len=400 speed=250 dmg=12 guided=0 ttl=5 "
|
||||
"mz=(1,12,1) relY=12 lv=(fallback)",
|
||||
"[projectile] DET at(9,2,3) dmg=12 lead=1 tgt=0x5a aim(9,2,3)",
|
||||
"[dmghit] mech=42 zone=3 vital=0 type=1 amt=12 burst=3 lvl 0->0.2",
|
||||
]
|
||||
expect("pre-#171 ballistic round", fx, "damage-carrying ballistic")
|
||||
expect("pre-#171 multi-panel t1", fx, "burst!=1")
|
||||
# 2. pre-#174: same zone cascades twice inside one life
|
||||
fx = list(HEALTHY) + [
|
||||
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
|
||||
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
|
||||
]
|
||||
expect("pre-#174 double cascade", fx, "#174 ALIVE")
|
||||
# 3. bystander FALSE positives (each must be flagged) ...
|
||||
fx = list(HEALTHY) + [
|
||||
"[projectile] BYSTANDER id=42 t=0.5 at(50,10,50) dmg=5 lockedTgt=0BADF00D",
|
||||
"[projectile] DET at(50,10,50) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
||||
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
||||
"(direct dispatch) (zone cyl-resolved)",
|
||||
]
|
||||
expect("bystander SELF-strike", fx, "SELF-STRIKE")
|
||||
fx = list(HEALTHY) + [
|
||||
"[projectile] BYSTANDER id=7 t=0.31 at(4,5,6) dmg=8 lockedTgt=0x5a",
|
||||
]
|
||||
expect("bystander phantom strike", fx, "phantom strike")
|
||||
fx = list(HEALTHY) + [
|
||||
"[projectile] BYSTANDER id=43 t=1 at(499,7,899) dmg=5 lockedTgt=0BADF00D",
|
||||
"[projectile] DET at(499,7,899) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
||||
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
||||
"(direct dispatch) (zone cyl-resolved)",
|
||||
]
|
||||
expect("bystander fuze-race at lock", fx, "raced the 4.0u fuze")
|
||||
fx = list(HEALTHY) + BYS_TRUE + [
|
||||
"[projectile] BYSTANDER id=43 t=0.9 at(200,10,200) dmg=5 "
|
||||
"lockedTgt=0BADF00D (receipt cap reached)",
|
||||
"[projectile] DET at(200,10,200) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
||||
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
||||
"(direct dispatch) (zone cyl-resolved)",
|
||||
]
|
||||
expect("bystander print-cap reached", fx, "cap (24) reached")
|
||||
# ... and the TRUE positive must NOT be flagged (no false alarm)
|
||||
logs = {"fx_a.log": parse(HEALTHY + BYS_TRUE), "fx_b.log": parse(HEALTHY)}
|
||||
fails = adjudicate(logs, verbose=False)
|
||||
bysf = [f for f in fails if "BYSTANDER" in f or "SELF" in f]
|
||||
print(" selftest %-28s %s" % ("bystander TRUE pos accepted",
|
||||
"CLEAN" if not bysf else "** FALSE ALARM **"))
|
||||
if bysf:
|
||||
ok = False
|
||||
for f in bysf:
|
||||
print(" got:", f)
|
||||
# 4. quiet rig: both logs empty must FAIL floors
|
||||
logs = {"fx_a.log": parse([]), "fx_b.log": parse([])}
|
||||
fails = adjudicate(logs, verbose=False)
|
||||
quiet = any("quiet rig" in f for f in fails) and any("floor" in f for f in fails)
|
||||
print(" selftest %-28s %s" % ("quiet rig floors",
|
||||
"DETECTED" if quiet else "** MISSED **"))
|
||||
ok = ok and quiet
|
||||
# 5. the healthy fixture itself must PASS (no false alarms in the detector)
|
||||
logs = {"fx_a.log": parse(HEALTHY), "fx_b.log": parse(HEALTHY)}
|
||||
fails = adjudicate(logs, verbose=False)
|
||||
print(" selftest %-28s %s" % ("healthy fixture passes",
|
||||
"CLEAN" if not fails else "** FALSE ALARM **"))
|
||||
if fails:
|
||||
ok = False
|
||||
for f in fails:
|
||||
print(" got:", f)
|
||||
print("DETECTOR:", "OK -- all negative controls detected" if ok else "BROKEN")
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 2 and sys.argv[1] == "--selftest":
|
||||
sys.exit(0 if selftest() else 1)
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: weapons_check.py <log_a> <log_b> | --selftest")
|
||||
sys.exit(2)
|
||||
logs = {fn: load(fn) for fn in sys.argv[1:]}
|
||||
fails = adjudicate(logs, verbose=True)
|
||||
sys.exit(0 if not fails else 1)
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# weapons_sweep.sh -- ONE comprehensive 2-node bench proving every weapons fix
|
||||
# on the final (2026-08-13) tree: #171 AC trigger-time hitscan, missile
|
||||
# contact through the 4.0u fuze at drag-governed speeds (b94636b), energy
|
||||
# emitters, the #174 cascade guard, and the lane-12 bystander sweep's
|
||||
# no-false-positive property in a 2-mech fight.
|
||||
#
|
||||
# RIG (modeled on cascade_bench.sh / kd_bench.sh -- the rigs that demonstrably
|
||||
# engage). KNOWN TRAP dodged: avatar-vs-avatar forms ZERO locks (three runs,
|
||||
# 0 dmghits even from lasers -- see HITSCAN_STATE.md / ac_bench.sh) while the
|
||||
# madcat family fights fine, so BOTH nodes fly madcat-family chassis:
|
||||
# node A (-net 1501 -> pilot 127.0.0.1:1502) : mad2 "Zanin Neko" (Madcat V2)
|
||||
# -- AFC50 autocannon + ERPPC + LRM15 + SRM6 x2 + ERMLaser x2:
|
||||
# every family from one shooter (AC carrier per L4GAUGE.CFG :3008).
|
||||
# node B (-net 1601 -> pilot 127.0.0.1:1602) : madcat
|
||||
# -- AFC100 + LRM15 x2 + ERLLaser + ERSLaser x2 (kd_bench's proven
|
||||
# engager), sparser trigger.
|
||||
# Engagement = the kd_bench pattern: BT_GOTO=enemy drives them onto each other
|
||||
# and the production aim ray designates the enemy (mech+0x388 lock). NOT
|
||||
# BT_FIRE_AT_ICON (that designates buildings -- fine for the cascade rig's
|
||||
# splash chaos, useless for typed mech-vs-mech receipts). AF periods 4/7:
|
||||
# throttled (unthrottled autofire trips the FailureHeat brick) and staggered.
|
||||
#
|
||||
# Assertions + PASS table: scratchpad/night16/weapons_check.py. The checker's
|
||||
# --selftest (run FIRST, gate on it) proves the detector flags the pre-#171
|
||||
# and pre-#174 log shapes, bystander false positives (self-strike / phantom /
|
||||
# fuze-race / print-cap), and a quiet rig -- the week's hard rule: prove the
|
||||
# bench CAN detect failure before trusting PASS.
|
||||
#
|
||||
# RESULT 2026-08-13 (evidence: scratchpad/night16/ws_a.log ws_b.log): PASS.
|
||||
# ENERGY 184 FIRED / 72 laser + 7 PPC hits; AC 42 FIRED / 17 type=1 hits all
|
||||
# burst=1 / 84 ballistic pushes all dmg=0; MISSILE 151 FIRED / 57 damage
|
||||
# IMPACTs (50 salvo bursts) / 593 type=2 hits; 11 deaths; cascades 8, none
|
||||
# re-descended. CALIBRATION FINDING: BYSTANDER receipts DO fire in a 2-mech
|
||||
# fight (11x) and all were TRUE positives -- the production aim ray locks
|
||||
# scenery, and the other mech crossing the round's path detonates it (the
|
||||
# binary world-sweep behavior). The checker asserts the real false-positive
|
||||
# conditions instead of a zero count; see weapons_check.py header.
|
||||
set -x
|
||||
python /c/git/bt411/scratchpad/night16/weapons_check.py --selftest || {
|
||||
echo "DETECTOR SELF-TEST FAILED -- fix the checker before benching"; exit 1; }
|
||||
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
bt_assert_player_env
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
|
||||
rm -f ws_a.log ws_b.log ws_r.log
|
||||
bt_expert_egg MP.EGG WS.EGG
|
||||
# map/time sed is MANDATORY (test-harness: MP.EGG authors cavern/night --
|
||||
# un-sedded copies park the mechs against cavern rock). Vehicle seds are
|
||||
# EXACT-LINE (the ^vehicle=.* form would blindly stamp both pilots the same).
|
||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=bhk1$/vehicle=mad2/; s/^vehicle=ava1$/vehicle=madcat/" WS.EGG
|
||||
grep -q "^vehicle=mad2$" WS.EGG && grep -q "^vehicle=madcat$" WS.EGG || {
|
||||
echo "FAIL: WS.EGG vehicle sed did not land (check MP.EGG pilot pages)"; exit 1; }
|
||||
grep -q "advancedDamage=1" WS.EGG || { echo "FAIL: egg lacks advancedDamage=1"; exit 1; }
|
||||
|
||||
# node B FIRST (back window): madcat, sparser trigger, still all families.
|
||||
( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
|
||||
export BT_DMG_LOG=1 BT_PROJ_LOG=1 BT_DEATH_LOG=1 BT_AMMO_LOG=1
|
||||
bt_launch ws_b.log WS.EGG 0x0C -net 1601 )
|
||||
sleep 2
|
||||
# node A: mad2 (Zanin Neko), the dense shooter -- AC + missiles + energy.
|
||||
( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
|
||||
export BT_DMG_LOG=1 BT_PROJ_LOG=1 BT_DEATH_LOG=1 BT_AMMO_LOG=1
|
||||
bt_launch ws_a.log WS.EGG 0x03 -net 1501 )
|
||||
sleep 5
|
||||
python ../tools/btconsole.py WS.EGG 127.0.0.1:1501 127.0.0.1:1601 > ws_r.log 2>&1 &
|
||||
R=$!
|
||||
sleep 300
|
||||
kill $R 2>/dev/null
|
||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
|
||||
|
||||
python /c/git/bt411/scratchpad/night16/weapons_check.py ws_a.log ws_b.log
|
||||
Reference in New Issue
Block a user