//===========================================================================// // File: mechweap.cpp // // Project: BattleTech Brick: Mech weapons // // Contents: MechWeapon -- the base weapon subsystem // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(COLOR_HPP) # include // RGBColor + Convert_From_Ascii (PipColor) #endif #if !defined(MECHWEAP_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(SEGMENT_HPP) # include #endif #include #include #if !defined(RANDOM_HPP) # include #endif #if !defined(BTPLAYER_HPP) # include #endif Derivation MechWeapon::ClassDerivations( PoweredSubsystem::ClassDerivations, "MechWeapon" ); // //############################################################################# // Message handlers (binary ids 9/10 @004b9550/@004b95b8) -- the weapon-group // config buttons, chained onto the PoweredSubsystem generator panel (4-8), // which chains HeatSink (3): a weapon's dispatch reaches the whole cockpit // column. (The set MUST be a real defined object: the surviving CODE // PPC.CPP binds the inherited name into PPC::DefaultData, and a // declared-but-undefined static links as a zero-filled common block.) //############################################################################# // const MechWeapon::HandlerEntry MechWeapon::MessageHandlerEntries[] = { MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550 MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8 }; MechWeapon::MessageHandlerSet MechWeapon::MessageHandlers( ELEMENTS(MechWeapon::MessageHandlerEntries), MechWeapon::MessageHandlerEntries, PoweredSubsystem::MessageHandlers ); // //############################################################################# // Attribute Support -- the 1995 binary table (@0x511890), IDs pinned so the // streamed per-mech control mappings resolve: TriggerState (0x13) is what the // authored fire buttons bind (the controls push then writes the trigger state // straight into fireImpulse each frame). The pads bridge our chain gap // 2..0x11 (parents publish nothing yet) -- REQUIRED, because Build leaves // uncovered gap slots uninitialized; they target rechargeLevel, harmless if a // stray mapping ever binds one (BT411 shipped the same). Pip*/WeaponRange // pads-to-rechargeLevel where the member isn't reconstructed yet are noted. //############################################################################# // #define MECHWEAP_PAD(n, name) \ { (int)MechWeapon::MechWeaponPadFirstAttributeID + n, name, \ (Simulation::AttributePointer)&MechWeapon::rechargeLevel } const MechWeapon::IndexEntry MechWeapon::AttributePointers[]= { // // TWO pads now, not three: HeatSink grew ReportLeak, so // PoweredSubsystem::NextAttributeID moved 0x0F -> 0x10 and the bridge // to the binary-pinned PercentDone (0x12) is one slot shorter. // MECHWEAP_PAD( 0, "MechWeaponPad11"), ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12 ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13 ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14 ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15 ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16 { (int)MechWeapon::PipPositionAttributeID, "PipPosition", // 0x17 (pip (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // members { (int)MechWeapon::PipColorAttributeID, "PipColor", // 0x18 not yet (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // recon- { (int)MechWeapon::PipExtendedRangeAttributeID, "PipExtendedRange", // 0x19 structed) (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C }; MechWeapon::AttributeIndexSet MechWeapon::AttributeIndex( ELEMENTS(MechWeapon::AttributePointers), MechWeapon::AttributePointers, PoweredSubsystem::AttributeIndex ); MechWeapon::SharedData MechWeapon::DefaultData( MechWeapon::ClassDerivations, MechWeapon::MessageHandlers, MechWeapon::AttributeIndex, Subsystem::StateCount ); MechWeapon::MechWeapon( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data) { Check(owner); Check_Pointer(subsystem_resource); rechargeRate = subsystem_resource->rechargeRate; weaponRange = subsystem_resource->weaponRange; damageAmount = subsystem_resource->damageAmount; damageType = subsystem_resource->damageType; heatCostToFire = subsystem_resource->heatCostToFire; explosionResourceID = subsystem_resource->explosionModelFile; targetWithinRange = False; damageData.damageType = damageType; damageData.damageAmount = damageAmount; // // Fire-machine state: spawn fully charged and Loaded (ready to fire). // fireImpulse = 0.0f; previousFireImpulse = 0.0f; rechargeLevel = 1.0f; rangeToTarget = 0.0f; effectiveRange = weaponRange; estimatedReadyTime = 0; recoil = 0.0f; weaponAlarm.Initialize(WeaponStateCount); weaponAlarm.SetLevel(LoadedState); // // REAR-FIRING resolve (the binary ctor tail @004b99a8): a weapon is // rear-mounted when its MOUNT SEGMENT's site name carries the 'b' (back) // marker -- the back gun ports (sitelbgunport / siterbgunport) are the only // port names containing it. Rear-mounted weapons fire only into the // LOOK-BACK view; everything else fires forward. The look-state commit // (Mech::CommitLookState) re-arms viewFireEnable on every view change; a // weapon spawns armed for the forward view. // rearFiring = False; { EntitySegment *mount = owner->GetSegment(GetSegmentIndex()); if (mount != NULL) { const char *mount_name = mount->GetName(); if (mount_name != NULL && strstr(mount_name, "b") != NULL) { rearFiring = True; } } } viewFireEnable = (rearFiring == False); if (getenv("BT_MECH_LOG")) { EntitySegment *mount = owner->GetSegment(GetSegmentIndex()); DEBUG_STREAM << "[weap] '" << GetName() << "' seg=" << GetSegmentIndex() << " mount="; if (mount != NULL) { DEBUG_STREAM << mount->GetName(); } else { DEBUG_STREAM << ""; } DEBUG_STREAM << " rear=" << (int)rearFiring << endl << flush; } Check_Fpu(); } // //############################################################################# // DeathReset -- the weapon half of the respawn restore. //############################################################################# // void MechWeapon::DeathReset(Logical full_reset) { Check(this); // // The FULL powered-chain restore (base heal + thermal + electrical // re-init): a mech that died hot must NOT respawn with guns still at // FailureHeat (the hard gate + the jam roll read the carried // temperature). The electrical FSM re-enters Starting and lifts to // Ready on the next sim frame (startTimer holds startTime). // PoweredSubsystem::DeathReset(full_reset); fireImpulse = 0.0f; previousFireImpulse = 0.0f; rechargeLevel = 0.0f; recoil = 0.0f; rangeToTarget = 0.0f; targetWithinRange = False; weaponAlarm.SetLevel(LoadingState); } // //############################################################################# // The weapon-group config buttons (ids 9/10). STAGED: the binary bodies // (@004b9550/@004b95b8) drive the group-assign flow with the HUD config // page; until that page exists the press is latched (configureActivePress) // and logged -- the dispatch path itself is the live, authentic part. //############################################################################# // void MechWeapon::ConfigureMappablesMessageHandler( ReceiverDataMessageOf *message) { Check(this); configureActivePress = message->dataContents; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[btn] '" << GetName() << "' ConfigureMappables press=" << message->dataContents << " (staged latch)" << endl << flush; } } void MechWeapon::ChooseButtonMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[btn] '" << GetName() << "' ChooseButton press=" << message->dataContents << " (staged)" << endl << flush; } } MechWeapon::~MechWeapon() { } Logical MechWeapon::TestClass(Mech &) { return True; } // //############################################################################# // CheckFireEdge -- the rising-edge detector on fireImpulse (binary @004b9608). // TriggerState (attr 0x13) carries the raw ControlsButton INT bit-copied by the // streamed direct mapping (+(button+1) press / -(button+1) release), so the // release value reads as a negative NaN in float terms; the binary's x87 // compare effectively treated it as "released". Comparing the BIT PATTERNS as // signed ints is sign-correct for the button ints AND for genuine float values // (0.0f = 0x0, positives > 0, negatives/NaNs < 0), reproducing the binary's // behavior exactly. //############################################################################# // Logical MechWeapon::CheckFireEdge() { Check(this); int current = *(int *)&fireImpulse; int previous = *(int *)&previousFireImpulse; Logical edge = (current > 0 && previous <= 0) ? True : False; previousFireImpulse = fireImpulse; return edge; } // //############################################################################# // ComputeOutputVoltage -- the recharge-dial writer (binary @004b9c9c, vtable // slot the Emitter overrides with its own voltage form): // rechargeLevel = (rechargeRate - recoil) / rechargeRate // The Loading tick decays recoil from rechargeRate toward 0, so the dial rises // 0 -> 1 over the authored RechargeRate seconds. //############################################################################# // void MechWeapon::ComputeOutputVoltage() { Check(this); rechargeLevel = (rechargeRate - recoil) / rechargeRate; } Logical MechWeapon::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // FireWeapon Abstract -- each concrete weapon (Emitter/PPC, ProjectileWeapon/ // Gauss/MissileLauncher) overrides this. Reaching the base is a bug. //############################################################################# // void MechWeapon::FireWeapon() { Fail("MechWeapon::FireWeapon -- should not be here (abstract)"); } // //############################################################################# // GetMuzzlePoint (binary @004b9948) -- the mount segment's world-frame // position: segmentToEntity x localToWorld (GetSegmentToWorld), translation // extracted by transforming the segment-space origin. This is what makes // fire leave FROM THE GUN and follow the torso twist. //############################################################################# // void MechWeapon::GetMuzzlePoint(Point3D &point) { Check(this); point = owner->localOrigin.linearPosition; if (GetSegmentIndex() >= 0) { EntitySegment *segment = owner->GetSegment(GetSegmentIndex()); if (segment != NULL) { LinearMatrix to_world; owner->GetSegmentToWorld(*segment, &to_world); point.Multiply(Point3D(0.0f, 0.0f, 0.0f), to_world); } } } // //############################################################################# // 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 *target, Damage &damage) { Check(this); Check(target); int zone = -1; if (target->damageZoneCount > 0) { zone = Random(target->damageZoneCount); // // DEV hook BT_FORCE_ZONE=n: pin every hit to zone n (concentrated // fire -- exercises the zone-death cascade headlessly). // { const char *force_zone = getenv("BT_FORCE_ZONE"); if (force_zone != NULL) { int forced = atoi(force_zone); if (forced >= 0 && forced < target->damageZoneCount) { zone = forced; } } } } Entity::TakeDamageMessage hit( Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), (owner != NULL) ? owner->GetEntityID() : EntityID::Null, zone, damage ); target->Dispatch(&hit); // // The DAMAGE SCORE: post the type-0 ScoreInflicted to OUR pilot (the // authentic producer sits in the unexported master-perf writer; the // port shape posts per delivered hit). senderMechID = the VICTIM. // if (owner != NULL && owner->GetInstance() != Entity::ReplicantInstance && owner->GetPlayerLink() != NULL) { BTPlayer::ScoreMessage damage_score( BTPlayer::ScoreInflictedMessageID, sizeof(BTPlayer::ScoreMessage), 0.0f, BTPlayer::DamageInflictedScore, damage.damageAmount, target->GetEntityID() ); owner->GetPlayerLink()->Dispatch(&damage_score); } 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; } } // //############################################################################# // CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed. //############################################################################# // // //############################################################################# // CreateStreamedSubsystem -- binary @004b9d10 (1868 bytes). The weapon // record parser: chains the powered record, stamps size 0x1BC and the // weapon-base classID (0x0BCD), primes the weapon fields on the first pass, // then reads the ballistics/pip block. Required unless already set: // DamageAmount, HeatCostToFire, PipPosition, PipColor, PipExtendedRange, // RechargeRate, WeaponRange, DamageType, ExplosionModelFile. PipColor uses // the engine's own RGB parser (Convert_From_Ascii -- the BT411 donor's // sscanf was drift); the explosion resolves as a model FAMILY // (ModelListResourceType -- the streamed id is a family id, the same truth // the missile work established from the runtime side). //############################################################################# // 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( model_file, model_name, subsystem_name, subsystem_resource, subsystem_file, directories, passes ) ) { return False; } subsystem_resource->subsystemModelSize = 0x1BC; subsystem_resource->classID = (RegisteredClass::ClassID)RegisteredClass::ProjectileWeaponClassID; // ^ 0x0BCD -- the binary's weapon-base stamp; our enum spells the value // ProjectileWeaponClassID. Every concrete leaf (Emitter, launcher...) // overwrites it with its own id after this call. if (passes == 1) { subsystem_resource->rechargeRate = -1.0f; subsystem_resource->weaponRange = -1.0f; subsystem_resource->explosionModelFile = (ResourceDescription::ResourceID)-1; subsystem_resource->damageAmount = -1.0f; subsystem_resource->heatCostToFire = -1.0f; subsystem_resource->pipPosition = -1; subsystem_resource->pipColor[0] = -1.0f; subsystem_resource->pipColor[1] = -1.0f; subsystem_resource->pipColor[2] = -1.0f; subsystem_resource->pipExtendedRange = -1; } if (subsystem_resource->segmentIndex == -1 && passes > 1) { DEBUG_STREAM << subsystem_name << " missing SegmentPageName!" << endl; return False; } if ( !subsystem_file->GetEntry(subsystem_name, "DamageAmount", &subsystem_resource->damageAmount) && subsystem_resource->damageAmount == -1.0f ) { DEBUG_STREAM << model_name << " Missing DamageAmount" << endl; return False; } if ( !subsystem_file->GetEntry(subsystem_name, "HeatCostToFire", &subsystem_resource->heatCostToFire) && subsystem_resource->heatCostToFire == -1.0f ) { DEBUG_STREAM << subsystem_name << " missing HeatCostToFire !" << endl; return False; } if ( !subsystem_file->GetEntry(subsystem_name, "PipPosition", &subsystem_resource->pipPosition) && subsystem_resource->pipPosition == -1 ) { DEBUG_STREAM << subsystem_name << " missing PipPosition!" << endl; return False; } const char *pip_color_name = "Unspecified"; if ( !subsystem_file->GetEntry(subsystem_name, "PipColor", &pip_color_name) && subsystem_resource->pipColor[0] == -1.0f ) { DEBUG_STREAM << subsystem_name << " missing PipColor!" << endl; return False; } if (strcmp(pip_color_name, "Unspecified") != 0) { RGBColor pip_color; Convert_From_Ascii(pip_color_name, &pip_color); subsystem_resource->pipColor[0] = pip_color.Red; subsystem_resource->pipColor[1] = pip_color.Green; subsystem_resource->pipColor[2] = pip_color.Blue; } if ( !subsystem_file->GetEntry(subsystem_name, "PipExtendedRange", &subsystem_resource->pipExtendedRange) && subsystem_resource->pipExtendedRange == -1 ) { DEBUG_STREAM << subsystem_name << " missing PipExtendedRange!" << endl; return False; } if ( !subsystem_file->GetEntry(subsystem_name, "RechargeRate", &subsystem_resource->rechargeRate) && subsystem_resource->rechargeRate == -1.0f ) { DEBUG_STREAM << model_name << " Missing RechargeRate" << endl; return False; } if ( !subsystem_file->GetEntry(subsystem_name, "WeaponRange", &subsystem_resource->weaponRange) && subsystem_resource->weaponRange == -1.0f ) { DEBUG_STREAM << model_name << " Missing WeaponRange" << endl; return False; } const char *damage_type_name = "Unspecified"; if ( !subsystem_file->GetEntry(subsystem_name, "DamageType", &damage_type_name) && subsystem_resource->damageAmount == -1.0f ) { DEBUG_STREAM << model_name << " missing DamageType" << endl; return False; } if (strcmp(damage_type_name, "Unspecified") != 0) { if (strcmp(damage_type_name, "CollisionDamage") == 0) { subsystem_resource->damageType = Damage::CollisionDamageType; } else if (strcmp(damage_type_name, "BallisticDamage") == 0) { subsystem_resource->damageType = Damage::BallisticDamageType; } else if (strcmp(damage_type_name, "ExplosiveDamage") == 0) { subsystem_resource->damageType = Damage::ExplosiveDamageType; } else if (strcmp(damage_type_name, "LaserDamage") == 0) { subsystem_resource->damageType = Damage::LaserDamageType; } else if (strcmp(damage_type_name, "EnergyDamage") == 0) { subsystem_resource->damageType = Damage::EnergyDamageType; } else { DEBUG_STREAM << model_name << " has an unknown DamageType of " << damage_type_name << endl; return False; } } const char *explosion_model_name = "Unspecified"; if ( !subsystem_file->GetEntry(subsystem_name, "ExplosionModelFile", &explosion_model_name) && subsystem_resource->explosionModelFile == (ResourceDescription::ResourceID)-1 ) { DEBUG_STREAM << model_name << " missing ExplosionModelFile" << endl; return False; } ResourceDescription *explosion = NULL; if (strcmp(explosion_model_name, "Unspecified") != 0) { explosion = resource_file->FindResourceDescription( explosion_model_name, ResourceDescription::ModelListResourceType); } if ( explosion == NULL && subsystem_resource->explosionModelFile == (ResourceDescription::ResourceID)-1 ) { DEBUG_STREAM << model_name << " cannot find " << explosion_model_name << " in resource file!" << endl; return False; } if (explosion != NULL) { subsystem_resource->explosionModelFile = explosion->resourceID; } Check_Fpu(); return True; }