Camera seat LIVE + Mech::PlayerLinkMessageHandler reconstructed (@0049f624)

The spectator/broadcast seat works end-to-end: a hostType=1 +
vehicle=camera pilot page boots a CameraShip whose 62-camera arena1
network loads from the BTL4.RES type-27 resource (the engine's
CreateStreamedCameraInstances existed all along -- the file probe falls
back to it), the BTCameraDirector locks onto the first live mech, and
the ship TRACKS it (sees=1, goal advancing) and CUTS between authored
camera positions (screenshot-verified: wide arena shot -> close
tracking shot).

The missing piece was the Mech override of PlayerLinkMessageHandler
(@0049f624), which the reconstruction never filled.  The engine base
resolves mech->player; BT's override adds:
  1. the REVERSE link player->playerVehicle = this on EVERY node
     (replicants included) -- how the camera director and scoreboard
     find a REMOTE player's mech; without it a spectator parks forever
     ('NO goal entity')
  2. clears NonScoringPlayerFlag (0x4000 = bit 14): a pilot with a
     vehicle is a SCORING player -- this admits REPLICATED players to
     the ranking pass, so cross-node rank/score displays work
  3. master only: seeds the heat bank's ambientTemperature (bank
     @0x1d4) from the mission [mission] temperature= (BTMission+0xf4)
     -- correcting the heat family's 'frozen 300' deviation note (it
     was never frozen; THIS is the writer)

Bridge BTSetBankAmbientTemperature lives in heatfamily_reslice.cpp
(mech.cpp cannot include subsystem headers -- local-stub collision).
BT_CAM_LOG diagnostics: camera-network count, director pick + Players
group census, ship follow state, link dispatch/receive.

Verified live: 2-seat (mech + camera) relay session -- replicated
player shows +veh, director goal locked w/ 30s timer, ship tracking a
moving mech through camera cuts; standard 2-node mech smoke PASS
(un-regressed).  Lesson re-learned: a 'clean' build filtered on 'error
C' missed a linker file-lock failure -- one whole test cycle ran
against a stale exe (the /FORCE gotcha's cousin; grep -i error, not
error C).

