//===========================================================================// // File: mechweap.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: MechWeapon -- abstract base for every mounted Mech weapon // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // --/--/95 JM Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra // pseudo-C for the module cluster @004b95ec-@004b9d10 (file=bt/mechweap.cpp) // plus the pure-virtual stub @004ba45c (file=bt/mechweap.hpp). Method and // member names follow the surviving derived sources (PPC.CPP, GAUSS.CPP, // MISLANCH.HPP), the resource-parser tag strings, and the RP WEAPSYS.h analog. // Each non-trivial method cites the originating @ADDR. // // Resolved read-only constants (4 LE bytes from section_dump CODE): // _DAT_004b9648 = 00 00 00 00 = 0.0f (fire-edge threshold) // _DAT_004b9b98 = 00 00 80 bf = -1.0f (default pip-colour component) // _DAT_004b9c98 = 00 00 80 3f = 1.0f (full-heat reference) // _DAT_004ba37c = 00 00 80 bf = -1.0f (resource "unset" sentinel) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004b0f74 PoweredSubsystem base constructor // FUN_004b115c PoweredSubsystem destructor // FUN_004b0e6c PoweredSubsystem::ResetToInitialState (vtable slot 10) // FUN_004b0efc PoweredSubsystem (vtable slot 9 -- ReadUpdateRecord) // FUN_004b0f48 PoweredSubsystem::GetStatusFlags (vtable slot 12) // FUN_004b1224 PoweredSubsystem::PrintState (vtable slot 13) // FUN_004b13ac PoweredSubsystem::CreateStreamedSubsystem // FUN_0041bd34 Subsystem::TakeDamage (vtable slot 6, base) // FUN_0041c500 Subsystem::WriteUpdateRecord (vtable slot 7, base) // FUN_0041b9ec AlarmIndicator(levels) FUN_0041baa4 ~AlarmIndicator // FUN_0041bbd8 AlarmIndicator::SetLevel(n) // FUN_0041db7c Damage::Damage() / init // FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_00408440 Vector copy FUN_0040a7f4 Point copy // FUN_00408644 Vector subtract (out = a - b) // FUN_004087f4 Vector negate/copy // FUN_0040aadc Matrix identity FUN_0040ad2c Matrix from angles // FUN_00409a70 apply transform FUN_00409968 Vector set // FUN_004dd138 sqrtf FUN_004dc8ec atan2f(double,double) // FUN_00408110 angle normalise // FUN_0041f216 / FUN_0041f235 segment-iterator construct / destruct // FUN_00424da8 resolve segment world point // FUN_0049b6d8 cockpit HUD: register pip / fire marker // FUN_004b13ac etc. (resource parser primitives): // FUN_00404118 NotationFile::Read(scalar) FUN_00404088 Read(string) // FUN_004040d8 Read(int/bool) FUN_00404190 Read(int) // FUN_00406824 parse colour FUN_00406ff8 ResourceFile::FindResource // FUN_004d4b58 strcmp FUN_004d4b14 memcmp FUN_004d4c78 strstr // FUN_004dbb24 DebugStream << FUN_004d9c38 DebugStream flush // FUN_0040385c Verify()/assert(msg,file,line) // #include #include // SubsystemMessageManager (task #8 damage submission) #include // MechControlsMapper -- the config-session virtuals (task #6) #pragma hdrstop #if !defined(MECHWEAP_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif //############################################################################# // Shared Data Support // Derivation MechWeapon::ClassDerivations( PoweredSubsystem::GetClassDerivations(), "MechWeapon" ); // task #6 -- the binary's MechWeapon handler table @0x511860 (section_dump: // {9, "ConfigureMappables"@0x511994, @004b9550}, {10, "ChooseButton"@0x5119A7, // @004b95b8}); chained onto the PoweredSubsystem set. Table INSIDE the // accessor (function-local static) -- the cross-TU static-init order trap // emptied the powersub table this same way (task #12); this one only worked // by TU-order luck. Receiver::MessageHandlerSet& MechWeapon::GetMessageHandlers() { static const Receiver::HandlerEntry entries[]= { MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550 MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8 }; static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(entries), entries, PoweredSubsystem::GetMessageHandlers() ); return messageHandlers; } Simulation::AttributeIndexSet MechWeapon::AttributeIndex; // legacy empty member (superseded by GetAttributeIndex) // gauge data-binding wave: OutputVoltage/PercentDone -> rechargeLevel (1.0 == // ready; ComputeOutputVoltage/the charge state machine drive it, so both animate // as the weapon recharges). Chained to PoweredSubsystem's dense index. // task #16: the ID GAP between our parent chain (ends 0x0D) and the pinned // binary ids MUST be padded -- see the enum note. Locked so a future parent // publish shows up here instead of silently double-assigning ids: static_assert((int)PoweredSubsystem::NextAttributeID == (int)MechWeapon::MechWeaponPadFirstAttributeID, "MechWeapon pad base != PoweredSubsystem::NextAttributeID -- re-count the pads"); const MechWeapon::IndexEntry MechWeapon::AttributePointers[]= { // task #16: the FULL binary table @0x511890 (eleven entries) + the // port-extra OutputVoltage alias. Every member offset matches the // binary's (layout static_assert-locked); TriggerState stays the streamed // fire-button binding (id 0x13 PINNED). // // The five PADS fill our chain-vs-binary id gap (0x0D..0x11) with valid, // named, never-bound entries (the binary's own ids here are the powered/ // aux-screen family we don't publish); target = rechargeLevel, harmless. { (int)MechWeapon::MechWeaponPadFirstAttributeID + 0, "MechWeaponPad0D", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, { (int)MechWeapon::MechWeaponPadFirstAttributeID + 1, "MechWeaponPad0E", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, { (int)MechWeapon::MechWeaponPadFirstAttributeID + 2, "MechWeaponPad0F", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, { (int)MechWeapon::MechWeaponPadFirstAttributeID + 3, "MechWeaponPad10", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, { (int)MechWeapon::MechWeaponPadFirstAttributeID + 4, "MechWeaponPad11", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12 @0x320 ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13 @0x31C ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14 @0x324 ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15 @0x34C ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16 @0x328 (heat-degraded) ATTRIBUTE_ENTRY(MechWeapon, PipPosition, pipPosition), // 0x17 @0x338 ATTRIBUTE_ENTRY(MechWeapon, PipColor, pipColor), // 0x18 @0x33C ATTRIBUTE_ENTRY(MechWeapon, PipExtendedRange, pipExtendedRange), // 0x19 @0x348 ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A @0x330 ATTRIBUTE_ENTRY(MechWeapon, RearFiring, usesExternalModel), // 0x1B @0x334 ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm), // 0x1C @0x350 ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel) // 0x1D (port alias) }; MechWeapon::AttributeIndexSet& MechWeapon::GetAttributeIndex() { static MechWeapon::AttributeIndexSet attributeIndex( ELEMENTS(MechWeapon::AttributePointers), MechWeapon::AttributePointers, PoweredSubsystem::GetAttributeIndex() ); return attributeIndex; } MechWeapon::SharedData MechWeapon::DefaultData( &MechWeapon::ClassDerivations, MechWeapon::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), MechWeapon::StateCount ); //############################################################################# // The CONFIG-MODE session handlers (task #6) // // // @004b9550 -- id 9 "ConfigureMappables": the press-and-HOLD session bracket. // dataContents = +-(element+1) (press/release, the L4 button value convention). // Press: open the session on THIS weapon -- the roster-slot-0 mapper flips the // global mode NonMapping->Mapping, latches &fireImpulse as the active configure // target, and temp-maps the fire buttons to send ChooseButton (id 10) here. // Release: close the session (mode restored, temp layer stripped). // The +0x110 write (0 open / -1 idle) is the binary's config-session flag // (ctor default -1, part_012.c:15775); the port field carries the older name // vitalSubsystemIndex -- no confirmed reader yet (read-site survey pending). // void MechWeapon::ConfigureMappablesMessageHandler( ReceiverDataMessageOf *message ) { Check(this); Check_Pointer(message); int value = message->dataContents; // +-(element+1) int element = ((value < 0) ? -value : value) - 1; MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper(); // **(mech+0x128) if (mapper == 0) { return; // (binary assumes slot 0 is live) } if (value < 1) { mapper->ExitConfiguration(element); // vtbl+0x3C vitalSubsystemIndex = -1; // this+0x110 -> idle } else { vitalSubsystemIndex = 0; // this+0x110 -> session open mapper->EnterConfiguration( // vtbl+0x38 element, (ControlsButton *)&fireImpulse, // this+0x31C (TriggerState) this, ChooseButtonMessageID, // 10 ConfigureMappablesMessageID, // 9 0); // active_message_id (direct bind) } } // // @004b95b8 -- id 10 "ChooseButton": while the session is open, a tapped fire // button sends its RAW 1-based value here; the mapper's direct AddOrErase // TOGGLES buttonGroup[value-1]'s permanent (0x10000) mapping onto // &fireImpulse -- join the group if absent, leave it if present. Press only. // void MechWeapon::ChooseButtonMessageHandler( ReceiverDataMessageOf *message ) { Check(this); Check_Pointer(message); if (message->dataContents > 0) { MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper(); if (mapper != 0) { mapper->AddOrErase( // vtbl+0x44 (direct overload) (unsigned int)message->dataContents, // RAW 1-based (ControlsButton *)&fireImpulse); } } } //############################################################################# // Firing -- abstract base body // // @004b95ec -- MechWeapon is abstract; the base FireWeapon() must never run. // Exact analog of RP GenericGun::Fire()'s "Should not be here!" stub. Real // firing lives in Emitter::FireWeapon (PPC/Gauss) and ProjectileWeapon // (MissileLauncher). Assert tag: MECHWEAP.CPP : 0xC9. // void MechWeapon::FireWeapon() { Fail("MechWeapon abstract class should not be fired"); } // // @004ba45c -- a second pure-virtual placeholder emitted from MECHWEAP.HPP:343 // ("ERROR: Accessing Pure Virtual Method"). Compiled into the next translation // unit (Emitter); reproduced here for completeness. A human should reconcile // which header-declared pure virtual this guards. // // void MechWeapon::() { // Verify(False, "ERROR: Accessing Pure Virtual Method", // "d:\\tesla\\bt\\bt\\mechweap.hpp", 0x157); // } //############################################################################# // Construction / Destruction // // @004b99a8 -- chains to the PoweredSubsystem ctor (@004b0f74), installs the // MechWeapon vtable (PTR_FUN_00511d2c), constructs the alarm indicator and the // Damage member, then copies the weapon parameters out of the resource record. // MechWeapon::MechWeapon( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), weaponAlarm(/*levels=*/ 3), // FUN_0041b9ec(this+0xd4, levels) damageData() // FUN_0041db7c(this+0xea) { Check(owner); Check_Pointer(subsystem_resource); // pip colour defaults to (-1,-1,-1) -- "use the configured/team colour". pipColor.Red = -1.0f; // _DAT_004b9b98 pipColor.Green = -1.0f; pipColor.Blue = -1.0f; // --- copy weapon parameters out of the resource --- rechargeRate = subsystem_resource->rechargeRate; // +0x190 explosionResourceID = subsystem_resource->explosionResourceID; // +0x198 damageData.damageType = subsystem_resource->damageType; // +0x1A0 damageData.damageAmount = subsystem_resource->damageAmount; // +0x19C segmentPageIndex = subsystem_resource->segmentIndex; // +0x28 weaponRange = subsystem_resource->weaponRange; // +0x194 heatCostToFire = subsystem_resource->heatCostToFire; // +0x1A4 pipPosition = subsystem_resource->pipPosition; // +0x1A8 pipColor = subsystem_resource->pipColor; // +0x1AC..+0x1B4 pipExtendedRange = subsystem_resource->pipExtendedRange; // +0x1B8 effectiveRange = weaponRange; // 0x328 primed to weaponRange (recomputed in UpdateTargeting) simulationFlags |= 0x8; // this[10] |= 8 (mark "weapon" subsystem) rangeToTarget = 0.0f; // 0x324 targetWithinRange = False; // 0x34C // segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end); // removed. The muzzle segment index is the inherited base slot this+0xdc (used by // GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip. // usesExternalModel: True if the model name contains the marker substring // (DAT_00511aa2); selects whether the configured pip is drawn. usesExternalModel = (strstr(GetName(), "EXT") != 0) ? True : False; fireImpulse = 0.0f; // 0x31C // task #6: the binary MechWeapon ctor defaults its controls-destination // pass-through to &fireImpulse when the caller passes NULL (@004b99a8 // `if (param_7==0) param_7 = this+0x31C`); the chain stores it at // MechSubsystem+0xE8/+0xEC where ConfigMapGauge's GetMapState reads it. controlDestination = (void *)&fireImpulse; // 0xE8 controlMessageID = 0; // 0xEC rechargeLevel = 1.0f; // 0x320 (fully charged) previousFireImpulse = 0.0f; // 0x3A4 recoil = 0.0f; // 0x3E8 useConfiguredPip = (usesExternalModel == 0); // 0x3E0 estimatedReadyTime = 0; // 0x330 (binary attr "EstimatedReadyTime") Check_Fpu(); } // // @004b9b9c -- destroys the alarm indicator, chains to ~PoweredSubsystem, and // (if the deleting-dtor bit is set) frees the object. // MechWeapon::~MechWeapon() { Check(this); // weaponAlarm (@0xd4) destroyed by its own dtor (FUN_0041baa4(this+0xd4, 2)); // base chain (FUN_004b115c) handles the rest. Check_Fpu(); } //############################################################################# // Test Class Support // Logical MechWeapon::TestClass(Mech &) { return True; } Logical MechWeapon::TestInstance() const { return IsDerivedFrom(ClassDerivations); } //############################################################################# // Subsystem virtual overrides // // // @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6 // ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage, // which dispatches on *param==4 == Damage::damageType -- damage semantics). // The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet // reconstructed; chain to the engine Subsystem base, and keep the damage-zone // alarm tracking this port has shipped with (best-effort; the binary's alarm // display on weapon damage flows through the unreconstructed @004b0efc chain). // void MechWeapon::TakeDamage(Damage &damage) { Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc) // Port behavior (kept): a destroyed weapon clears its display alarm; // otherwise the alarm tracks the resolved damage-zone state. ::DamageZone *dz = this->Subsystem::damageZone; if (dz != 0 && dz->GetGraphicState() == DamageZone::DestroyedGraphicState) { weaponAlarm.SetLevel(0); } else { weaponAlarm.SetLevel( (dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0 ); } } // // @004b9690 -- slot 7. Append the weapon's replication fields to the update // record. DISASM CORRECTIONS (task #51): `*message = 0x18` writes the record // LENGTH (record[0] = recordLength), not recordID; and message[5] derives from // this+0x360 == weaponAlarm+0x10 (LevelCountB), not from the level. BASE-TYPED // param: with the class's own UpdateRecord typedef this silently failed to // override the engine virtual, so the engine base ran and no weapon field // ever serialized. // void MechWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model) { Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500 (header + subsystemID) MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message; rec->recordLength = sizeof(MechWeapon__UpdateRecord); // *message = 0x18 rec->alarmState = weaponAlarm.GetLevel(); // message[4] = *(this+0x364) rec->weaponIdle = (weaponAlarm.LevelCountB() == 0) ? 1 : 0; // message[5] = (*(this+0x360)==0) } // // @004b964c -- slot 6 (TASK #51 RENAME: was mislabeled TakeDamage). Apply a // weapon update record on the REPLICANT: chain the Simulation base // (@0041bd34 -- lastUpdate + simulationState), then the weapon alarm -- // reset-through-0 first when the record's weaponIdle flag is set, then the // replicated alarm level. // void MechWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message) { Simulation::ReadUpdateRecord(message); // FUN_0041bd34 MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message; if (rec->weaponIdle != 0) // record+0x14 { weaponAlarm.SetLevel(0); } weaponAlarm.SetLevel(rec->alarmState); // record+0x10 } // // @004b96ec -- slot 10. Reset the firing/recharge state, then chain to base. // void MechWeapon::ResetToInitialState() { PoweredSubsystem::ResetToInitialState(); // FUN_004b0e6c recoil = 0.0f; // this[0xfa] = 0 rechargeLevel = 1.0f; // this[200] = 0x3f800000 fireImpulse = 0.0f; // this[199] = 0 previousFireImpulse = 0.0f; // this[0xe9] = 0 } // // @004b9d00 -- slot 13. Defers to the PoweredSubsystem print routine. // void MechWeapon::PrintState() { PoweredSubsystem::PrintState(); // FUN_004b1224 } //############################################################################# // Targeting / aiming helpers // // // @004b9bdc -- recompute the range to the owning Mech's current target and the // heat-degraded effective range, then decide whether the target is in range. // // no target (entity+0x388 == 0): // rangeToTarget = -1; targetWithinRange = False // else: // v = entity->targetPoint(+0x37c) - entity->muzzlePoint(+0x100) // rangeToTarget = |v| // effectiveRange = (1.0 - heatSource->heatLoad(+0x158)) * weaponRange // targetWithinRange = (effectiveRange > rangeToTarget) // Logical MechWeapon::UpdateTargeting() { if (!HasActiveTarget()) // *(entity+0x388) == 0 { rangeToTarget = -1.0f; // 0xbf800000 targetWithinRange = False; } else { Point3D target, muzzle; GetTargetPosition(target); GetMuzzlePoint(muzzle); Vector3D delta; delta.Subtract(target, muzzle); // FUN_00408644 rangeToTarget = (Scalar)Sqrt( // FUN_004dd138 delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); // AUTHENTIC (decomp @004b9bdc:6983): effectiveRange = (1 - hostZoneDamage) * // weaponRange, where hostZoneDamage is this weapon's OWN DamageZone.damageLevel // (the QUALIFIED Subsystem::damageZone @0xE0 -> damageLevel +0x158), NOT // heatLoad. The old `heatLoad` read was the SAME @0xE0-vs-heat misattribution // already corrected in HeatSink::UpdateCoolant (heat.cpp:803): for a charge/ // discharge weapon (ER laser) the weapon's OWN heatLoad swings 0..1 every cycle, // so effectiveRange collapsed toward 0 and the weapon was perpetually "out of // range" -- no damage submission, hence no impact explosion (user report: // lackluster/absent laser hits on mechs AND buildings, esp. the ER medium). // An UNDAMAGED weapon has damageLevel 0 -> full, STABLE weaponRange. Use the // QUALIFIED engine zone -- MechSubsystem::damageZone is a shim SHADOW (heat.cpp:812). // 1.0f == _DAT_004b9c98. ::DamageZone *hostZone = this->Subsystem::damageZone; // @0xE0 Scalar hostZoneDamage = (hostZone != 0) ? hostZone->damageLevel : 0.0f; // +0x158 effectiveRange = (1.0f - hostZoneDamage) * weaponRange; // *0x32c targetWithinRange = (effectiveRange > rangeToTarget) ? True : False; } return targetWithinRange; } // // @004b9608 -- rising-edge detector on fireImpulse (threshold _DAT_004b9648 == 0.0f). // Returns True the frame fireImpulse becomes positive while the previous sample // was non-positive, then stores the current sample for next time. // // ⚠ BIT-PATTERN COMPARE (task #8 root-cause): TriggerState carries the raw // ControlsButton INT bit-copied by the streamed direct mapping (+(button+1) // press / -(button+1) release) -- the release value (e.g. -65 = 0xFFFFFFBF) // reads as a NEGATIVE NaN in float terms. The binary's x87 compare (FNSTSW/ // SAHF + JBE: unordered sets CF|ZF => branch TAKEN) effectively treated the // NaN as "released", so its edge re-armed; an IEEE-correct float compare // makes NaN<=0 FALSE and the detector LATCHES SHUT after the first release // (observed live: one startup edge, then never again). Compare the BIT // PATTERNS as signed ints instead -- sign-correct for the button ints AND // for genuine float values (0.0f = 0x0, positives > 0, negatives/NaNs < 0), // reproducing the binary's effective behavior exactly. // Logical MechWeapon::CheckFireEdge() { int current = *(int *)&fireImpulse; int previous = *(int *)&previousFireImpulse; Logical edge = (current > 0 && previous <= 0) ? True : False; previousFireImpulse = fireImpulse; return edge; } // // @004b9cbc -- copy the owning Mech's current target position (entity+0x1c4). // void MechWeapon::GetTargetPosition(Point3D &position) { // E3: read the owning Mech's current target position (mech+0x37c, set by mech4's // targeting step) -- the local `targetPoint` member isn't refreshed in the port. char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) if (o != 0) position = *(Point3D *)(o + 0x37c); // MECH_TARGET_POS // (no owner -> leave position unchanged; the phantom `targetPoint` member is gone -- // binary GetTargetPosition @004b9cbc unconditionally reads the owner target slot.) } // Faithful FUN_004b9948 muzzle resolve, defined in mech4.cpp (a complete-Mech TU); // mechweap.cpp reaches the owner Mech only as a raw pointer, so it bridges via void*. extern void BTResolveWeaponMuzzle(void *ownerMech, int segIndex, Point3D &out); // // @004b9cdc -- direction from a supplied point to the weapon mount (entity+0x37c). // void MechWeapon::GetVectorToWeapon(const Point3D &from, Vector3D &direction) { Point3D muzzle; GetMuzzlePoint(muzzle); // resolve live (phantom muzzlePoint member removed) direction.Subtract(muzzle, from); // FUN_00408644 } // // @004b9948 -- resolve the weapon's muzzle/site world position by looking up its // segment in the owning Mech's segment table and transforming it. // void MechWeapon::GetMuzzlePoint(Point3D &point) { // @004b9948 -- resolve the weapon's mount segment in the owning Mech's // segment table and transform it to world (FUN_00424da8). // DATABINDING FIX (muzzle wave, 2026-07-12): the binary's this+0xdc IS the // engine Subsystem's segmentIndex -- but the old raw `*(this+0xdc)` read // OUR compiled layout at that offset (garbage), so every weapon resolved a // RANDOM segment or none: missiles launched from heads/feet/below terrain // (user-hit on the Avatar) and the beam/missile paths grew gun-port // name-lookup hacks to compensate. GetSegmentIndex() is the named member // [T0 SUBSYSTM.h:108] -- authentic mounts on every chassis. int segIndex = GetSegmentIndex(); BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded) } // // @004b9864 -- build an aim orientation from a direction vector (yaw/pitch via // atan2), producing a rotation matrix. // void MechWeapon::ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction) { // Build a yaw/pitch aim orientation from a direction vector (atan2), roll 0. Scalar yaw = (Scalar)atan2(-direction.x, -direction.z); // FUN_004dc8ec Scalar len = (Scalar)Sqrt(direction.z * direction.z + direction.x * direction.x); Scalar pitch = (Scalar)atan2(direction.y, len); // FUN_004dc8ec EulerAngles angles( Radian(Radian::Normalize(pitch)), Radian(Radian::Normalize(yaw)), Radian(0.0f) ); orientation = angles; // AffineMatrix::operator=(EulerAngles) } // // ⚠ IDENTITY CORRECTION (task #7): @004b9728 is MechWeapon::SendDamageMessage // -- the weapon-fire damage submission into the owning mech's // SubsystemMessageManager (mech+0x434 -> AddDamageMessage @0049b6d8), NOT a // HUD pip. The "HUD element record" reading below was the same 0x434 // misattribution the mapper suffered. This body is the LIVE per-shot // beam-damage path (called from Emitter::FireWeapon, emitter.cpp:289; decomp // part_013.c:7758-7764) carrying the authentic per-weapon damageData + // inflictingSubsystemID -- the damage-economy reconciliation (task #60) // CONFIRMED the authored-DamageAmount economy; kShotDamage is retired to the // kill-score and env-gated diag hooks (task #60). // void MechWeapon::SendDamageMessage(Entity *target) { // // @004b9728 -- package this weapon's just-fired damageData as an // Entity::TakeDamageMessage and submit it to the owning mech's // SubsystemMessageManager for per-frame consolidation (AddDamageMessage // @0049b6d8) -- the authentic beam damage delivery (task #8; the caller // Emitter::FireWeapon fills damageAmount/damageForce/impactPoint first). // // Binary gate (part_013.c:6746): a Mech victim with NO designated zone // (owner+0x38C == -1) is SKIPPED. The port PASSES -1 through [T3 // divergence]: our Mech::TakeDamageMessageHandler cylinder-resolves the // unaimed zone (STEP 6), preserving the verified aiming model. // Mech *mech = (Mech *)GetEntity(); // weapon+0xD0 if (mech == 0 || target == 0) { return; } extern int BTMechTargetZone(void *mech); // mech+0x38C (mech4 TU) int zone = BTMechTargetZone(mech); Entity::TakeDamageMessage message( Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), mech->GetEntityID(), // inflicting = owner (+0x184) zone, // designated zone (or -1 -> cylinder) damageData, // the 12-dword Damage @0x3A8 subsystemID); // inflictingSubsystemID (+0xD8) -- // lights up the messmgr's per-weapon // explosion bundling (weapon+0x3E4) SubsystemMessageManager *manager = (SubsystemMessageManager *)mech->GetMessageManager(); // mech+0x434 if (manager != 0) { manager->AddDamageMessage(target, &message); // FUN_0049b6d8 } else { target->Dispatch(&message); // no manager: direct (bring-up) } // PORT bookkeeping (score lived in the retired mech4 fire block): credit // the delivered amount to the shooter-side score exactly as before. extern void BTPostDamageScore(Entity *victim, Scalar damage); extern Logical BTIsRegisteredMech(Entity *e); if (BTIsRegisteredMech(target) && !((Mech *)target)->IsMechDestroyed()) { BTPostDamageScore(target, damageData.damageAmount); } } //############################################################################# // CreateStreamedSubsystem // // @004b9d10 -- parse the MechWeapon-specific resource fields after the // PoweredSubsystem base parser. On the first pass every field is primed to the // "unset" sentinel (-1.0f == _DAT_004ba37c, or -1 for ints); a field is an error // only if both the read fails AND the field is still unset. Stamps // subsystemModelSize = 0x1BC and classID = MechWeaponClassID. // int MechWeapon::CreateStreamedSubsystem( ResourceFile *resource_file, NotationFile *model_file, const char *model_name, const char *subsystem_name, SubsystemResource *subsystem_resource, NotationFile *subsystem_file, const ResourceDirectories *directories, int passes ) { if ( !PoweredSubsystem::CreateStreamedSubsystem( // FUN_004b13ac model_file, model_name, subsystem_name, subsystem_resource, subsystem_file, directories, passes ) ) { return False; } subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource); // 0x1BC subsystem_resource->classID = (RegisteredClass::ClassID)Mech::MechWeaponClassID; // 0xBCD if (passes == 1) { // prime all weapon fields to "unset" subsystem_resource->rechargeRate = -1.0f; subsystem_resource->weaponRange = -1.0f; subsystem_resource->explosionResourceID = -1; subsystem_resource->damageAmount = -1.0f; subsystem_resource->heatCostToFire = -1.0f; subsystem_resource->pipPosition = -1; subsystem_resource->pipColor.Red = -1.0f; subsystem_resource->pipColor.Green = -1.0f; subsystem_resource->pipColor.Blue = -1.0f; subsystem_resource->pipExtendedRange = -1; } // "SegmentPageName" must be present on streaming passes after the first. if (subsystem_resource->segmentIndex == -1 && passes > 1) // +0x28 { DEBUG_STREAM << subsystem_name << " missing SegmentPageName!"; return False; } if ( !model_file->GetEntry(subsystem_name, "DamageAmount", &subsystem_resource->damageAmount) && subsystem_resource->damageAmount == -1.0f ) { DEBUG_STREAM << model_name << " Missing DamageAmount!" << std::endl; return False; } if ( !model_file->GetEntry(subsystem_name, "HeatCostToFire", &subsystem_resource->heatCostToFire) && subsystem_resource->heatCostToFire == -1.0f ) { DEBUG_STREAM << subsystem_name << " missing HeatCostToFire!"; return False; } if ( !model_file->GetEntry(subsystem_name, "PipPosition", &subsystem_resource->pipPosition) && subsystem_resource->pipPosition == -1 ) { DEBUG_STREAM << subsystem_name << " missing PipPosition!"; return False; } // // PipColor: optional named colour; "Unspecified" leaves the resource value, // otherwise it is parsed. A missing/short value is a hard error. // const char *pipColorName = "Unspecified"; if (!model_file->GetEntry(subsystem_name, "PipColor", &pipColorName)) { // no value supplied -- require the resource already to hold one RGBColor probe(-1.0f, -1.0f, -1.0f); if (probe == subsystem_resource->pipColor) { DEBUG_STREAM << subsystem_name << " missing PipColor!"; return False; } } if (strcmp(pipColorName, "Unspecified") != 0) { // FUN_00406824 -- the shipped build mapped a colour token to an RGB triple; // accept an explicit "R G B" triple here (named-colour table is data-driven). float r = -1.0f, g = -1.0f, b = -1.0f; if (sscanf(pipColorName, "%f %f %f", &r, &g, &b) == 3) { subsystem_resource->pipColor.Red = r; subsystem_resource->pipColor.Green = g; subsystem_resource->pipColor.Blue = b; } } if ( !model_file->GetEntry(subsystem_name, "PipExtendedRange", &subsystem_resource->pipExtendedRange) && subsystem_resource->pipExtendedRange == -1 ) { DEBUG_STREAM << subsystem_name << " missing PipExtendedRange!" << std::endl; return False; } if ( !model_file->GetEntry(subsystem_name, "RechargeRate", &subsystem_resource->rechargeRate) && subsystem_resource->rechargeRate == -1.0f ) { DEBUG_STREAM << model_name << " Missing RechargeRate!" << std::endl; return False; } if ( !model_file->GetEntry(subsystem_name, "WeaponRange", &subsystem_resource->weaponRange) && subsystem_resource->weaponRange == -1.0f ) { DEBUG_STREAM << model_name << " Missing WeaponRange!" << std::endl; return False; } // // DamageType: maps a name onto the Damage::DamageType enum. // const char *damageTypeName = "Unspecified"; if ( !model_file->GetEntry(subsystem_name, "DamageType", &damageTypeName) && subsystem_resource->damageAmount == -1.0f ) { DEBUG_STREAM << model_name << " missing DamageType!"; return False; } if (strcmp(damageTypeName, "Unspecified") != 0) { if (strcmp(damageTypeName, "CollisionDamage") == 0) subsystem_resource->damageType = Damage::CollisionDamageType; // 0 else if (strcmp(damageTypeName, "BallisticDamage") == 0) subsystem_resource->damageType = Damage::BallisticDamageType; // 1 else if (strcmp(damageTypeName, "ExplosiveDamage") == 0) subsystem_resource->damageType = Damage::ExplosiveDamageType; // 2 else if (strcmp(damageTypeName, "LaserDamage") == 0) subsystem_resource->damageType = Damage::LaserDamageType; // 3 else if (strcmp(damageTypeName, "EnergyDamage") == 0) subsystem_resource->damageType = Damage::EnergyDamageType; // 4 else { DEBUG_STREAM << model_name << " has an unknown DamageType of " << damageTypeName << std::endl; return False; } } // // ExplosionModelFile: optional; "Unspecified" leaves it unset, otherwise the // named model is looked up in the resource file. // const char *explosionModelName = "Unspecified"; if ( !model_file->GetEntry(subsystem_name, "ExplosionModelFile", &explosionModelName) && subsystem_resource->explosionResourceID == -1 ) { DEBUG_STREAM << model_name << " missing ExplosionModelFile!"; return False; } ResourceDescription *explosion = 0; if (strcmp(explosionModelName, "Unspecified") != 0) { explosion = resource_file->FindResourceDescription( // FUN_00406ff8 explosionModelName, (ResourceDescription::ResourceType)1, -1); } if (explosion == 0 && subsystem_resource->explosionResourceID == -1) { DEBUG_STREAM << model_name << " cannot find " << explosionModelName << " in resource file!"; return False; } if (explosion != 0) { subsystem_resource->explosionResourceID = explosion->resourceID; } return True; }