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>
459 lines
16 KiB
C++
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();
|
|
}
|