#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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
dfefdfa1d0
commit
e2ab17665d
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=<amount per second>, off by default).
|
||||
// The whole RESPAWN family -- torso twist after respawn (#70), ammo/weapon
|
||||
// reset (#22), coolant/heat/generator restore (#55), the respawn latch
|
||||
|
||||
@@ -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)
|
||||
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user