From 76cfc643532b93f05e655fc1e402f116435a631c Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 24 Jul 2026 10:21:22 -0500 Subject: [PATCH] BT410 Phase 5.3.16: targeting + damage -- two-mech fights are LIVE The simulation is now a FIGHT. Mech target slot (+0x388), authentic Loaded->Firing gate (viewFireEnable && HasActiveTarget), UpdateTargeting range refresh, and SendDamage via the engine TakeDamageMessage. MECHDMG.CPP is BORN against the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP: the Mech ctor's Pass-3 hull zone fill constructs Mech__DamageZone per zone (the Entity base only allocates the raw pointer array -- unfilled slots were crash #1; a base-class fill parses each record short and skews the stream -- crash #2). Streamed ctor parses the full BT tail (flags / Scalar-weighted criticals with the Master+Dynamic plug gate / LOD redirect table) and normalizes the armor economy: scale[type]=1/(raw x armorPoints), legs halved -- BT411-verified @0049ce50. Live two-mech verification (BT_SPAWN_ENEMY harness in DropZoneReply): 20 named hull zones stream on both mechs; PPC lands 11.77/hit (12 x chargeRatio^2, type 4), ER-M laser 3.43 (type 3), SRM6 salvos 5.83 x 6 rounds on independent zones (type 2, one message per missile -- burstCount is NOT in the zone formula); repeat hits climb linearly, leg zones absorb at half scale, zones reach 1.0 Burning/Destroyed. No-target and novice regressions hold (weapons load but never fire without a target). Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/BTPLAYER.CPP | 56 ++++ restoration/source410/BT/BTPLAYER.NOTES.md | 11 + restoration/source410/BT/EMITTER.CPP | 21 +- restoration/source410/BT/MECH.CPP | 62 ++++- restoration/source410/BT/MECH.HPP | 14 +- restoration/source410/BT/MECH.NOTES.md | 15 ++ restoration/source410/BT/MECHDMG.CPP | 281 +++++++++++++++++++++ restoration/source410/BT/MECHDMG.NOTES.md | 83 ++++++ restoration/source410/BT/MECHWEAP.CPP | 91 ++++++- restoration/source410/BT/MECHWEAP.HPP | 11 + restoration/source410/BT/MECHWEAP.NOTES.md | 24 ++ restoration/source410/BT/MISLANCH.CPP | 19 ++ restoration/source410/BT/PROJWEAP.CPP | 8 +- 13 files changed, 686 insertions(+), 10 deletions(-) create mode 100644 restoration/source410/BT/MECHDMG.CPP create mode 100644 restoration/source410/BT/MECHDMG.NOTES.md diff --git a/restoration/source410/BT/BTPLAYER.CPP b/restoration/source410/BT/BTPLAYER.CPP index b5e1b8b9..f35c6ee1 100644 --- a/restoration/source410/BT/BTPLAYER.CPP +++ b/restoration/source410/BT/BTPLAYER.CPP @@ -304,6 +304,62 @@ void // AlwaysExecute(); deathCount = 0; + + // + // DEV harness (BT_SPAWN_ENEMY=1): spawn a second, unowned mech 150 u + // down +Z from the drop zone through the SAME make machinery (its + // Entity ctor self-registers with the host manager; no player link, so + // its gates read dev-permissive) and hand it to our mech as the + // TARGET. The authentic target writer is the reticle pick (the + // render/mech4 wave); this gives the fire/damage path a live opponent + // headlessly. + // + if (getenv("BT_SPAWN_ENEMY") + && playerVehicle->GetClassID() == (Entity::ClassID)MechClassID) + { + HostManager *host_manager = application->GetHostManager(); + Check(host_manager); + ResourceDescription *mech_res = + application->GetResourceFile()->FindResourceDescription( + playerMission->GetGameModel(), + ResourceDescription::ModelListResourceType + ); + Check(mech_res); + mech_res->Lock(); + + Origin enemy_location = message->dropZoneLocation; + enemy_location.linearPosition.z += 150.0f; + + Mech::MakeMessage + create_enemy( + Mech::MakeMessageID, + sizeof(Mech::MakeMessage), + EntityID(host_manager->GetLocalHostID()), + (Entity::ClassID)MechClassID, + EntityID::Null, + mech_res->resourceID, + Mech::DefaultFlags, + enemy_location, + Motion::Identity, + Motion::Identity, + playerMission->GetBadgeName(), + playerMission->GetColorName(), + ((BTMission *)playerMission)->GetPatchName() + ); + + mech_res->Unlock(); + + Mech *enemy = Mech::Make(&create_enemy); + Register_Object(enemy); + ((Mech *)playerVehicle)->SetTargetEntity(enemy); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[enemy] spawned " << (void *)enemy + << " at z+150, zones=" << enemy->damageZoneCount + << " -> target locked" << endl << flush; + } + } } else if (deathCount == message->deathCount) { diff --git a/restoration/source410/BT/BTPLAYER.NOTES.md b/restoration/source410/BT/BTPLAYER.NOTES.md index 2a89c1f4..7cfcd089 100644 --- a/restoration/source410/BT/BTPLAYER.NOTES.md +++ b/restoration/source410/BT/BTPLAYER.NOTES.md @@ -161,3 +161,14 @@ Public accessors IsSimLive()/IsHeatModelOn()/IsAdvancedDamageOn(); a gated `[exp]` sentinel prints the derived flags per master player. Consumers: HeatSink::HeatModelActive() (HEAT.CPP), ProjectileWeapon::LiveFireEnabled() (PROJWEAP.CPP) -- both walk mech->GetPlayerLink(), NULL-permissive. + +## 5.3.16 — BT_SPAWN_ENEMY dev harness + +DropZoneReplyMessageHandler, after the fresh-spawn block: `BT_SPAWN_ENEMY=1` +clones the CreatePlayerVehicle MakeMessage recipe (same game-model resource, +badge/color/patch, `Mech::DefaultFlags`) at dropZone+150 on Z, calls +`Mech::Make()` directly (the Entity ctor self-registers + uniquifies the ID) +and sets it as the player mech's target. The enemy has NO player link, so its +experience gates read dev-permissive; it never fires back (no target of its +own — set one for a mutual fight). DEV-ONLY: the real opponent path is the +network mesh / mission roster. diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index 10a7666d..05c199bc 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -309,10 +309,18 @@ void * chargeRatio * chargeRatio; // - // The damage record carries this shot's portion (the submission at the - // owner's target joins the targeting/damage wave). + // The damage submission (binary @004bace8 tail): the shot's portion rides + // the inherited Damage record to the owner's target when it is inside the + // weapon's effective range. (The beam build / impact point join the + // render wave.) // damageData.damageAmount = damagePortion; + damageData.burstCount = 1; + if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL) + { + SendDamage(owner->GetTargetEntity(), damageData); + } if (HeatModelActive()) { @@ -417,6 +425,8 @@ void Logical fireEdge = CheckFireEdge(); + targetWithinRange = UpdateTargeting(); + switch (GetWeaponState()) { case FiringState: @@ -433,7 +443,12 @@ void case LoadedState: if (fireEdge) { - if (viewFireEnable) + // + // The authentic Loaded->Firing gate: armed in the current look + // view AND a target under the reticle -- a denied shot is the + // one-frame blip, no discharge. + // + if (viewFireEnable && HasActiveTarget()) { weaponAlarm.SetLevel(FiringState); FireWeapon(); diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index e873228a..04cebdfb 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -52,6 +52,7 @@ #include // MechControlsMapper -- the drive reads its demands #include // Joint / JointSubsystem -- ResolveJoint #include // EntitySegment -- the skeleton segment table +#include // Mech::DamageZone -- the hull zone fill (Pass 3) // //############################################################################# @@ -165,6 +166,7 @@ Mech::Mech( eyepointRotation = EulerAngles::Identity; lookPitch = 0.0f; lookYaw = 0.0f; + targetEntity = NULL; // // Look-view angles: defaults until the GameModel read below overrides them @@ -176,7 +178,7 @@ Mech::Mech( lookBackAngle = 0.0f; { - for (int i = 0; i < 202; ++i) + for (int i = 0; i < 201; ++i) { reservedState[i] = 0; } @@ -459,6 +461,64 @@ Mech::Mech( } } + // + //----------------------------------------------------------------------- + // Fill the hull damage-zone array. The Entity base ctor only READS the + // zone count and allocates the raw POINTER array (entity.cpp:1032) -- the + // derived entity must construct the streamed DamageZone objects into the + // slots itself (the CulturalIcon ctor is the surviving reference, + // cultural.cpp:349). LOAD-BEARING: an unfilled slot is heap garbage, and + // the first TakeDamageMessage that lands on it calls through a trash + // vtable (crash = EIP inside the heap). Runs AFTER the segment walk + // because the streamed DamageZone ctor resolves its effect-site segments + // via GetSegment() on JointedMover-derived owners. + //----------------------------------------------------------------------- + // + if (damageZones != NULL && damageZoneCount > 0) + { + ResourceDescription *dmg_res = + application->GetResourceFile()->SearchList( + resourceID, + ResourceDescription::DamageZoneStreamResourceType + ); + Check(dmg_res); + dmg_res->Lock(); + DynamicMemoryStream damage_stream( + dmg_res->resourceAddress, + dmg_res->resourceSize + ); + damage_stream.AdvancePointer(sizeof(damageZoneCount)); + // + // Every entry is the MECH-specific zone subclass (the class-scope + // DamageZone typedef = Mech__DamageZone, binary ctor @0049ce50): its + // streamed ctor chains the engine base parse and consumes the BT + // per-zone TAIL (flags / criticals / LOD redirects) -- a base-class + // fill parses zone 0 short and skews every following zone. + // + { + for (int dz = 0; dz < damageZoneCount; ++dz) + { + damageZones[dz] = new DamageZone(this, dz, &damage_stream); + Register_Object(damageZones[dz]); + } + for (int dp = 0; dp < damageZoneCount; ++dp) + { + ((DamageZone *)damageZones[dp])->SetLODParentPointers(); + } + } + dmg_res->Unlock(); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[mech] damage zones streamed: " << damageZoneCount; + for (int zz = 0; zz < damageZoneCount; ++zz) + { + DEBUG_STREAM << (zz ? "," : " [") << damageZones[zz]->damageZoneName; + } + DEBUG_STREAM << "]" << endl << flush; + } + } + // // Seed the condenser flow shares: every valve spawns at 1, so the initial // recompute yields equal shares across the bank (a MoveValve press diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 9defb9b8..33d917ab 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -323,6 +323,17 @@ const EulerAngles& GetEyepointRotation() const { Check(this); return eyepointRotation; } + // + // The current target (the binary's mech target slot @0x388): read by + // the weapons' HasActiveTarget / UpdateTargeting and the fire path. + // The authentic writer is the reticle targeting step (mech4, the + // render wave); the dev harness sets it directly. + // + Entity* + GetTargetEntity() const { Check(this); return targetEntity; } + void + SetTargetEntity(Entity *target) { Check(this); targetEntity = target; } + // // Look-state commit (called by the controls mapper on a look-button // state change): re-aim the eyepoint from the model's authored look @@ -399,6 +410,7 @@ EulerAngles eyepointRotation; Scalar lookPitch; Scalar lookYaw; + Entity *targetEntity; // // The model's authored look-view angles (rad; deg in the resource). @@ -413,7 +425,7 @@ // mech2/mech3/mech4 simulation. Reserved until that path is // reconstructed with named fields. // - int reservedState[202]; + int reservedState[201]; }; #endif diff --git a/restoration/source410/BT/MECH.NOTES.md b/restoration/source410/BT/MECH.NOTES.md index 51ed97f8..4873b475 100644 --- a/restoration/source410/BT/MECH.NOTES.md +++ b/restoration/source410/BT/MECH.NOTES.md @@ -365,3 +365,18 @@ event queue, so the mech never received a directly-dispatched message -- the PlayerLink bind silently never landed and every player-experience gate read a NULL player (dev-permissive heat-always-on) until this line was restored. Found while verifying the novice-mode heat lockout end-to-end. + +## 5.3.16 — the target slot + hull zone fill (Pass 3) + +- `Entity *targetEntity` @mech+0x388 (Get/SetTargetEntity; ctor NULLs it; + reservedState [202]→[201]). The authentic WRITER is the reticle pick + (render/mech4 wave); the BT_SPAWN_ENEMY harness (BTPLAYER.CPP) sets it for + headless fights. +- Hull damage-zone fill after the model-param block: re-open the type-0x14 + DamageZoneStream, skip the count word the Entity base ctor consumed, and + construct `new DamageZone(this, dz, &stream)` per zone — the CLASS-SCOPE + typedef = `Mech__DamageZone` (authentic CODE/BT/BT/MECHDMG.HPP), whose ctor + parses the full BT per-zone record. Then the SetLODParentPointers pass over + all zones (binary Pass-3 order, FUN_004a1674). Full forensics in + MECHDMG.NOTES.md — the Entity ctor ONLY allocates the pointer array; an + unfilled or base-class fill is the 5.3.16 crash pair. diff --git a/restoration/source410/BT/MECHDMG.CPP b/restoration/source410/BT/MECHDMG.CPP new file mode 100644 index 00000000..5529e157 --- /dev/null +++ b/restoration/source410/BT/MECHDMG.CPP @@ -0,0 +1,281 @@ +//===========================================================================// +// File: mechdmg.cpp // +// Project: BattleTech Brick: Mech damage // +// Contents: Mech::DamageZone -- the hull zone armor economy // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// +// +// The class interface is the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP +// (06/05/95 JM). The bodies here are reconstructions: the streamed ctor / +// SetLODParentPointers / dtor follow the BT411-verified binary parse +// (@0049ce50 / @0049d1d0), while TakeDamage and SetGraphicState are STAGED +// pass-throughs to the engine base -- the LOD artifact router (@0049c690), +// the critical destruction cascade and the death-handler graphics are the +// mechdmg cascade wave (see MECHDMG.NOTES.md). +// + +#include +#pragma hdrstop + +#if !defined(MECHDMG_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +const int + Mech__DamageZone::NullDamageZone = -1; + +// +//############################################################################# +// MechCriticalSubsystem (binary ctor @0049dd18): one critical-subsystem +// entry of a hull zone -- the plug binds the roster subsystem this zone +// destroys when its structure runs out; weight / percentage stream in the +// zone ctor below. +//############################################################################# +// +MechCriticalSubsystem::MechCriticalSubsystem(DamageZone *owner): + subsystemPlug(owner) +{ + Check(owner); + criticalWeight = 0.0f; + damagePercentage = 0.0f; + damagePercentageUsed = 0.0f; +} + +MechCriticalSubsystem::~MechCriticalSubsystem() +{ +} + +Logical + MechCriticalSubsystem::TestInstance() const +{ + return True; +} + +// +//############################################################################# +// Mech::DamageZone construction (binary @0049ce50). +// +// Chains to the engine DamageZone streamed ctor (name / effect sites / +// defaultArmorPoints / damageScale[5] / material lists -- DAMAGE.CPP:234), +// then consumes the BT mech-specific TAIL of the same per-zone record: +// +// Logical descendOnDestruction, destroySiblingsOnDestruction; +// int segmentIndex; +// Logical leftLeg, rightLeg, vitalDamageZone; +// int criticalSubsystemCount; +// critCount x { Scalar criticalWeight; Scalar damagePercentage; +// int subsystemIndex -> subsystemPlug } +// int redirectCount; redirectCount x int childZoneIndex +// +// Parsing the FULL record is LOAD-BEARING: the Mech ctor streams all hull +// zones sequentially from ONE stream, so a short read here skews every +// following zone (its effect-site indices then read garbage and +// GetSegment() returns NULL -- the 5.3.16 bring-up crash). +// +// THE ARMOR ECONOMY (BT411 combat-damage, verified vs the binary): the +// stream authors armor POINTS; each damageScale cell is normalized to a +// damage-per-point fraction scale[type] = 1/(raw[type] * armorPoints) +// (cells already equal to 1/armorPoints pass through), then LEG zones halve +// every type's scale. The engine base TakeDamage applies +// damageLevel += amount * damageScale[type] (1.0 = destroyed). +//############################################################################# +// +Mech__DamageZone::Mech__DamageZone( + Mech *mech, + int damage_zone_index, + MemoryStream *stream +): + DamageZone(mech, damage_zone_index, stream), + redirectTable(NULL, False), + parentArtifactZone(NULL) +{ + Check(mech); + Check(stream); + + lastHitZone = 0; + lastDamageTime = 0.0f; + lastInflicting = mech->GetEntityID(); + criticalWeightSum = 0.0f; + criticalSubsystems = NULL; + + // + // The streamed scalar/flag block (binary order @0049cf00-0049cf5c). + // + *stream >> descendOnDestruction; + *stream >> destroySiblingsOnDestruction; + *stream >> segmentIndex; + *stream >> leftLeg; + *stream >> rightLeg; + *stream >> vitalDamageZone; + *stream >> criticalSubsystemCount; + + // + // Normalize the per-type armor scales in place (binary @0049cf60, + // constants 1e-4 / 0.5). + // + { + for (int type = 0; type < Damage::DamageTypeCount; ++type) + { + Scalar even = 1.0f / defaultArmorPoints; + Scalar diff = damageScale[type] - even; + if (diff < 0.0f) + { + diff = -diff; + } + if (diff > 1.0e-4f) + { + damageScale[type] = + 1.0f / (damageScale[type] * defaultArmorPoints); + } + if (leftLeg || rightLeg) + { + damageScale[type] *= 0.5f; + } + } + } + + // + // The critical-subsystem table. The streamed roster index binds the + // subsystem plug on the MASTER mech only (binary gate @0049d0e1: + // MasterInstance + DynamicFlag -- replicant plugs stay unbound, subsystem + // criticals are master-authoritative). A NULL roster slot is left + // unbound: slot 0 (the control mapper) installs post-ctor and + // Link::AddToPlug derefs the plug. + // + if (criticalSubsystemCount > 0) + { + criticalSubsystems = + new MechCriticalSubsystem *[criticalSubsystemCount]; + Register_Pointer(criticalSubsystems); + for (int cc = 0; cc < criticalSubsystemCount; ++cc) + { + criticalSubsystems[cc] = new MechCriticalSubsystem(this); + Register_Object(criticalSubsystems[cc]); + + int subsystem_index = -1; + *stream >> criticalSubsystems[cc]->criticalWeight; + *stream >> criticalSubsystems[cc]->damagePercentage; + *stream >> subsystem_index; + criticalWeightSum += criticalSubsystems[cc]->criticalWeight; + + if ( + mech->GetInstance() == Entity::MasterInstance + && mech->IsDynamic() + && subsystem_index >= 0 + && subsystem_index < mech->GetSubsystemCount() + ) + { + Subsystem *target = mech->GetSubsystem(subsystem_index); + if (target != NULL) + { + criticalSubsystems[cc]->subsystemPlug.Add(target); + } + } + } + } + + // + // The LOD redirect table: an ARTIFACT zone (low-LOD hull shell) lists + // the real child zones a hit on it lands on. + // + int redirect_count = 0; + *stream >> redirect_count; + { + for (int rr = 0; rr < redirect_count; ++rr) + { + int child_zone = 0; + *stream >> child_zone; + IntegerPlug *plug = new IntegerPlug(child_zone); + Register_Object(plug); + redirectTable.AddValue(plug, child_zone); + } + } + + Check_Fpu(); +} + +Mech__DamageZone::~Mech__DamageZone() +{ + if (criticalSubsystems != NULL) + { + for (int cc = 0; cc < criticalSubsystemCount; ++cc) + { + Unregister_Object(criticalSubsystems[cc]); + delete criticalSubsystems[cc]; + } + Unregister_Pointer(criticalSubsystems); + delete [] criticalSubsystems; + } + + DamageZoneIndexTableIterator iterator(redirectTable); + iterator.DeletePlugs(); +} + +// +//############################################################################# +// SetLODParentPointers (binary @0049d1d0). Run by the Mech ctor after EVERY +// zone of the mech exists: each redirect child's parentArtifactZone slot +// points back at this artifact zone (the parent-level damage recompute walks +// it -- cascade wave). +//############################################################################# +// +void + Mech__DamageZone::SetLODParentPointers() +{ + Check(this); + + Mech *mech = (Mech *)GetOwningSimulation(); + Check(mech); + + DamageZoneIndexTableIterator iterator(redirectTable); + IntegerPlug *plug; + while ((plug = iterator.ReadAndNext()) != NULL) + { + int child = *plug; + if (child >= 0 && child < mech->damageZoneCount) + { + Mech__DamageZone *child_zone = + (Mech__DamageZone *)mech->damageZones[child]; + child_zone->parentArtifactZone.Add(this); + } + } +} + +// +//############################################################################# +// Damage entry. STAGED: the engine base armor model applies directly -- +// the authentic override (@0049c690) first routes ARTIFACT hits through the +// LOD redirect table to a real child zone, recomputes the artifact parent's +// displayed level, rolls critical-subsystem hits and updates the leg-failure +// state. All of that is the mechdmg cascade wave; the armor economy +// (amount x normalized scale, clamp, Burning/Destroyed at 1.0) is live NOW. +//############################################################################# +// +void + Mech__DamageZone::TakeDamage(Damage &damage) +{ + Check(this); + DamageZone::TakeDamage(damage); +} + +// +//############################################################################# +// Graphic state. STAGED pass-through: the authentic override also flips the +// zone's skeleton-segment video objects to the destroyed skins (cascade +// wave); the base keeps the state machine + changed flags correct for the +// network update records. +//############################################################################# +// +void + Mech__DamageZone::SetGraphicState(Enumeration new_state) +{ + Check(this); + DamageZone::SetGraphicState(new_state); +} diff --git a/restoration/source410/BT/MECHDMG.NOTES.md b/restoration/source410/BT/MECHDMG.NOTES.md new file mode 100644 index 00000000..d7b56888 --- /dev/null +++ b/restoration/source410/BT/MECHDMG.NOTES.md @@ -0,0 +1,83 @@ +# MECHDMG.CPP — Mech::DamageZone (hull zone armor economy) + +**Status: PARTIAL (5.3.16) — streamed ctor / LOD parent pass / dtor reconstructed; TakeDamage + SetGraphicState are staged base pass-throughs.** + +## The header is AUTHENTIC + +`CODE/BT/BT/MECHDMG.HPP` (06/05/95 JM) **survives in the archive** and wins the +include path (`$C/BT` precedes `$S/BT`), so this TU compiles against the real +1995 interface: `MechCriticalSubsystem` with a `SlotOf` plug and +**Scalar** criticalWeight, `Logical` zone flags, `TableOf*,int>` +redirectTable, `SlotOf parentArtifactZone`. Only the .CPP is +a reconstruction. (A from-scratch staged header was written and then DELETED +the same session once the survivor was found — do not resurrect it.) + +## Why this TU exists at wave 5.3.16 (the bring-up crash) + +`Entity::Entity` (engine, ENTITY.CPP:1032) only reads the zone COUNT from the +type-0x14 DamageZoneStream and allocates the raw `damageZones[count]` POINTER +array — **it never constructs the zones**. The derived entity must fill the +slots itself (CulturalIcon = the surviving engine reference, CULTURAL.CPP:349; +the Mech = binary Pass 3, FUN_004a1674). Two crashes taught this: + +1. Unfilled slots are heap garbage → the FIRST TakeDamageMessage dispatched at + a mech called through a trash vtable (exception 0E, EIP inside the heap). +2. Filling with the engine BASE `::DamageZone` streamed ctor parses each zone + record SHORT (the BT tail below goes unread) → the stream skews → the next + zone's effect-site indices read garbage → `GetSegment()` NULL → + `Link::AddToPlug(NULL)` (exception 0E at AddToPlug+9). The class-scope + `DamageZone` typedef (= `Mech__DamageZone`) in the authentic mech ctor is + the load-bearing signal: mech zones are ALWAYS the subclass. + +## The per-zone stream record (type 0x14, after the count word) + +Engine base part (DAMAGE.CPP:234, authentic order, VERIFIED identical RP↔BT411): +`CString name` · per 5 effect-site classes `{int n; n×int segIdx}` (JointedMover +owners resolve them via GetSegment; non-jointed read 5 filler ints) · +`Scalar defaultArmorPoints` · `Scalar damageScale[5]` · material lists. + +BT mech tail (this ctor, BT411-verified @0049ce50): +`Logical descend, destroySiblings` · `int segmentIndex` · `Logical leftLeg, +rightLeg, vital` · `int critCount` · critCount × `{Scalar weight; Scalar +damagePct; int rosterIndex}` · `int redirectCount` · redirectCount × `int +childZone`. + +## The armor economy (BT411 combat-damage.md, binary-verified) + +- Stream authors armor POINTS; ctor normalizes in place: + `scale[type] = 1/(raw[type]×armorPoints)` (cells already `1/armorPoints` + pass through, tolerance 1e-4); leg zones halve every type's scale. +- Base `DamageZone::TakeDamage` then applies + `damageLevel += amount × scale[type]`, clamp [0,1], 1.0 = Burning/Destroyed. +- `damageType` indexes the 5 cells DIRECTLY: 0 Collision / 1 Ballistic / + 2 Explosive / 3 Laser / 4 Energy. **burstCount is NOT in the formula** — + one message = one application (it only feeds gyro bounce + splash falloff). +- Live-verified 5.3.16 (two-mech fight): PPC 11.77/hit type 4 (authored 12 × + chargeRatio²), ER-M laser 3.43 type 3, SRM 5.83×6 type 2; repeat hits climb + linearly; 70-pt leg zones absorb at the halved scale; zones reach 1.0. + +## Crit table binding + +Streamed roster index → `subsystemPlug.Add(GetSubsystem(idx))`, gated +MasterInstance + DynamicFlag (binary @0049d0e1 — replicant plugs stay unbound, +criticals are master-authoritative) AND NULL-guarded: roster slot 0 (control +mapper) installs post-ctor, slot 1 (voltage bus) can be empty, and +`Link::AddToPlug` derefs the plug. + +## Staged / deferred (the mechdmg cascade wave) + +- `TakeDamage` override = base pass-through. Authentic (@0049c690): artifact + zones route hits through the redirect table to a real child + (`LODDamageRouter`/`RandomRedirect`/`lastHitZone`), parent recomputes its + displayed level (`UpdateLODDamageLevel`), critical rolls (`CriticalHit`), + generator shorting, leg-failure state. +- `SetGraphicState` override = base pass-through (authentic also flips the + segment video objects to destroyed skins). +- Declared-not-defined (unreferenced, so they link): `LODDamageRouter`, + `RandomRedirect`, `UpdateLODDamageLevel`, `ShortAttachedGenerators`, + `SendSubsystemDamage`, `RecurseSegmentTable`, `CriticalHit`, + `CreateStreamedDamageZone`, `GetSegmentIndex`. +- `lastHitZone` staged 0 (authentic seeds via RandomRedirect when redirects + exist); `lastDamageTime` staged 0 (authentic = TheTime tick / TicksPerSecond). +- The type-0x1e critical-descriptor stream + MechDeathHandler (BT411 + mech.cpp Pass-3 tail) are NOT loaded yet — same cascade wave. diff --git a/restoration/source410/BT/MECHWEAP.CPP b/restoration/source410/BT/MECHWEAP.CPP index 7851dbb0..fe36217c 100644 --- a/restoration/source410/BT/MECHWEAP.CPP +++ b/restoration/source410/BT/MECHWEAP.CPP @@ -25,6 +25,10 @@ #include +#if !defined(RANDOM_HPP) +# include +#endif + Derivation MechWeapon::ClassDerivations( PoweredSubsystem::ClassDerivations, @@ -261,14 +265,93 @@ void // //############################################################################# -// SendDamage Deliver this weapon's damage to a struck entity. The damage/ -// networking path is not yet reconstructed. +// HasActiveTarget -- the owner mech's target slot (binary owner+0x388). +//############################################################################# +// +Logical + MechWeapon::HasActiveTarget() +{ + Check(this); + return (owner != NULL && owner->GetTargetEntity() != NULL) + ? True : False; +} + +// +//############################################################################# +// UpdateTargeting -- refresh the range to the current target and the +// within-range flag (binary @004b9bdc). effectiveRange is the (heat-degraded, +// later) weapon range. +//############################################################################# +// +Logical + MechWeapon::UpdateTargeting() +{ + Check(this); + + Entity *target = (owner != NULL) ? owner->GetTargetEntity() : NULL; + if (target == NULL) + { + targetWithinRange = False; + return False; + } + + Vector3D span; + span.Subtract( + target->localOrigin.linearPosition, + owner->localOrigin.linearPosition + ); + rangeToTarget = span.Length(); + targetWithinRange = (rangeToTarget <= effectiveRange) ? True : False; + return targetWithinRange; +} + +// +//############################################################################# +// SendDamage Deliver this weapon's damage to the struck entity: build the +// engine TakeDamageMessage (inflictor = our mech) and dispatch it AT the +// target, whose handler routes damageZones[zone]->TakeDamage with the +// authored per-type scales. INTERIM zone pick: a uniform roll over the +// target's hull zones -- the authentic resolver is the reticle hit (aimed) or +// the cylinder hit-location table (unaimed), both in the render/mech3 wave. //############################################################################# // void - MechWeapon::SendDamage(Entity *, Damage &) + MechWeapon::SendDamage(Entity *target, Damage &damage) { - Fail("MechWeapon::SendDamage -- mechweap.cpp not yet reconstructed"); + Check(this); + Check(target); + + int zone = -1; + if (target->damageZoneCount > 0) + { + zone = Random(target->damageZoneCount); + } + + Entity::TakeDamageMessage + hit( + Entity::TakeDamageMessageID, + sizeof(Entity::TakeDamageMessage), + (owner != NULL) ? owner->GetEntityID() : EntityID::Null, + zone, + damage + ); + target->Dispatch(&hit); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[dmg] '" << GetName() + << "' hit zone " << zone + << " amount=" << damage.damageAmount + << " type=" << damage.damageType; + if (zone >= 0 && zone < target->damageZoneCount + && target->damageZones != NULL + && target->damageZones[zone] != NULL) + { + DEBUG_STREAM << " -> zoneLevel=" + << target->damageZones[zone]->damageLevel; + } + DEBUG_STREAM << endl << flush; + } } // diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index dab40cc2..c97110c7 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -193,6 +193,17 @@ ComputeOutputVoltage(); // vtable slot 17; the Emitter overrides // with its charge-voltage form + // + // Targeting: the owner mech's target slot gates the Loaded->Firing + // transition (no target = the denial blip, no shot); UpdateTargeting + // (binary @004b9bdc) refreshes rangeToTarget / targetWithinRange from + // the target's position each frame. + // + Logical + HasActiveTarget(); + Logical + UpdateTargeting(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local Data // diff --git a/restoration/source410/BT/MECHWEAP.NOTES.md b/restoration/source410/BT/MECHWEAP.NOTES.md index bed305b7..a87fd18a 100644 --- a/restoration/source410/BT/MECHWEAP.NOTES.md +++ b/restoration/source410/BT/MECHWEAP.NOTES.md @@ -126,3 +126,27 @@ cycling — the NoAmmo roach-motel holds. Zero Fail. common block — the same silent-NULL trap as the Emitter::AttributeIndex crash. Real weapon handlers (fire/damage messages) come with the combat wave (grow the table in place). + +## 5.3.16 — targeting + damage delivery + +- `HasActiveTarget()` = owner's target slot non-NULL (binary owner+0x388); + `UpdateTargeting()` (@004b9bdc) refreshes rangeToTarget/targetWithinRange + from `target->localOrigin` every FSM tick (both weapon families call it + right after the trigger sample). The Loaded→Firing gate is now the authentic + `viewFireEnable && HasActiveTarget()` — a denied pull is the one-frame blip. +- `SendDamage(target, damage)`: builds the engine 5-arg + `Entity::TakeDamageMessage` (inflictor = our mech's EntityID) and Dispatches + AT the target; the engine handler routes `damageZones[zone]->TakeDamage`. + INTERIM zone pick = `Random(target->damageZoneCount)` — the authentic + resolvers are the reticle aimed hit and the cylinder hit-location table + (unaimed, zone −1 + the Mech TakeDamage override), both later waves. +- Salvo semantics (BT411-verified): **burstCount is NOT in the zone armor + formula** — one message = one amount×scale application. MissileLauncher + therefore dispatches ONE message PER MISSILE (each carrying the ctor's + salvo-split amount, each on its own random zone); a single split message + would under-deliver the salvo by the round count. Interim all-rounds-hit + until the Missile entity flight/seeker wave. +- Live-verified (two-mech fight): PPC 11.77/hit (12×chargeRatio², type 4 + Energy), ER-M 3.43 (type 3 Laser), SRM6 5.83×6 (type 2 Explosive); repeat + hits climb linearly; zones reach 1.0 Burning/Destroyed; no-target runs hold + fire (0 FIRED across a 65s force-fire run without a target). diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index fd1ef5c0..2fb459b2 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -112,6 +112,25 @@ void AddPendingHeat(heatCostToFire); } + // + // The salvo delivery: ONE TakeDamageMessage per missile, each carrying + // the salvo-split per-missile damage (the ctor split the authored amount + // across missileCount). The zone armor formula IGNORES burstCount + // (BT411 combat-damage: one message = one amount*scale application), so + // a single split message would under-deliver the salvo by the round + // count. INTERIM instant-hit with every round hitting (its own random + // zone) -- the Missile entity flight / seeker / proximity fuse joins the + // entity-spawn wave. + // + if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL) + { + for (int mm = 0; mm < missileCount; ++mm) + { + SendDamage(owner->GetTargetEntity(), damageData); + } + } + if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() diff --git a/restoration/source410/BT/PROJWEAP.CPP b/restoration/source410/BT/PROJWEAP.CPP index 3a939978..dacd6cc8 100644 --- a/restoration/source410/BT/PROJWEAP.CPP +++ b/restoration/source410/BT/PROJWEAP.CPP @@ -289,6 +289,8 @@ void // Logical trigger = CheckFireEdge(); + targetWithinRange = UpdateTargeting(); + // // Gate 1 (@4bbd36): weapon DESTROYED or its own sink at FailureHeat -> pin // full recoil + the unavailable alarm. No return -- the frame continues @@ -327,7 +329,11 @@ void case LoadedState: if (trigger) { - if (viewFireEnable) + // + // The authentic fire gate (@4bbee6): armed in the current look + // view AND a target under the reticle. + // + if (viewFireEnable && HasActiveTarget()) { // // The ammo pull happens HERE, in the caller -- a failed pull