PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels. Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.
- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets. The mapper's
FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
[1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
field -> perpetual 0). FIX per "if it doesn't work, fix it": read the real
deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).
Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot). Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1206 lines
37 KiB
C++
1206 lines
37 KiB
C++
//===========================================================================//
|
|
// File: mechmppr.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Mech controls mapper -- maps pilot control inputs and view //
|
|
// selection onto the Mech's motion / torso / eyepoint demands //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? 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 mechmppr cluster (@004afbe0-@004b08c0); class / member /
|
|
// method names follow the Red Planet sibling RP\VTVMPPR.cpp (VTVControlsMapper)
|
|
// and the strings recovered from the .rdata class string pool. Each non-
|
|
// trivial method cites the originating @ADDR.
|
|
//
|
|
// Read-only globals / constants resolved from section_dump.txt:
|
|
// _DAT_004b0274 = 0x00000000 = 0.0f
|
|
// _DAT_004b0278 = 0x3f800000 = 1.0f
|
|
// _DAT_004b027c = 0x38d1b717 = 1.0e-4f (JM_CLOSE_ENOUGH)
|
|
// 0x40490fdb = PI (3.14159274f) (eyepoint yaw, "look behind")
|
|
// DAT_004e0f8c = EulerAngles::Identity (all zero)
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004ac530 Subsystem base constructor (8-arg)
|
|
// FUN_004ac868 Subsystem destructor
|
|
// FUN_0040385c Verify()/Fail(msg,file,line)
|
|
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
|
|
// FUN_00417ab4 SharedData::Resolve()
|
|
// FUN_00408644 Vector3D::Subtract(result, a, b)
|
|
// FUN_00408e90 EulerAngles::operator=(dst, src)
|
|
// FUN_0049fb54 Entity::IsDestroyed() -> Logical
|
|
// FUN_004022b0 Allocate(bytes) FUN_004022e8 Free(ptr)
|
|
// FUN_004022d0 operator delete FUN_004024d8 Resource::Release
|
|
// FUN_00403ad0 Group::FindGroup(root, name)
|
|
// FUN_00421414 CollectionIterator::CollectionIterator(it, collection)
|
|
// FUN_00421452 CollectionIterator::~CollectionIterator(it)
|
|
// FUN_004afacf ChildIterator::ChildIterator(it, list) (eyepoint list)
|
|
// FUN_004afb0d ChildIterator::~ChildIterator(it)
|
|
// FUN_004dcd00 fabsf()
|
|
// FUN_004bff74 VoiceAssist::Toggle() (lives in another module)
|
|
// DAT_004efc94 the global Application object
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MECHMPPR_HPP)
|
|
# include <mechmppr.hpp>
|
|
#endif
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(NTTMGR_HPP)
|
|
# include <nttmgr.hpp> // EntityManager / EntityGroup / FindGroup
|
|
#endif
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
#if !defined(PILOT_HPP)
|
|
# include <pilot.hpp>
|
|
#endif
|
|
#include <torso.hpp> // real Torso: analog aim axes @0x1F0/0x1F4, horizontalEnabled @0x250
|
|
#include <hud.hpp> // real HUD: freeAimSlew @0x28C
|
|
|
|
#define JM_CLOSE_ENOUGH 1.0e-4f // _DAT_004b027c
|
|
|
|
//
|
|
// Reconstruction helpers -- engine internals referenced by the decomp that
|
|
// have no direct analog in the surviving WinTesla MUNGA/MUNGA_L4 headers.
|
|
// (CROSS-FAMILY / engine gap -- see report.) Declared here so the recovered
|
|
// bodies compile; the linker will bind them to the real engine symbols.
|
|
//
|
|
// FUN_004bff74 toggles the voice-assist flag on the pilot subsystem.
|
|
// FUN_0049fb54 Entity::IsDestroyed() reached through an entity handle.
|
|
//
|
|
extern void ToggleVoiceAssist(int voice_assist_subsystem);
|
|
extern Logical Is_Destroyed(int entity_handle);
|
|
|
|
//
|
|
// Mech::GetHorizontalFiringReach -- the reachable horizontal firing half-arc
|
|
// (radians) the mech's torso can bring its guns to bear off dead-ahead. Weapons
|
|
// carry no arc field (the .SUB resources have none); the TORSO mount is what lets
|
|
// a mech point its guns to the side, so the authentic per-mech weapon traverse IS
|
|
// the torso's horizontal twist range (0 for a fixed torso like the Blackhawk).
|
|
// Defined here (not mech.cpp) because the cast to the full Torso type needs its
|
|
// header, which mech.cpp deliberately does not include (subsystem-stub collision).
|
|
//
|
|
Scalar
|
|
Mech::GetHorizontalFiringReach()
|
|
{
|
|
Torso *torso = (Torso *)GetTorsoSubsystem(); // @0x438 cache (real Torso or 0)
|
|
return (torso != 0) ? torso->GetHorizontalReach() : 0.0f;
|
|
}
|
|
|
|
//
|
|
// ChildIterator -- walks the eyepoint / camera child list hung off the torso
|
|
// articulation block (engine FUN_004afacf/FUN_004afb0d). The real type is a
|
|
// scene-graph DCS child iterator; the recovered InterpretControls() touches
|
|
// the list head as a raw address and the entries as raw int* records, so the
|
|
// iterator is reconstructed here as a minimal intrusive-list walk. BEST-EFFORT.
|
|
//
|
|
namespace {
|
|
struct ChildIterator {
|
|
int *node;
|
|
explicit ChildIterator(int list_head)
|
|
: node(*(int **)list_head)
|
|
{}
|
|
int *Next() {
|
|
int *current = node;
|
|
if (current != 0) {
|
|
node = *(int **)current; // next-link assumed at offset 0
|
|
}
|
|
return current;
|
|
}
|
|
};
|
|
}
|
|
|
|
//
|
|
// Owner (Mech) sub-object offsets touched by this mapper. The exact Mech
|
|
// layout is not recoverable from the pseudo-C; these accessors document the
|
|
// observed byte offsets and are flagged best-effort. A human should fold them
|
|
// back onto the real Mech / TorsoArticulation / Cockpit accessors.
|
|
//
|
|
// mech + 0x438 -> torso articulation block
|
|
// +0x220/+0x224 current torso yaw / pitch demand
|
|
// +0x228/+0x22c neutral (centered) yaw / pitch
|
|
// +0x230/+0x234 torso yaw / pitch travel limits
|
|
// +0x1f0 torso free-aim demand
|
|
// +0x274 torso auto-center flag
|
|
// +0x1f4 (500) torso pitch demand
|
|
// +0x250 free-aim-enabled flag
|
|
// +0x34c maximum yaw rate (also reverse scale)
|
|
// +0x534 minimum (high-speed) yaw rate
|
|
// +0x5c0 forward throttle scale
|
|
// +0x360 eyepoint rotation (EulerAngles)
|
|
// +0x378 eyepoint slave flag
|
|
// +0x410 eyepoint slave amount
|
|
// +0x564/+0x568/+0x56c/+0x570 look L/R/down/behind eyepoint pitches
|
|
// +0x7bc eyepoint / camera child list
|
|
// mech + 0x5b4 -> cockpit block (+0x28c torso free-aim demand, +0x2a0 flag)
|
|
//
|
|
|
|
//
|
|
// Pilot record offsets (entries of pilotArray):
|
|
// pilot + 0x100 world position (Vector3D)
|
|
// pilot + 0x1e0 pilot id (network ordinal)
|
|
// pilot + 0x1fc linked entity handle
|
|
// pilot + 0x284 current target handle (written on the LOCAL pilot)
|
|
//
|
|
|
|
|
|
// Player drive input, owned by the launcher (btbuild/btl4main.cpp). Consumed by
|
|
// the dev-box key bridge at the top of MechControlsMapper::InterpretControls.
|
|
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
|
|
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
|
|
extern BTDriveInput gBTDrive;
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// MechControlsMapper
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
MechControlsMapper::SharedData
|
|
MechControlsMapper::DefaultData(
|
|
MechControlsMapper::GetClassDerivations(),
|
|
MechControlsMapper::GetMessageHandlers(),
|
|
MechControlsMapper::GetAttributeIndex(),
|
|
MechControlsMapper::StateCount
|
|
);
|
|
|
|
Derivation*
|
|
MechControlsMapper::GetClassDerivations() // @0050ee10
|
|
{
|
|
static Derivation classDerivations(
|
|
Subsystem::GetClassDerivations(),
|
|
"MechControlsMapper" // @0050f173
|
|
);
|
|
return &classDerivations;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Messaging Support
|
|
//
|
|
// Message table @0050ee40. IDs 3..0x13 share the single ConfigureMappable
|
|
// handler; the last three drive the dedicated cycle / toggle handlers. The
|
|
// engine MESSAGE_ENTRY macro forces handler-name == message-name, so the rows
|
|
// that fan into the shared handler are written out explicitly.
|
|
//
|
|
#define MAPPABLE_ENTRY(message) \
|
|
{ \
|
|
MechControlsMapper::message##MessageID, \
|
|
#message, \
|
|
(Receiver::Handler) \
|
|
&MechControlsMapper::ConfigureMappableMessageHandler \
|
|
}
|
|
|
|
const MechControlsMapper::HandlerEntry
|
|
MechControlsMapper::MessageHandlerEntries[]=
|
|
{
|
|
MAPPABLE_ENTRY(Aux1Quad),
|
|
MAPPABLE_ENTRY(Aux1Eng1),
|
|
MAPPABLE_ENTRY(Aux1Eng2),
|
|
MAPPABLE_ENTRY(Aux1Eng3),
|
|
MAPPABLE_ENTRY(Aux1Eng4),
|
|
MAPPABLE_ENTRY(Aux2Quad),
|
|
MAPPABLE_ENTRY(Aux2Eng1),
|
|
MAPPABLE_ENTRY(Aux2Eng2),
|
|
MAPPABLE_ENTRY(Aux2Eng3),
|
|
MAPPABLE_ENTRY(Aux2Eng4),
|
|
MAPPABLE_ENTRY(Aux3Quad),
|
|
MAPPABLE_ENTRY(Aux3Eng1),
|
|
MAPPABLE_ENTRY(Aux3Eng2),
|
|
MAPPABLE_ENTRY(Aux3Eng3),
|
|
MAPPABLE_ENTRY(Aux3Eng4),
|
|
MAPPABLE_ENTRY(ZoomIn),
|
|
MAPPABLE_ENTRY(ZoomOut),
|
|
MESSAGE_ENTRY(MechControlsMapper, CycleControlMode),
|
|
MESSAGE_ENTRY(MechControlsMapper, CycleDisplayMode),
|
|
MESSAGE_ENTRY(MechControlsMapper, ToggleVoiceAssist)
|
|
};
|
|
|
|
#undef MAPPABLE_ENTRY
|
|
|
|
MechControlsMapper::MessageHandlerSet&
|
|
MechControlsMapper::GetMessageHandlers()
|
|
{
|
|
static MessageHandlerSet messageHandlers(
|
|
ELEMENTS(MechControlsMapper::MessageHandlerEntries),
|
|
MechControlsMapper::MessageHandlerEntries,
|
|
Subsystem::GetMessageHandlers()
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Attribute Support
|
|
//
|
|
// Attribute table @0050efd0. (Recorded offsets carry the engine scalar tag,
|
|
// e.g. 0x115 -> stickPosition @0x114.)
|
|
//
|
|
const MechControlsMapper::IndexEntry
|
|
MechControlsMapper::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust, reverseThrust), // 0x124
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand, speedDemand), // 0x128
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand, turnDemand), // 0x12C
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, LookForward, lookForward), // 0x130
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft, lookLeft), // 0x134
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, LookRight, lookRight), // 0x138
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind, lookBehind), // 0x13C
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, LookDown, lookDown), // 0x140
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp, torsoUp), // 0x144
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown, torsoDown), // 0x148
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft, torsoLeft), // 0x14C
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight, torsoRight), // 0x150
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter, torsoCenter), // 0x154
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode), // 0x190
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode), // 0x194
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage), // 0x158
|
|
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray) // 0x15C
|
|
};
|
|
|
|
MechControlsMapper::AttributeIndexSet&
|
|
MechControlsMapper::GetAttributeIndex()
|
|
{
|
|
static AttributeIndexSet attributeIndex(
|
|
ELEMENTS(MechControlsMapper::AttributePointers),
|
|
MechControlsMapper::AttributePointers,
|
|
Subsystem::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b02f0
|
|
//
|
|
MechControlsMapper::MechControlsMapper(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
Subsystem( // FUN_004ac530(...,0,0)
|
|
owner,
|
|
subsystem_ID,
|
|
subsystem_resource,
|
|
shared_data
|
|
)
|
|
{
|
|
Check(owner);
|
|
|
|
//
|
|
// This mapper is always the active "Performance" of its Mech; the engine
|
|
// installs the default InterpretControls member-pointer (vtable @0050f120)
|
|
// into the simulation slot (this[7..9]).
|
|
//
|
|
SetPerformance(&MechControlsMapper::InterpretControls);
|
|
|
|
//
|
|
// Clear all published control inputs / demands.
|
|
//
|
|
stickPosition.x = 0.0f;
|
|
stickPosition.y = 0.0f;
|
|
throttlePosition = 0.0f;
|
|
pedalsPosition = 0.0f;
|
|
speedDemand = 0.0f;
|
|
turnDemand = 0.0f;
|
|
reverseThrust = 0;
|
|
lookForward = 0;
|
|
lookLeft = 0;
|
|
lookRight = 0;
|
|
lookDown = 0;
|
|
lookBehind = 0;
|
|
lookState = LookNone;
|
|
|
|
torsoUp = 0;
|
|
torsoDown = 0;
|
|
torsoLeft = 0;
|
|
torsoRight = 0;
|
|
torsoCenter = 0;
|
|
|
|
controlMode = BasicMode;
|
|
displayMode = 0;
|
|
pilotArrayPage = 0;
|
|
|
|
//
|
|
// Recenter the torso articulation: current yaw/pitch <- neutral yaw/pitch.
|
|
//
|
|
// TODO(bring-up): the shipped code reads a torso-source pointer at the binary
|
|
// byte offset owner+0x438 (Mech::sinkSourceSubsystem) and copies that object's
|
|
// +0x228/+0x22c into +0x220/+0x224. Our RECONSTRUCTED Mech has a different
|
|
// object layout (sizeof != the binary's 0x854), so this raw offset yields a
|
|
// garbage pointer and faults. Skipped: the Mech ctor already identity-inits
|
|
// torsoAimCurrent/torsoAimTarget, so the torso starts centered. Re-enable
|
|
// via a named Mech accessor once the cross-module field layout is mapped.
|
|
{
|
|
int torso = *(int *)((int)owner + 0x438); // torso articulation block
|
|
if (torso != 0 && torso != (int)0xcdcdcdcd)
|
|
{
|
|
// (intentionally inert until the layout is mapped -- see note above)
|
|
}
|
|
(void)torso;
|
|
}
|
|
|
|
pilotArrayBuilt = False;
|
|
pilotIDs = 0;
|
|
pilotArray[0] = 0;
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b044c -- frees the pilot id table, then chains to ~Subsystem.
|
|
//
|
|
MechControlsMapper::~MechControlsMapper()
|
|
{
|
|
Check(this);
|
|
|
|
if (pilotIDs != 0)
|
|
{
|
|
delete [] pilotIDs; // FUN_004022e8 (Free of the id table)
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b08c0
|
|
//
|
|
Logical
|
|
MechControlsMapper::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50ee10)
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Message Handlers
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004afbc4 -- shared handler for the auxiliary-equipment and zoom buttons.
|
|
// The decomp body for this thunk was not captured; per the VTVControlsMapper
|
|
// pattern it routes the press through the configuration / mapping interface.
|
|
// BEST-EFFORT.
|
|
//
|
|
void
|
|
MechControlsMapper::ConfigureMappableMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
if (message->dataContents > 0)
|
|
{
|
|
// TODO: verify against @004afbc4 -- expected to add or erase a
|
|
// temporary mapping for the pressed mappable button.
|
|
AddOrErase(message->dataContents, (ControlsButton *)0);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004afbe0 -- cycle BasicMode -> StandardMode -> VeteranMode -> BasicMode.
|
|
// Each mode reconfigures the torso articulation: basic mode recenters and
|
|
// auto-centers the torso, the assisted modes free the torso to its limits.
|
|
//
|
|
void
|
|
MechControlsMapper::CycleControlModeMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
if (message->dataContents > 0)
|
|
{
|
|
controlMode = (ControlMode)(controlMode + 1);
|
|
if (controlMode > VeteranMode)
|
|
{
|
|
controlMode = BasicMode;
|
|
}
|
|
NotifyOfControlModeChange(controlMode); // vtable+0x48
|
|
|
|
Mech *mech = GetMech();
|
|
int torso = *(int *)((int)mech + 0x438);
|
|
int cockpit = *(int *)((int)mech + 0x5b4);
|
|
|
|
if (controlMode == BasicMode)
|
|
{
|
|
*(LWord *)(torso + 0x1f0) = 0; // no free aim
|
|
*(LWord *)(torso + 0x274) = 1; // auto-center on
|
|
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x228); // recenter yaw
|
|
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x22c); // recenter pitch
|
|
*(LWord *)(cockpit + 0x2a0) = 1;
|
|
}
|
|
else if ((unsigned)(controlMode - 1) < 2) // Standard / Veteran
|
|
{
|
|
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x230); // yaw -> limit
|
|
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x234); // pitch -> limit
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004afcac -- cycle the HUD display mode (0 -> 1 -> 2 -> 0).
|
|
//
|
|
void
|
|
MechControlsMapper::CycleDisplayModeMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
if (message->dataContents > 0)
|
|
{
|
|
displayMode = displayMode + 1;
|
|
if (displayMode > 2)
|
|
{
|
|
displayMode = 0;
|
|
}
|
|
NotifyOfDisplayModeChange(displayMode); // vtable+0x4c
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004afce8 -- toggle voice assist on the Mech's pilot subsystem.
|
|
//
|
|
void
|
|
MechControlsMapper::ToggleVoiceAssistMessageHandler(
|
|
ReceiverDataMessageOf<ControlsButton> *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
|
|
if (message->dataContents > 0)
|
|
{
|
|
// FUN_004bff74(mech->subsystem(0x190)) -- toggles the voice-assist flag.
|
|
ToggleVoiceAssist(*(int *)((int)GetMech() + 0x190));
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Model Support -- InterpretControls
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// Owner accessor. The mapper is always owned by a Mech; the decomp used the
|
|
// stored owner pointer directly (this[?]). Routed here through the engine's
|
|
// Subsystem::GetEntity().
|
|
//
|
|
Mech*
|
|
MechControlsMapper::GetMech()
|
|
{
|
|
Check(this);
|
|
return (Mech *)GetEntity();
|
|
}
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004afd10 -- the active master-Mech performance. Reads the raw stick,
|
|
// throttle, pedals and look buttons and writes the locomotion demands
|
|
// (speedDemand / turnDemand), the torso aim and the eyepoint pose. The
|
|
// per-control-mode steering/torso math is faithful to the decomp; the Mech
|
|
// sub-object offsets are documented at the top of this file. BEST-EFFORT on
|
|
// the precise Mech-field semantics.
|
|
//
|
|
void
|
|
MechControlsMapper::InterpretControls(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// OFFSET RECONCILIATION (the revival fix): the earlier draft read the owner
|
|
// at RAW binary offsets, including TYPED-POINTER arithmetic on Mech*
|
|
// ("*(Scalar *)(mech + 0x34c)" == mech + 0x34c*sizeof(Mech) -- a wild pointer,
|
|
// the AV that kept this tick bypassed). Every owner access now goes through
|
|
// the reconciled members: mech+0x34c -> reverseStrideLength (naming caveat:
|
|
// LoadLocomotionClips measures it from the rr* clips -- it is the TOP/run
|
|
// cycle speed the throttle scales), mech+0x534 -> walkStrideLength,
|
|
// mech+0x5c0 -> forwardThrottleScale, mech+0x438 -> sinkSourceSubsystem (the
|
|
// re-based binary-exact Torso), mech+0x5b4 -> hudSubsystem (the HUD -- raw
|
|
// factory part_012.c:10164; MechTech's 0x104 alloc cannot hold +0x28c).
|
|
// Torso/HUD writes go through their real members (analog axes @0x1F0/0x1F4,
|
|
// horizontalEnabled @0x250, freeAimSlew @0x28C). Null-guards are bring-up
|
|
// safety for mechs without those subsystems (the binary trusts the data).
|
|
//
|
|
Mech *mech = GetMech();
|
|
Torso *torso = (Torso *)mech->GetTorsoSubsystem(); // @0x438 (raw iVar5)
|
|
HUD *cockpit= (HUD *)mech->GetHudSubsystem(); // @0x5b4 (raw iVar3)
|
|
|
|
//
|
|
// BRING-UP INPUT BRIDGE (dev box; env BT_KEY_BRIDGE=0 to disable on pods).
|
|
// The engine controls push refreshes the registered input attributes from
|
|
// DEVICE elements every frame BEFORE this Performance runs; on a keyboard
|
|
// dev box the RIO scalar throttle channel doesn't exist and its element
|
|
// reads a constant (observed: throttlePosition forced back to 1.0 each
|
|
// frame -> the mech could never stop). So key state is written HERE --
|
|
// after the push, immediately before interpretation -- making the keyboard
|
|
// authoritative on the dev box. Interpretation below stays 100% authentic.
|
|
//
|
|
{
|
|
static const int s_keyBridge =
|
|
(getenv("BT_KEY_BRIDGE") == 0 || *getenv("BT_KEY_BRIDGE") != '0');
|
|
if (s_keyBridge && mech != 0 && application != 0
|
|
&& (Entity *)mech == application->GetViewpointEntity())
|
|
{
|
|
float key_throttle = gBTDrive.forced
|
|
? gBTDrive.forcedThrottle : gBTDrive.throttle;
|
|
float key_turn = gBTDrive.forced ? 0.0f : gBTDrive.turn;
|
|
// Headless harness (forced mode only): BT_FORCE_TURN holds a steering
|
|
// demand; BT_FORCE_SECONDS releases the forced throttle after n
|
|
// sim-seconds while KEEPING the turn (the turn-in-place repro).
|
|
if (gBTDrive.forced)
|
|
{
|
|
static float s_hClock = 0.0f, s_hLimit = -1.0f, s_hTurn = -999.0f;
|
|
if (s_hLimit < 0.0f)
|
|
{
|
|
const char *fs = getenv("BT_FORCE_SECONDS");
|
|
s_hLimit = fs ? (float)atof(fs) : 0.0f;
|
|
}
|
|
if (s_hTurn < -900.0f)
|
|
{
|
|
const char *ft = getenv("BT_FORCE_TURN");
|
|
s_hTurn = ft ? (float)atof(ft) : 0.0f;
|
|
}
|
|
key_turn = s_hTurn;
|
|
// BT_GOTO beeline (DEBUG harness): the turn demand is computed in
|
|
// mech4.cpp's drive block (where position/heading are in scope)
|
|
// and published through gBTGotoTurn/gBTGotoActive.
|
|
{
|
|
extern int gBTGotoActive;
|
|
extern float gBTGotoTurn;
|
|
extern float gBTGotoThrottle;
|
|
if (gBTGotoActive)
|
|
{
|
|
key_turn = gBTGotoTurn;
|
|
key_throttle = key_throttle * gBTGotoThrottle;
|
|
}
|
|
}
|
|
// BT_FORCE_FLIP=<t>: invert the forced throttle after t sim-seconds
|
|
// (the mid-stride direction-change repro).
|
|
static float s_hFlip = -1.0f;
|
|
if (s_hFlip < 0.0f)
|
|
{
|
|
const char *ff = getenv("BT_FORCE_FLIP");
|
|
s_hFlip = ff ? (float)atof(ff) : 0.0f;
|
|
}
|
|
if (s_hLimit > 0.0f || s_hFlip > 0.0f)
|
|
{
|
|
s_hClock += time_slice;
|
|
if (s_hFlip > 0.0f && s_hClock > s_hFlip)
|
|
{
|
|
key_throttle = -key_throttle;
|
|
}
|
|
else if (s_hLimit > 0.0f && s_hClock > s_hLimit)
|
|
{
|
|
key_throttle = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
|
|
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
|
|
stickPosition.x = key_turn;
|
|
stickPosition.y = 0.0f;
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Throttle -> forward speed demand (reverse thrust inverts & rescales)
|
|
//------------------------------------------------------------------
|
|
//
|
|
// CANARY HEAL: something stomps mech->forwardThrottleScale at runtime
|
|
// nondeterministically (observed 0.14 and -1.0 across runs; ctor sets 1.0
|
|
// guarded) -- a wild raw-offset write, hunt via ba w4 on mech+0x5c0 (on the
|
|
// ledger). Until the writer is caught, restore a sane value and log.
|
|
// (band tightened: the stomp value 0.14 -- observed repeatedly, ~8 degrees
|
|
// in radians, likely an angle write landing on the wrong member -- passed
|
|
// the old 0.01..100 sanity band and the user's forward speed silently
|
|
// capped at 8.6 u/s while reverse ran full. The ctor guard currently
|
|
// always yields exactly 1.0, so heal anything else.)
|
|
if (mech->forwardThrottleScale != 1.0f)
|
|
{
|
|
DEBUG_STREAM << "[mppr] forwardThrottleScale STOMPED to "
|
|
<< mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush;
|
|
mech->forwardThrottleScale = 1.0f;
|
|
}
|
|
if (reverseThrust < 1)
|
|
{
|
|
speedDemand =
|
|
mech->reverseStrideLength * throttlePosition * mech->forwardThrottleScale;
|
|
}
|
|
else
|
|
{
|
|
speedDemand = -mech->reverseStrideLength * throttlePosition;
|
|
}
|
|
{
|
|
static int s_cTrace = -1;
|
|
if (s_cTrace < 0) { const char *e = getenv("BT_MPPR_TRACE"); s_cTrace = (e && *e != '0') ? 1 : 0; }
|
|
if (s_cTrace)
|
|
{
|
|
static float s_cAcc = 0.0f; s_cAcc += time_slice;
|
|
if (s_cAcc >= 0.5f) { s_cAcc = 0.0f;
|
|
DEBUG_STREAM << "[mppr-c] thr=" << throttlePosition << " rev=" << reverseThrust
|
|
<< " topSpd=" << mech->reverseStrideLength
|
|
<< " fScale=" << mech->forwardThrottleScale
|
|
<< " -> dmd=" << speedDemand << "\n" << std::flush; }
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Square the stick for a soft response; cube the pedals. Preserve sign.
|
|
//------------------------------------------------------------------
|
|
//
|
|
Scalar stick_x = stickPosition.x * stickPosition.x;
|
|
Scalar stick_y = stickPosition.y * stickPosition.y;
|
|
Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition;
|
|
if (stickPosition.x < 0.0f) stick_x = -stick_x; // _DAT_004b0274 == 0.0f
|
|
if (stickPosition.y < 0.0f) stick_y = -stick_y;
|
|
|
|
turnDemand = 0.0f;
|
|
|
|
if (controlMode == BasicMode)
|
|
{
|
|
//
|
|
// Basic: stick yaw drives the turn directly; stick pitch drives the
|
|
// torso pitch. The achievable turn rate is clamped down with speed.
|
|
//
|
|
turnDemand = stick_x;
|
|
if (torso)
|
|
{
|
|
torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 (500)
|
|
torso->SetAnalogTwistAxis(0.0f); // raw torso+0x1f0
|
|
}
|
|
if (cockpit)
|
|
{
|
|
cockpit->SetFreeAimSlew(0.0f); // raw cockpit+0x28c
|
|
}
|
|
|
|
Scalar max_turn =
|
|
(mech->reverseStrideLength - mech->walkStrideLength)
|
|
* (1.0f - Abs(turnDemand)) // _DAT_004b0278 == 1.0f
|
|
+ mech->walkStrideLength;
|
|
|
|
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH) // _DAT_004b027c
|
|
{
|
|
Clamp(speedDemand, -max_turn, max_turn);
|
|
}
|
|
}
|
|
else if (controlMode == StandardMode)
|
|
{
|
|
//
|
|
// Standard: torso free-aim follows the stick yaw; pedals provide the
|
|
// turn. Turn rate clamped as in basic mode.
|
|
//
|
|
if (torso == 0 || !torso->GetHorizontalEnabled()) // raw torso+0x250 == 0
|
|
{
|
|
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // raw cockpit+0x28c
|
|
}
|
|
else
|
|
{
|
|
torso->SetAnalogTwistAxis(stick_x); // raw torso+0x1f0
|
|
}
|
|
if (torso) torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4
|
|
|
|
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
|
|
|
|
Scalar max_turn =
|
|
(mech->reverseStrideLength - mech->walkStrideLength)
|
|
* (1.0f - Abs(turnDemand))
|
|
+ mech->walkStrideLength;
|
|
|
|
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH)
|
|
{
|
|
Clamp(speedDemand, -max_turn, max_turn);
|
|
}
|
|
}
|
|
else if (controlMode == VeteranMode)
|
|
{
|
|
//
|
|
// Veteran: as standard, but no speed-dependent turn clamp.
|
|
//
|
|
if (torso == 0 || !torso->GetHorizontalEnabled())
|
|
{
|
|
if (cockpit) cockpit->SetFreeAimSlew(stick_x);
|
|
}
|
|
else
|
|
{
|
|
torso->SetAnalogTwistAxis(stick_x);
|
|
}
|
|
if (torso) torso->SetAnalogElevationAxis(stick_y);
|
|
|
|
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Look / eyepoint selection. Choose a look direction from the buttons;
|
|
// when it changes, re-aim the eyepoint and re-slave the camera children.
|
|
//------------------------------------------------------------------
|
|
//
|
|
previousLookState = lookState;
|
|
if (lookLeft > 0) lookState = LookLeftState;
|
|
else if (lookRight > 0) lookState = LookRightState;
|
|
else if (lookBehind > 0) lookState = LookBehindState;
|
|
else if (lookDown > 0) lookState = LookDownState;
|
|
else lookState = LookNone;
|
|
|
|
if (lookState != previousLookState)
|
|
{
|
|
// TODO(look-eyepoint): the commit block below writes the eyepoint pose at
|
|
// binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is
|
|
// claimed by mechName, +0x410 by stateFlags, +0x7bc by poweredSubsystems --
|
|
// and the old draft even routed them through the TORSO pointer). Arbitrate
|
|
// those labels against the raw before enabling; the port has no eyepoint
|
|
// consumer yet. Look-state SELECTION above stays live (this-only).
|
|
DEBUG_STREAM << "[mppr] look state -> " << lookState
|
|
<< " (eyepoint commit deferred)\n" << std::flush;
|
|
}
|
|
else if (0) // DEFERRED body (kept for the reconstruction record)
|
|
{
|
|
const int torso = 0; // shadow: the raw-offset text below predates
|
|
// the reconciliation (see TODO above)
|
|
EulerAngles eyepoint;
|
|
ChildIterator cameras(torso + 0x7bc); // FUN_004afacf
|
|
|
|
switch (lookState)
|
|
{
|
|
case LookNone:
|
|
eyepoint = EulerAngles::Identity; // DAT_004e0f8c
|
|
*(LWord *)(torso + 0x378) = 1; // re-slave eyepoint
|
|
for (int *camera; (camera = cameras.Next()) != 0; )
|
|
{
|
|
*(LWord *)(camera + 0x3e0) = (*(int *)(camera + 0x334) == 0);
|
|
}
|
|
break;
|
|
|
|
case LookLeftState:
|
|
eyepoint = EulerAngles::Identity;
|
|
eyepoint.pitch = *(Scalar *)(torso + 0x564);
|
|
*(LWord *)(torso + 0x378) = 0;
|
|
for (int *camera; (camera = cameras.Next()) != 0; )
|
|
{
|
|
*(LWord *)(camera + 0x3e0) = 0;
|
|
}
|
|
break;
|
|
|
|
case LookRightState:
|
|
eyepoint = EulerAngles::Identity;
|
|
eyepoint.pitch = *(Scalar *)(torso + 0x568);
|
|
*(LWord *)(torso + 0x378) = 0;
|
|
for (int *camera; (camera = cameras.Next()) != 0; )
|
|
{
|
|
*(LWord *)(camera + 0x3e0) = 0;
|
|
}
|
|
break;
|
|
|
|
case LookBehindState:
|
|
eyepoint = EulerAngles::Identity;
|
|
eyepoint.yaw = PI; // 0x40490fdb
|
|
eyepoint.pitch = *(Scalar *)(torso + 0x570);
|
|
*(LWord *)(torso + 0x378) = *(LWord *)(torso + 0x410);
|
|
for (int *camera; (camera = cameras.Next()) != 0; )
|
|
{
|
|
*(LWord *)(camera + 0x3e0) = *(int *)(camera + 0x334);
|
|
}
|
|
break;
|
|
|
|
case LookDownState:
|
|
eyepoint = EulerAngles::Identity;
|
|
eyepoint.pitch = *(Scalar *)(torso + 0x56c);
|
|
*(LWord *)(torso + 0x378) = 0;
|
|
for (int *camera; (camera = cameras.Next()) != 0; )
|
|
{
|
|
*(LWord *)(camera + 0x3e0) = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
*(EulerAngles *)(torso + 0x360) = eyepoint; // commit eyepoint rotation
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Pilot-array (other-player roster) management
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b0600 -- build the pilot roster once, lazily, from the application's
|
|
// "Players" group (skipping spectators flagged 0x40).
|
|
//
|
|
void
|
|
MechControlsMapper::BuildPilotArray()
|
|
{
|
|
if (pilotArrayBuilt)
|
|
{
|
|
return;
|
|
}
|
|
pilotArrayBuilt = True;
|
|
|
|
pilotCount = 0;
|
|
EntityGroup *players =
|
|
application->GetEntityManager()->FindGroup("Players"); // @0050f44b
|
|
|
|
if (players != 0)
|
|
{
|
|
ChainIteratorOf<Node*> pilots(players->groupMembers); // FUN_00421414
|
|
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
|
|
{
|
|
if ((*(byte *)((int)entry + 0x29) & 0x40) == 0) // not a spectator
|
|
{
|
|
++pilotCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
pilotIDs = new int[pilotCount]; // FUN_004022b0
|
|
|
|
for (int i = 0; i <= pilotCount; ++i)
|
|
{
|
|
pilotArray[i] = 0;
|
|
}
|
|
|
|
FillPilotArray(); // FUN_004b06cc
|
|
ChooseDefaultPilot(); // FUN_004b07f0
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b06cc -- slot 0 holds the local pilot; the remaining slots are filled
|
|
// from the "Players" group (paged by pilotArrayPage).
|
|
//
|
|
void
|
|
MechControlsMapper::FillPilotArray()
|
|
{
|
|
for (int i = 0; i < pilotCount; ++i)
|
|
{
|
|
pilotIDs[i] = -1;
|
|
}
|
|
|
|
//
|
|
// The local "station" (the local network player record) lives at
|
|
// application+0x6c in the binary; its pilot roster entry at +0x190.
|
|
// ENGINE DRIFT FIX (the AV that kept the mapper tick bypassed): the WinTesla
|
|
// Application exposes the local player through GetMissionPlayer() (APP.h:169)
|
|
// -- the same reconciliation SendFakeButtonEvent uses (this file's sibling in
|
|
// btl4mppr.cpp). The old draft read the 1995 byte offsets through the 2007
|
|
// object -> wild "pilot" pointer -> access violation.
|
|
//
|
|
Player *local_pilot =
|
|
(application != 0) ? application->GetMissionPlayer() : 0;
|
|
if (local_pilot == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
pilotArray[0] = (Pilot *)local_pilot;
|
|
pilotIDs[0] = local_pilot->GetEntityID(); // raw read pilot+0x1e0 (the entity id)
|
|
|
|
EntityGroup *players =
|
|
application->GetEntityManager()->FindGroup("Players");
|
|
if (players != 0)
|
|
{
|
|
ChainIteratorOf<Node*> pilots(players->groupMembers);
|
|
|
|
// skip earlier pages
|
|
int skip = pilotArrayPage * pilotCount;
|
|
while (skip > 0 && pilots.ReadAndNext() != 0)
|
|
{
|
|
--skip;
|
|
}
|
|
|
|
int slot = 1;
|
|
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
|
|
{
|
|
// TODO(mp-roster): the binary skips spectators via a flag byte at
|
|
// entry+0x29 (bit 0x40) -- its WinTesla home is unmapped; solo has no
|
|
// spectators, so the check is omitted until the flag is reconciled.
|
|
if (entry == (Node *)local_pilot) // already slot 0
|
|
{
|
|
continue;
|
|
}
|
|
pilotArray[slot] = (Pilot *)entry;
|
|
pilotIDs[slot] = ((Entity *)entry)->GetEntityID(); // raw entry+0x1e0
|
|
if (++slot >= pilotCount)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b07f0 -- pick the initial target for the local pilot: the next pilot id
|
|
// after the local one (or the first non-self pilot).
|
|
//
|
|
void
|
|
MechControlsMapper::ChooseDefaultPilot()
|
|
{
|
|
int target = 0;
|
|
|
|
if (pilotCount != 1)
|
|
{
|
|
int index = 0;
|
|
|
|
if (pilotCount == pilotIDs[0])
|
|
{
|
|
for (int i = 1; i < pilotCount; ++i)
|
|
{
|
|
if (pilotIDs[i] != 1)
|
|
{
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 1; i < pilotCount; ++i)
|
|
{
|
|
if (pilotIDs[i] == pilotIDs[0] + 1)
|
|
{
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Pilot *chosen = pilotArray[index];
|
|
if (chosen != 0)
|
|
{
|
|
target = *(int *)((int)chosen + 0x1fc);
|
|
}
|
|
*(int *)((int)pilotArray[0] + 0x284) = target;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b049c -- set the local pilot's current target to the pilot on the given
|
|
// roster page.
|
|
//
|
|
void
|
|
MechControlsMapper::UpdateCurrentPilot(int page)
|
|
{
|
|
if (pilotArrayBuilt && pilotArray[0] != 0)
|
|
{
|
|
int target = 0;
|
|
Pilot *chosen = pilotArray[page];
|
|
if (chosen != 0)
|
|
{
|
|
target = *(int *)((int)chosen + 0x1fc);
|
|
}
|
|
*(int *)((int)pilotArray[0] + 0x284) = target;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b04d8 -- choose the nearest living pilot (other than self) as the local
|
|
// pilot's current target.
|
|
//
|
|
void
|
|
MechControlsMapper::ChooseNearestPilot(int self_id)
|
|
{
|
|
if (!pilotArrayBuilt || pilotArray[0] == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int target = 0;
|
|
Scalar nearest_distance = 0.0f;
|
|
int nearest_index = 0;
|
|
Logical none_yet = True;
|
|
|
|
for (int i = 1; i < pilotCount; ++i)
|
|
{
|
|
int entity = *(int *)((int)pilotArray[i] + 0x1fc);
|
|
Vector3D delta;
|
|
delta.Subtract( // FUN_00408644
|
|
*(Vector3D *)((int)pilotArray[i] + 0x100),
|
|
*(Vector3D *)((int)pilotArray[0] + 0x100)
|
|
);
|
|
Scalar distance = delta.x * delta.x + delta.y * delta.y + delta.z * delta.z;
|
|
|
|
if (none_yet)
|
|
{
|
|
if (!Is_Destroyed(entity) && entity != self_id)
|
|
{
|
|
none_yet = False;
|
|
nearest_distance = distance;
|
|
nearest_index = i;
|
|
}
|
|
}
|
|
else if (!Is_Destroyed(entity) && entity != self_id
|
|
&& distance < nearest_distance)
|
|
{
|
|
nearest_distance = distance;
|
|
nearest_index = i;
|
|
}
|
|
}
|
|
|
|
if (!none_yet)
|
|
{
|
|
Pilot *chosen = pilotArray[nearest_index];
|
|
if (chosen != 0)
|
|
{
|
|
target = *(int *)((int)chosen + 0x1fc);
|
|
}
|
|
*(int *)((int)pilotArray[0] + 0x284) = target;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// @004b0898 -- bounds-checked roster accessor.
|
|
//
|
|
Pilot *
|
|
MechControlsMapper::GetPilot(int index)
|
|
{
|
|
if (index >= pilotCount || index < 0)
|
|
{
|
|
return 0;
|
|
}
|
|
return pilotArray[index];
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Configuration / event-mapping interface (secondary vtable @0050f498)
|
|
//
|
|
// These are "not overridden" defaults: a platform-specific derived mapper is
|
|
// expected to supply real implementations. The base class traps the call.
|
|
//
|
|
|
|
//
|
|
// @004b029c
|
|
//
|
|
void
|
|
MechControlsMapper::ExitConfiguration()
|
|
{
|
|
Fail("ExitConfiguration not overridden!\n"); // MECHMPPR.CPP:0x245
|
|
}
|
|
|
|
//
|
|
// (companion to the above; "EnterConfiguration not overridden!" @0050f370)
|
|
//
|
|
void
|
|
MechControlsMapper::EnterConfiguration(
|
|
ControlsButton * /*destination*/,
|
|
Receiver * /*receiver*/,
|
|
Receiver::MessageID /*activation_message_id*/,
|
|
Receiver::MessageID /*configuration_message_id*/
|
|
)
|
|
{
|
|
Fail("EnterConfiguration not overridden!\n");
|
|
}
|
|
|
|
//
|
|
// @004b02b8
|
|
//
|
|
void
|
|
MechControlsMapper::CreateTemporaryEventMappings(
|
|
Receiver * /*receiver*/,
|
|
Receiver::MessageID /*config_message_id*/
|
|
)
|
|
{
|
|
Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x24f
|
|
}
|
|
|
|
//
|
|
// @004b02d4
|
|
//
|
|
void
|
|
MechControlsMapper::AddOrErase(
|
|
unsigned int /*button_ID*/,
|
|
ControlsButton * /*destination*/
|
|
)
|
|
{
|
|
Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x25a
|
|
}
|
|
|
|
//
|
|
// @004b048c -- vtable+0x48; defaults to a no-op (override for HUD feedback).
|
|
//
|
|
void
|
|
MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/)
|
|
{
|
|
}
|
|
|
|
//
|
|
// @004b0494 -- vtable+0x4c; defaults to a no-op.
|
|
//
|
|
void
|
|
MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/)
|
|
{
|
|
}
|
|
|
|
//
|
|
// gauge wave P2: BTResolveRosterPilot -- the bridge PilotList (btl4gau3.cpp, the
|
|
// Comm KILLS/DEATHS roster) uses to read the viewpoint mech's pilot roster. The
|
|
// roster lives on the mech's ControlsMapper (subsystemArray[0]); this complete-Mech
|
|
// TU resolves it, PilotList reads the returned pilot at raw BTPlayer offsets.
|
|
// Faithful to the binary's Execute: **(App+0x6c mech +0x128)[slot] -> GetPilot.
|
|
//
|
|
void *BTResolveRosterPilot(int slot)
|
|
{
|
|
if (application == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
Mech *mech = (Mech *)application->GetViewpointEntity(); // App+0x6c local mech
|
|
if (mech == 0 || mech->GetSubsystemCount() < 1)
|
|
{
|
|
return 0;
|
|
}
|
|
MechControlsMapper *mapper = (MechControlsMapper *)mech->GetSubsystem(0); // subsystemArray[0]
|
|
if (mapper == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
return (void *)mapper->GetPilot(slot); // FUN_004b0898
|
|
}
|