Improve Demo end handing

- Demos/Ghosts that end before ticking once are now correctly ignored. (Resolves KartKrew/RingRacers#168)
    - There was code for discovering it on read! It was just placed slightly too early, probably due to the conversion for netreplays! I'm very mad!
- As a preventative measure, demos *recorded* before ticking will simply not save in the first place.
    - This was also a frustratingly easy fix for the amount of headache it's caused us.
- Reduced the amount of copypasted boilerplate by simplifying the places where DEMOMARKER can be written (and therefore read).
    - Previously, like half the write functions tried to guess their own output size and potentially end the demo at any point.
    - At best, this will grant us a few tics of reprireve for large netgames and MAYBE a handful of seconds for time attack, The Mode In Which The Aim Is To Go Fast.
    - Instead, double the size of the deadspace buffer extension and just check to see if we've crossed into that territory.
This commit is contained in:
toaster 2025-05-30 23:34:46 +01:00
parent 565733224f
commit 9e0510d674
3 changed files with 111 additions and 122 deletions

View file

@ -270,6 +270,26 @@ static ticcmd_t oldcmd[MAXPLAYERS];
static mobj_t oldghost[MAXPLAYERS];
boolean G_ConsiderEndingDemoWrite(void)
{
// chill, we reserved extra memory so it's
// "safe" to have written a bit past the end
if (demobuf.p < demobuf.end)
return false;
G_CheckDemoStatus();
return true;
}
boolean G_ConsiderEndingDemoRead(void)
{
if (*demobuf.p != DEMOMARKER)
return false;
G_CheckDemoStatus();
return true;
}
void G_ReadDemoExtraData(void)
{
INT32 p, extradata, i;
@ -460,13 +480,6 @@ void G_ReadDemoExtraData(void)
p = READUINT8(demobuf.p);
}
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
}
void G_WriteDemoExtraData(void)
@ -623,13 +636,6 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
}
G_CopyTiccmd(cmd, &oldcmd[playernum], 1);
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
}
void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
@ -739,14 +745,6 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
WRITEUINT16(botziptic_p, botziptic);
}
// attention here for the ticcmd size!
// latest demos with mouse aiming byte in ticcmd
if (!(demoflags & DF_GHOST) && ziptic_p > demobuf.end - 9)
{
G_CheckDemoStatus(); // no more space
return;
}
}
void G_GhostAddFlip(INT32 playernum)
@ -794,8 +792,11 @@ void G_GhostAddHit(INT32 playernum, mobj_t *victim)
void G_WriteAllGhostTics(void)
{
boolean toobig = false;
INT32 i, counter = leveltime;
if (!demobuf.p || !(demoflags & DF_GHOST))
return; // No ghost data to write.
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
@ -811,22 +812,9 @@ void G_WriteAllGhostTics(void)
WRITEUINT8(demobuf.p, i);
G_WriteGhostTic(players[i].mo, i);
// attention here for the ticcmd size!
// latest demos with mouse aiming byte in ticcmd
if (demobuf.p >= demobuf.end - (13 + 9 + 9))
{
toobig = true;
break;
}
}
WRITEUINT8(demobuf.p, 0xFF);
if (toobig)
{
G_CheckDemoStatus(); // no more space
return;
}
}
void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
@ -835,11 +823,6 @@ void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
UINT8 *ziptic_p;
UINT32 i;
if (!demobuf.p)
return;
if (!(demoflags & DF_GHOST))
return; // No ghost data to write.
ziptic_p = demobuf.p++; // the ziptic, written at the end of this function
#define MAXMOM (0xFFFF<<8)
@ -1061,7 +1044,7 @@ void G_ConsAllGhostTics(void)
{
UINT8 p;
if (!demobuf.p || !demo.deferstart)
if (!demobuf.p || !(demoflags & DF_GHOST) || !demo.deferstart)
return;
p = READUINT8(demobuf.p);
@ -1071,13 +1054,6 @@ void G_ConsAllGhostTics(void)
G_ConsGhostTic(p);
p = READUINT8(demobuf.p);
}
if (*demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
}
// Uses ghost data to do consistency checks on your position.
@ -1089,9 +1065,6 @@ void G_ConsGhostTic(INT32 playernum)
mobj_t *testmo;
UINT32 syncleeway;
if (!(demoflags & DF_GHOST))
return; // No ghost data to use.
testmo = players[playernum].mo;
// Grab ghost data.
@ -1282,13 +1255,6 @@ void G_ConsGhostTic(INT32 playernum)
}
}
}
if (*demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
}
void G_GhostTicker(void)
@ -1309,11 +1275,31 @@ void G_GhostTicker(void)
continue;
readghosttic:
#define follow g->mo->tracer
// Skip normal demo data.
ziptic = READUINT8(g->p);
xziptic = 0;
// Demo ends after ghost data.
if (ziptic == DEMOMARKER)
{
fadeghost:
g->mo->momx = g->mo->momy = g->mo->momz = 0;
g->mo->fuse = TICRATE;
if (follow)
{
follow->fuse = TICRATE;
}
g->done = true;
if (p)
{
p->next = g->next;
}
continue;
}
while (ziptic != DW_END) // Get rid of extradata stuff
{
if (ziptic < MAXPLAYERS)
@ -1414,9 +1400,11 @@ readghosttic:
// Grab ghost data.
ziptic = READUINT8(g->p);
if (ziptic == DEMOMARKER) // Had to end early for some reason
goto fadeghost;
if (ziptic == 0xFF)
goto skippedghosttic; // Didn't write ghost info this frame
else if (ziptic != 0)
if (ziptic != 0)
I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this
ziptic = READUINT8(g->p);
@ -1530,7 +1518,6 @@ readghosttic:
g->mo->renderflags &= ~RF_DONTDRAW;
}
#define follow g->mo->tracer
if (ziptic & GZT_FOLLOW)
{ // Even more...
UINT8 followtic = READUINT8(g->p);
@ -1620,28 +1607,6 @@ skippedghosttic:
if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here.
I_Error("Ghost is not a record attack ghost GHOSTEND"); //@TODO lmao don't blow up like this
// Demo ends after ghost data.
if (*g->p == DEMOMARKER)
{
g->mo->momx = g->mo->momy = g->mo->momz = 0;
#if 0 // freeze frame (maybe more useful for time attackers) (2024-03-11: you leave it behind anyway!)
g->mo->colorized = true;
g->mo->fuse = 10*TICRATE;
if (follow)
follow->colorized = true;
#else // dissapearing act
g->mo->fuse = TICRATE;
if (follow)
follow->fuse = TICRATE;
#endif
g->done = true;
if (p)
{
p->next = g->next;
}
continue;
}
// If the timer started, skip ahead until the ghost starts too.
if (starttime <= leveltime && !g->linecrossed && G_TimeAttackStart())
goto readghosttic;
@ -1885,7 +1850,7 @@ void G_RecordDemo(const char *name)
functions will check if they overran the buffer, but it
should be safe enough because they'll think there's less
memory than there actually is and stop early. */
const size_t deadspace = 1024;
const size_t deadspace = 2048;
I_Assert(demobuf.size > deadspace);
demobuf.size -= deadspace;
demobuf.end -= deadspace;
@ -3363,22 +3328,6 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum)
// Load "mapmusrng" used for altmusic selection
mapmusrng = READUINT8(demobuf.p);
// Sigh ... it's an empty demo.
if (*demobuf.p == DEMOMARKER)
{
snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
M_StartMessage("Demo Playback", msg, NULL, MM_NOTHING, NULL, "Return to Menu");
Z_Free(demo.skinlist);
demo.skinlist = NULL;
Z_Free(pdemoname);
Z_Free(demobuf.buffer);
demo.playback = false;
return;
}
Z_Free(pdemoname);
memset(&oldcmd,0,sizeof(oldcmd));
memset(&oldghost,0,sizeof(oldghost));
memset(&ghostext,0,sizeof(ghostext));
@ -3607,6 +3556,22 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum)
players[p].lastfakeskin = lastfakeskin[p];
}
// Sigh ... it's an empty demo.
if (*demobuf.p == DEMOMARKER)
{
snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
M_StartMessage("Demo Playback", msg, NULL, MM_NOTHING, NULL, "Return to Menu");
Z_Free(demo.skinlist);
demo.skinlist = NULL;
Z_Free(pdemoname);
Z_Free(demobuf.buffer);
demo.playback = false;
return;
}
Z_Free(pdemoname);
demo.deferstart = true;
CV_StealthSetValue(&cv_playbackspeed, 1);
@ -3756,14 +3721,6 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname)
p++; // mapmusrng
if (*p == DEMOMARKER)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), defdemoname);
Z_Free(skinlist);
P_SaveBufferFree(buffer);
return;
}
p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once
// any invalidating flags?
@ -3811,6 +3768,14 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname)
return;
}
if (*p == DEMOMARKER)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), defdemoname);
Z_Free(skinlist);
P_SaveBufferFree(buffer);
return;
}
gh = static_cast<demoghost*>(Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL));
gh->sizes = ghostsizes;
@ -4173,7 +4138,7 @@ boolean G_CheckDemoStatus(void)
// Keep the demo open and don't boot to intermission
// YET, pause demo playback.
if (!demo.waitingfortally && modeattacking && exitcountdown)
demo.waitingfortally = true;
;
else if (!demo.attract)
G_FinishExitLevel();
else
@ -4191,6 +4156,8 @@ boolean G_CheckDemoStatus(void)
D_SetDeferredStartTitle(true);
}
demo.waitingfortally = true; // if we've returned early for some reason...
return true;
}
@ -4235,6 +4202,13 @@ void G_SaveDemo(void)
if (currentMenu == &TitleEntryDef)
M_ClearMenus(true);
if (!leveltime)
{
// Why would you save if nothing has been recorded
G_ResetDemoRecording();
return;
}
// Ensure extrainfo pointer is always available, even if no info is present.
if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
{

View file

@ -172,6 +172,8 @@ extern UINT8 demo_writerng;
boolean G_CompatLevel(UINT16 level);
// Record/playback tics
boolean G_ConsiderEndingDemoRead(void);
boolean G_ConsiderEndingDemoWrite(void);
void G_ReadDemoExtraData(void);
void G_WriteDemoExtraData(void);
void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);

