Files
TeslaRel410/restoration/source410/MUNGA_L4/L4SPLR.CPP
T
CydandClaude Fable 5 d2be54a3b9 BT410 5.3.106: MISSION REVIEW LINKS -- the playback app, the winners circle, and the last Fail() in main
BTL4PB.CPP reconstructed against its AUTHENTIC surviving header (BTL4PB.HPP)
from the binary's 0x4d3b74-0x4d40d8 cluster, cross-checked against the
surviving RP411 twin -- and the review/camera launch in BTL4.CPP's main() is
real code now.  btl4.lib 11 -> 12 members, link clean, zero unresolved: the
linker pulled the entire playback chain for the first time.

The launch decode closed on two identifications:

  The `if (spool_file)` guard in the decomp means the manager getter is
  GetStoredSpoolFile -- the empty-spool getter cannot fail, so a review pod
  with no recorded battle simply starts nothing, which is the right field
  behaviour.

  The camera app's ctor (FUN_0047c3fc) sits in the ENGINE range with the
  (ResourceFile*, ApplicationID) shape: the camera pod's recorder IS the
  engine's own L4SpoolingApplication -- restored one commit ago -- started
  with ApplicationID 1 == BTL4.  BT shipped no camera app of its own.

What the playback app does, all binary-anchored: BTSpoolerTask pumps
recorded packets through DispatchPacket -> the client's own
ReceiveNetworkPacket; RunMission synchronises the clock (timeBias = Now -
the recorded run packet's stamp); the frame loop runs controls ONLY in
interactive review (mode 2), then update/interest/renderer/intercom; and
StopMission plays the WINNERS CIRCLE -- release the spool, park each
player's mech on the dropzone named "win1".."winN" by finishing rank,
freeze it (DoNothing performance), and tilt the camera up 45 degrees over
the tableau.

DOS-authentic ctor shape throughout: the binary's base call is (resource,
0x3eb, DefaultData) -- no windowing parameters ever existed in 4.10; the
RP411 twin's HINSTANCE/HWND threading is port drift, dropped.

Also fixed from 5.3.105's debt: L4PlaybackNetworkManager chains
NetworkManager::DefaultData -- the 1995 header declares no statics of its
own; the RP411 self-reference was drift too.

One [T3] in source: the idle frame's renderer service (the descendant's
ExecuteIdle) has no 1995 entry point; pre-egg the idle frame spins.

RUNTIME TEST PENDING, stated plainly: exercising review needs a
camera/review egg and a recorded spool on the rig (CAMERA-REVIEW-NOTES.md
has the roles).  The normal pod path regressed clean (mode gate untouched).

The Fail() census moves: BTL4.CPP and SEQCTL.CPP are now stub-free; 22
stubs remain across 15 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 15:54:31 -05:00

746 lines
19 KiB
C++

