From e30a61a62fa7147091f9c1f508bf3e0915a0c2eb Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 00:06:00 -0500 Subject: [PATCH] World-pick targeting: 0x388 = whatever is downrange -- mech, terrain, or nothing (task #41) User video evidence (lasers firing at nothing) + the colleague's torso-locked report + the binary all reconcile into one model: The boresight pick hits the WORLD, and the target slot mech+0x388 holds whatever entity is downrange: - enemy mech under the boresight -> aimed target (hull point -> STEP-6 zone under the boresight, hotbox + lock ring, damage); - else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and missiles fire at the scenery ("firing at nothing", as in the pod videos), range caret reads the ground distance (authentic :5639), no damage; - else (sky) -> no target, and the weapon's own double 0x388 gate refuses the discharge. Binary evidence for non-mech targets: HudSimulation :5620 explicitly handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code if only mechs were ever targeted. The pick is automatic every frame (0x388: 11 reads / 0 direct stores in CODE). Implementation: gBTTerrainEntity captured in MakeEntityRenderables (Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else ground ray -> terrain entity + ground point; damage/hotbox/lock only for the mech pick; the task-40 enemy auto-converge REMOVED (facing away now fires at the scenery, not magically at the enemy -- the user's complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 / 0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge. Fourth (and evidence-complete) targeting model iteration; KB updated. Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 45 ++++----- game/reconstructed/btl4vid.cpp | 21 +++++ game/reconstructed/mech4.cpp | 156 ++++++++++++++++---------------- game/reconstructed/projweap.cpp | 13 ++- 4 files changed, 133 insertions(+), 102 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index 0daa9dd..e3092b5 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -58,27 +58,30 @@ basis via `BTSetAimCamera`, the render loop publishes proj._11/_22 + backbuffer `Mech::PickRayHit` (world ray → local frame → `BoundingBox::HitBy` slab test vs the collision template's ExtentBox → world hit point). -**FIRING NEEDS A TARGET BUT NOT A MANUAL LOCK / PINPOINT AIM (task #40, binary-verified [T1]).** -The weapon fire path is DOUBLY gated on `mech+0x388 != 0`: `Emitter::EmitterSimulation` -(`FUN_004baa88` :7689) only calls FireWeapon with a target, AND `Emitter::FireWeapon` -(`FUN_004bace8` :7727) wraps its ENTIRE body (incl. `beamFlag`=+0x46c=1 :7749) in the same check — -so **no target ⇒ no beam, ever** (you cannot fire into truly empty space). BUT a capstone scan of -the whole CODE section finds **11 reads of `+0x388` and ZERO direct stores** — the target is -written INDIRECTLY (a message/selector via a `this`-pointer), i.e. the game **AUTO-ACQUIRES** the -target for you; there is no manual "lock to fire." The spinning-ring **LOCK** (HudSimulation -5619-5634) is a SEPARATE, stricter state layered on top (host-zone < 0.75 damage AND targeted-zone -< 1.0). **Port model:** `mech+0x388` = the enemy whenever it is ALIVE (`!IsMechDestroyed()`) — the -pod's auto-target — so firing works freely with the enemy merely present, NO pinpoint needed -(verified: `BT_AIM="0.5 0.2"` off-hull → fires + lands damage; before this fix off-hull dealt 0). -A pinpoint hull hit (`PickRayHit` → `hotTarget`) UPGRADES the shot: `mech+0x37c` = the picked hull -point → STEP-6 resolves the zone under the crosshair (aimed fire; center-aim → head-band zone 13), -and lights the lock ring. Off-hull, `0x37c` = the target's centre mass (body hits, near-centre -zone). (⚠ THREE earlier cuts were wrong and are corrected: a STICKY designation — locked forever; -a MOUSE cursor slew — a free cursor the pod never had; and a PINPOINT-required fire gate — you had -to boresight the hull exactly to fire at all. All from over-reading the RP-shared `Reticle` struct; -removed.) The HUD draws the aim group at the boresight ([0x9a] translate), the designator hotbox at -the target's projected point (subB9 hot / subB8 designated, `BTProjectHotBox`/`BTProjectToReticle`), -and the edge arrows when off-screen/behind. `BT_FIRE_ARC` is now an EXPLICIT OPT-IN presentation clamp (unset = authentic +**THE WORLD-PICK TARGET MODEL (task #41, the reconciliation — binary + pod-video evidence).** +The target slot `mech+0x388` holds **whatever entity is downrange of the torso boresight** — a +mech OR a WORLD entity (terrain/mesa/building). Evidence: (a) the weapon fire path is DOUBLY +gated on `0x388 != 0` (`FUN_004baa88` :7689 AND `FUN_004bace8` :7727 wrap everything incl. +`beamFlag`=+0x46c :7749 — no target ⇒ no discharge [T1]); (b) pod demo VIDEOS show lasers firing +with no mech targeted — reconciled by (c): HudSimulation :5620 **explicitly handles a target +WITHOUT damage zones** (`target->0x120 == 0`) — dead code unless non-mech entities get targeted; +and (d) `0x388` has **11 reads / ZERO direct stores** across CODE (capstone scan) — the pick is +written indirectly, AUTOMATICALLY, every frame; never a manual player lock. So: **fire at the +enemy under your boresight → aimed zone damage; fire anywhere else → the beam hits the scenery +downrange ("firing at nothing"); only a true sky shot (nothing within range) refuses to +discharge.** The spinning-ring **LOCK** (HudSimulation 5619-5634) is a SEPARATE, stricter state +(mech target only + host-zone < 0.75 damage + targeted-zone < 1.0). **Port implementation:** the +boresight ray tests the enemy's collision box (`PickRayHit` → aimed hull point → STEP-6 zone +under the boresight + hotbox + lock ring), else the terrain (`BTGroundRayHit` to 1200 → +`gBTTerrainEntity` + the ground point — beam/missiles fly to the scenery, NO damage dispatch; +the range caret authentically reads the ground distance, :5639), else no target (sky — the +double gate refuses). Verified all three states (`BT_AIM` 0,0 / 0.6,0.5 / 0,−0.8). (⚠ This is +the FOURTH targeting model iteration; the first three — sticky lock, mouse cursor, enemy +auto-converge — all over-read the RP-shared `Reticle` struct or under-read the pick. The +world-pick model is the one that fits ALL the evidence.) The HUD draws the aim group at the +boresight ([0x9a] translate), the designator hotbox at the mech target's projected point (subB9 +hot / subB8 designated, `BTProjectHotBox`/`BTProjectToReticle`), and the edge arrows when +off-screen/behind. `BT_FIRE_ARC` is now an EXPLICIT OPT-IN presentation clamp (unset = authentic no-arc). The old hardwired lock, the ±30°-default cone, and the projectile path's gEnemyMech fallback are all REMOVED. LMB fires lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection bug was fixed en route: every diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index dc73ce2..7453414 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -58,11 +58,19 @@ #if !defined(APP_HPP) # include #endif +#if !defined(TERRAIN_HPP) +# include // Terrain::GetClassDerivations (world-pick target, task #41) +#endif #include #include #include // clock() -- the threat-trail ages (task #37) +// WORLD-PICK TARGET (task #41): a live Terrain entity mech4 cites in +// mech+0x388 when the boresight pick lands on the ground (the binary's target +// slot holds non-mech world entities). Captured in MakeEntityRenderables. +Entity *gBTTerrainEntity = 0; + // // Material-name substitution placeholders. Mirrors RPL4VideoRenderer's @@ -205,6 +213,19 @@ void // `this_instance` in the CulturalIcon/Landmark arm has been fixed in // L4VIDEO.cpp. // + // WORLD-PICK TARGET support (task #41): the boresight pick can hit + // TERRAIN (the binary's target slot holds non-mech world entities -- + // HudSimulation part_013.c:5620 explicitly handles a target without + // damage zones). Capture a live Terrain entity for mech4's ground + // pick to cite in mech+0x388 (the pick POINT carries the geometry; + // which specific instance matters only to damage, which never routes + // to terrain). + { + extern Entity *gBTTerrainEntity; + if (gBTTerrainEntity == 0 + && entity->IsDerivedFrom(*Terrain::GetClassDerivations())) + gBTTerrainEntity = entity; + } DPLRenderer::MakeEntityRenderables( entity, model_resource, view_type); break; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 566c0f7..e00332c 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2286,38 +2286,62 @@ void } } - // --- TARGETING (task #36 -- the AUTHENTIC Reticle acquisition, - // engine/MUNGA/RETICLE.h [T0]): the pilot slews the crosshair - // (gBTAimX/Y); a pick ray through it tests the enemy's collision - // volume; the mech UNDER the crosshair becomes the designated target - // (sticky -- it persists when the crosshair drifts off, marked by - // the HUD designator; re-hover to refresh, burial clears it). The - // designation feeds the engine-Entity target slots the whole weapon - // path reads (target position @0x37c, target entity @0x388, zone - // @0x38c). While the crosshair is ON the target ("hot"), the aim - // point is the PICKED HULL POINT -- the STEP-6 cylinder lookup then - // resolves damage to the zone under the crosshair (aimed fire). - Entity *hotTarget = 0; // mech under the crosshair NOW + // --- TARGETING: the WORLD-PICK model (task #41, the reconciliation of + // ALL the evidence). The boresight ray picks whatever is DOWNRANGE: + // 1. the enemy mech's collision volume -> aimed target (hull point + // -> STEP-6 zone under the boresight; hotbox + lock ring); + // 2. else the TERRAIN (BTGroundRayHit) -> the ground point becomes + // the target -- the beam fires at the scenery ("firing at + // nothing", seen in the pod demo videos), no damage; + // 3. else (sky, nothing within range) -> NO target, and the + // weapon's own double gate (FUN_004baa88:7689 + FUN_004bace8: + // 7727 -- both require mech+0x388 != 0) refuses the discharge. + // Binary evidence for non-mech targets: HudSimulation :5620 + // explicitly handles a target WITHOUT damage zones (target->0x120 + // == 0) -- dead code if only mechs were ever targeted. The pick is + // AUTOMATIC every frame (0x388 has 11 reads / 0 direct stores in + // CODE -- written indirectly, never a manual player lock). + Entity *hotTarget = 0; // the enemy mech under the boresight Point3D hotPoint; // picked world point on its hull - static int gAimNoRay = 0, gAimNoPick = 0, gAimHits = 0; // 1Hz diagnostics + Entity *pickTarget = 0; // what the boresight ray hit (mech/terrain) + Point3D pickPoint; // where it hit + static int gAimNoRay = 0, gAimGround = 0, gAimHits = 0; // 1Hz diagnostics { extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]); + extern Entity *gBTTerrainEntity; // captured by MakeEntityRenderables float rs[3], rd[3]; if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd)) { ++gAimNoRay; } - else if (gEnemyMech != 0) + else { Point3D rayStart(rs[0], rs[1], rs[2]); Vector3D rayDir(rd[0], rd[1], rd[2]); - if (((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint)) + if (gEnemyMech != 0 && !((Mech *)gEnemyMech)->IsMechDestroyed() + && ((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint)) { - hotTarget = gEnemyMech; + hotTarget = gEnemyMech; + pickTarget = gEnemyMech; + pickPoint = hotPoint; ++gAimHits; } else - ++gAimNoPick; + { + // the ground downrange of the guns (max = the pod's HUD + // range scale; past it the shot is a sky shot -> no target) + extern bool BTGroundRayHit(float,float,float, float,float,float, + float, float*,float*,float*); + float hx, hy, hz; + if (gBTTerrainEntity != 0 && + BTGroundRayHit(rs[0], rs[1], rs[2], rd[0], rd[1], rd[2], + 1200.0f, &hx, &hy, &hz)) + { + pickTarget = gBTTerrainEntity; + pickPoint.x = hx; pickPoint.y = hy; pickPoint.z = hz; + ++gAimGround; + } + } } // The Reticle struct (the mech's TargetReticle attribute): position, @@ -2325,53 +2349,44 @@ void // damage delivery (the authentic percent-table roll, STEP 6). targetReticle.reticlePosition.x = gBTAimX; targetReticle.reticlePosition.y = gBTAimY; - targetReticle.targetEntity = hotTarget; + targetReticle.targetEntity = pickTarget; targetReticle.targetDamageZone = -1; - if (hotTarget != 0) - targetReticle.rayIntersection = hotPoint; + if (pickTarget != 0) + targetReticle.rayIntersection = pickPoint; - // TARGET (mech+0x388) is AUTO-ACQUIRED, not manually locked (task #40, - // binary-confirmed): the weapon's fire path (Emitter FUN_004baa88 :7689 - // + FireWeapon FUN_004bace8 :7727) is DOUBLY gated on mech+0x388 != 0 -- - // no target, NO beam, ever -- but 0x388 is WRITTEN INDIRECTLY (a - // disasm scan of the whole CODE section finds 11 reads and ZERO direct - // stores), i.e. the game AUTO-SELECTS the target for you; you never - // manually "lock" to fire. The spinning-ring LOCK (HudSimulation - // 5619-5634) is a SEPARATE, stricter state layered on top. So: 0x388 - // = the enemy whenever it is alive (the pod's auto-target); firing - // needs only that, NOT a pinpoint boresight hit. A pinpoint hull hit - // (hotTarget) UPGRADES the shot to aimed-zone damage + the lock ring. - Entity *autoTarget = - (gEnemyMech != 0 && !((Mech *)gEnemyMech)->IsMechDestroyed()) - ? gEnemyMech : 0; - if (autoTarget != 0) + // the engine-Entity target slots the whole weapon path reads + if (pickTarget != 0) { - MECH_TARGET_ENTITY(this) = autoTarget; + MECH_TARGET_ENTITY(this) = pickTarget; MECH_TARGET_SUBIDX(this) = -1; - if (hotTarget != 0) - MECH_TARGET_POS(this) = hotPoint; // aimed: the picked hull point - else - { - // no pinpoint pick -> converge on the target's centre mass - // (the beam still fires; the zone resolves near-centre). - Point3D cm = ((Mech *)autoTarget)->localOrigin.linearPosition; - cm.y += kMuzzleHeight; - MECH_TARGET_POS(this) = cm; - } + MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick } else { - MECH_TARGET_ENTITY(this) = 0; // no living enemy -> no target + MECH_TARGET_ENTITY(this) = 0; // sky: no target, no discharge MECH_TARGET_SUBIDX(this) = -1; } } // HUD feeds: the range caret + the hotbox (world point + state) + the // recovered-Execute instruments (compass, twist tape, group mask). + // The range caret tracks the PICK (authentic: :5639 computes it from + // mech+0x37c whatever the target is -- a terrain pick reads the ground + // distance); the hotbox + lock exist ONLY for a mech target (:5620 -- + // a target with no damage zones gets neither). { extern void BTSetHudTargetRange(Scalar range); Entity *des = MECH_TARGET_ENTITY(this); - if (des != 0) + if (des != 0 && des != hotTarget) + { + // terrain pick: range to the ground point; no hotbox, no ring. + Point3D tp = MECH_TARGET_POS(this); + float hddx = tp.x - localOrigin.linearPosition.x; + float hddz = tp.z - localOrigin.linearPosition.z; + BTSetHudTargetRange((Scalar)sqrtf(hddx*hddx + hddz*hddz)); + gBTHudLockState = 0; + } + else if (des != 0) { Mech *dm = (Mech *)des; Point3D dp = dm->localOrigin.linearPosition; @@ -2426,7 +2441,8 @@ void { Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition; - const int designated = (MECH_TARGET_ENTITY(this) != 0); + // damage routes ONLY when the boresight pick is the enemy mech + const int designated = (hotTarget != 0); float ddx = enemyPos.x - localOrigin.linearPosition.x; float ddy = enemyPos.y - localOrigin.linearPosition.y; @@ -2438,14 +2454,15 @@ void { gTargetLogAccum = 0.0f; DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")" - << (hotTarget != 0 ? " HOT-aimed (zone under crosshair)" - : (designated ? " auto-target (fire freely, body hits)" : " no living target")) + << (hotTarget != 0 ? " ENEMY under boresight (aimed)" + : (pickTarget != 0 ? " terrain downrange (beam at scenery)" + : " sky (no target, no discharge)")) << " range=" << range << (range <= kWeaponRange ? " IN RANGE" : "") - << " [aimedHits=" << gAimHits << " noRay=" << gAimNoRay - << " offHull=" << gAimNoPick << "]" + << " [enemyPicks=" << gAimHits << " groundPicks=" << gAimGround + << " noRay=" << gAimNoRay << "]" << "\n" << std::flush; - gAimHits = 0; gAimNoRay = 0; gAimNoPick = 0; + gAimHits = 0; gAimNoRay = 0; gAimGround = 0; } // --- FIRING (bring-up): on the trigger, with a target in range and the @@ -2502,29 +2519,14 @@ void gFireCooldown = kFireCooldown; ++gShotCount; - // Impact point (task #36): while the crosshair is ON the target the - // shot lands at the PICKED HULL POINT -- the STEP-6 cylinder lookup - // resolves damage to the zone under the crosshair (aimed fire: leg - // shots hit legs, torso shots hit the torso). Off-crosshair (the - // sticky designation converging), the shot enters on the surface - // toward the shooter at chest height (the unaimed synth point -- - // exactly the axis point would be angularly degenerate for the - // cylinder sector lookup). - Point3D impact; - if (hotTarget != 0) - { - impact = hotPoint; - } - else - { - impact = enemyPos; - if (range > 1e-3f) - { - impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter - impact.z -= (ddz / range) * 3.0f; - } - impact.y += kMuzzleHeight; // chest height (beam aim height) - } + // Impact point (tasks #36/#41): damage happens ONLY when the enemy + // is under the boresight (designated == hotTarget != 0, checked in + // the gate above), so the shot always lands at the PICKED HULL + // POINT -- the STEP-6 cylinder lookup resolves damage to the zone + // under the boresight (aimed fire: leg shots hit legs, torso shots + // the torso). Terrain picks never reach here (beam + scenery + // impact only, no damage dispatch). + Point3D impact = hotPoint; Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin; exp_origin.linearPosition = impact; // at the hit point diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index e997cf2..ecfef56 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -620,13 +620,18 @@ void // reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine // Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|. char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) - void *target = (o != 0) ? *(void **)(o + 0x388) : 0; // owner's locked target entity (null in bring-up) - Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; // owner target point + // The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's + // targeting block writes; a consistent producer/consumer pair on our + // compiled object). Task #41: the slots hold the WORLD PICK -- the enemy + // mech under the boresight, or the terrain downrange (a missile fired at + // the scenery flies to the ground point and detonates without damage), or + // null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate + // wouldn't have fired either). + void *target = (o != 0) ? *(void **)(o + 0x388) : 0; + Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x + launchVelocity.y*launchVelocity.y + launchVelocity.z*launchVelocity.z); - // Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target - // slot isn't populated (bring-up). BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount); simulationFlags |= 0x1; // replication-dirty