Remaining (task open): ranking-window overlay draw (L4VIDRND stubs),
operator-app camera-seat row, shot polish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 16:10:29 -05:00
co-authored by Claude Fable 5
parent 74d9d3b6ff
commit 4459262b9c
9 changed files with 469 additions and 5 deletions
+33
View File
@@ -247,6 +247,39 @@ void
}
}
// BT port diag (BT_CAM_LOG, 1 Hz): why is/isn't the director tracking
if (getenv("BT_CAM_LOG"))
{
static float s_acc = 0.0f; s_acc += time_slice;
if (s_acc >= 1.0f)
{
s_acc = 0.0f;
DEBUG_STREAM << "[cam] director pick: player=" << (void*)player
<< " vehicle=" << (void*)vehicle
<< " goal=" << (void*)GetGoalEntity()
<< " timer=" << timeLeftOnPlayer;
// who IS in the Players group here?
EntityGroup *pg =
application->GetEntityManager()->FindGroup("Players");
if (pg)
{
DEBUG_STREAM << " group:[";
Player *p;
ChainIteratorOf<Node*> it(pg->groupMembers);
while ((p = (Player *)it.ReadAndNext()) != NULL)
{
DEBUG_STREAM << " bmp=" << p->playerBitmapIndex
<< (p->GetPlayerVehicle() ? "+veh" : "-veh");
}
DEBUG_STREAM << " ]";
}
else
{
DEBUG_STREAM << " group:MISSING";
}
DEBUG_STREAM << "\n" << std::flush;
}
}
if (player == 0 || vehicle == 0)
{
return;
+4
View File
@@ -1074,6 +1074,10 @@ void
playerVehicle->Dispatch(&player_link_message);
playerVehicle->DispatchToReplicants(&player_link_message);
if (getenv("BT_CAM_LOG"))
DEBUG_STREAM << "[cam] InitializePlayerLink dispatched (player "
<< GetEntityID() << " -> vehicle "
<< playerVehicle->GetEntityID() << ")\n" << std::flush;
Check_Fpu();
}
+17
View File
@@ -765,6 +765,23 @@ AggregateHeatSink::AttributeIndexSet&
return attributeIndex;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BTSetBankAmbientTemperature -- bridge for Mech::PlayerLinkMessageHandler
// (@0049f624 writes bank+0x1d4 = mission temperature): mech.cpp cannot see
// AggregateHeatSink (local-stub collision), so the write lands here where the
// type is complete. This is the AUTHENTIC ambientTemperature writer -- the
// old "frozen 300" note stands corrected (the ctor default only holds until
// the mission's PlayerLink pass runs on the master).
//
void
BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature)
{
if (bank != NULL)
{
((AggregateHeatSink *)bank)->ambientTemperature = temperature;
}
}
//#############################################################################
// Shared Data Support
//
+53
View File
@@ -80,6 +80,12 @@
// for the same reason.
#include "dmgtable.hpp"
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
extern void BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature);
#if !defined(BTMSSN_HPP)
# include <btmssn.hpp> // BTMission::GetMissionTemperature (PlayerLink)
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
@@ -454,6 +460,7 @@ const Receiver::HandlerEntry
Mech::MessageHandlerEntries[] =
{
MESSAGE_ENTRY(Mech, TakeDamage),
MESSAGE_ENTRY(Mech, PlayerLink),
};
Receiver::MessageHandlerSet
@@ -609,6 +616,52 @@ Scalar
return tmpl ? tmpl->maxY : 0.0f; // ResolveHit guards <= 0
}
//
// Mech override of Entity::PlayerLinkMessageHandler (binary @0049f624). The
// engine base resolves the FORWARD link (mech->playerLink @0x190 from the
// message's playerID); BT then:
// 1. sets the REVERSE link player->playerVehicle = this -- on EVERY node,
// replicants included. This is how the 1995 camera director
// (BeABTDirector's GetPlayerByIndex -> GetPlayerVehicle) and the
// scoreboard find a REMOTE player's mech; without it a spectator seat
// parks forever ("no goal entity", camera-seat bring-up 2026-07-18).
// 2. clears the player's NonScoringPlayerFlag (0x4000 == bit 14): a pilot
// with a vehicle is a SCORING player -- this is what admits them to the
// ranking pass (Player::IsScoringPlayer gates RankPlayers).
// 3. master only: seeds the heat bank's ambientTemperature (@0x1d4 on the
// 0xBBE AggregateHeatSink at mech+0x7dc) from the mission's [mission]
// temperature (BTMission+0xf4) -- resolving the heat family's
// "frozen 300" deviation note: it was never frozen, THIS writes it.
//
void
Mech::PlayerLinkMessageHandler(PlayerLinkMessage *message)
{
Check(message);
Entity::PlayerLinkMessageHandler(message); // FUN_0041fd18: mech+0x190
Player *owner = GetPlayerLink();
if (getenv("BT_CAM_LOG"))
DEBUG_STREAM << "[cam] Mech::PlayerLink inst="
<< (int)(GetInstance() == ReplicantInstance)
<< " owner=" << (void*)owner << "\n" << std::flush;
if (owner != NULL)
{
owner->SetPlayerVehicle(this); // player+0x1fc = this
owner->simulationFlags &= ~Player::NonScoringPlayerFlag; // &= ~0x4000
if (GetInstance() != ReplicantInstance && sensorSubsystem != NULL)
{
Mission *mission = owner->GetMission();
if (mission != NULL)
{
BTSetBankAmbientTemperature(sensorSubsystem,
((BTMission *)mission)->GetMissionTemperature());
}
}
}
Check_Fpu();
}
//
// Mech override of Entity::TakeDamageMessageHandler (binary @0x4a037a, the two
// call sites into the glue @0x49ed0c). An unaimed hit arrives with
+8
View File
@@ -1165,6 +1165,14 @@ protected:
// Mech override of Entity::TakeDamageMessageHandler: resolve an unaimed
// (invalidDamageZone) hit's zone via the cylinder table, then base-route.
void TakeDamageMessageHandler(TakeDamageMessage *message);
// Mech override of Entity::PlayerLinkMessageHandler (@0049f624): the
// base resolves mech->player; BT adds the REVERSE link
// player->playerVehicle = this on EVERY node (replicants included --
// this is how the camera director / scoreboard find a remote player's
// mech), clears the player's NonScoringPlayerFlag (a pilot with a
// vehicle SCORES/ranks), and on the master seeds the heat bank's
// ambientTemperature from the mission's [mission] temperature.
void PlayerLinkMessageHandler(PlayerLinkMessage *message);
// --- damage-routing support (mechdmg / mech4) -----------------------
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;