//===========================================================================//
// File: l4splr.cpp //
// Project: MUNGA Brick: L4 Spooling / Playback //
// Contents: The mission-review spool layer -- the recorder-side network //
// managers, the spooling application, and the playback pump //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTION: no 1995 L4SPLR.CPP survives, but its HEADER does
// (CODE/RP/MUNGA_L4/L4SPLR.HPP), and the direct descendant source survives
// in the RP411 tree (MUNGA_L4/L4SPLR.cpp) -- this file is that source
// BACK-DATED onto the 1995 engine APIs the header declares, the same method
// as the MUNGA backfills (BACKFILLS.NOTES.md). Deltas from the RP411 body:
//
// * the windowing-era ctor (HINSTANCE/HWND) reverts to the header's
// (ResourceFile *, ApplicationID);
// * SOCKADDR_IN + two-arg ResolveAddress revert to the 1995
// unsigned-long address (L4NET.HPP:265) and FindHost(NetworkAddress);
// * L4Host construction takes NullNetworkAddress / socket 0, the same
// idiom as L4NET.CPP's own console-host build;
// * PostQuitMessage(AbortExitCodeID) becomes Exit_Code = AbortExitCodeID
// (the 1995 loop gate ExecuteForeground already tests);
// * accessor-style statics revert to the header's static members;
// * MakeGaugeRenderer()/Shutdown() lose their later-era parameters.
//
#include <mungal4.hpp>
#pragma hdrstop
#if !defined(L4SPLR_HPP)
# include <l4splr.hpp>
#endif
#if !defined(L4NET_HPP)
# include <l4net.hpp>
#endif
#if !defined(MISSION_HPP)
# include <mission.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(L4CTRL_HPP)
# include <l4ctrl.hpp>
#endif
//#############################################################################
//###################### SpoolingInterestManager ##########################
//#############################################################################
SpoolingInterestManager::SpoolingInterestManager()
{
}
SpoolingInterestManager::~SpoolingInterestManager()
{
}
//
//#############################################################################
//#############################################################################
//
void
SpoolingInterestManager::LoadInterestArenas(Mission *)
{
Check(this);
}
void
SpoolingInterestManager::LoadMission(Mission *)
{
Check(this);
}
//
//#############################################################################
// Every network message reaching the interest manager is spooled.
//#############################################################################
//
void
SpoolingInterestManager::ReceiveNetworkPacket(
NetworkPacket *packet,
Receiver::Message *
)
{
packet->timeStamp = Now();
Check(l4_spooling_application);
SpoolFile
*spool = l4_spooling_application->GetSpoolFile();
Check(spool);
spool->SpoolPacket(packet);
}
void
SpoolingInterestManager::Shutdown()
{
Check(this);
}
//#############################################################################
//###################### L4SpoolingNetworkManager #########################
//#############################################################################
L4SpoolingNetworkManager::L4SpoolingNetworkManager()
{
}
L4SpoolingNetworkManager::~L4SpoolingNetworkManager()
{
}
//
//#############################################################################
// Start the connection normally, then write the spool PREAMBLE: the
// application ID, the resource major version, and per mission host a
// remote/local flag plus its host ID -- everything playback needs to
// reconstruct the host table without a live network.
//#############################################################################
//
void
L4SpoolingNetworkManager::StartConnecting(Mission *mission)
{
L4NetworkManager::StartConnecting(mission);
Check(l4_spooling_application);
SpoolFile
*spool = l4_spooling_application->GetSpoolFile();
Check(spool);
*(ApplicationID *)spool->GetPointer() = application->GetApplicationID();
spool->AdvancePointer(sizeof(ApplicationID));
ResourceFile
*res_file = application->GetResourceFile();
Check(res_file);
int
major_version = res_file->versionArray[1];
*(int *)spool->GetPointer() = major_version;
spool->AdvancePointer(sizeof(major_version));
HostManager
*host_mgr = application->GetHostManager();
Check(host_mgr);
Mission::HostIterator
mission_host_iterator(mission);
MissionHostData
*mission_host_data;
while ((mission_host_data = mission_host_iterator.ReadAndNext()) != NULL)
{
CString
host_name(mission_host_data->GetAddressString());
unsigned long
net_address = ResolveAddress(host_name);
Host
*host = host_mgr->FindHost(net_address);
*(Logical *)spool->GetPointer() = (host != host_mgr->GetLocalHost());
spool->AdvancePointer(sizeof(Logical));
*(HostID *)spool->GetPointer() = host->GetHostID();
spool->AdvancePointer(sizeof(HostID));
}
}
//
//#############################################################################
// Egg traffic is spooled on its way through.
//#############################################################################
//
void
L4SpoolingNetworkManager::ReceiveNetworkPacket(
NetworkPacket *packet,
Receiver::Message *message
)
{
if (message->messageID == ReceiveEggFileMessageID)
{
packet->timeStamp = Now();
Check(l4_spooling_application);
SpoolFile
*spool = l4_spooling_application->GetSpoolFile();
Check(spool);
spool->SpoolPacket(packet);
}
L4NetworkManager::ReceiveNetworkPacket(packet, message);
}
//#############################################################################
//###################### L4PlaybackNetworkManager #########################
//#############################################################################
//
// The 1995 header declares no statics of its own (the DefaultData reference
// in the RP411 descendant was later-era drift); chain the base's.
//
L4PlaybackNetworkManager::L4PlaybackNetworkManager():
NetworkManager(NetworkManager::DefaultData)
{
}
L4PlaybackNetworkManager::~L4PlaybackNetworkManager()
{
}
//
//#############################################################################
// "Connect" from the spool instead of the wire: read back the preamble the
// spooler wrote, verify it belongs to this application and data version,
// rebuild the host table from the recorded flags, and post LoadMission.
//#############################################################################
//
void
L4PlaybackNetworkManager::StartConnecting(Mission *mission)
{
Mission::HostIterator
mission_host_iterator(mission);
MissionHostData
*mission_host_data;
Check(application);
SpoolFile
*spool = application->GetSpoolFile();
Check(spool);
ApplicationID
id = *(ApplicationID *)spool->GetPointer();
spool->AdvancePointer(sizeof(id));
int
major_rev = *(int *)spool->GetPointer();
spool->AdvancePointer(sizeof(major_rev));
if (id != application->GetApplicationID())
{
DEBUG_STREAM
<< "\n\nError - Not a spool file for this application!\n" << flush;
Exit_Code = AbortExitCodeID;
return;
}
ResourceFile
*resource_file = application->GetResourceFile();
Check(resource_file);
if (major_rev != resource_file->versionArray[1])
{
DEBUG_STREAM
<< "\n\nError - Spool file major data version should be "
<< (int)resource_file->versionArray[1] << ", not " << major_rev
<< "!\n" << flush;
Exit_Code = AbortExitCodeID;
return;
}
HostManager
*host_mgr = application->GetHostManager();
Check(host_mgr);
while ((mission_host_data = mission_host_iterator.ReadAndNext()) != NULL)
{
Check(mission_host_data);
CString
host_name(mission_host_data->GetAddressString());
Logical
remote = *(Logical *)spool->GetPointer();
spool->AdvancePointer(sizeof(Logical));
HostID
host_id = *(HostID *)spool->GetPointer();
spool->AdvancePointer(sizeof(HostID));
Host
*my_host =
new L4Host(
host_id,
mission_host_data->GetHostType(),
NullNetworkAddress,
0,
host_name
);
Register_Object(my_host);
if (remote)
{
host_mgr->AdoptRemoteHost(my_host);
}
else
{
host_mgr->AdoptLocalHost(my_host);
}
}
Check(application);
Application::Message
load_message(
Application::LoadMissionMessageID,
sizeof(Application::Message)
);
application->Post(DefaultEventPriority, application, &load_message);
}
//
//#############################################################################
//#############################################################################
//
Logical
L4PlaybackNetworkManager::Shutdown()
{
Check(this);
NetworkManager::Shutdown();
Check(application);
HostManager
*host_mgr = application->GetHostManager();
Check(host_mgr);
HostManager::RemoteHostIterator
remote_hosts(host_mgr);
Host
*my_host;
while ((my_host = remote_hosts.ReadAndNext()) != NULL)
{
Check(my_host);
host_mgr->OrphanRemoteHost(my_host);
Unregister_Object(my_host);
delete my_host;
}
return True;
}
//
// Playback has no live wire: the buffers are always empty.
//
Logical
L4PlaybackNetworkManager::CheckBuffers(NetworkPacket *)
{
return False;
}
//#############################################################################
//###################### L4SpoolingApplication ##########################
//#############################################################################
L4SpoolingApplication
*&l4_spooling_application = (L4SpoolingApplication *&)application;
const Receiver::HandlerEntry
L4SpoolingApplication::MessageHandlerEntries[] =
{
MESSAGE_ENTRY(L4SpoolingApplication, RunMission),
MESSAGE_ENTRY(L4SpoolingApplication, StopMission),
MESSAGE_ENTRY(L4SpoolingApplication, AbortMission),
MESSAGE_ENTRY(L4SpoolingApplication, LoadMission)
};
L4SpoolingApplication::MessageHandlerSet
L4SpoolingApplication::MessageHandlers(
ELEMENTS(L4SpoolingApplication::MessageHandlerEntries),
L4SpoolingApplication::MessageHandlerEntries,
L4Application::MessageHandlers
);
Derivation
L4SpoolingApplication::ClassDerivations(
L4Application::ClassDerivations,
"L4SpoolingApplication"
);
L4SpoolingApplication::SharedData
L4SpoolingApplication::DefaultData(
L4SpoolingApplication::ClassDerivations,
L4SpoolingApplication::MessageHandlers
);
//
//#############################################################################
//#############################################################################
//
L4SpoolingApplication::L4SpoolingApplication(
ResourceFile *resource_file,
ApplicationID application_ID
):
L4Application(
resource_file,
application_ID,
L4ApplicationClassID,
DefaultData
)
{
missionSpooled = False;
}
L4SpoolingApplication::~L4SpoolingApplication()
{
}
Logical
L4SpoolingApplication::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// Message handlers.
//#############################################################################
//
void
L4SpoolingApplication::LoadMissionMessageHandler(Message *)
{
applicationState.SetState(WaitingForLaunch);
}
void
L4SpoolingApplication::RunMissionMessageHandler(RunMissionMessage *)
{
Check(this);
applicationState.SetState(RunningMission);
}
//
// The stop is deferred until the mission has fully spooled, then the spool
// is handed to the application manager for storage.
//
void
L4SpoolingApplication::StopMissionMessageHandler(StopMissionMessage *message)
{
if (!missionSpooled)
{
missionSpooled = True;
Time
post_time = Now();
post_time += 3.0f;
//
// The StopMissionMessage IS a Receiver::Message (NetworkClient
// derives Receiver, so NetworkClient::Message resolves there); the
// explicit cast is for BC4.52's nested-scope upcast quirk only.
//
Post(
DefaultEventPriority, this,
(Receiver::Message *)message, post_time);
}
else
{
MissionReviewApplicationManager
*app_mgr = GetApplicationManager();
Check(app_mgr);
SpoolFile
*spool = GetSpoolFile();
Check(spool);
app_mgr->StoreSpoolFile(spool);
spoolFile = NULL;
L4Application::StopMissionMessageHandler(message);
}
}
void
L4SpoolingApplication::AbortMissionMessageHandler(
AbortMissionMessage *message
)
{
Check(this);
Check(spoolFile);
spoolFile->Rewind();
L4Application::AbortMissionMessageHandler(message);
}
//
//#############################################################################
// Run/stop traffic is spooled on its way through -- the stop only once the
// mission has been marked spooled, so the recorded file ends exactly once.
//#############################################################################
//
void
L4SpoolingApplication::ReceiveNetworkPacket(
NetworkPacket *packet,
Receiver::Message *message
)
{
Check(this);
SpoolFile
*spool;
switch (message->messageID)
{
case StopMissionMessageID:
if (missionSpooled)
{
packet->timeStamp = Now();
spool = GetSpoolFile();
Check(spool);
spool->SpoolPacket(packet);
break;
}
// fall through -- an un-spooled stop records like a run
case RunMissionMessageID:
packet->timeStamp = Now();
spool = GetSpoolFile();
Check(spool);
spool->SpoolPacket(packet);
break;
}
L4Application::ReceiveNetworkPacket(packet, message);
}
//
//#############################################################################
// The maker overrides: a spooler records; it does not render.
//#############################################################################
//
Registry *
L4SpoolingApplication::MakeRegistry()
{
return NULL;
}
InterestManager *
L4SpoolingApplication::MakeInterestManager()
{
return new SpoolingInterestManager;
}
NetworkManager *
L4SpoolingApplication::MakeNetworkManager()
{
return new L4SpoolingNetworkManager;
}
VideoRenderer *
L4SpoolingApplication::MakeVideoRenderer()
{
return NULL;
}
AudioRenderer *
L4SpoolingApplication::MakeAudioRenderer()
{
return NULL;
}
GaugeRenderer *
L4SpoolingApplication::MakeGaugeRenderer()
{
return NULL;
}
Mission *
L4SpoolingApplication::MakeMission(
NotationFile *notation_file,
ResourceFile *resources
)
{
Check(this);
Check(notation_file);
Check(resources);
return new Mission(notation_file, resources);
}
Entity *
L4SpoolingApplication::MakeViewpointEntity(Entity__MakeMessage *)
{
return NULL;
}
//
//#############################################################################
//#############################################################################
//
void
L4SpoolingApplication::Initialize()
{
Check(this);
Application::Initialize();
L4NetworkManager
*net_mgr = GetNetworkManager();
Check(net_mgr);
net_mgr->CreateConsoleHost();
MissionReviewApplicationManager
*app_mgr = GetApplicationManager();
Check(app_mgr);
spoolFile = app_mgr->GetEmptySpoolFile();
}
void
L4SpoolingApplication::LoadBackgroundTasks()
{
}
//
//#############################################################################
//#############################################################################
//
Logical
L4SpoolingApplication::ExecuteForeground(
Time /* start_of_frame */,
Scalar /* frame_duration */
)
{
SET_FOREGROUND_PROCESSING();
Check(this);
//
// The 1995 base ControlsManager has no Execute; the LBE4 subclass owns
// the per-frame poll (L4CTRL.HPP:225), same cast L4APP itself uses.
//
Check(controlsManager);
((LBE4ControlsManager *)controlsManager)->Execute();
CLEAR_FOREGROUND_PROCESSING();
return executeFrames && !Exit_Code;
}
void
L4SpoolingApplication::ExecuteBackgroundTask()
{
Check(this);
Check(networkManager);
if (!networkManager->RoutePacket())
{
ProcessOneEvent(DefaultEventPriority);
}
}
Logical
L4SpoolingApplication::Shutdown()
{
Check(this);
L4Application::Shutdown();
return !Exit_Code && !missionSpooled;
}
//#############################################################################
//########################### SpoolerTask ###############################
//#############################################################################
SpoolerTask::SpoolerTask()
{
}
SpoolerTask::~SpoolerTask()
{
}
void
SpoolerTask::DispatchPacket(NetworkPacket *)
{
Fail("SpoolerTask::DispatchPacket - shouldn't be here!");
}
long
SpoolerTask::GetTimeBias()
{
Fail("SpoolerTask::GetTimeBias - shouldn't be here!");
return 0;
}
//
//#############################################################################
// The playback pump: interpret the spool against the application state --
// egg packets while waiting for the egg, interest packets while loading,
// then everything else doled out on the recorded timestamps biased by when
// the run began.
//#############################################################################
//
void
SpoolerTask::Execute()
{
Check(this);
Check(application);
SpoolFile
*spool = application->GetSpoolFile();
if (!spool)
{
return;
}
Check(spool);
NetworkPacket
*packet;
switch (application->GetApplicationState())
{
case Application::WaitingForEgg:
packet = (NetworkPacket *)spool->GetPointer();
while (packet->clientID == NetworkClient::NetworkManagerClientID)
{
spool->NextPacket();
DispatchPacket(packet);
packet = (NetworkPacket *)spool->GetPointer();
}
break;
case Application::CreatingMission:
case Application::LoadingMission:
packet = (NetworkPacket *)spool->GetPointer();
while (
packet->clientID != NetworkClient::ApplicationClientID &&
spool->GetBytesRemaining() > 0
)
{
spool->NextPacket();
DispatchPacket(packet);
packet = (NetworkPacket *)spool->GetPointer();
}
break;
case Application::LaunchingMission:
case Application::RunningMission:
while (spool && spool->GetBytesRemaining() > 0)
{
packet = (NetworkPacket *)spool->GetPointer();
Time
when = packet->timeStamp;
when.ticks += GetTimeBias();
if (when > Now())
{
break;
}
spool->NextPacket();
DispatchPacket(packet);
spool = application->GetSpoolFile();
}
break;
}
}