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]; 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) void G_ReadDemoExtraData(void)
{ {
INT32 p, extradata, i; INT32 p, extradata, i;
@ -460,13 +480,6 @@ void G_ReadDemoExtraData(void)
p = READUINT8(demobuf.p); p = READUINT8(demobuf.p);
} }
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
} }
void G_WriteDemoExtraData(void) void G_WriteDemoExtraData(void)
@ -623,13 +636,6 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
} }
G_CopyTiccmd(cmd, &oldcmd[playernum], 1); 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) void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
@ -739,14 +745,6 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
WRITEUINT16(botziptic_p, botziptic); 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) void G_GhostAddFlip(INT32 playernum)
@ -794,8 +792,11 @@ void G_GhostAddHit(INT32 playernum, mobj_t *victim)
void G_WriteAllGhostTics(void) void G_WriteAllGhostTics(void)
{ {
boolean toobig = false;
INT32 i, counter = leveltime; INT32 i, counter = leveltime;
if (!demobuf.p || !(demoflags & DF_GHOST))
return; // No ghost data to write.
for (i = 0; i < MAXPLAYERS; i++) for (i = 0; i < MAXPLAYERS; i++)
{ {
if (!playeringame[i] || players[i].spectator) if (!playeringame[i] || players[i].spectator)
@ -811,22 +812,9 @@ void G_WriteAllGhostTics(void)
WRITEUINT8(demobuf.p, i); WRITEUINT8(demobuf.p, i);
G_WriteGhostTic(players[i].mo, 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); WRITEUINT8(demobuf.p, 0xFF);
if (toobig)
{
G_CheckDemoStatus(); // no more space
return;
}
} }
void G_WriteGhostTic(mobj_t *ghost, INT32 playernum) void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
@ -835,11 +823,6 @@ void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
UINT8 *ziptic_p; UINT8 *ziptic_p;
UINT32 i; 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 ziptic_p = demobuf.p++; // the ziptic, written at the end of this function
#define MAXMOM (0xFFFF<<8) #define MAXMOM (0xFFFF<<8)
@ -1061,7 +1044,7 @@ void G_ConsAllGhostTics(void)
{ {
UINT8 p; UINT8 p;
if (!demobuf.p || !demo.deferstart) if (!demobuf.p || !(demoflags & DF_GHOST) || !demo.deferstart)
return; return;
p = READUINT8(demobuf.p); p = READUINT8(demobuf.p);
@ -1071,13 +1054,6 @@ void G_ConsAllGhostTics(void)
G_ConsGhostTic(p); G_ConsGhostTic(p);
p = READUINT8(demobuf.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. // Uses ghost data to do consistency checks on your position.
@ -1089,9 +1065,6 @@ void G_ConsGhostTic(INT32 playernum)
mobj_t *testmo; mobj_t *testmo;
UINT32 syncleeway; UINT32 syncleeway;
if (!(demoflags & DF_GHOST))
return; // No ghost data to use.
testmo = players[playernum].mo; testmo = players[playernum].mo;
// Grab ghost data. // 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) void G_GhostTicker(void)
@ -1309,11 +1275,31 @@ void G_GhostTicker(void)
continue; continue;
readghosttic: readghosttic:
#define follow g->mo->tracer
// Skip normal demo data. // Skip normal demo data.
ziptic = READUINT8(g->p); ziptic = READUINT8(g->p);
xziptic = 0; 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 while (ziptic != DW_END) // Get rid of extradata stuff
{ {
if (ziptic < MAXPLAYERS) if (ziptic < MAXPLAYERS)
@ -1414,9 +1400,11 @@ readghosttic:
// Grab ghost data. // Grab ghost data.
ziptic = READUINT8(g->p); ziptic = READUINT8(g->p);
if (ziptic == DEMOMARKER) // Had to end early for some reason
goto fadeghost;
if (ziptic == 0xFF) if (ziptic == 0xFF)
goto skippedghosttic; // Didn't write ghost info this frame 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 I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this
ziptic = READUINT8(g->p); ziptic = READUINT8(g->p);
@ -1530,7 +1518,6 @@ readghosttic:
g->mo->renderflags &= ~RF_DONTDRAW; g->mo->renderflags &= ~RF_DONTDRAW;
} }
#define follow g->mo->tracer
if (ziptic & GZT_FOLLOW) if (ziptic & GZT_FOLLOW)
{ // Even more... { // Even more...
UINT8 followtic = READUINT8(g->p); UINT8 followtic = READUINT8(g->p);
@ -1620,28 +1607,6 @@ skippedghosttic:
if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here. 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 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 the timer started, skip ahead until the ghost starts too.
if (starttime <= leveltime && !g->linecrossed && G_TimeAttackStart()) if (starttime <= leveltime && !g->linecrossed && G_TimeAttackStart())
goto readghosttic; goto readghosttic;
@ -1885,7 +1850,7 @@ void G_RecordDemo(const char *name)
functions will check if they overran the buffer, but it functions will check if they overran the buffer, but it
should be safe enough because they'll think there's less should be safe enough because they'll think there's less
memory than there actually is and stop early. */ memory than there actually is and stop early. */
const size_t deadspace = 1024; const size_t deadspace = 2048;
I_Assert(demobuf.size > deadspace); I_Assert(demobuf.size > deadspace);
demobuf.size -= deadspace; demobuf.size -= deadspace;
demobuf.end -= deadspace; demobuf.end -= deadspace;
@ -3363,22 +3328,6 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum)
// Load "mapmusrng" used for altmusic selection // Load "mapmusrng" used for altmusic selection
mapmusrng = READUINT8(demobuf.p); 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(&oldcmd,0,sizeof(oldcmd));
memset(&oldghost,0,sizeof(oldghost)); memset(&oldghost,0,sizeof(oldghost));
memset(&ghostext,0,sizeof(ghostext)); memset(&ghostext,0,sizeof(ghostext));
@ -3607,6 +3556,22 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum)
players[p].lastfakeskin = lastfakeskin[p]; 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; demo.deferstart = true;
CV_StealthSetValue(&cv_playbackspeed, 1); CV_StealthSetValue(&cv_playbackspeed, 1);
@ -3756,14 +3721,6 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname)
p++; // mapmusrng 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 p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once
// any invalidating flags? // any invalidating flags?
@ -3811,6 +3768,14 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname)
return; 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 = static_cast<demoghost*>(Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL));
gh->sizes = ghostsizes; gh->sizes = ghostsizes;
@ -4173,7 +4138,7 @@ boolean G_CheckDemoStatus(void)
// Keep the demo open and don't boot to intermission // Keep the demo open and don't boot to intermission
// YET, pause demo playback. // YET, pause demo playback.
if (!demo.waitingfortally && modeattacking && exitcountdown) if (!demo.waitingfortally && modeattacking && exitcountdown)
demo.waitingfortally = true; ;
else if (!demo.attract) else if (!demo.attract)
G_FinishExitLevel(); G_FinishExitLevel();
else else
@ -4191,6 +4156,8 @@ boolean G_CheckDemoStatus(void)
D_SetDeferredStartTitle(true); D_SetDeferredStartTitle(true);
} }
demo.waitingfortally = true; // if we've returned early for some reason...
return true; return true;
} }
@ -4235,6 +4202,13 @@ void G_SaveDemo(void)
if (currentMenu == &TitleEntryDef) if (currentMenu == &TitleEntryDef)
M_ClearMenus(true); 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. // Ensure extrainfo pointer is always available, even if no info is present.
if (demoinfo_p && *(UINT32 *)demoinfo_p == 0) if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
{ {

View file

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

View file

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