//===========================================================================// // File: btl4pb.cpp // // Project: BattleTech Brick: BattleTech LBE Application // // Contents: BTL4PlaybackApplication -- the LBE mission-review / playback // // application, and BTSpoolerTask, the background task that pumps // // recorded NetworkPackets out of a SpoolFile back through the // // network manager so a finished battle can be re-watched. // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary BTL4OPT.EXE. The class declarations // are VERBATIM from the surviving header BTL4PB.HPP; the method bodies are // recovered from the Ghidra pseudo-C for the 0x4d3b74-0x4d3f44 cluster // (all/part_014.c) and cross-checked against the direct Red Planet analog // RP_L4\RPL4PB.cpp (RPSpoolerTask / RPL4PlaybackApplication), which is a line- // for-line structural twin. // // The object file maps onto these recovered functions: // // BTSpoolerTask --------------------------------- vtable @0051f204 // @004d3b74 BTSpoolerTask::BTSpoolerTask (ctor) // @004d3bb8 BTSpoolerTask::~BTSpoolerTask (scalar-deleting dtor) // @004d3b90 BTSpoolerTask::DispatchPacket // @004d3ba8 BTSpoolerTask::GetTimeBias (16-byte tail-call thunk) // // BTL4PlaybackApplication ----------------------- vtable @0051f1a4 // @004d3be4 BTL4PlaybackApplication::BTL4PlaybackApplication (ctor) // @004d3ea0 BTL4PlaybackApplication::~BTL4PlaybackApplication (dtor) // @004d40d8 BTL4PlaybackApplication::TestInstance (out of OPT vtable) // @004d3c14 BTL4PlaybackApplication::MakeNetworkManager // @004d3c34 BTL4PlaybackApplication::Initialize // @004d3c44 BTL4PlaybackApplication::LoadBackgroundTasks // @004d3d40 BTL4PlaybackApplication::ExecuteForeground // @004d3ecc BTL4PlaybackApplication::DispatchPacket // @004d3ef8 BTL4PlaybackApplication::RunMissionMessageHandler // @004d3f44 BTL4PlaybackApplication::StopMissionMessageHandler // (ExecuteBackgroundTask is inherited from L4Application -- the OPT build // did not emit a BT override; see note at that method.) // // Engine-helper name mapping (FUN_ / DAT_ -> intended symbol): // FUN_0047c5d8 SpoolerTask::SpoolerTask (base ctor) // FUN_0047c734 SpoolerTask::~SpoolerTask (base dtor) // FUN_004d34c4 BTL4Application::BTL4Application (base ctor, btl4app.cpp) // FUN_004d350c BTL4Application::~BTL4Application (base dtor) // FUN_004d3a2c BTL4Application::RunMissionMessageHandler // FUN_004d3a94 BTL4Application::StopMissionMessageHandler // FUN_0047b468 L4Application::InitializeTillConsole // FUN_0047bfc4 L4PlaybackNetworkManager::L4PlaybackNetworkManager (0x28) // FUN_00402298 operator new (Make_Pointer) // FUN_004022d0 operator delete // FUN_0044de1c TaskManager::AddTask(task) (backgroundTasks@+0x64) // FUN_0044dd08 ApplicationTask::ApplicationTask(priority) // FUN_0041d7f0 NetworkManager::GetNetworkClientPointer(clientID) // FUN_00414b60 Now() -> Time* // FUN_0044f850 SpoolFile::NextPacket // FUN_0044e13c Application::GetApplicationManager // FUN_0044fac0 MissionReviewApplicationManager::ReleaseSpoolFile // FUN_00403ad0 Group::FindGroup(root, name) // FUN_0042efe4 CameraDirector::FindPlayerByRank(rank) // FUN_00421414/00421452 ChainIteratorOf ctor/dtor // FUN_0049fb74 Mech::Reset(origin, MissionReviewReset) // FUN_00419cb8 Entity::FlushEvents // FUN_00436668/004364e4 rebuild/replace name bitmap entry // FUN_00460840 DPLRenderer::SortAndReloadNameBitmaps // FUN_0045a7a8 DPLRenderer::SetViewAngle // FUN_0041a1a4 IsDerivedFrom(classDerivations) // DAT_004efc94 the global Application* // DAT_004efc98 Exit_Code // DAT_004fd550 the global mission-review mode (== 2 -> interactive review) // // Observed Application sub-object byte offsets (this == Application*): // +0x18 spoolFile (inherited L4Application member; SetSpoolFile) // +0x20 networkManager // +0x24 entityManager // +0x34 updateManager // +0x38 rendererManager // +0x3c controlsManager // +0x40 intercomManager // +0x48 videoRenderer (DPLRenderer) // +0x58 secondsRemainingInGame +0x5c gameStarted // +0x64 backgroundTasks // +0x70 executeFrames +0x88 applicationState // +0xc8 currentMission +0xd4 timeBias (BTL4PlaybackApplication) // #include #pragma hdrstop #if !defined(BTL4PB_HPP) # include #endif #if !defined(L4VIDEO_HPP) # include #endif #if !defined(MISSION_HPP) # include #endif #if !defined(L4NET_HPP) # include #endif #if !defined(UPDATE_HPP) # include #endif #if !defined(RENDERER_HPP) # include #endif #if !defined(ICOM_HPP) # include #endif #if !defined(DROPZONE_HPP) # include #endif #if !defined(DIRECTOR_HPP) # include #endif #if !defined(NTTMGR_HPP) # include // EntityManager / EntityGroup / FindGroup #endif #if !defined(MECH_HPP) # include #endif //############################################################################# //########################## BTSpoolerTask ############################## //############################################################################# // //############################################################################# // @004d3b74 -- chains to SpoolerTask() (FUN_0047c5d8) then stamps the // BTSpoolerTask vtable (&0051f204). Adds no data of its own. //############################################################################# // BTSpoolerTask::BTSpoolerTask() { } // //############################################################################# // @004d3bb8 -- scalar-deleting destructor; reinstalls the vtable and chains // to ~SpoolerTask (FUN_0047c734). //############################################################################# // BTSpoolerTask::~BTSpoolerTask() { Check_Fpu(); } // //############################################################################# // @004d3b90 -- the spooler's per-packet callback simply forwards to the // running playback application (the global Application*), exactly as the Red // Planet RPSpoolerTask::DispatchPacket does. //############################################################################# // void BTSpoolerTask::DispatchPacket(NetworkPacket *packet) { BTL4PlaybackApplication *playback_app = Cast_Object(BTL4PlaybackApplication*, application); playback_app->DispatchPacket(packet); Check_Fpu(); } // //############################################################################# // @004d3ba8 -- 16-byte tail-call thunk: returns the application's playback // time bias so the spooler can re-time recorded packets. //############################################################################# // long BTSpoolerTask::GetTimeBias() { BTL4PlaybackApplication *playback_app = Cast_Object(BTL4PlaybackApplication*, application); return playback_app->GetTimeBias(); } //############################################################################# //####################### BTL4PlaybackApplication ####################### //############################################################################# //############################################################################# // Message Support // const Receiver::HandlerEntry BTL4PlaybackApplication::MessageHandlerEntries[]= { MESSAGE_ENTRY(BTL4PlaybackApplication, RunMission), // handler @004d3ef8 MESSAGE_ENTRY(BTL4PlaybackApplication, StopMission) // handler @004d3f44 }; Receiver::MessageHandlerSet& BTL4PlaybackApplication::GetMessageHandlers() { static Receiver::MessageHandlerSet messageHandlers( ELEMENTS(BTL4PlaybackApplication::MessageHandlerEntries), BTL4PlaybackApplication::MessageHandlerEntries, BTL4Application::GetMessageHandlers() // inherited Application set ); return messageHandlers; } //############################################################################# // Shared Data Support // Derivation* BTL4PlaybackApplication::GetClassDerivations() // @0051ee2c { static Derivation classDerivations( BTL4Application::GetClassDerivations(), "BTL4PlaybackApplication" // @0051f182 ); return &classDerivations; } BTL4PlaybackApplication::SharedData BTL4PlaybackApplication::DefaultData( // @0051ee5c BTL4PlaybackApplication::GetClassDerivations(), BTL4PlaybackApplication::GetMessageHandlers() ); // //############################################################################# // BTL4PlaybackApplication @004d3be4 //############################################################################# // // Base call FUN_004d34c4(this, resource, 0x3eb, DefaultData) in the DOS binary: // BattleTech's BTL4Application ctor there took (ResourceFile*, ClassID, // SharedData&) -- the DOS build carried no HINSTANCE/HWND. 0x3eb is the // L4ApplicationClassID. For the WinTesla port the L4Application base ctor // requires (HINSTANCE, HWND, ...), so those are threaded through here exactly // as RPL4PlaybackApplication does. The spool file is stashed in the inherited // spoolFile slot (this+0x18). // BTL4PlaybackApplication::BTL4PlaybackApplication( HINSTANCE hInstance, HWND hWnd, ResourceFile *resource_file, SpoolFile *spool_file ): BTL4Application(hInstance, hWnd, resource_file, L4ApplicationClassID, DefaultData) { // vtable installed by the compiler == &PTR_FUN_0051f1a4 spoolFile = spool_file; // this[6] == this+0x18 Check_Fpu(); } // //############################################################################# // ~BTL4PlaybackApplication @004d3ea0 //############################################################################# // BTL4PlaybackApplication::~BTL4PlaybackApplication() { // scalar-deleting dtor; reinstalls vtable and chains to ~BTL4Application Check_Fpu(); } // //############################################################################# // MakeNetworkManager @004d3c14 //############################################################################# // // new(0x28)/FUN_0047bfc4 -- identical to RP: the playback application drives a // L4PlaybackNetworkManager rather than a live one. // NetworkManager* BTL4PlaybackApplication::MakeNetworkManager() { Check_Fpu(); return new L4PlaybackNetworkManager; } // //############################################################################# // Initialize @004d3c34 //############################################################################# // void BTL4PlaybackApplication::Initialize() { Check(this); L4Application::InitializeTillConsole(); // FUN_0047b468 } // //############################################################################# // LoadBackgroundTasks @004d3c44 //############################################################################# // // Adds the playback task pipeline. The first task is our own BTSpoolerTask // (new/FUN_004d3b74); the remaining four are stock ApplicationTasks built with // priority 0x48 and the engine task vtables observed in the decomp. (In the // shipped binary the trailing tasks are only distinguished by their vtable // pointers; the RP analog names them ProcessEventTask, GaugeRendererTask, // CompleteCyclesTask, FryDeathRowTask, AudioRendererTask -- BT emits five of // these stock tasks plus the spooler. Names below are best-effort against RP.) // void BTL4PlaybackApplication::LoadBackgroundTasks() { Check(this); ApplicationTask *application_task; application_task = new BTSpoolerTask; // FUN_004d3b74 Register_Object(application_task); backgroundTasks->AddTask(application_task); // FUN_0044de1c(this+0x64,..) application_task = new ProcessEventTask; // vtable &PTR_FUN_004f037c Register_Object(application_task); backgroundTasks->AddTask(application_task); application_task = new GaugeRendererTask; // vtable &PTR_FUN_004f031c Register_Object(application_task); backgroundTasks->AddTask(application_task); application_task = new CompleteCyclesTask; // vtable &PTR_FUN_004f0304 Register_Object(application_task); backgroundTasks->AddTask(application_task); application_task = new AudioRendererTask; // vtable &PTR_FUN_004f0364 Register_Object(application_task); backgroundTasks->AddTask(application_task); Check_Fpu(); } // //############################################################################# // ExecuteForeground @004d3d40 //############################################################################# // // The mission-review frame loop. Structurally the RP ExecuteForeground: // when idle/not executing it merely services the video renderer; otherwise it // runs the controls manager (only in interactive review, DAT_004fd550==2), the // update manager, the interest/renderer managers, the intercom, and -- when // actually running a mission (state==5) -- recomputes the remaining game time. // Logical BTL4PlaybackApplication::ExecuteForeground( Time start_of_frame, Scalar frame_duration ) { SET_FOREGROUND_PROCESSING(); Check(this); // // executeFrames(+0x70)==0, or state(+0x88)==WaitingForEgg(1): idle. // if (!executeFrames || GetApplicationState() == WaitingForEgg) { videoRenderer->ExecuteIdle(); CLEAR_FOREGROUND_PROCESSING(); return executeFrames && !Exit_Code; // DAT_004efc98 } Time frame_ticks; frame_ticks = frame_duration; // Time::operator=(Scalar) // // Controls only matter in interactive ("scrub") review mode. // if (GetMissionReviewMode() == 2) // DAT_004fd550 { Check(controlsManager); controlsManager->Execute(); // vtbl+0x18 } // // Update manager -> interest manager -> renderer manager -> intercom. // SET_UPDATE_MANAGER(); Check(updateManager); updateManager->Execute(start_of_frame); // FUN_00435794 / FUN_004363ec CLEAR_UPDATE_MANAGER(); Check(rendererManager); rendererManager->UpdateInterestOrigins(start_of_frame); // FUN_00434a08 Check(interestManager); interestManager->Execute(); // FUN_0043649c SET_RENDERER_MANAGER(); Check(rendererManager); rendererManager->Execute(start_of_frame, frame_ticks, frame_ticks); // FUN_00436440 CLEAR_RENDERER_MANAGER(); Check(intercomManager); intercomManager->Execute(); // (this+0x40)->vtbl+0x24 // // Running a recorded mission: keep the on-screen game clock current. // if (GetApplicationState() == RunningMission) // state(+0x88)==5 { // The decomp showed "(Now()-gameStarted)/TicksPerSecond": that // division reflects the DOS Time-in-ticks representation. In the // ported engine Time::operator-(Time) already yields Scalar seconds // (time.h Time_To_Float), so the value is used directly here -- the // same net result, matching the RP twin. secondsRemainingInGame = // +0x58 currentMission->GetGameLength() // (this+0xc8)+0xe0 - (Now() - gameStarted); // +0x5c } CLEAR_FOREGROUND_PROCESSING(); Check_Fpu(); return executeFrames && !Exit_Code; } // //############################################################################# // ExecuteBackgroundTask //############################################################################# // // NOTE: the OPT build did not emit a BT-specific override for this slot -- the // playback application inherits L4Application::ExecuteBackgroundTask unchanged // (the RP twin's body -- "if (ProcessOneEvent(HighEventPriority)) return; else // backgroundTasks->Execute();" -- already lives in the L4 base here). Declared // in the header for completeness; reconstructed best-effort to match RP. // void BTL4PlaybackApplication::ExecuteBackgroundTask() { Check(this); if (ProcessOneEvent(HighEventPriority)) { return; } Check(backgroundTasks); backgroundTasks->Execute(); } // //############################################################################# // DispatchPacket @004d3ecc //############################################################################# // // Route a recorded packet to its client. packet->clientID is the first word // of the NetworkPacket; messageData lives at packet+0x10. // void BTL4PlaybackApplication::DispatchPacket(NetworkPacket *packet) { Check(this); NetworkManager *net_mgr = GetNetworkManager(); // this+0x20 Check(net_mgr); NetworkClient *client = net_mgr->GetNetworkClientPointer(packet->clientID); // FUN_0041d7f0 Check(client); client->ReceiveNetworkPacket(packet, &packet->messageData); // vtbl+0x14 Check_Fpu(); } // //############################################################################# // RunMissionMessageHandler @004d3ef8 //############################################################################# // // On the very first launch (state==WaitingForLaunch, 3) synchronise the // playback clock: timeBias = Now - (timestamp of the spooled RunMission // packet), then advance the spool one packet. Then chain to the base. // void BTL4PlaybackApplication::RunMissionMessageHandler( RunMissionMessage *message ) { Check(this); if (GetApplicationState() == WaitingForLaunch) // state(+0x88)==3 { SpoolFile *spool = GetSpoolFile(); // this+0x18 Check(spool); NetworkPacket *packet = (NetworkPacket*)spool->GetPointer(); // vtbl slot0 timeBias = // this+0xd4 Now().ticks - packet->timeStamp.ticks; // FUN_00414b60, packet+0xc spool->NextPacket(); // FUN_0044f850 } BTL4Application::RunMissionMessageHandler(message); // FUN_004d3a2c Check_Fpu(); } // //############################################################################# // StopMissionMessageHandler @004d3f44 //############################################################################# // char winners_spot[]="win?"; // @0051ee63 (literal "win?\0") void BTL4PlaybackApplication::StopMissionMessageHandler( StopMissionMessage *message ) { MissionReviewApplicationManager *app_mgr = GetApplicationManager(); // FUN_0044e13c Check(app_mgr); SpoolFile *spool = GetSpoolFile(); // this+0x18 if (spool) { app_mgr->ReleaseSpoolFile(spool); // FUN_0044fac0 spoolFile = NULL; // // Move every player into the dropzone matching its finishing rank // (the "winners circle"). // EntityManager *entity_manager = application->GetEntityManager(); // DAT_004efc94+0x24 EntityGroup *dropzones = entity_manager->FindGroup("DropZones"); // FUN_00403ad0 char *place = winners_spot + 3; // the trailing '?' Player *p; for ( int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; // FUN_0042efe4 ++rank ) { *place = (char)('1' + rank); // "win1".."winN" ChainIteratorOf iterator(dropzones->groupMembers); // FUN_00421414 DropZone *dropzone; while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL) { if (!strcmp(dropzone->GetDropZoneName(), winners_spot)) // name@+0x1c8 { break; } } if (!dropzone) { continue; } // // Park the player's Mech on that dropzone for the freeze-frame. // Mech *mech = (Mech*)p->GetPlayerVehicle(); // p+0x1fc if (mech->GetClassID() == MechClassID) // classID(+4)==0xbb9 { mech->Reset(dropzone->localOrigin, Mech::MissionReviewReset); // FUN_0049fb74, +0x100, 0 mech->SetPerformance(&Mech::DoNothing); // mech[7..9]=&PTR_0051ee69.. mech->FlushEvents(); // FUN_00419cb8 // // Refresh this player's name bitmap entry. // // RECONSTRUCTION NOTE: the shipped BTL4OPT.EXE here built a // per-entity NameBitmapEntry (FUN_00436668) and handed it to // RendererManager::ReplaceNameBitmap (FUN_004364e4). That // per-entry name-bitmap API belonged to the original Division // IG image-generator renderer and was removed wholesale in the // WinTesla port (L4D3D); RendererManager / NameBitmapEntry no // longer expose it. The bulk reload below // (SortAndReloadNameBitmaps) -- which the port retained and the // RP twin RPL4PlaybackApplication::StopMissionMessageHandler // relies on alone -- performs the equivalent refresh, so the // per-entry rebuild is intentionally folded into it here. } } // // Re-sort the name bitmaps and tilt the camera up for the circle. // DPLRenderer *dpl_renderer = GetVideoRenderer(); // this+0x48 Check(dpl_renderer); dpl_renderer->SortAndReloadNameBitmaps(); // FUN_00460840 dpl_renderer->SetViewAngle(Degree(45.0f)); // FUN_0045a7a8(.,4); cf. literal @0051ee66 } // // Let the application stop normally. // BTL4Application::StopMissionMessageHandler(message); // FUN_004d3a94 Check_Fpu(); } // //############################################################################# // TestInstance @004d40d8 //############################################################################# // Logical BTL4PlaybackApplication::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(.,0x51ee2c) }