MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified: COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity per contact and ProcessCollisionList walks EVERY touched solid per frame; with 2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy). Fix: frameEntryWorldVelocity restore per contact (damage always priced at the real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry. Gotcha #16 (engine-facility drift class). MISSILES: peer-visible salvos (the launcher record extension carries a salvo counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone never serialized), the authentic arc (authored MuzzleVelocity vector + the Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78), world-impact bursts (rounds detonate on cave geometry instead of phasing through), contact-only damage (flight-cap expiry = fizzle, no more teleport damage), live re-lead, and ballistic (unguided) shells for autocannons. projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6 corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded. RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244 is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation (never the translation); worldToView now Invert(view) built rotation-first. CulturalIcons sorted out of the moving grid (the phantom red pips were map props), visible-radius culls on all three draw passes, live pip verified at |delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape). WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X lamps, the panel look, the children enable and the ready-lamp gate (which had NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev composite shows ALL pages at once, so off-page dials legitimately stopped; under BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio untouched). The SEH gauge guard now names its kills; repaint-heal resets the incremental arc after panel repaints; [panel]/[arc] probes added. COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole Player__StatusMessage queue; wired the binary's one producer -- the kill branch, victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS counted via the observed-death tally (each node scores every pilot from locally observed events, the same model as the KILLS credit) and the -2/-1 engine seed clamped for display. DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480, true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering, BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar (first second-chassis live outing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
dd27238ceb
commit
bb795e2805
+210
-12
@@ -764,13 +764,15 @@ int
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
struct BTProjectile {
|
||||
Point3D pos;
|
||||
Vector3D dir;
|
||||
Scalar speed;
|
||||
Vector3D vel; // world velocity (authored MuzzleVelocity, steered per frame)
|
||||
Scalar speed; // |vel| held constant through the steer
|
||||
Scalar traveled;
|
||||
Scalar range;
|
||||
Entity *target;
|
||||
Point3D targetPos;
|
||||
Scalar damage;
|
||||
Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead)
|
||||
Scalar damage; // <= 0 -> VISUAL-ONLY round (replicant-side salvo mirror)
|
||||
int guided; // 1 = missile (seeker loft + steering); 0 = ballistic (straight)
|
||||
int active;
|
||||
};
|
||||
static BTProjectile gProjectiles[64];
|
||||
@@ -782,7 +784,7 @@ extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, fl
|
||||
// locked target entity + point, the launch speed (|muzzleVelocity|), and per-shot damage.
|
||||
void
|
||||
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
|
||||
Scalar speed, Scalar damage)
|
||||
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
|
||||
{
|
||||
// The weapon's GetMuzzlePoint falls back to the mech ORIGIN (feet) when its mount
|
||||
// segment doesn't resolve, so a projectile appeared to launch from the mech's feet.
|
||||
@@ -824,7 +826,9 @@ void
|
||||
// like the energy weapons. (The old gEnemyMech fallback pre-dated the
|
||||
// acquisition and let missiles bypass it.)
|
||||
Point3D tpos = targetPos;
|
||||
if (target == 0)
|
||||
// A VISUAL round (damage <= 0, the replicant-side salvo mirror) flies on the
|
||||
// replicated aim POINT alone; a live round still requires the designation.
|
||||
if (target == 0 && damage > 0.0f)
|
||||
return;
|
||||
Vector3D d;
|
||||
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
|
||||
@@ -838,13 +842,45 @@ void
|
||||
if (gProjectiles[i].active) continue;
|
||||
BTProjectile &p = gProjectiles[i];
|
||||
p.pos = mz; // resolved launch port
|
||||
p.dir.x = d.x/len; p.dir.y = d.y/len; p.dir.z = d.z/len;
|
||||
p.speed = (speed > 1.0f) ? speed : 120.0f; // |launchVelocity|; sane fallback
|
||||
|
||||
// AUTHENTIC LAUNCH (missile-arc wave): the launcher's authored
|
||||
// MuzzleVelocity is a VECTOR in the shooter's frame (typically
|
||||
// up-tilted); rotate it into world by the shooter's pose so the round
|
||||
// leaves the rack climbing -- the front half of the pod's missile arc.
|
||||
// (The old code collapsed it to |v| along the straight line to the
|
||||
// target: dead-flat flight.) Fall back to the straight line when no
|
||||
// authored vector reaches us (non-missile callers).
|
||||
int haveLaunch = 0;
|
||||
if (launch_velocity != 0 && shooter != 0)
|
||||
{
|
||||
Mech *sm = (Mech *)shooter;
|
||||
UnitVector ax, ay, az;
|
||||
sm->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y + az.x*launch_velocity->z;
|
||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y + az.y*launch_velocity->z;
|
||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y + az.z*launch_velocity->z;
|
||||
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
|
||||
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
|
||||
}
|
||||
if (!haveLaunch)
|
||||
{
|
||||
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 + 40.0f; // impact by the target, else expire just past it
|
||||
p.range = len * 1.3f + 60.0f; // arc margin; expire past the target
|
||||
p.target = (Entity *)target;
|
||||
p.targetPos = tpos; // resolved target position (fallback-aware)
|
||||
// live re-lead (authentic: the Seeker re-leads the MOVING target every
|
||||
// slice): remember the aim's height above the target's origin so the
|
||||
// per-frame refresh keeps striking the same body height.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
p.aimOffsetY = (target != 0 && BTIsRegisteredMech((Entity *)target))
|
||||
? (tpos.y - ((Mech *)target)->localOrigin.linearPosition.y) : 0.0f;
|
||||
p.damage = damage;
|
||||
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
|
||||
p.active = 1;
|
||||
return;
|
||||
}
|
||||
@@ -859,23 +895,117 @@ static void
|
||||
BTProjectile &p = gProjectiles[i];
|
||||
if (!p.active) continue;
|
||||
Point3D prev = p.pos;
|
||||
Scalar step = p.speed * dt;
|
||||
p.pos.x += p.dir.x*step; p.pos.y += p.dir.y*step; p.pos.z += p.dir.z*step;
|
||||
p.traveled += step;
|
||||
|
||||
// AUTHENTIC GUIDANCE (missile-arc wave; Seeker::LeadTarget @004beae4 +
|
||||
// the @004bef78 steering [T1]). The seeker LOFTS the aim point while
|
||||
// the round is far out -- beyond 200 m the aim rises by
|
||||
// 0.1 x clamp(range-200, 0, 300) (up to +30 m above the target) -- so
|
||||
// the missile climbs at range and dives as it closes: the pod's
|
||||
// "gravity arc". Steering is the port shape of the binary's
|
||||
// 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.
|
||||
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);
|
||||
if (range > 200.0f) // SeekerLeadMinRange @004bec18
|
||||
{
|
||||
Scalar lead = range - 200.0f;
|
||||
if (lead > 300.0f) lead = 300.0f; // SeekerLeadMaxClamp @004bec24
|
||||
aim.y += 0.1f * lead; // SeekerLeadCoef @004bec28
|
||||
ry = aim.y - p.pos.y;
|
||||
}
|
||||
if (range > 0.001f)
|
||||
{
|
||||
Scalar inv = 1.0f / range;
|
||||
Scalar cx = p.vel.x / p.speed, cy = p.vel.y / p.speed, cz = p.vel.z / p.speed;
|
||||
// MissileTurnGain (4.0, _DAT_004bf5a4) far out; DOUBLED inside
|
||||
// the no-loft radius so the dive converges onto the contact
|
||||
// sphere (the port shape of the binary's squared-error terminal
|
||||
// aggressiveness -- error^2 steering climbs steeply near boresight).
|
||||
Scalar k = ((range < 200.0f) ? 8.0f : 4.0f) * dt;
|
||||
cx += (rx*inv - cx) * k; cy += (ry*inv - cy) * k; cz += (rz*inv - cz) * k;
|
||||
Scalar cl = (Scalar)sqrtf(cx*cx + cy*cy + cz*cz);
|
||||
if (cl > 0.001f)
|
||||
{
|
||||
p.vel.x = cx/cl*p.speed; p.vel.y = cy/cl*p.speed; p.vel.z = cz/cl*p.speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// WORLD IMPACT (authentic: the binary missile runs a world collision
|
||||
// query every frame, FUN_0042291c, and DETONATES on geometry). Ray the
|
||||
// flight step against the terrain/cave solids -- a lofted round in a
|
||||
// low cavern bursts on the CEILING instead of punching through it.
|
||||
{
|
||||
extern bool BTGroundRayHit(float,float,float, float,float,float,
|
||||
float, float*,float*,float*);
|
||||
extern Entity *gBTTerrainEntity; // captured by MakeEntityRenderables
|
||||
Scalar step = p.speed * dt;
|
||||
if (gBTTerrainEntity != 0 && step > 0.0001f)
|
||||
{
|
||||
Vector3D rd;
|
||||
rd.x = p.vel.x/p.speed; rd.y = p.vel.y/p.speed; rd.z = p.vel.z/p.speed;
|
||||
float hx, hy, hz;
|
||||
if (BTGroundRayHit(prev.x, prev.y, prev.z, rd.x, rd.y, rd.z,
|
||||
step + 1.0f, &hx, &hy, &hz))
|
||||
{
|
||||
// burst on the rock: a tight puff cluster at the hit, no damage
|
||||
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
|
||||
for (int pf = 0; pf < 4; ++pf)
|
||||
BTPfxTrailPuff(0, hx, hy, hz, -rd.x, -rd.y, -rd.z, 2);
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] WORLD burst at(" << hx << ","
|
||||
<< hy << "," << hz << ")\n" << std::flush;
|
||||
p.active = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// AUTHENTIC missile look: the round is a short hot streak, and the trail
|
||||
// is the real dsrm smoke-trail effect (psfx 0, "the lrm smoke trail")
|
||||
// puffed along the flight path each frame with local +Z = backward (the
|
||||
// .PFX's velocities stream the smoke behind the round). This replaces
|
||||
// the old 3-segment white tracer lines (a bring-up placeholder).
|
||||
Vector3D bd = p.dir;
|
||||
Vector3D bd;
|
||||
bd.x = p.vel.x/p.speed; bd.y = p.vel.y/p.speed; bd.z = p.vel.z/p.speed;
|
||||
BTPushBeam(prev.x, prev.y, prev.z, p.pos.x, p.pos.y, p.pos.z, 0x00FFB040u, 0.10f, 0.9f); // the round (hot streak)
|
||||
{
|
||||
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
|
||||
BTPfxTrailPuff(0, prev.x, prev.y, prev.z, -bd.x, -bd.y, -bd.z, 2);
|
||||
}
|
||||
|
||||
// CONTACT-ONLY damage (user-verified bug: an arcing round that ran out
|
||||
// its flight cap while still descending "applied damage without making
|
||||
// contact"). Proximity = the hit; the flight-cap expiry is a FIZZLE --
|
||||
// no damage, matching the binary (a missile that dies mid-air detonates
|
||||
// nothing; only the world-collision hit spawns the Damage entity).
|
||||
Scalar dx = p.targetPos.x - p.pos.x, dy = p.targetPos.y - p.pos.y, dz = p.targetPos.z - p.pos.z;
|
||||
if (dx*dx + dy*dy + dz*dz < (10.0f*10.0f) || p.traveled >= p.range)
|
||||
const int contact = (dx*dx + dy*dy + dz*dz < (10.0f*10.0f));
|
||||
if (!contact && p.traveled >= p.range)
|
||||
{
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] FIZZLE (flight cap, no contact)\n" << std::flush;
|
||||
p.active = 0;
|
||||
continue;
|
||||
}
|
||||
if (contact)
|
||||
{
|
||||
Entity *tgt = p.target;
|
||||
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||
@@ -1053,6 +1183,12 @@ void
|
||||
projectedVelocity = Motion::Identity;
|
||||
updateVelocity = Motion::Identity;
|
||||
updateAcceleration = Motion::Identity;
|
||||
// The Mover motion scalars too (the binary Reset zeroes the whole motion
|
||||
// block): a respawn is a TELEPORT -- stale death-frame velocity must never
|
||||
// survive into the first post-respawn collision/publish.
|
||||
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
localVelocity = Motion::Identity;
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
|
||||
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
|
||||
// the decomp zeroes map to these named members in our layout) ---
|
||||
@@ -1281,6 +1417,21 @@ void
|
||||
DEBUG_STREAM << "[damage] *** " << GetEntityID()
|
||||
<< " DESTROYED (death effects dispatched from the death transition) ***\n"
|
||||
<< std::flush;
|
||||
|
||||
// OBSERVED-DEATH tally (MP DEATHS fix): a REPLICANT's death observed
|
||||
// here counts onto its owning player's LOCAL score copy -- the
|
||||
// symmetric half of the cross-pod KILLS credit (see btplayer.cpp
|
||||
// BTPlayerCountObservedDeath). Masters count through their own
|
||||
// VehicleDead(-1) path; counting them here too would double-tally.
|
||||
if (GetInstance() == ReplicantInstance)
|
||||
{
|
||||
Player *owning_player = GetOwningPlayer(); // entity+0x190
|
||||
if (owning_player != 0)
|
||||
{
|
||||
extern void BTPlayerCountObservedDeath(void *);
|
||||
BTPlayerCountObservedDeath(owning_player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1398,6 +1549,20 @@ void
|
||||
const float fdot = -((float)wv.x * (float)zAxR.x
|
||||
+ (float)wv.z * (float)zAxR.z); // mech faces -Z
|
||||
replMppr->speedDemand = (fdot < 0.0f) ? -spd : spd;
|
||||
// REPLICANT TURN-STEP (user-reported: a turning peer
|
||||
// statue-rotates while the local mech steps): the leg SM's
|
||||
// Standing case arms the turn-in-place "trn" clip (state 4,
|
||||
// mech2.cpp:590) from mppr->turnDemand -- which nothing fed on
|
||||
// a replicant, so pivots never stepped. Feed it from the
|
||||
// REPLICATED yaw rate (updateVelocity.angularMotion.y, stored
|
||||
// by Mover::ReadUpdateRecord from the master's localVelocity
|
||||
// -- the same stream DeadReckon rotates the body with). Only
|
||||
// the +-0.05 threshold matters to the SM (the trn clip
|
||||
// advances at fixed cadence), so a signed unit demand
|
||||
// suffices; the deadband ignores dead-reckon jitter.
|
||||
const float yawRate = (float)updateVelocity.angularMotion.y;
|
||||
replMppr->turnDemand = (yawRate > 0.02f) ? 1.0f
|
||||
: (yawRate < -0.02f) ? -1.0f : 0.0f;
|
||||
// Prime the same clip-advance scalars the master's gait block sets
|
||||
// each frame -- uninitialized on a replicant they read 0, freezing
|
||||
// the clip at advance-time dt*0 (observed: legState engaged at 11,
|
||||
@@ -2406,6 +2571,7 @@ void
|
||||
if (cols)
|
||||
{
|
||||
Point3D before = localOrigin.linearPosition;
|
||||
frameEntryWorldVelocity = worldLinearVelocity; // collision-damage guard (see mech.hpp)
|
||||
Damage collisionDamage; // filled by ProcessCollisionList (unused)
|
||||
ProcessCollisionList(cols, dt, collisionOldPos, &collisionDamage); // pushes localOrigin out
|
||||
Scalar dx = localOrigin.linearPosition.x - before.x;
|
||||
@@ -3621,6 +3787,12 @@ void
|
||||
if (isPlayer && gBlockCooldown > 0.0f)
|
||||
gBlockCooldown -= dt; // decay the out-of-contact window
|
||||
// BINARY-TAIL-DEFERRED: collisionTemporaryState = 0 here (@4aa741).
|
||||
// COLLISION-DAMAGE ECONOMY GUARD: snapshot the TRUE frame motion for the
|
||||
// per-contact velocity restore in Mech::ProcessCollision (see mech.hpp) --
|
||||
// the list walk below reflects worldLinearVelocity at every StaticBounce,
|
||||
// and a multi-solid frame compounds those reflections into a garbage
|
||||
// velocity that priced mech-vs-mech damage 4x-40x too high (mp_a.log:32651).
|
||||
frameEntryWorldVelocity = savedWorldVel;
|
||||
Damage dmg; // ctor zeroes damageAmount
|
||||
if (cols != 0)
|
||||
ProcessCollisionList(cols, dt, old_position, &dmg);
|
||||
@@ -3823,6 +3995,20 @@ static void
|
||||
DEBUG_STREAM << "[collide] dmg=" << resolved->damageAmount
|
||||
<< " -> victim class=" << (int)victim->GetClassID()
|
||||
<< " (zone==-1, resolved by receiver)\n" << std::flush;
|
||||
|
||||
// RAM ECONOMY TELEMETRY (the MP ram one-shot investigation): every
|
||||
// dispatched ram, with the exact velocity StaticBounce priced it from --
|
||||
// the decisive evidence channel for any future out-of-economy amount.
|
||||
if (getenv("BT_MP_NET"))
|
||||
{
|
||||
const Vector3D &v = inflictor->GetWorldLinearVelocity();
|
||||
DEBUG_STREAM << "[collide-tx] " << inflictor->GetEntityID()
|
||||
<< " rams " << victim->GetEntityID()
|
||||
<< " dmg=" << resolved->damageAmount
|
||||
<< " |v|=" << (Scalar)sqrtf(v.x*v.x + v.y*v.y + v.z*v.z)
|
||||
<< " at(" << dmg.impactPoint.x << "," << dmg.impactPoint.y
|
||||
<< "," << dmg.impactPoint.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -3838,6 +4024,18 @@ void
|
||||
return;
|
||||
}
|
||||
|
||||
// COLLISION-DAMAGE ECONOMY GUARD (see mech.hpp frameEntryWorldVelocity):
|
||||
// this runs once PER CONTACT in the frame's ProcessCollisionList walk, and
|
||||
// the StaticBounce below reflects worldLinearVelocity (+= delta_v) every
|
||||
// time. Restore the TRUE frame-entry motion first so a multi-solid frame
|
||||
// (mech + terrain solids + debris) cannot compound the reflections into an
|
||||
// amplified velocity: every contact -- and every dispatched mech-vs-mech
|
||||
// damage amount -- is priced at the mech's real approach speed, which is
|
||||
// all the 1995 binary's StaticBounce ever saw (its ground was a heightfield
|
||||
// probe, never a collision-list entry). The post-list velocity is
|
||||
// overwritten by the response policy / next frame's derive regardless.
|
||||
worldLinearVelocity = frameEntryWorldVelocity;
|
||||
|
||||
// --- the BoxedSolid resolver (:15302-15304); miss => nothing runs -------
|
||||
Scalar penetration = 0.0f;
|
||||
if (!collisionVolume->ProcessCollision(collision, worldLinearVelocity,
|
||||
|
||||
Reference in New Issue
Block a user