Files
BT411/game/reconstructed/btl4mssn.cpp
arcattackandClaude Fable 5 abaa145b6f Cockpit: authentic eyepoint + canopy lattice render (task #55)
The first-person cockpit now shows by default and matches gameplay footage
(verified on the Madcat). Two decomp-verified reconstructions:

EYEPOINT (FUN_004579a8 + caller part_014.c:5525, FUN_004c22c4):
- eye offset = siteeyepoint GetBaseOffset (not GetSegmentToEntity, not the
  upright hack), parented on the PARENT segment DCS (btl4vid.cpp)
- live view = affine INVERSE of the eye world matrix (L4VIDRND Execute) --
  replaces the hand-rolled LookAtRH whose +Z-forward/+Y-up row guess aimed
  some mechs into the canopy; combine order fixed to baseOffset * R
- chase camera basis + aim boresight re-expressed for the inverse convention
  (row2 = back); boresight now derives from the view matrix (the stale rows
  aimed the pick ray at the sky = no target, no discharge)

CANOPY (*_COP.BGF, dpl_Punchize geogroups):
- every shell is an open strut lattice (38-59% boundary edges, all 12 mechs);
  rendered single-sided with per-face INWARD winding (l/r torso patches are
  mirrored, so no global winding works) the struts are the dark frame and the
  openings show the world
- frame colour = the 'softer' ramp near its dark end (texture-less unlit ramp;
  BLXSKIN.BMF has no texture -- the old "punch texels" theory was wrong)
- scoped by filename (meshIsCop); wrong drop-punch + view-cut paths removed
- BT_FORCE_MODEL=<name> forces the player mech for per-mech bring-up

KB: new context/cockpit-view.md topic; punch split corrected in bgf-format;
gotchas 14 (LookAt axis guess) + 15 (per-patch edge namespaces); env gates +
eye/punch addresses in decomp-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:46:54 -05:00

459 lines
16 KiB
C++

//===========================================================================//
// File: btl4mssn.cpp //
// Project: BattleTech Brick: //
// Contents: Implementation for the L4 (game-level) mission //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 04/07/95 ECH Initial coding. //
// 09/12/95 JM Derived from L4Mission //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary BTL4OPT.EXE. The only non-trivial
// method, SetPlayerData, is recovered in full from the Ghidra pseudo-C at
// @004d2c30 (the lone function the linker attributed to bt_l4/btl4mssn.cpp
// via its embedded assert path "d:\tesla_bt\bt_l4\BTL4MSSN.CPP"). The
// constructor / destructor were recovered from the adjacent code cluster
// (@004d2be0 ctor, @004d2c04 deleting-dtor) and from the BTL4Mission vtable
// @0051e870 = { dtor@004d2c04, SetPlayerData@004d2c30 }. TestInstance is a
// debug-only consistency check; it is absent from the OPT-build vtable
// (inlined to nothing) and is reconstructed best-effort to mirror the
// surviving Red-Planet analog RPL4Mission::TestInstance.
//
// Ground-truth interfaces consulted (all survived in source):
// BTL4MSSN.HPP - this class (BTL4Mission : public BTMission)
// BTMSSN.HPP/CPP - the BTMission base (member layout + SetPlayerData stub)
// SCNROLE.HPP - ScenarioRole (per-role damage modifiers)
// Structural analog (Red Planet): RP_L4/RPL4MSSN.cpp / .h.
//
// Member-offset map (this == BTMission*, observed byte offsets):
// +0x20 colorName char* "color" (Mission base)
// +0x24 badgeName char* "badge" (Mission base)
// +0x28 dropZoneName char* "dropzone" (Mission base)
// +0x2c gameModel char* "vehicle" (Mission base)
// +0x3c dropZoneHost Logical "loadzones" (Mission base)
// +0x4c playerBitmapIndex int "bitmapindex" (Mission base)
// +0x50 scenarioRoles locked dictionary <CString -> ScenarioRole*>
// +0x68 (name-bitmap table; GetLargeNameBitmap)
// +0xe4 experienceLevel Enumeration (BTMission)
// +0xe8 roleName CString "role" (BTMission)
// +0xec teamName CString (see note) (BTMission)
// +0xf0 advancedDamageOn Logical "advancedDamage" (BTMission)
// +0xf4 missionTemperature Scalar "temperature" (BTMission, set in ctor)
// +0xf8 patchName CString "patch" (BTMission)
//
// Engine-helper name mapping (FUN_ -> intended symbol):
// FUN_0049b54c BTMission::BTMission(notation_file, resources) [base ctor]
// FUN_0049b648 BTMission::~BTMission (deleting thunk)
// FUN_0049b6bc BTMission::SetPlayerData (Fail stub - "should never reach")
// DAT_004efc94 the global Application*
// FUN_00404088 NotationFile::GetEntry(node, key, const char**) -> Logical
// FUN_004040d8 NotationFile::GetEntry(node, key, int*/Logical*) -> Logical
// FUN_00404118 NotationFile::GetEntry(node, key, Scalar*) -> Logical
// FUN_00402300 strdup (+ Register_Pointer)
// FUN_00402a98 CString::operator=(const char*)
// FUN_004023f4 / FUN_00402460 CString constructors
// FUN_004024d8 CString release (ref-count)
// FUN_00402298 operator new (Make_Pointer)
// FUN_00429a9c ScenarioRole::ScenarioRole(role_name, model_file)
// FUN_004323c7 / FUN_00432405 dictionary Locker acquire / release
// FUN_004d4b58 strcmp
// FUN_004dbb24 DEBUG_STREAM << (stream insert)
// FUN_004d9c38 std::flush / endl on DEBUG_STREAM
// FUN_004036a0 PostQuitMessage / Exit(AbortExitCodeID)
// FUN_0040385c Verify()/Fail(msg, file, line)
// DAT_00524e20 / DAT_00524e68 the two debug streams (info / error)
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(BTL4MSSN_HPP)
# include <btl4mssn.hpp>
#endif
#if !defined(BTL4APP_HPP)
# include <btl4app.hpp>
#endif
#if !defined(L4GREND_HPP)
# include <l4grend.hpp>
#endif
#if !defined(L4HOST_HPP)
# include <l4host.hpp>
#endif
#if !defined(HOSTMGR_HPP)
# include <hostmgr.hpp>
#endif
#if !defined(NOTATION_HPP)
# include <notation.hpp>
#endif
#if !defined(SCNROLE_HPP)
# include <scnrole.hpp>
#endif
//##########################################################################
//########################### BTL4Mission ############################
//##########################################################################
//
//#############################################################################
// @004d2be0 -- chains to BTMission(notation_file, resources) (FUN_0049b54c),
// then stamps the BTL4Mission vtable (&0051e870). Adds no data members of
// its own (the surviving header has an empty class body); the base ctor reads
// "mission"/"temperature" and seeds experienceLevel / advancedDamageOn.
//#############################################################################
//
BTL4Mission::BTL4Mission(
NotationFile *notation_file,
ResourceFile *resources
):
BTMission(notation_file, resources)
{
Check_Fpu();
}
//
//#############################################################################
// @004d2c04 (deleting-destructor thunk; chains to ~BTMission @0049b648).
// The author-level body is the trivial debug check, exactly as in the
// Red-Planet analog ~RPL4Mission.
//#############################################################################
//
BTL4Mission::~BTL4Mission()
{
Check(this);
Check_Fpu();
}
//
//#############################################################################
// Best-effort: not present in the OPT-build vtable @0051e870 (only the dtor
// and SetPlayerData slots survive), so the body was inlined away. Mirrors
// RPL4Mission::TestInstance -- defer to the base and report healthy.
//#############################################################################
//
Logical
BTL4Mission::TestInstance() const
{
BTMission::TestInstance();
return True;
}
//
//#############################################################################
// @004d2c30 -- the override of the BTMission::SetPlayerData "should never
// reach" stub (BTMSSN.CPP / FUN_0049b6bc). Pulls this host's player page
// out of the notation file and populates the mission's player description:
// model, dropzone, color, badge, patch, experience level, advanced-damage
// flag, scenario role (+ its damage modifiers), and the plasma-display badge
// bitmap. Occupies vtable slot 1 (it is virtual via BTMission, though the
// surviving headers do not repeat the `virtual` keyword).
//#############################################################################
//
void
BTL4Mission::SetPlayerData(NotationFile *notation_file)
{
Check(this);
//
//--------------------------------------------------------------
// Figure out which page we should be using to create the player
//--------------------------------------------------------------
//
Check(application);
HostManager *host_manager = application->GetHostManager();
Check(host_manager);
L4Host *host = (L4Host*)host_manager->GetLocalHost();
Check(host);
CString
player_node = host->GetSymbolicName();
//
//----------------------------------------------------------
// Copy the game model and dropzone strings into the mission
//----------------------------------------------------------
//
gameModel = NULL; // @+0x2c
const char *player_model = NULL;
notation_file->GetEntry(player_node, "vehicle", &player_model);
// DIAGNOSTIC: force a specific player mech model (e.g. BT_FORCE_MODEL=madcat) so we can
// bring up / verify a given cockpit shell in-game with its own authentic eyepoint.
{
const char *force = getenv("BT_FORCE_MODEL");
if (force && force[0]) { player_model = force; }
}
if (!player_model)
{
DEBUG_STREAM << "Error: vehicle not specified!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
Check_Pointer(player_model);
gameModel = strdup(player_model);
Register_Pointer(gameModel);
dropZoneName = NULL; // @+0x28
const char *drop_zone = NULL;
notation_file->GetEntry(player_node, "dropzone", &drop_zone);
if (!drop_zone)
{
DEBUG_STREAM << "Error: dropzone not specified!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
Check_Pointer(drop_zone);
dropZoneName = strdup(drop_zone);
Register_Pointer(dropZoneName);
dropZoneHost = False; // @+0x3c
notation_file->GetEntry(player_node, "loadzones", &dropZoneHost);
//
//----------------------------------------------------------
// Color
//----------------------------------------------------------
//
colorName = NULL; // @+0x20
const char *color_name = NULL;
notation_file->GetEntry(player_node, "color", &color_name);
if (!color_name)
{
DEBUG_STREAM << "Error: color not specified!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
Check_Pointer(color_name);
colorName = strdup(color_name);
Register_Pointer(colorName);
//
//----------------------------------------------------------
// Patch (CString member, @+0xf8)
//----------------------------------------------------------
//
const char *patch_name = NULL;
notation_file->GetEntry(player_node, "patch", &patch_name);
if (!patch_name)
{
DEBUG_STREAM << "Error: patch not specified!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
Check_Pointer(patch_name);
patchName = patch_name;
//
//----------------------------------------------------------
// Badge (legacy char* member, @+0x24)
//----------------------------------------------------------
//
badgeName = NULL; // @+0x24
const char *badge_name = NULL;
notation_file->GetEntry(player_node, "badge", &badge_name);
if (!badge_name)
{
DEBUG_STREAM << "Error: badge not specified!\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
}
Check_Pointer(badge_name);
badgeName = strdup(badge_name);
Register_Pointer(badgeName);
//
//----------------------------------------------------------
// teamName (CString, @+0xec). NOTE: the shipped binary reads the
// "badge" key here as well (a second, distinct "badge" string
// literal is present in .rdata) and defaults to "DEFAULT" when the
// page omits it. Faithful to BTL4OPT.EXE @004d2c30.
//----------------------------------------------------------
//
const char *team_name = NULL;
if (!notation_file->GetEntry(player_node, "badge", &team_name))
{
teamName = "DEFAULT";
}
else
{
Check_Pointer(team_name);
teamName = team_name;
}
//
//----------------------------------------------------------
// Experience level (@experienceLevel +0xe4). Absence is tolerated
// only for the "camera" page; "Unspecified" leaves the default.
//----------------------------------------------------------
//
const char *experience = "Unspecified";
if (!notation_file->GetEntry(player_node, "experience", &experience))
{
if (strcmp(gameModel, "camera") != 0)
{
DEBUG_STREAM << player_node << " page missing expereince!" << std::endl;
Fail("Exiting", "d:\\tesla_bt\\bt_l4\\BTL4MSSN.CPP", 240);
}
}
if (strcmp("Unspecified", experience) != 0)
{
if (strcmp(experience, "novice") == 0)
{
experienceLevel = BTMission::NoviceMode;
}
else if (strcmp(experience, "standard") == 0)
{
experienceLevel = BTMission::StandardMode;
}
else if (strcmp(experience, "veteran") == 0)
{
experienceLevel = BTMission::VeteranMode;
}
else if (strcmp(experience, "expert") == 0)
{
experienceLevel = BTMission::ExpertMode;
}
else
{
DEBUG_STREAM << player_node
<< " has an unknown experience field of "
<< experience;
Fail("Exiting", "d:\\tesla_bt\\bt_l4\\BTL4MSSN.CPP", 264);
}
}
//
//----------------------------------------------------------
// Advanced damage flag (@advancedDamageOn +0xf0)
//----------------------------------------------------------
//
Logical advanced_damage = False;
if (notation_file->GetEntry(player_node, "advancedDamage", &advanced_damage))
{
advancedDamageOn = advanced_damage ? True : False;
}
//
//----------------------------------------------------------
// Scenario role (@roleName +0xe8) -- every player page must name a
// role, and the role page must name a model. The ScenarioRole is
// looked up (and created on first use) in the mission's role
// dictionary (this+0x50), then its per-page score modifiers applied.
//----------------------------------------------------------
//
const char *role_node = NULL;
if (!notation_file->GetEntry(player_node, "role", &role_node))
{
DEBUG_STREAM << "Each Player page must have a role field !" << std::endl;
Fail("Exiting", "d:\\tesla_bt\\bt_l4\\BTL4MSSN.CPP", 451);
}
else
{
Check_Pointer(role_node);
roleName = role_node;
const char *role_model = NULL;
if (!notation_file->GetEntry(role_node, "model", &role_model))
{
DEBUG_STREAM << role_node << " page or model field missing !" << std::endl;
Fail("Exiting", "d:\\tesla_bt\\bt_l4\\BTL4MSSN.CPP", 445);
}
else
{
//
// Find-or-create the ScenarioRole keyed by roleName. The DOS
// binary kept the per-mission role dictionary inline (this+0x50)
// and called dictionary.Find()/.Add() directly; the WinTesla
// Mission base exposes that dictionary (scenarioRoleChain) through
// GetScenarioRole()/AddScenarioRole() -- the latter already guards
// against duplicate role names, so the inner Find() is folded in.
//
ScenarioRole *role = GetScenarioRole(roleName);
if (role == NULL)
{
role = new ScenarioRole(roleName, CString(role_model));
Register_Object(role);
AddScenarioRole(role);
}
//
// Optional per-page score modifiers.
//
Scalar value;
if (notation_file->GetEntry(role_node, "dmgInflctdMdfr", &value))
{
role->SetDamageInflictedModifier(value); // ScenarioRole +0x0c
}
if (notation_file->GetEntry(role_node, "dmgRcvdMdfr", &value))
{
role->SetDamageReceivedModifier(value); // ScenarioRole +0x10
}
if (notation_file->GetEntry(role_node, "dmgBias", &value))
{
role->SetDamageBias(value); // ScenarioRole +0x18
}
if (notation_file->GetEntry(role_node, "friendlyFire", &value))
{
role->SetFriendlyFirePenalty(value); // ScenarioRole +0x14
}
int return_from_death;
if (notation_file->GetEntry(role_node, "return", &return_from_death))
{
role->SetReturnFromDeath(return_from_death); // ScenarioRole +0x28
}
if (notation_file->GetEntry(role_node, "killBonus", &value))
{
role->SetKillBonus(value); // ScenarioRole +0x1c
}
}
}
//
//~~~~~~~~~~~~~~~~~~~~~~
// Get playerBitmapIndex (@+0x4c) and, if present, blit the badge
// bitmap onto the external (plasma) display.
//~~~~~~~~~~~~~~~~~~~~~~
//
if (!notation_file->GetEntry(player_node, "bitmapindex", &playerBitmapIndex))
{
DEBUG_STREAM << "No Bitmap for player" << player_node;
DEBUG_STREAM << std::flush;
}
else
{
BitMap *bitmap = GetLargeNameBitmap(playerBitmapIndex);
if (bitmap != NULL)
{
Check(application);
L4GaugeRenderer
*gauge_renderer = ((BTL4Application*)application)->
GetGaugeRenderer();
if (gauge_renderer != NULL)
{
Check(gauge_renderer);
GraphicsDisplay *graphics_display =
gauge_renderer->GetExternalDisplay();
if (graphics_display != NULL)
{
graphics_display->DrawBitMapOpaque(
1, // foreground color
0, // background color
0xFF, // bitmask
GraphicsDisplay::Replace, // operation
0, // rotation
0, 0, // position
bitmap,
0, // sx1
0, // sy1
bitmap->Data.Size.x - 1, // sx2
bitmap->Data.Size.y - 1 // sy2
);
graphics_display->Update(True);
graphics_display->WaitForUpdate();
}
}
}
}
Check_Fpu();
}