From e2ab17665d0471b6d8dd2aa40a2a485ae7f3047d Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 11 Aug 2026 04:39:37 -0500 Subject: [PATCH] #168 settled from the binary: projectiles are TIME-capped, never range-capped -- plain shell 5.0s (@4bddec), missile burnTime+10s + y<-1 kill-plane (@4bef78), WeaponRange is fire-control only (FireWeapon @4bcc60 has no range gate), so AFC50 hits at 2000+u are authentic. Two port defects killed: the invented len*1.3+60 flight cap (replaced by the authentic clocks) and the stale-point auto-hit (unguided contact tested the fire-time pick, so a locked AC could not miss a moving target -- the pool now tracks the live target). KB: decomp-reference gains the flight/expiry section, CLASSMAP's swapped authoritative/ghost Performance labels corrected (ReplicantInstance=4 [T0]), missile.cpp's misdecoded @4bf594 constant pool re-annotated, Missile proximity fuse 4.0u recorded. Bench: proj_ttl_bench.sh (probe fizzles 5.012/12.012 vs caps 5.0/12.0; 86 field launches, 0 early, 0 phantoms). Rides along: the #163 BTSteamNet_ShutdownAll header decl that missed d3f4b9a. Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 8 +++ context/decomp-reference.md | 28 ++++++++ engine/MUNGA_L4/L4STEAMNET.h | 4 ++ game/reconstructed/CLASSMAP.md | 21 +++++- game/reconstructed/mech4.cpp | 89 ++++++++++++++++++++----- game/reconstructed/missile.cpp | 34 +++++++--- scratchpad/night15/proj_ttl_bench.sh | 99 ++++++++++++++++++++++++++++ 7 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 scratchpad/night15/proj_ttl_bench.sh diff --git a/context/combat-damage.md b/context/combat-damage.md index 42580b3..2f3ec95 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -199,6 +199,14 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test (part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange = dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap. +⚠ `weaponRange` is **fire-control data only** (#168 audit [T1]): projectile FLIGHT is never +range-capped — a plain shell (AFC) lives exactly 5.0 s (@4bddec) and a Missile lives +BurnTime+10 s with a y<−1 kill-plane (@4bef78); FireWeapon @4bcc60 has no range gate, so an +AFC50 (authored range 750) landing hits at 2000+ u is AUTHENTIC arcade behavior. Constants + +the whole flight/expiry chain: [[decomp-reference]] §5 "Projectile/Missile flight & expiry". +The same audit killed the pool's stale-point auto-hit (unguided contact was tested against +the fire-time pick, so a locked AC shell could not miss a moving target — fixed 2026-08-11: +the contact test now tracks the target's live position on every round kind). ⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange` (mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 6b3c8dc..b6f7705 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -253,6 +253,34 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a @004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30 (generator client capacity @0x1e4/count @0x1e8). +### Projectile/Missile flight & expiry (#168 audit, 2026-08-11) [T1] +- **No range cap exists anywhere in the flight chain.** WeaponRange feeds fire-control only + (effectiveRange @0x328 = (1−hostZoneDamage)×WeaponRange — targeting/reticle "in range" call). + FireWeapon @4bcc60 reads no +0x328: the trigger fires at any target distance. +- **Plain Projectile (AFC shell) live integrator @4bddec** (master); expiry: age since + spawn(+0x19c) > `_DAT_004be054` = **5.0 s** → FUN_0042061c kill. Dead-reckon divergence² + vs predicted pos(+0x260, predictor FUN_00422060 via +0x254) > `_DAT_004be050` = 0.1 → + updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill). +- **Missile live integrator @4bef78** (master); expiry: age > burnTime(+0x340, model rec + +0x44) + `_DAT_004bf5ac` = **10.0 s** coast margin, OR pos.y < `_DAT_004bf5b0` = **−1.0** + kill-plane → FUN_0042061c. Keepalive: no update for `_DAT_004bf594`=2.0 s → dirty bit. + **Proximity fuse**: seeker rangeToTarget(+0x10C) < `_DAT_004bf5a4` = **4.0** → detonate on + targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9, + FUN_0049fb54). +- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance + (flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics); + master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped — + corrected 2026-08-11.) +- **FUN_0042061c = Entity kill**: removes from world list + simulationFlags(+0x28) |= 0x2000|2. +- All tick→seconds via `DAT_0052140c` = 28.0 (the frame-rate global). +- Port consequences (both fixed in mech4.cpp, 2026-08-11): (a) the pool's old + `p.range = len×1.3+60` distance-to-target cap was an invention — replaced by the + authentic time caps; (b) the pool's contact sphere tested UNGUIDED rounds against + the FIRE-TIME pick point and dispatched damage to the entity wherever it moved — + a locked AC/Gauss could not miss a moving target (bench: 86/86 "hits" vs a + circling mech). The pool now tracks the target's live position for the contact + test on every round kind; only guided rounds steer. + --- ### The CONFIG-MODE session chain (task #6) [T1] diff --git a/engine/MUNGA_L4/L4STEAMNET.h b/engine/MUNGA_L4/L4STEAMNET.h index d151480..e5552b4 100644 --- a/engine/MUNGA_L4/L4STEAMNET.h +++ b/engine/MUNGA_L4/L4STEAMNET.h @@ -32,6 +32,10 @@ int BTSteamNet_Active(); void BTSteamNet_Pump(); // callbacks + rx drain (game thread) +void + BTSteamNet_ShutdownAll(); // #163: ordered transport death for + // process exit -- close every + // connection, then SteamAPI_Shutdown // // The wire-seam surface (BTNet* wrappers in L4NET.CPP + the marshal). diff --git a/game/reconstructed/CLASSMAP.md b/game/reconstructed/CLASSMAP.md index 5c05a66..dbf831a 100644 --- a/game/reconstructed/CLASSMAP.md +++ b/game/reconstructed/CLASSMAP.md @@ -398,10 +398,29 @@ Missile flight-entity cluster (spawned by MissileLauncher::FireWeapon @4bcc60, s members (best-effort, past Entity base): modelUpdateContext@0x250(+bound update PTR_FUN_005129dc@0x254), world@0x300, currentSegment@0x304, segmentIndex@0x308(desc+0x84), sourceEntity@0x30C(desc+0x88), sourceWeapon@0x310(desc+0x8C), launchPosition@0x314(desc+0x90), launchVelocity@0x320(desc+0x9C), aimDirection@0x32C(desc+0xA8), modelIndex@0x338(desc+0xB4), damageRecord@0x33C(desc+0xB8). - Performance variants PTR_LAB_005129e8(authoritative)/005129f4(ghost); base MoveAndCollide body past window — live integrator is Missile override @4bef78. + Performance variants (CORRECTED 2026-08-11, was swapped): ctor @4be1bc picks by instance — + (flags&0xC)==ReplicantInstance(4, ENTITY.h:318 [T0]) → PTR_LAB_005129e8 → FUN_004221c0 = the REPLICANT + dead-reckoning smoother (blends toward predicted state, no physics/damage); else (master/local) → + PTR_LAB_005129f4 → FUN_004bddec = the LIVE plain-Projectile integrator (was "past window" in the old + export; present in the 2026-08-06 rebuild). Same split in the Missile ctor: 512da0→4221c0 (replicant), + 512dac→4bef78 (master integrator). + Plain-Projectile flight/expiry @4bddec [T1]: integrate → world/entity collide FUN_0042291c (shooter + excluded via @4be058) → hit: detonate+damage FUN_004be078; no-hit: dead-reckon predictor + FUN_00422060 via +0x254 refreshes predicted pos +0x260, divergence² > _DAT_004be050=0.1 sets + +0x18|=1 (replication-dirty, NOT kill); tail: age since spawn(+0x19c) > _DAT_004be054=5.0s → + FUN_0042061c kill (remove + flags|=0x2000|2). NO WeaponRange / distance cap on flight — a shell + flies exactly 5.0s (#168: the authored range=750 is fire-control data only; FireWeapon @4bcc60 + reads no effectiveRange +0x328 — the trigger has no range gate). └─ Missile vtable 00512f2c ctor @4bf5b4 dtor @4bf890 alloc @4bf8bc (size 0x368) [missile.cpp] MoveAndCollide @4bef78 (age/guide/integrate/collide; collision FUN_0042291c, detonate FUN_004be078 streamed ClassID 0x5C), WriteUpdateRecord @4bef4c (slot7, tag 0x78). + Expiry @4bef78 tail [T1]: age since spawn(+0x19c) > burnTime(+0x340, model rec +0x44) + + _DAT_004bf5ac=10.0s coast margin, OR pos.y(+0x104) < _DAT_004bf5b0=-1.0 kill-plane → FUN_0042061c + kill. Also: no replication update for _DAT_004bf594=2.0s → +0x18|=1 (dirty keepalive); + dead-reckon divergence² > _DAT_004bf5a8=0.1 → +0x18|=1. Seeker target DROPPED when the victim's + movementMode ∈ {2,9} = destroyed (FUN_0049fb54). PROXIMITY FUSE: seeker rangeToTarget(+0x10C) < + _DAT_004bf5a4=4.0 → treat targetEntity as the collision victim (detonate) even without a geometry + hit. LRM "range" is therefore physics (speed × lifetime), not a gate. Hosts 2 subsystems: this[0x49]=2 @0x124, roster @0x128 → {Seeker, MissileThruster}. enum {SeekerSubsystem=0, MissileThrusterSubsystem=1}. members (best-effort): lifetime@0x340 ageFraction@0x344 thrustScale@0x348 targetConn@0x34C targetOffset@0x350 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 85197c2..cad33ba 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -844,8 +844,9 @@ struct BTProjectile { Point3D pos; Vector3D vel; // world velocity (authored MuzzleVelocity, steered per frame) Scalar speed; // |vel| held constant through the steer - Scalar traveled; - Scalar range; + Scalar age; // flight seconds since launch (#168) + Scalar ttl; // authentic lifetime cap (s): 5.0 plain shell (@004bddec), + // BurnTime+10.0 missile (@004bef78) -- see the launch site Entity *target; Point3D targetPos; Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead) @@ -1473,6 +1474,8 @@ void ? (mz.y - ((Mech *)shooter)->localOrigin.linearPosition.y) : mz.y; DEBUG_STREAM << "[projectile] PUSH target=" << (void*)target << " len=" << len << " speed=" << speed << " dmg=" << damage + << " guided=" << guided + << " ttl=" << (guided ? ((thrust_burn > 0.0f ? thrust_burn : 0.0f) + 10.0f) : 5.0f) << " mz=(" << mz.x << "," << mz.y << "," << mz.z << ") relY=" << relY << " lv=" << (launch_velocity ? "(auth)" : "(fallback)") << "\n" << std::flush; @@ -1645,8 +1648,18 @@ void { p.vel.x = d.x/len*p.speed; p.vel.y = d.y/len*p.speed; p.vel.z = d.z/len*p.speed; } - p.traveled = 0.0f; - p.range = len * 1.3f + 60.0f; // arc margin; expire past the target + p.age = 0.0f; + // AUTHENTIC lifetime (#168). The old cap (len*1.3+60, distance-to-target) + // was a port invention: the binary never range-caps a round in flight. + // The plain Projectile lives exactly 5.0s (@004bddec tail: + // age > _DAT_004be054=5.0 -> FUN_0042061c kill) and the Missile lives + // BurnTime+10.0s (@004bef78 tail: age > burnTime(+0x340) + + // _DAT_004bf5ac=10.0), plus a kill-plane at y < -1 (_DAT_004bf5b0). + // WeaponRange is FIRE-CONTROL data only (effectiveRange @0x328) -- the + // trigger itself has no range gate (@004bcc60 reads no +0x328), so a + // shell landing far beyond its listed range is authentic arcade behavior. + p.ttl = guided ? ((thrust_burn > 0.0f ? thrust_burn : 0.0f) + 10.0f) + : 5.0f; p.target = (Entity *)target; p.targetPos = tpos; // resolved target position (fallback-aware) // live re-lead (authentic: the Seeker re-leads the MOVING target every @@ -1724,19 +1737,23 @@ static void // squared-error guidance: rotate the velocity toward the lofted aim at // the decomp turn gain (MissileTurnGain = 4.0, _DAT_004bf5a4), speed // held at the authored launch speed. + // LIVE TARGET TRACKING -- for EVERY round kind, not just guided (#168 + // audit). The contact sphere below is the port's stand-in for the + // binary's real-geometry collision (FUN_0042291c hits the mech's BODY + // wherever it is NOW); testing it against the fire-time pick point let + // an unguided shell "contact" empty air after the target moved and + // still dispatch full damage to the departed mech -- a locked + // autocannon could not miss a moving target. Track the entity for the + // contact test; only GUIDED rounds also steer onto it. + extern int BTIsRegisteredMech(Entity *e); + if (p.target != 0 && BTIsRegisteredMech(p.target) + && !((Mech *)p.target)->IsMechDestroyed()) + { + p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition; + p.targetPos.y += p.aimOffsetY; + } if (p.guided) { - // LIVE RE-LEAD (authentic: Seeker::LeadTarget runs every slice on - // the MOVING target): refresh the aim from the target's current - // position, preserving the launch aim's body height. - extern int BTIsRegisteredMech(Entity *e); - if (p.target != 0 && BTIsRegisteredMech(p.target) - && !((Mech *)p.target)->IsMechDestroyed()) - { - p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition; - p.targetPos.y += p.aimOffsetY; - } - Point3D aim = p.targetPos; Scalar rx = aim.x - p.pos.x, ry = aim.y - p.pos.y, rz = aim.z - p.pos.z; Scalar range = (Scalar)sqrtf(rx*rx + ry*ry + rz*rz); @@ -1789,7 +1806,7 @@ static void p.speed = ns; } p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt; - p.traveled += p.speed * dt; + p.age += dt; // WORLD IMPACT (authentic: the binary missile runs a world collision // query every frame, FUN_0042291c, and DETONATES on geometry). Ray the @@ -1894,10 +1911,13 @@ static void contactD2 = cx*cx + cy*cy + cz*cz; } const int contact = (contactD2 < (10.0f*10.0f)); - if (!contact && p.traveled >= p.range) + if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f))) { if (getenv("BT_PROJ_LOG")) - DEBUG_STREAM << "[projectile] FIZZLE (flight cap, no contact)\n" << std::flush; + DEBUG_STREAM << "[projectile] FIZZLE t=" << p.age + << " ttl=" << p.ttl << " guided=" << p.guided + << ((p.guided && p.pos.y < -1.0f) ? " KILLPLANE" : "") + << "\n" << std::flush; p.active = 0; continue; } @@ -7106,6 +7126,39 @@ void gBTGenSelKey = pulse ? s_gsID : 0; } + // TIMEOUT PROBE (BT_PROJ_TEST=1, off by default; the #168 bench). At + // t=+5s push two DAMAGE-0 rounds aimed at a point 40000u overhead -- + // unreachable at speed 400, no target, no terrain in the path -- so + // each MUST end in the [projectile] FIZZLE receipt at exactly its + // lifetime cap: unguided ttl=5.0 (@004bddec), guided burn=2 -> + // ttl=12.0 (@004bef78). The 2-node rig proves real flights never + // fizzle EARLY; this proves the timeout itself fires at the authentic + // second. + { + static int s_ptArmed = -1; + static float s_ptClock = 0.0f; + if (s_ptArmed < 0) + s_ptArmed = (getenv("BT_PROJ_TEST") != 0) ? 1 : 0; + if (s_ptArmed == 1 + && (Entity *)this == application->GetViewpointEntity()) + { + s_ptClock += dt; + if (s_ptClock >= 5.0f) + { + s_ptArmed = 2; // once per process + extern void BTPushProjectile(const Point3D &, void *, void *, + const Point3D &, Scalar, Scalar, const Vector3D *, int, + int, int, int, int, Scalar, Scalar, int); + Point3D mzp = localOrigin.linearPosition; mzp.y += 14.0f; + Point3D upp = mzp; upp.y += 40000.0f; + BTPushProjectile(mzp, this, 0, upp, 400.0f, 0.0f, 0, 0, + -1, 0, -1, 2, 0.0f, 0.0f, 0); + BTPushProjectile(mzp, this, 0, upp, 400.0f, 0.0f, 0, 1, + -1, 0, -1, 2, 0.0f, 2.0f, 0); + } + } + } + // SELF-DAMAGE HARNESS (BT_SELF_DAMAGE=, off by default). // The whole RESPAWN family -- torso twist after respawn (#70), ammo/weapon // reset (#22), coolant/heat/generator restore (#55), the respawn latch diff --git a/game/reconstructed/missile.cpp b/game/reconstructed/missile.cpp index 4b3faa3..9ac1ebb 100644 --- a/game/reconstructed/missile.cpp +++ b/game/reconstructed/missile.cpp @@ -20,9 +20,23 @@ // excluded : the Projectile base ctor @004be1bc and the 0x41xxxx/0x42xxxx // Entity engine vtable slots // -// Decoded MoveAndCollide tuning constants (.rdata @004bf594..@004bf5b0): -// 0x40000000 = 2.0f 0x38d1b717 = 1.0e-4f 0x40800000 = 4.0f -// 0x3dcccccd = 0.1f 0x41200000 = 10.0f 0xbf800000 = -1.0f +// Decoded MoveAndCollide tuning constants (.rdata @004bf594..@004bf5b0). +// ⚠ RE-DECODED 2026-08-11 (#168 audit, byte-read + full @4bef78 walk) -- the +// original decode below misattributed most of these. Verified semantics: +// _DAT_004bf594 = 2.0f replication KEEPALIVE: no update sent for 2.0s +// -> updateModel(+0x18)|=1 (ForceUpdate), NOT a death +// _DAT_004bf598 = 0.0f per-axis drag-coefficient sign pick (rel-vel vs wind) +// _DAT_004bf59c/a0 = ∓1e-4 signed-square deadband in the DRAG term +// _DAT_004bf5a4 = 4.0f PROXIMITY FUSE: seeker rangeToTarget(+0x10C) < 4.0 +// -> detonate on targetEntity without a geometry hit +// _DAT_004bf5a8 = 0.1f dead-reckon divergence² (vs predictor FUN_00422060 +// pos +0x260) -> replication-dirty +// _DAT_004bf5ac = 10.0f COAST MARGIN: expiry at age > burnTime(+0x340)+10.0s +// _DAT_004bf5b0 = -1.0f kill-plane: pos.y < -1 -> FUN_0042061c retire +// The steering/lifetime code below still reflects the OLD decode -- this TU is +// DORMANT (nothing constructs Missile; the mech4.cpp pool is the live path) -- +// rework it against context/decomp-reference.md §5 "Projectile/Missile flight +// & expiry" before ever wiring it live. // // Helper-function name mapping: // FUN_004be1bc Projectile base constructor (vtable @00512a5c) @@ -53,12 +67,14 @@ // // MoveAndCollide tuning constants (read-only globals in the decomp). // -static const Scalar MissileLifeWarn = 2.0f; // _DAT_004bf594 age-ratio dead threshold -static const Scalar MissileSteerEps = 1.0e-4f;// _DAT_004bf598/9c/a0 steering deadband -static const Scalar MissileTurnGain = 4.0f; // _DAT_004bf5a4 -static const Scalar MissileDriftGain = 0.1f; // _DAT_004bf5a8 -static const Scalar MissileMinSpeed2 = 10.0f; // _DAT_004bf5ac/a8 min squared-speed before fizzle -static const Scalar MissileDead = -1.0f; // _DAT_004bf5b0 +// ⚠ names below carry the OLD (wrong) decode -- see the corrected table in the +// header comment; kept only so the dormant skeleton still compiles unchanged. +static const Scalar MissileLifeWarn = 2.0f; // _DAT_004bf594 actually: replication keepalive (s) +static const Scalar MissileSteerEps = 1.0e-4f;// _DAT_004bf59c/a0 actually: drag signed-square deadband +static const Scalar MissileTurnGain = 4.0f; // _DAT_004bf5a4 actually: proximity-fuse radius (u) +static const Scalar MissileDriftGain = 0.1f; // _DAT_004bf5a8 actually: dead-reckon divergence² +static const Scalar MissileMinSpeed2 = 10.0f; // _DAT_004bf5ac actually: coast margin (s) past burnTime +static const Scalar MissileDead = -1.0f; // _DAT_004bf5b0 kill-plane y (this one was right) //########################################################################### //########################################################################### diff --git a/scratchpad/night15/proj_ttl_bench.sh b/scratchpad/night15/proj_ttl_bench.sh new file mode 100644 index 0000000..76684e1 --- /dev/null +++ b/scratchpad/night15/proj_ttl_bench.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# #168: projectile lifetime bench (v5). Two phases: +# +# PHASE 1 (probe, solo ~40s): BT_PROJ_TEST pushes two damage-0 rounds straight +# up (no target, no terrain in the path) -- each MUST fizzle at exactly its +# authentic cap: unguided ttl=5.0s (@004bddec _DAT_004be054), guided burn=2 -> +# ttl=12.0s (@004bef78 burnTime + _DAT_004bf5ac=10). Proves the timeout fires +# at the authentic second. +# +# PHASE 2 (field, 2-node ~250s): A (avatar) autofires AC + launcher salvos at +# B (madcat) walking at full throttle. Proves: every launch stamps the +# authentic ttl (5.0 / burn+10), NO round fizzles early (the old len*1.3+60 +# distance cap is gone), and unguided DETs land only on the target's LIVE +# position (stale-point phantom hits dead: DET at() must ~= aim()). +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3 +rm -f pttl_probe.log pttl_a.log pttl_b.log pttl_relay.log + +# ---------------- PHASE 1: the timeout probe (solo) ---------------- +bt_expert_egg MP.EGG PTTL.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=avatar/" PTTL.EGG +( export BT_PROJ_TEST=1 BT_PROJ_LOG=1 + bt_launch pttl_probe.log PTTL.EGG 0x03 -egg PTTL.EGG ) +sleep 75 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3 + +# ---------------- PHASE 2: the field rig (2-node) ---------------- +bt_expert_egg MP.EGG PTTL.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/" PTTL.EGG +python - <<'EOF' +lines = open('PTTL.EGG').read().splitlines(True) +n = 0 +for i, l in enumerate(lines): + if l.startswith('vehicle='): + n += 1 + lines[i] = 'vehicle=avatar\n' if n == 1 else 'vehicle=madcat\n' +open('PTTL.EGG', 'w').writelines(lines) +print('vehicles set:', n) +EOF +( export BT_AUTODRIVE=1.0 BT_SPIN_SELF=45 BT_MP_LOG=1 + bt_launch pttl_b.log PTTL.EGG 0x0C -net 1601 ) +sleep 2 +( export BT_SPIN_SELF=30 BT_KEY_NOFOCUS=1 + export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7 + export BT_PROJ_LOG=1 BT_MP_LOG=1 + bt_launch pttl_a.log PTTL.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py PTTL.EGG 127.0.0.1:1501 127.0.0.1:1601 > pttl_relay.log 2>&1 & +RELAY=$! +sleep 240 +kill $RELAY 2>/dev/null +sleep 2 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1 + +python - <<'PY' +import io, re, math +ok = True + +# ---- phase 1: the probe ---- +ptxt = io.open("pttl_probe.log", encoding="latin-1", errors="replace").read() +pfz = re.findall(r"\[projectile\] FIZZLE t=([0-9.]+) ttl=([0-9.]+) guided=(\d)", ptxt) +u = [(float(t), float(c)) for t, c, g in pfz if g == '0'] +g = [(float(t), float(c)) for t, c, g in pfz if g == '1'] +print("=== #168 PHASE 1: TIMEOUT PROBE ===") +print("probe fizzles unguided/guided:", u, "/", g) +p1 = (len(u) == 1 and len(g) == 1 + and abs(u[0][1] - 5.0) < 0.001 and u[0][0] >= 5.0 and u[0][0] < 5.3 + and abs(g[0][1] - 12.0) < 0.001 and g[0][0] >= 12.0 and g[0][0] < 12.3) +print("phase 1:", "PASS -- timeouts fired at the authentic caps" if p1 else "FAIL") +ok &= p1 + +# ---- phase 2: the field rig ---- +txt = io.open("pttl_a.log", encoding="latin-1", errors="replace").read() +push = re.findall(r"\[projectile\] PUSH .*guided=(\d) ttl=([0-9.]+)", txt) +fz = re.findall(r"\[projectile\] FIZZLE t=([0-9.]+) ttl=([0-9.]+) guided=(\d)", txt) +p0 = [t for gg, t in push if gg == '0']; p1n = [t for gg, t in push if gg == '1'] +badp0 = [t for t in p0 if abs(float(t) - 5.0) > 0.001] +badp1 = [t for t in p1n if float(t) < 10.0] +early = [(t, c, gg) for t, c, gg in fz if float(t) < float(c) - 0.05] +# phantom check: every unguided DET must burst ON the live aim (at ~= aim) +phantom = [] +for m in re.finditer(r"DET at\(([-0-9.]+),([-0-9.]+),([-0-9.]+)\) dmg=13 .*aim\(([-0-9.]+),([-0-9.]+),([-0-9.]+)\)", txt): + ax, ay, az, bx, by, bz = map(float, m.groups()) + if math.sqrt((ax-bx)**2 + (ay-by)**2 + (az-bz)**2) > 12.0: + phantom.append(m.group(0)[:90]) +print("=== #168 PHASE 2: FIELD RIG ===") +print("PUSH unguided/guided :", len(p0), "/", len(p1n)) +print("bad ttl stamps :", len(badp0) + len(badp1)) +print("early fizzles :", len(early), early[:3]) +print("phantom unguided DETs:", len(phantom), phantom[:2]) +p2 = (len(p0) > 0 and len(p1n) > 0 and not badp0 and not badp1 + and not early and not phantom) +print("phase 2:", "PASS -- stamps authentic, none early, no stale-point hits" if p2 else "FAIL") +ok &= p2 + +print("VERDICT:", "PASS" if ok else "FAIL") +PY