Cyd's question, and the answer is yes. A spool records what MOVED and never the track it moved through, so it cannot be replayed without the egg the race was run on - and frontend.egg is rewritten by the next race set up on the machine. A recording kept on its own therefore stops being playable the moment somebody picks a different track, silently, and by then the egg that would have opened it is gone. So the egg is saved with it: SPOOLS\<timestamp>.egg beside SPOOLS\<timestamp>.spl, and last.egg beside last.spl. The console sets the path, because the console is where the egg's name is actually known. And playback looks for it. Given no -egg it takes the spool's name, swaps the extension, and uses that if it is there. Naming the egg by hand is not just tedious, it is dangerous: a spool played against a DIFFERENT track loads perfectly happily and shows nonsense, and frontend.egg is exactly the wrong egg by default because it belongs to whatever was set up last. Verified on the way here: the header this build writes reads back exactly as playback expects it - major version 3, host 2 local (the camera), host 3 remote (the racer, matching every packet's fromHost), 8263 packets after a 24 byte header ending precisely at EOF. Playback then loaded it with no complaint about application ID or version, which is the check that failed before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
4.7 KiB
C++
165 lines
4.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);
|
|
|
|
// Where the egg for this race lives, so it can be kept beside the
|
|
// recording. A spool records what MOVED and never the track it moved
|
|
// through, so the two are one artifact: without its egg a spool cannot
|
|
// be replayed, and frontend.egg is overwritten by the next race set up
|
|
// on this machine. Saving them together is what stops a recording
|
|
// going stale the moment somebody picks a different track.
|
|
void
|
|
SetEggPath(const char *path);
|
|
|
|
// Write SPOOLS\<timestamp>.spl, the matching .egg beside it, and copy
|
|
// the spool to last.spl.
|
|
void
|
|
Save();
|
|
|
|
void
|
|
Disarm();
|
|
|
|
Logical
|
|
IsArmed() const
|
|
{ return armed; }
|
|
|
|
//
|
|
// A spool opens with a header - the application ID, the resource
|
|
// major version, and one (remote, hostID) pair per host named in the
|
|
// egg - and playback refuses a spool without it. All of that is
|
|
// network-layer knowledge, so it is written by the network manager
|
|
// once the hosts exist, and these three are what let it.
|
|
//
|
|
SpoolFile*
|
|
GetSpool()
|
|
{ return spool; }
|
|
Logical
|
|
HeaderWritten() const
|
|
{ return headerWritten; }
|
|
void
|
|
MarkHeaderWritten()
|
|
{ headerWritten = True; }
|
|
|
|
protected:
|
|
char *buffer;
|
|
SpoolFile *spool;
|
|
size_t bufferSize;
|
|
Logical armed;
|
|
Logical full;
|
|
Logical headerWritten;
|
|
int packetsRecorded;
|
|
char eggPath[260];
|
|
};
|
|
|
|
// 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;
|
|
};
|