View file

@ -762,17 +762,23 @@ void P_Ticker(boolean run)
if (demo.recording)
{
G_WriteDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
G_WriteDemoTiccmd(&players[i].cmd, i);
if (!G_ConsiderEndingDemoWrite())
{
G_WriteDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
G_WriteDemoTiccmd(&players[i].cmd, i);
}
}
if (demo.playback && !demo.waitingfortally)
{
G_ReadDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
G_ReadDemoTiccmd(&players[i].cmd, i);
if (!G_ConsiderEndingDemoRead())
{
G_ReadDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
G_ReadDemoTiccmd(&players[i].cmd, i);
}
}
LUA_ResetTicTimers();
@ -1168,14 +1174,21 @@ void P_Ticker(boolean run)
if (demo.recording)
{
G_WriteAllGhostTics();
if (cv_recordmultiplayerdemos.value && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime)
G_CheckDemoTitleEntry();
if (!G_ConsiderEndingDemoWrite())
{
G_WriteAllGhostTics();
}
}
else if (demo.playback && !demo.waitingfortally) // Use Ghost data for consistency checks.
else if (demo.playback && !demo.waitingfortally)
{
G_ConsAllGhostTics();
if (!G_ConsiderEndingDemoRead())
{
// Use Ghost data for consistency checks.
G_ConsAllGhostTics();
}
}
if (modeattacking)