The recording tee, which is what the Live Cam was for. L4SpoolingApplication turned out not to be the obstacle it looked like. The review build already records by teeing - it spools each packet and then hands it on - and what tied that to a review build was never the recording but where the buffer came from. MissionReviewApplicationManager is only a pool allocator, and SpoolFile takes whatever buffer it is handed, so SpoolRecorder owns one buffer and needs none of it. One hook covers what the review build taps in two places. Both InterestManager and NetworkManager derive from NetworkClient, so NetworkClient::ReceiveNetworkPacket sees entity updates and mission control alike, and it sits before Dispatch so a packet is kept whether or not anything downstream wants it. The recorder arms on the first packet rather than at the green light, because playback rebuilds the world from the LoadMission and RunMission packets and a spool that starts at the flag cannot be replayed. Two things a live recorder must do that the review one did not. It must not touch the packet. The spooler restamps in place with local arrival time, which is right in itself - playback paces off those stamps and packets from different senders carry different clock origins - but the sender's timestamp is what Simulation::ReadUpdateRecord hands to RP412NETCLOCK and from there to the projection. Overwriting it live would feed arrival jitter into where remote pods are drawn, which is the tick just fixed. So the write position is taken first and the COPY is stamped, in the spool, afterwards. And it must not take the race down. SpoolFile::SpoolPacket answers a full buffer with PostQuitMessage - a fair end to a replay, and killing the race being recorded on a live host. The recorder checks the room first and stops, and says so. RP412RECORDSIZE defaults to 100MB rather than the review build's 6, on Cyd's call: a full grid sends around 17KB a second, so six megabytes is six minutes and a hundred is an hour and a half, which costs nothing on any machine that can run this. Saved at the buzzer - the first of the two StopMissions, the end of the race rather than the fade timer - into SPOOLS\<timestamp>.spl and copied to last.spl, matching where the review build looks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
3.7 KiB
C++
136 lines
3.7 KiB
C++
#pragma once
|
|
|
|
#include "appmgr.h"
|
|
|
|
//##########################################################################
|
|
//########################### SpoolFile ################################
|
|
//##########################################################################
|
|
|
|
class SpoolFile:
|
|
public MemoryStream
|
|
{
|
|
public:
|
|
SpoolFile(
|
|
void *stream_start,
|
|
size_t stream_size,
|
|
size_t initial_offset=0
|
|
);
|
|
SpoolFile(SpoolFile& spool);
|
|
~SpoolFile();
|
|
|
|
enum SpoolState {
|
|
Empty,
|
|
Spooling,
|
|
Stored,
|
|
Playing
|
|
};
|
|
SpoolState
|
|
spoolState;
|
|
|
|
void
|
|
SaveAs(const char *name);
|
|
void
|
|
Read(const char *name);
|
|
|
|
void
|
|
SpoolPacket(NetworkPacket *packet);
|
|
NetworkPacket*
|
|
NextPacket();
|
|
};
|
|
|
|
//##########################################################################
|
|
//########################## SpoolRecorder #############################
|
|
//##########################################################################
|
|
//
|
|
// Keeping a race from a station that is also PLAYING one.
|
|
//
|
|
// The review build already records by teeing: L4SpoolingNetworkManager
|
|
// spools each packet and then hands it to the ordinary receive path. What
|
|
// tied that to a review build was never the recording - it was where the
|
|
// buffer came from. MissionReviewApplicationManager is only a pool
|
|
// allocator, and SpoolFile takes whatever buffer it is handed, so a
|
|
// recorder that owns one buffer needs none of it.
|
|
//
|
|
// Two things a live recorder must do that the review one did not:
|
|
//
|
|
// It must not touch the packet. The spooler restamps each packet with
|
|
// local arrival time, which is right - playback paces off those stamps and
|
|
// packets from different senders carry different clock origins, so they
|
|
// have to be put on one clock. But doing it in place would overwrite the
|
|
// sender's timestamp that Simulation::ReadUpdateRecord feeds to
|
|
// RP412NETCLOCK and the dead reckoner, which is exactly the input behind
|
|
// the projection tick. So it stamps the COPY, in the spool, after writing.
|
|
//
|
|
// And it must not take the race down with it. SpoolFile::SpoolPacket
|
|
// answers a full buffer with PostQuitMessage, which for a review is a fair
|
|
// end to a replay and for a live host is killing the race being recorded.
|
|
// The recorder checks the room first and simply stops.
|
|
//
|
|
class SpoolRecorder
|
|
{
|
|
public:
|
|
SpoolRecorder();
|
|
~SpoolRecorder();
|
|
|
|
// Allocate and begin. Call before the mission loads: playback rebuilds
|
|
// the world from the LoadMission and RunMission packets, so a spool
|
|
// armed at the green light cannot be replayed.
|
|
Logical
|
|
Arm();
|
|
|
|
// Tee one received packet. Safe to call unarmed or after the buffer
|
|
// has filled - both do nothing.
|
|
void
|
|
Record(const NetworkPacket *packet);
|
|
|
|
// Write SPOOLS\<timestamp>.spl and copy it to last.spl.
|
|
void
|
|
Save();
|
|
|
|
void
|
|
Disarm();
|
|
|
|
Logical
|
|
IsArmed() const
|
|
{ return armed; }
|
|
|
|
protected:
|
|
char *buffer;
|
|
SpoolFile *spool;
|
|
size_t bufferSize;
|
|
Logical armed;
|
|
Logical full;
|
|
int packetsRecorded;
|
|
};
|
|
|
|
// The process-wide recorder, made on first use.
|
|
SpoolRecorder *
|
|
SpoolRecorder_Get();
|
|
|
|
//##########################################################################
|
|
//############## MissionReviewApplicationManager #####################
|
|
//##########################################################################
|
|
|
|
class MissionReviewApplicationManager:
|
|
public ApplicationManager
|
|
{
|
|
public:
|
|
MissionReviewApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate, int spool_count, size_t spool_size);
|
|
~MissionReviewApplicationManager();
|
|
|
|
SpoolFile*
|
|
GetEmptySpoolFile();
|
|
SpoolFile*
|
|
GetStoredSpoolFile(CString spoolFileName);
|
|
void
|
|
StoreSpoolFile(SpoolFile *spool_file);
|
|
void
|
|
ReleaseSpoolFile(SpoolFile *spool_file);
|
|
|
|
protected:
|
|
int spoolCount;
|
|
SpoolFile **spoolFiles;
|
|
char **spoolBuffers;
|
|
size_t spoolSize;
|
|
};
|