// SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- // Copyright (C) 1993-1996 by id Software, Inc. // Copyright (C) 1998-2000 by DooM Legacy Team. // Copyright (C) 1999-2020 by Sonic Team Junior. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file g_demo.c /// \brief Demo recording and playback #include "doomdef.h" #include "console.h" #include "d_main.h" #include "d_player.h" #include "d_clisrv.h" #include "p_setup.h" #include "i_system.h" #include "m_random.h" #include "p_local.h" #include "r_draw.h" #include "r_main.h" #include "g_game.h" #include "g_demo.h" #include "m_misc.h" #include "m_menu.h" #include "m_argv.h" #include "hu_stuff.h" #include "z_zone.h" #include "i_video.h" #include "byteptr.h" #include "i_joy.h" #include "r_local.h" #include "r_skins.h" #include "y_inter.h" #include "v_video.h" #include "lua_hook.h" #include "md5.h" // demo checksums boolean timingdemo; // if true, exit with report on completion boolean nodrawers; // for comparative timing purposes boolean noblit; // for comparative timing purposes tic_t demostarttime; // for comparative timing purposes static char demoname[64]; boolean demorecording; boolean demoplayback; boolean titledemo; // Title Screen demo can be cancelled by any key static UINT8 *demobuffer = NULL; static UINT8 *demo_p, *demotime_p; static UINT8 *demoend; static UINT8 demoflags; static UINT16 demoversion; boolean singledemo; // quit after playing a demo from cmdline boolean demo_start; // don't start playing demo right away boolean demosynced = true; // console warning message boolean metalrecording; // recording as metal sonic mobj_t *metalplayback; static UINT8 *metalbuffer = NULL; static UINT8 *metal_p; static UINT16 metalversion; // extra data stuff (events registered this frame while recording) static struct { UINT8 flags; // EZT flags // EZT_COLOR UINT16 color, lastcolor; // EZT_SCALE fixed_t scale, lastscale; // EZT_HIT UINT16 hits; mobj_t **hitlist; } ghostext; // Your naming conventions are stupid and useless. // There is no conflict here. typedef struct demoghost { UINT8 checksum[16]; UINT8 *buffer, *p, fadein; UINT16 color; UINT16 version; mobj_t oldmo, *mo; struct demoghost *next; } demoghost; demoghost *ghosts = NULL; // // DEMO RECORDING // #define DEMOVERSION 0x000e #define DEMOHEADER "\xF0" "SRB2Replay" "\x0F" #define DF_GHOST 0x01 // This demo contains ghost data too! #define DF_RECORDATTACK 0x02 // This demo is from record attack and contains its final completion time, score, and rings! #define DF_NIGHTSATTACK 0x04 // This demo is from NiGHTS attack and contains its time left, score, and mares! #define DF_ATTACKMASK 0x06 // This demo is from ??? attack and contains ??? #define DF_ATTACKSHIFT 1 // For demos #define ZT_FWD 0x01 #define ZT_SIDE 0x02 #define ZT_ANGLE 0x04 #define ZT_BUTTONS 0x08 #define ZT_AIMING 0x10 #define DEMOMARKER 0x80 // demoend #define METALDEATH 0x44 #define METALSNICE 0x69 static ticcmd_t oldcmd; // For Metal Sonic and time attack ghosts #define GZT_XYZ 0x01 #define GZT_MOMXY 0x02 #define GZT_MOMZ 0x04 #define GZT_ANGLE 0x08 #define GZT_FRAME 0x10 // Animation frame #define GZT_SPR2 0x20 // Player animations #define GZT_EXTRA 0x40 #define GZT_FOLLOW 0x80 // Followmobj // GZT_EXTRA flags #define EZT_THOK 0x01 // Spawned a thok object #define EZT_SPIN 0x02 // Because one type of thok object apparently wasn't enough #define EZT_REV 0x03 // And two types wasn't enough either yet #define EZT_THOKMASK 0x03 #define EZT_COLOR 0x04 // Changed color (Super transformation, Mario fireflowers/invulnerability, etc.) #define EZT_FLIP 0x08 // Reversed gravity #define EZT_SCALE 0x10 // Changed size #define EZT_HIT 0x20 // Damaged a mobj #define EZT_SPRITE 0x40 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever) #define EZT_HEIGHT 0x80 // Changed height // GZT_FOLLOW flags #define FZT_SPAWNED 0x01 // just been spawned #define FZT_SKIN 0x02 // has skin #define FZT_LINKDRAW 0x04 // has linkdraw (combine with spawned only) #define FZT_COLORIZED 0x08 // colorized (ditto) #define FZT_SCALE 0x10 // different scale to object // spare FZT slots 0x20 to 0x80 static mobj_t oldmetal, oldghost; void G_SaveMetal(UINT8 **buffer) { I_Assert(buffer != NULL && *buffer != NULL); WRITEUINT32(*buffer, metal_p - metalbuffer); } void G_LoadMetal(UINT8 **buffer) { I_Assert(buffer != NULL && *buffer != NULL); G_DoPlayMetal(); metal_p = metalbuffer + READUINT32(*buffer); } void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum) { UINT8 ziptic; if (!demo_p || !demo_start) return; ziptic = READUINT8(demo_p); if (ziptic & ZT_FWD) oldcmd.forwardmove = READSINT8(demo_p); if (ziptic & ZT_SIDE) oldcmd.sidemove = READSINT8(demo_p); if (ziptic & ZT_ANGLE) oldcmd.angleturn = READINT16(demo_p); if (ziptic & ZT_BUTTONS) oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT)); if (ziptic & ZT_AIMING) oldcmd.aiming = READINT16(demo_p); G_CopyTiccmd(cmd, &oldcmd, 1); players[playernum].angleturn = cmd->angleturn; if (!(demoflags & DF_GHOST) && *demo_p == DEMOMARKER) { // end of demo data stream G_CheckDemoStatus(); return; } } void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum) { char ziptic = 0; UINT8 *ziptic_p; (void)playernum; if (!demo_p) return; ziptic_p = demo_p++; // the ziptic, written at the end of this function if (cmd->forwardmove != oldcmd.forwardmove) { WRITEUINT8(demo_p,cmd->forwardmove); oldcmd.forwardmove = cmd->forwardmove; ziptic |= ZT_FWD; } if (cmd->sidemove != oldcmd.sidemove) { WRITEUINT8(demo_p,cmd->sidemove); oldcmd.sidemove = cmd->sidemove; ziptic |= ZT_SIDE; } if (cmd->angleturn != oldcmd.angleturn) { WRITEINT16(demo_p,cmd->angleturn); oldcmd.angleturn = cmd->angleturn; ziptic |= ZT_ANGLE; } if (cmd->buttons != oldcmd.buttons) { WRITEUINT16(demo_p,cmd->buttons); oldcmd.buttons = cmd->buttons; ziptic |= ZT_BUTTONS; } if (cmd->aiming != oldcmd.aiming) { WRITEINT16(demo_p,cmd->aiming); oldcmd.aiming = cmd->aiming; ziptic |= ZT_AIMING; } *ziptic_p = ziptic; // attention here for the ticcmd size! // latest demos with mouse aiming byte in ticcmd if (!(demoflags & DF_GHOST) && ziptic_p > demoend - 9) { G_CheckDemoStatus(); // no more space return; } } void G_GhostAddThok(void) { if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST))) return; ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_THOK; } void G_GhostAddSpin(void) { if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST))) return; ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_SPIN; } void G_GhostAddRev(void) { if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST))) return; ghostext.flags = (ghostext.flags & ~EZT_THOKMASK) | EZT_REV; } void G_GhostAddFlip(void) { if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST))) return; ghostext.flags |= EZT_FLIP; } void G_GhostAddColor(ghostcolor_t color) { if (!demorecording || !(demoflags & DF_GHOST)) return; if (ghostext.lastcolor == (UINT16)color) { ghostext.flags &= ~EZT_COLOR; return; } ghostext.flags |= EZT_COLOR; ghostext.color = (UINT16)color; } void G_GhostAddScale(fixed_t scale) { if (!metalrecording && (!demorecording || !(demoflags & DF_GHOST))) return; if (ghostext.lastscale == scale) { ghostext.flags &= ~EZT_SCALE; return; } ghostext.flags |= EZT_SCALE; ghostext.scale = scale; } void G_GhostAddHit(mobj_t *victim) { if (!demorecording || !(demoflags & DF_GHOST)) return; ghostext.flags |= EZT_HIT; ghostext.hits++; ghostext.hitlist = Z_Realloc(ghostext.hitlist, ghostext.hits * sizeof(mobj_t *), PU_LEVEL, NULL); ghostext.hitlist[ghostext.hits-1] = victim; } void G_WriteGhostTic(mobj_t *ghost) { char ziptic = 0; UINT8 *ziptic_p; UINT32 i; fixed_t height; if (!demo_p) return; if (!(demoflags & DF_GHOST)) return; // No ghost data to write. ziptic_p = demo_p++; // the ziptic, written at the end of this function #define MAXMOM (0xFFFF<<8) // GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic. if (abs(ghost->x-oldghost.x) > MAXMOM || abs(ghost->y-oldghost.y) > MAXMOM || abs(ghost->z-oldghost.z) > MAXMOM) { oldghost.x = ghost->x; oldghost.y = ghost->y; oldghost.z = ghost->z; ziptic |= GZT_XYZ; WRITEFIXED(demo_p,oldghost.x); WRITEFIXED(demo_p,oldghost.y); WRITEFIXED(demo_p,oldghost.z); } else { // For moving normally: fixed_t momx = ghost->x-oldghost.x; fixed_t momy = ghost->y-oldghost.y; if (momx != oldghost.momx || momy != oldghost.momy) { oldghost.momx = momx; oldghost.momy = momy; ziptic |= GZT_MOMXY; WRITEFIXED(demo_p,momx); WRITEFIXED(demo_p,momy); } momx = ghost->z-oldghost.z; if (momx != oldghost.momz) { oldghost.momz = momx; ziptic |= GZT_MOMZ; WRITEFIXED(demo_p,momx); } // This SHOULD set oldghost.x/y/z to match ghost->x/y/z oldghost.x += oldghost.momx; oldghost.y += oldghost.momy; oldghost.z += oldghost.momz; } #undef MAXMOM // Only store the 8 most relevant bits of angle // because exact values aren't too easy to discern to begin with when only 8 angles have different sprites // and it does not affect this mode of movement at all anyway. if (ghost->player && ghost->player->drawangle>>24 != oldghost.angle) { oldghost.angle = ghost->player->drawangle>>24; ziptic |= GZT_ANGLE; WRITEUINT8(demo_p,oldghost.angle); } // Store the sprite frame. if ((ghost->frame & FF_FRAMEMASK) != oldghost.frame) { oldghost.frame = (ghost->frame & FF_FRAMEMASK); ziptic |= GZT_FRAME; WRITEUINT8(demo_p,oldghost.frame); } if (ghost->sprite == SPR_PLAY && ghost->sprite2 != oldghost.sprite2) { oldghost.sprite2 = ghost->sprite2; ziptic |= GZT_SPR2; WRITEUINT8(demo_p,oldghost.sprite2); } // Check for sprite set changes if (ghost->sprite != oldghost.sprite) { oldghost.sprite = ghost->sprite; ghostext.flags |= EZT_SPRITE; } if ((height = FixedDiv(ghost->height, ghost->scale)) != oldghost.height) { oldghost.height = height; ghostext.flags |= EZT_HEIGHT; } if (ghostext.flags) { ziptic |= GZT_EXTRA; if (ghostext.color == ghostext.lastcolor) ghostext.flags &= ~EZT_COLOR; if (ghostext.scale == ghostext.lastscale) ghostext.flags &= ~EZT_SCALE; WRITEUINT8(demo_p,ghostext.flags); if (ghostext.flags & EZT_COLOR) { WRITEUINT16(demo_p,ghostext.color); ghostext.lastcolor = ghostext.color; } if (ghostext.flags & EZT_SCALE) { WRITEFIXED(demo_p,ghostext.scale); ghostext.lastscale = ghostext.scale; } if (ghostext.flags & EZT_HIT) { WRITEUINT16(demo_p,ghostext.hits); for (i = 0; i < ghostext.hits; i++) { mobj_t *mo = ghostext.hitlist[i]; //WRITEUINT32(demo_p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.) WRITEUINT32(demo_p,mo->type); WRITEUINT16(demo_p,(UINT16)mo->health); WRITEFIXED(demo_p,mo->x); WRITEFIXED(demo_p,mo->y); WRITEFIXED(demo_p,mo->z); WRITEANGLE(demo_p,mo->angle); } Z_Free(ghostext.hitlist); ghostext.hits = 0; ghostext.hitlist = NULL; } if (ghostext.flags & EZT_SPRITE) WRITEUINT16(demo_p,oldghost.sprite); if (ghostext.flags & EZT_HEIGHT) { WRITEFIXED(demo_p, height); } ghostext.flags = 0; } if (ghost->player && ghost->player->followmobj && !(ghost->player->followmobj->sprite == SPR_NULL || (ghost->player->followmobj->flags2 & MF2_DONTDRAW))) // bloats tails runs but what can ya do { fixed_t temp; UINT8 *followtic_p = demo_p++; UINT8 followtic = 0; ziptic |= GZT_FOLLOW; if (ghost->player->followmobj->skin) followtic |= FZT_SKIN; if (!(oldghost.flags2 & MF2_AMBUSH)) { followtic |= FZT_SPAWNED; WRITEINT16(demo_p,ghost->player->followmobj->info->height>>FRACBITS); if (ghost->player->followmobj->flags2 & MF2_LINKDRAW) followtic |= FZT_LINKDRAW; if (ghost->player->followmobj->colorized) followtic |= FZT_COLORIZED; if (followtic & FZT_SKIN) WRITEUINT8(demo_p,(UINT8)(((skin_t *)(ghost->player->followmobj->skin))-skins)); oldghost.flags2 |= MF2_AMBUSH; } if (ghost->player->followmobj->scale != ghost->scale) { followtic |= FZT_SCALE; WRITEFIXED(demo_p,ghost->player->followmobj->scale); } temp = ghost->player->followmobj->x-ghost->x; WRITEFIXED(demo_p,temp); temp = ghost->player->followmobj->y-ghost->y; WRITEFIXED(demo_p,temp); temp = ghost->player->followmobj->z-ghost->z; WRITEFIXED(demo_p,temp); if (followtic & FZT_SKIN) WRITEUINT8(demo_p,ghost->player->followmobj->sprite2); WRITEUINT16(demo_p,ghost->player->followmobj->sprite); WRITEUINT8(demo_p,(ghost->player->followmobj->frame & FF_FRAMEMASK)); WRITEUINT16(demo_p,ghost->player->followmobj->color); *followtic_p = followtic; } else oldghost.flags2 &= ~MF2_AMBUSH; *ziptic_p = ziptic; // attention here for the ticcmd size! // latest demos with mouse aiming byte in ticcmd if (demo_p >= demoend - (13 + 9 + 9)) { G_CheckDemoStatus(); // no more space return; } } // Uses ghost data to do consistency checks on your position. // This fixes desynchronising demos when fighting eggman. void G_ConsGhostTic(void) { UINT8 ziptic; UINT16 px,py,pz,gx,gy,gz; mobj_t *testmo; if (!demo_p || !demo_start) return; if (!(demoflags & DF_GHOST)) return; // No ghost data to use. testmo = players[0].mo; // Grab ghost data. ziptic = READUINT8(demo_p); if (ziptic & GZT_XYZ) { oldghost.x = READFIXED(demo_p); oldghost.y = READFIXED(demo_p); oldghost.z = READFIXED(demo_p); } else { if (ziptic & GZT_MOMXY) { oldghost.momx = (demoversion < 0x000e) ? READINT16(demo_p)<<8 : READFIXED(demo_p); oldghost.momy = (demoversion < 0x000e) ? READINT16(demo_p)<<8 : READFIXED(demo_p); } if (ziptic & GZT_MOMZ) oldghost.momz = (demoversion < 0x000e) ? READINT16(demo_p)<<8 : READFIXED(demo_p); oldghost.x += oldghost.momx; oldghost.y += oldghost.momy; oldghost.z += oldghost.momz; } if (ziptic & GZT_ANGLE) demo_p++; if (ziptic & GZT_FRAME) demo_p++; if (ziptic & GZT_SPR2) demo_p++; if (ziptic & GZT_EXTRA) { // But wait, there's more! UINT8 xziptic = READUINT8(demo_p); if (xziptic & EZT_COLOR) demo_p += (demoversion==0x000c) ? 1 : sizeof(UINT16); if (xziptic & EZT_SCALE) demo_p += sizeof(fixed_t); if (xziptic & EZT_HIT) { // Resync mob damage. UINT16 i, count = READUINT16(demo_p); thinker_t *th; mobj_t *mobj; UINT32 type; UINT16 health; fixed_t x; fixed_t y; fixed_t z; for (i = 0; i < count; i++) { //demo_p += 4; // reserved. type = READUINT32(demo_p); health = READUINT16(demo_p); x = READFIXED(demo_p); y = READFIXED(demo_p); z = READFIXED(demo_p); demo_p += sizeof(angle_t); // angle, unnecessary for cons. mobj = NULL; for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) { if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) continue; mobj = (mobj_t *)th; if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z) break; } if (th != &thlist[THINK_MOBJ] && mobj->health != health) // Wasn't damaged?! This is desync! Fix it! { if (demosynced) CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n")); demosynced = false; P_DamageMobj(mobj, players[0].mo, players[0].mo, 1, 0); } } } if (xziptic & EZT_SPRITE) demo_p += sizeof(UINT16); if (xziptic & EZT_HEIGHT) demo_p += (demoversion < 0x000e) ? sizeof(INT16) : sizeof(fixed_t); } if (ziptic & GZT_FOLLOW) { // Even more... UINT8 followtic = READUINT8(demo_p); if (followtic & FZT_SPAWNED) { demo_p += sizeof(INT16); if (followtic & FZT_SKIN) demo_p++; } if (followtic & FZT_SCALE) demo_p += sizeof(fixed_t); // momx, momy and momz demo_p += (demoversion < 0x000e) ? sizeof(INT16) * 3 : sizeof(fixed_t) * 3; if (followtic & FZT_SKIN) demo_p++; demo_p += sizeof(UINT16); demo_p++; demo_p += (demoversion==0x000c) ? 1 : sizeof(UINT16); } // Re-synchronise px = testmo->x>>FRACBITS; py = testmo->y>>FRACBITS; pz = testmo->z>>FRACBITS; gx = oldghost.x>>FRACBITS; gy = oldghost.y>>FRACBITS; gz = oldghost.z>>FRACBITS; if (px != gx || py != gy || pz != gz) { if (demosynced) CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced!\n")); demosynced = false; P_UnsetThingPosition(testmo); testmo->x = oldghost.x; testmo->y = oldghost.y; P_SetThingPosition(testmo); testmo->z = oldghost.z; } if (*demo_p == DEMOMARKER) { // end of demo data stream G_CheckDemoStatus(); return; } } void G_GhostTicker(void) { demoghost *g,*p; for(g = ghosts, p = NULL; g; g = g->next) { // Skip normal demo data. UINT8 ziptic = READUINT8(g->p); UINT8 xziptic = 0; if (ziptic & ZT_FWD) g->p++; if (ziptic & ZT_SIDE) g->p++; if (ziptic & ZT_ANGLE) g->p += 2; if (ziptic & ZT_BUTTONS) g->p += 2; if (ziptic & ZT_AIMING) g->p += 2; // Grab ghost data. ziptic = READUINT8(g->p); if (ziptic & GZT_XYZ) { g->oldmo.x = READFIXED(g->p); g->oldmo.y = READFIXED(g->p); g->oldmo.z = READFIXED(g->p); } else { if (ziptic & GZT_MOMXY) { g->oldmo.momx = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); g->oldmo.momy = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); } if (ziptic & GZT_MOMZ) g->oldmo.momz = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); g->oldmo.x += g->oldmo.momx; g->oldmo.y += g->oldmo.momy; g->oldmo.z += g->oldmo.momz; } if (ziptic & GZT_ANGLE) g->mo->angle = READUINT8(g->p)<<24; if (ziptic & GZT_FRAME) g->oldmo.frame = READUINT8(g->p); if (ziptic & GZT_SPR2) g->oldmo.sprite2 = READUINT8(g->p); // Update ghost P_UnsetThingPosition(g->mo); g->mo->x = g->oldmo.x; g->mo->y = g->oldmo.y; g->mo->z = g->oldmo.z; P_SetThingPosition(g->mo); g->mo->frame = g->oldmo.frame | tr_trans30<fadein) { g->mo->frame += (((--g->fadein)/6)<fadein is bad, and it's only set once, so... g->mo->flags2 &= ~MF2_DONTDRAW; } g->mo->sprite2 = g->oldmo.sprite2; if (ziptic & GZT_EXTRA) { // But wait, there's more! xziptic = READUINT8(g->p); if (xziptic & EZT_COLOR) { g->color = (g->version==0x000c) ? READUINT8(g->p) : READUINT16(g->p); switch(g->color) { default: case GHC_RETURNSKIN: g->mo->skin = g->oldmo.skin; /* FALLTHRU */ case GHC_NORMAL: // Go back to skin color g->mo->color = g->oldmo.color; break; // Handled below case GHC_SUPER: case GHC_INVINCIBLE: break; case GHC_FIREFLOWER: // Fireflower g->mo->color = SKINCOLOR_WHITE; break; case GHC_NIGHTSSKIN: // not actually a colour g->mo->skin = &skins[DEFAULTNIGHTSSKIN]; break; } } if (xziptic & EZT_FLIP) g->mo->eflags ^= MFE_VERTICALFLIP; if (xziptic & EZT_SCALE) { g->mo->destscale = READFIXED(g->p); if (g->mo->destscale != g->mo->scale) P_SetScale(g->mo, g->mo->destscale); } if (xziptic & EZT_THOKMASK) { // Let's only spawn ONE of these per frame, thanks. mobj_t *mobj; UINT32 type = MT_NULL; if (g->mo->skin) { skin_t *skin = (skin_t *)g->mo->skin; switch (xziptic & EZT_THOKMASK) { case EZT_THOK: type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem; break; case EZT_SPIN: type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem; break; case EZT_REV: type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem; break; } } if (type != MT_NULL) { if (type == MT_GHOST) { mobj = P_SpawnGhostMobj(g->mo); // does a large portion of the work for us mobj->frame = (mobj->frame & ~FF_FRAMEMASK)|tr_trans60<mo, 0, 0, -FixedDiv(FixedMul(g->mo->info->height, g->mo->scale) - g->mo->height,3*FRACUNIT), MT_THOK); mobj->sprite = states[mobjinfo[type].spawnstate].sprite; mobj->frame = (states[mobjinfo[type].spawnstate].frame & FF_FRAMEMASK) | tr_trans60<color = g->mo->color; mobj->skin = g->mo->skin; P_SetScale(mobj, (mobj->destscale = g->mo->scale)); if (type == MT_THOK) // spintrail-specific modification for MT_THOK { mobj->frame = FF_TRANS80; mobj->fuse = mobj->tics; } mobj->tics = -1; // nope. } mobj->floorz = mobj->z; mobj->ceilingz = mobj->z+mobj->height; P_UnsetThingPosition(mobj); mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up... P_SetThingPosition(mobj); if (!mobj->fuse) mobj->fuse = 8; P_SetTarget(&mobj->target, g->mo); } } if (xziptic & EZT_HIT) { // Spawn hit poofs for killing things! UINT16 i, count = READUINT16(g->p), health; UINT32 type; fixed_t x,y,z; angle_t angle; mobj_t *poof; for (i = 0; i < count; i++) { //g->p += 4; // reserved type = READUINT32(g->p); health = READUINT16(g->p); x = READFIXED(g->p); y = READFIXED(g->p); z = READFIXED(g->p); angle = READANGLE(g->p); if (!(mobjinfo[type].flags & MF_SHOOTABLE) || !(mobjinfo[type].flags & (MF_ENEMY|MF_MONITOR)) || health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad. continue; poof = P_SpawnMobj(x, y, z, MT_GHOST); poof->angle = angle; poof->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up... poof->health = 0; P_SetMobjStateNF(poof, S_XPLD1); } } if (xziptic & EZT_SPRITE) g->mo->sprite = READUINT16(g->p); if (xziptic & EZT_HEIGHT) { fixed_t temp = (g->version < 0x000e) ? READINT16(g->p)<p); g->mo->height = FixedMul(temp, g->mo->scale); } } // Tick ghost colors (Super and Mario Invincibility flashing) switch(g->color) { case GHC_SUPER: // Super (P_DoSuperStuff) if (g->mo->skin) { skin_t *skin = (skin_t *)g->mo->skin; g->mo->color = skin->supercolor; } else g->mo->color = SKINCOLOR_SUPERGOLD1; g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4); break; case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer) g->mo->color = (UINT16)(SKINCOLOR_RUBY + (leveltime % (FIRSTSUPERCOLOR - SKINCOLOR_RUBY))); // Passes through all saturated colours break; default: break; } #define follow g->mo->tracer if (ziptic & GZT_FOLLOW) { // Even more... UINT8 followtic = READUINT8(g->p); fixed_t temp; if (followtic & FZT_SPAWNED) { if (follow) P_RemoveMobj(follow); P_SetTarget(&follow, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST)); P_SetTarget(&follow->tracer, g->mo); follow->tics = -1; temp = READINT16(g->p)<height = FixedMul(follow->scale, temp); if (followtic & FZT_LINKDRAW) follow->flags2 |= MF2_LINKDRAW; if (followtic & FZT_COLORIZED) follow->colorized = true; if (followtic & FZT_SKIN) follow->skin = &skins[READUINT8(g->p)]; } if (follow) { if (followtic & FZT_SCALE) follow->destscale = READFIXED(g->p); else follow->destscale = g->mo->destscale; if (follow->destscale != follow->scale) P_SetScale(follow, follow->destscale); P_UnsetThingPosition(follow); temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); follow->x = g->mo->x + temp; temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); follow->y = g->mo->y + temp; temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p); follow->z = g->mo->z + temp; P_SetThingPosition(follow); if (followtic & FZT_SKIN) follow->sprite2 = READUINT8(g->p); else follow->sprite2 = 0; follow->sprite = READUINT16(g->p); follow->frame = (READUINT8(g->p)) | (g->mo->frame & FF_TRANSMASK); follow->angle = g->mo->angle; follow->color = (g->version==0x000c) ? READUINT8(g->p) : READUINT16(g->p); if (!(followtic & FZT_SPAWNED)) { if (xziptic & EZT_FLIP) { follow->flags2 ^= MF2_OBJECTFLIP; follow->eflags ^= MFE_VERTICALFLIP; } } } } else if (follow) { P_RemoveMobj(follow); P_SetTarget(&follow, NULL); } // Demo ends after ghost data. if (*g->p == DEMOMARKER) { g->mo->momx = g->mo->momy = g->mo->momz = 0; #if 1 // freeze frame (maybe more useful for time attackers) g->mo->colorized = true; if (follow) follow->colorized = true; #else // dissapearing act g->mo->fuse = TICRATE; if (follow) follow->fuse = TICRATE; #endif if (p) p->next = g->next; else ghosts = g->next; Z_Free(g); continue; } p = g; #undef follow } } void G_ReadMetalTic(mobj_t *metal) { UINT8 ziptic; UINT8 xziptic = 0; if (!metal_p) return; if (!metal->health) { G_StopMetalDemo(); return; } switch (*metal_p) { case METALSNICE: break; case METALDEATH: if (metal->tracer) P_RemoveMobj(metal->tracer); P_KillMobj(metal, NULL, NULL, 0); /* FALLTHRU */ case DEMOMARKER: default: // end of demo data stream G_StopMetalDemo(); return; } metal_p++; ziptic = READUINT8(metal_p); // Read changes from the tic if (ziptic & GZT_XYZ) { // make sure the values are read in the right order oldmetal.x = READFIXED(metal_p); oldmetal.y = READFIXED(metal_p); oldmetal.z = READFIXED(metal_p); P_TeleportMove(metal, oldmetal.x, oldmetal.y, oldmetal.z); oldmetal.x = metal->x; oldmetal.y = metal->y; oldmetal.z = metal->z; } else { if (ziptic & GZT_MOMXY) { oldmetal.momx = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); oldmetal.momy = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); } if (ziptic & GZT_MOMZ) oldmetal.momz = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); oldmetal.x += oldmetal.momx; oldmetal.y += oldmetal.momy; oldmetal.z += oldmetal.momz; } if (ziptic & GZT_ANGLE) metal->angle = READUINT8(metal_p)<<24; if (ziptic & GZT_FRAME) oldmetal.frame = READUINT32(metal_p); if (ziptic & GZT_SPR2) oldmetal.sprite2 = READUINT8(metal_p); // Set movement, position, and angle // oldmetal contains where you're supposed to be. metal->momx = oldmetal.momx; metal->momy = oldmetal.momy; metal->momz = oldmetal.momz; P_UnsetThingPosition(metal); metal->x = oldmetal.x; metal->y = oldmetal.y; metal->z = oldmetal.z; P_SetThingPosition(metal); metal->frame = oldmetal.frame; metal->sprite2 = oldmetal.sprite2; if (ziptic & GZT_EXTRA) { // But wait, there's more! xziptic = READUINT8(metal_p); if (xziptic & EZT_FLIP) { metal->eflags ^= MFE_VERTICALFLIP; metal->flags2 ^= MF2_OBJECTFLIP; } if (xziptic & EZT_SCALE) { metal->destscale = READFIXED(metal_p); if (metal->destscale != metal->scale) P_SetScale(metal, metal->destscale); } if (xziptic & EZT_THOKMASK) { // Let's only spawn ONE of these per frame, thanks. mobj_t *mobj; UINT32 type = MT_NULL; if (metal->skin) { skin_t *skin = (skin_t *)metal->skin; switch (xziptic & EZT_THOKMASK) { case EZT_THOK: type = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem; break; case EZT_SPIN: type = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem; break; case EZT_REV: type = skin->revitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem; break; } } if (type != MT_NULL) { if (type == MT_GHOST) { mobj = P_SpawnGhostMobj(metal); // does a large portion of the work for us } else { mobj = P_SpawnMobjFromMobj(metal, 0, 0, -FixedDiv(FixedMul(metal->info->height, metal->scale) - metal->height,3*FRACUNIT), MT_THOK); mobj->sprite = states[mobjinfo[type].spawnstate].sprite; mobj->frame = states[mobjinfo[type].spawnstate].frame; mobj->angle = metal->angle; mobj->color = metal->color; mobj->skin = metal->skin; P_SetScale(mobj, (mobj->destscale = metal->scale)); if (type == MT_THOK) // spintrail-specific modification for MT_THOK { mobj->frame = FF_TRANS70; mobj->fuse = mobj->tics; } mobj->tics = -1; // nope. } mobj->floorz = mobj->z; mobj->ceilingz = mobj->z+mobj->height; P_UnsetThingPosition(mobj); mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up... P_SetThingPosition(mobj); if (!mobj->fuse) mobj->fuse = 8; P_SetTarget(&mobj->target, metal); } } if (xziptic & EZT_SPRITE) metal->sprite = READUINT16(metal_p); if (xziptic & EZT_HEIGHT) { fixed_t temp = (metalversion < 0x000e) ? READINT16(metal_p)<height = FixedMul(temp, metal->scale); } } #define follow metal->tracer if (ziptic & GZT_FOLLOW) { // Even more... UINT8 followtic = READUINT8(metal_p); fixed_t temp; if (followtic & FZT_SPAWNED) { if (follow) P_RemoveMobj(follow); P_SetTarget(&follow, P_SpawnMobjFromMobj(metal, 0, 0, 0, MT_GHOST)); P_SetTarget(&follow->tracer, metal); follow->tics = -1; temp = READINT16(metal_p)<height = FixedMul(follow->scale, temp); if (followtic & FZT_LINKDRAW) follow->flags2 |= MF2_LINKDRAW; if (followtic & FZT_COLORIZED) follow->colorized = true; if (followtic & FZT_SKIN) follow->skin = &skins[READUINT8(metal_p)]; } if (follow) { if (followtic & FZT_SCALE) follow->destscale = READFIXED(metal_p); else follow->destscale = metal->destscale; if (follow->destscale != follow->scale) P_SetScale(follow, follow->destscale); P_UnsetThingPosition(follow); temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); follow->x = metal->x + temp; temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); follow->y = metal->y + temp; temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p); follow->z = metal->z + temp; P_SetThingPosition(follow); if (followtic & FZT_SKIN) follow->sprite2 = READUINT8(metal_p); else follow->sprite2 = 0; follow->sprite = READUINT16(metal_p); follow->frame = READUINT32(metal_p); // NOT & FF_FRAMEMASK here, so 32 bits follow->angle = metal->angle; follow->color = (metalversion==0x000c) ? READUINT8(metal_p) : READUINT16(metal_p); if (!(followtic & FZT_SPAWNED)) { if (xziptic & EZT_FLIP) { follow->flags2 ^= MF2_OBJECTFLIP; follow->eflags ^= MFE_VERTICALFLIP; } } } } else if (follow) { P_RemoveMobj(follow); P_SetTarget(&follow, NULL); } #undef follow } void G_WriteMetalTic(mobj_t *metal) { UINT8 ziptic = 0; UINT8 *ziptic_p; fixed_t height; if (!demo_p) // demo_p will be NULL until the race start linedef executor is activated! return; WRITEUINT8(demo_p, METALSNICE); ziptic_p = demo_p++; // the ziptic, written at the end of this function #define MAXMOM (0xFFFF<<8) // GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic. if (abs(metal->x-oldmetal.x) > MAXMOM || abs(metal->y-oldmetal.y) > MAXMOM || abs(metal->z-oldmetal.z) > MAXMOM) { oldmetal.x = metal->x; oldmetal.y = metal->y; oldmetal.z = metal->z; ziptic |= GZT_XYZ; WRITEFIXED(demo_p,oldmetal.x); WRITEFIXED(demo_p,oldmetal.y); WRITEFIXED(demo_p,oldmetal.z); } else { // For moving normally: // Store movement as a fixed value fixed_t momx = metal->x-oldmetal.x; fixed_t momy = metal->y-oldmetal.y; if (momx != oldmetal.momx || momy != oldmetal.momy) { oldmetal.momx = momx; oldmetal.momy = momy; ziptic |= GZT_MOMXY; WRITEFIXED(demo_p,momx); WRITEFIXED(demo_p,momy); } momx = metal->z-oldmetal.z; if (momx != oldmetal.momz) { oldmetal.momz = momx; ziptic |= GZT_MOMZ; WRITEFIXED(demo_p,momx); } // This SHOULD set oldmetal.x/y/z to match metal->x/y/z oldmetal.x += oldmetal.momx; oldmetal.y += oldmetal.momy; oldmetal.z += oldmetal.momz; } #undef MAXMOM // Only store the 8 most relevant bits of angle // because exact values aren't too easy to discern to begin with when only 8 angles have different sprites // and it does not affect movement at all anyway. if (metal->player && metal->player->drawangle>>24 != oldmetal.angle) { oldmetal.angle = metal->player->drawangle>>24; ziptic |= GZT_ANGLE; WRITEUINT8(demo_p,oldmetal.angle); } // Store the sprite frame. if ((metal->frame & FF_FRAMEMASK) != oldmetal.frame) { oldmetal.frame = metal->frame; // NOT & FF_FRAMEMASK here, so 32 bits ziptic |= GZT_FRAME; WRITEUINT32(demo_p,oldmetal.frame); } if (metal->sprite == SPR_PLAY && metal->sprite2 != oldmetal.sprite2) { oldmetal.sprite2 = metal->sprite2; ziptic |= GZT_SPR2; WRITEUINT8(demo_p,oldmetal.sprite2); } // Check for sprite set changes if (metal->sprite != oldmetal.sprite) { oldmetal.sprite = metal->sprite; ghostext.flags |= EZT_SPRITE; } if ((height = FixedDiv(metal->height, metal->scale)) != oldmetal.height) { oldmetal.height = height; ghostext.flags |= EZT_HEIGHT; } if (ghostext.flags & ~(EZT_COLOR|EZT_HIT)) // these two aren't handled by metal ever { ziptic |= GZT_EXTRA; if (ghostext.scale == ghostext.lastscale) ghostext.flags &= ~EZT_SCALE; WRITEUINT8(demo_p,ghostext.flags); if (ghostext.flags & EZT_SCALE) { WRITEFIXED(demo_p,ghostext.scale); ghostext.lastscale = ghostext.scale; } if (ghostext.flags & EZT_SPRITE) WRITEUINT16(demo_p,oldmetal.sprite); if (ghostext.flags & EZT_HEIGHT) { WRITEFIXED(demo_p, height); } ghostext.flags = 0; } if (metal->player && metal->player->followmobj && !(metal->player->followmobj->sprite == SPR_NULL || (metal->player->followmobj->flags2 & MF2_DONTDRAW))) { fixed_t temp; UINT8 *followtic_p = demo_p++; UINT8 followtic = 0; ziptic |= GZT_FOLLOW; if (metal->player->followmobj->skin) followtic |= FZT_SKIN; if (!(oldmetal.flags2 & MF2_AMBUSH)) { followtic |= FZT_SPAWNED; WRITEINT16(demo_p,metal->player->followmobj->info->height>>FRACBITS); if (metal->player->followmobj->flags2 & MF2_LINKDRAW) followtic |= FZT_LINKDRAW; if (metal->player->followmobj->colorized) followtic |= FZT_COLORIZED; if (followtic & FZT_SKIN) WRITEUINT8(demo_p,(UINT8)(((skin_t *)(metal->player->followmobj->skin))-skins)); oldmetal.flags2 |= MF2_AMBUSH; } if (metal->player->followmobj->scale != metal->scale) { followtic |= FZT_SCALE; WRITEFIXED(demo_p,metal->player->followmobj->scale); } temp = metal->player->followmobj->x-metal->x; WRITEFIXED(demo_p,temp); temp = metal->player->followmobj->y-metal->y; WRITEFIXED(demo_p,temp); temp = metal->player->followmobj->z-metal->z; WRITEFIXED(demo_p,temp); if (followtic & FZT_SKIN) WRITEUINT8(demo_p,metal->player->followmobj->sprite2); WRITEUINT16(demo_p,metal->player->followmobj->sprite); WRITEUINT32(demo_p,metal->player->followmobj->frame); // NOT & FF_FRAMEMASK here, so 32 bits WRITEUINT16(demo_p,metal->player->followmobj->color); *followtic_p = followtic; } else oldmetal.flags2 &= ~MF2_AMBUSH; *ziptic_p = ziptic; // attention here for the ticcmd size! // latest demos with mouse aiming byte in ticcmd if (demo_p >= demoend - 32) { G_StopMetalRecording(false); // no more space return; } } // // G_RecordDemo // void G_RecordDemo(const char *name) { INT32 maxsize; strcpy(demoname, name); strcat(demoname, ".lmp"); maxsize = 1024*1024; if (M_CheckParm("-maxdemo") && M_IsNextParm()) maxsize = atoi(M_GetNextParm()) * 1024; // if (demobuffer) // free(demobuffer); demo_p = NULL; demobuffer = malloc(maxsize); demoend = demobuffer + maxsize; demorecording = true; } void G_RecordMetal(void) { INT32 maxsize; maxsize = 1024*1024; if (M_CheckParm("-maxdemo") && M_IsNextParm()) maxsize = atoi(M_GetNextParm()) * 1024; demo_p = NULL; demobuffer = malloc(maxsize); demoend = demobuffer + maxsize; metalrecording = true; } void G_BeginRecording(void) { UINT8 i; char name[MAXCOLORNAME+1]; player_t *player = &players[consoleplayer]; if (demo_p) return; memset(name,0,sizeof(name)); demo_p = demobuffer; demoflags = DF_GHOST|(modeattacking<>DF_ATTACKSHIFT) { case ATTACKING_NONE: // 0 break; case ATTACKING_RECORD: // 1 demotime_p = demo_p; WRITEUINT32(demo_p,UINT32_MAX); // time WRITEUINT32(demo_p,0); // score WRITEUINT16(demo_p,0); // rings break; case ATTACKING_NIGHTS: // 2 demotime_p = demo_p; WRITEUINT32(demo_p,UINT32_MAX); // time WRITEUINT32(demo_p,0); // score break; default: // 3 break; } WRITEUINT32(demo_p,P_GetInitSeed()); // Name for (i = 0; i < 16 && cv_playername.string[i]; i++) name[i] = cv_playername.string[i]; for (; i < 16; i++) name[i] = '\0'; M_Memcpy(demo_p,name,16); demo_p += 16; // Skin for (i = 0; i < 16 && cv_skin.string[i]; i++) name[i] = cv_skin.string[i]; for (; i < 16; i++) name[i] = '\0'; M_Memcpy(demo_p,name,16); demo_p += 16; // Color for (i = 0; i < MAXCOLORNAME && cv_playercolor.string[i]; i++) name[i] = cv_playercolor.string[i]; for (; i < MAXCOLORNAME; i++) name[i] = '\0'; M_Memcpy(demo_p,name,MAXCOLORNAME); demo_p += MAXCOLORNAME; // Stats WRITEUINT8(demo_p,player->charability); WRITEUINT8(demo_p,player->charability2); WRITEUINT8(demo_p,player->actionspd>>FRACBITS); WRITEUINT8(demo_p,player->mindash>>FRACBITS); WRITEUINT8(demo_p,player->maxdash>>FRACBITS); WRITEUINT8(demo_p,player->normalspeed>>FRACBITS); WRITEUINT8(demo_p,player->runspeed>>FRACBITS); WRITEUINT8(demo_p,player->thrustfactor); WRITEUINT8(demo_p,player->accelstart); WRITEUINT8(demo_p,player->acceleration); WRITEFIXED(demo_p,player->height); WRITEFIXED(demo_p,player->spinheight); WRITEUINT8(demo_p,player->camerascale>>FRACBITS); WRITEUINT8(demo_p,player->shieldscale>>FRACBITS); // Trying to convert it back to % causes demo desync due to precision loss. // Don't do it. WRITEFIXED(demo_p, player->jumpfactor); // And mobjtype_t is best with UINT32 too... WRITEUINT32(demo_p, player->followitem); // Save pflag data - see SendWeaponPref() { UINT8 buf = 0; pflags_t pflags = 0; if (cv_flipcam.value) { buf |= 0x01; pflags |= PF_FLIPCAM; } if (cv_analog[0].value) { buf |= 0x02; pflags |= PF_ANALOGMODE; } if (cv_directionchar[0].value) { buf |= 0x04; pflags |= PF_DIRECTIONCHAR; } if (cv_autobrake.value) { buf |= 0x08; pflags |= PF_AUTOBRAKE; } if (cv_usejoystick.value) buf |= 0x10; CV_SetValue(&cv_showinputjoy, !!(cv_usejoystick.value)); WRITEUINT8(demo_p,buf); player->pflags = pflags; } // Save netvar data CV_SaveDemoVars(&demo_p); memset(&oldcmd,0,sizeof(oldcmd)); memset(&oldghost,0,sizeof(oldghost)); memset(&ghostext,0,sizeof(ghostext)); ghostext.lastcolor = ghostext.color = GHC_NORMAL; ghostext.lastscale = ghostext.scale = FRACUNIT; if (player->mo) { oldghost.x = player->mo->x; oldghost.y = player->mo->y; oldghost.z = player->mo->z; oldghost.angle = player->mo->angle>>24; // preticker started us gravity flipped if (player->mo->eflags & MFE_VERTICALFLIP) ghostext.flags |= EZT_FLIP; } } void G_BeginMetal(void) { mobj_t *mo = players[consoleplayer].mo; #if 0 if (demo_p) return; #endif demo_p = demobuffer; // Write header. M_Memcpy(demo_p, DEMOHEADER, 12); demo_p += 12; WRITEUINT8(demo_p,VERSION); WRITEUINT8(demo_p,SUBVERSION); WRITEUINT16(demo_p,DEMOVERSION); // demo checksum demo_p += 16; M_Memcpy(demo_p, "METL", 4); demo_p += 4; memset(&ghostext,0,sizeof(ghostext)); ghostext.lastscale = ghostext.scale = FRACUNIT; // Set up our memory. memset(&oldmetal,0,sizeof(oldmetal)); oldmetal.x = mo->x; oldmetal.y = mo->y; oldmetal.z = mo->z; oldmetal.angle = mo->angle>>24; } void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings) { if (!demorecording || !demotime_p) return; if (demoflags & DF_RECORDATTACK) { WRITEUINT32(demotime_p, ptime); WRITEUINT32(demotime_p, pscore); WRITEUINT16(demotime_p, prings); demotime_p = NULL; } else if (demoflags & DF_NIGHTSATTACK) { WRITEUINT32(demotime_p, ptime); WRITEUINT32(demotime_p, pscore); demotime_p = NULL; } } // Returns bitfield: // 1 == new demo has lower time // 2 == new demo has higher score // 4 == new demo has higher rings UINT8 G_CmpDemoTime(char *oldname, char *newname) { UINT8 *buffer,*p; UINT8 flags; UINT32 oldtime, newtime, oldscore, newscore; UINT16 oldrings, newrings, oldversion; size_t bufsize ATTRUNUSED; UINT8 c; UINT16 s ATTRUNUSED; UINT8 aflags = 0; // load the new file FIL_DefaultExtension(newname, ".lmp"); bufsize = FIL_ReadFile(newname, &buffer); I_Assert(bufsize != 0); p = buffer; // read demo header I_Assert(!memcmp(p, DEMOHEADER, 12)); p += 12; // DEMOHEADER c = READUINT8(p); // VERSION I_Assert(c == VERSION); c = READUINT8(p); // SUBVERSION I_Assert(c == SUBVERSION); s = READUINT16(p); I_Assert(s >= 0x000c); p += 16; // demo checksum I_Assert(!memcmp(p, "PLAY", 4)); p += 4; // PLAY p += 2; // gamemap p += 16; // map md5 flags = READUINT8(p); // demoflags aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK); I_Assert(aflags); if (flags & DF_RECORDATTACK) { newtime = READUINT32(p); newscore = READUINT32(p); newrings = READUINT16(p); } else if (flags & DF_NIGHTSATTACK) { newtime = READUINT32(p); newscore = READUINT32(p); newrings = 0; } else // appease compiler return 0; Z_Free(buffer); // load old file FIL_DefaultExtension(oldname, ".lmp"); if (!FIL_ReadFile(oldname, &buffer)) { CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), oldname); return UINT8_MAX; } p = buffer; // read demo header if (memcmp(p, DEMOHEADER, 12)) { CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname); Z_Free(buffer); return UINT8_MAX; } p += 12; // DEMOHEADER p++; // VERSION p++; // SUBVERSION oldversion = READUINT16(p); switch(oldversion) // demoversion { case DEMOVERSION: // latest always supported case 0x000c: // all that changed between then and now was longer color name break; // too old, cannot support. default: CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname); Z_Free(buffer); return UINT8_MAX; } p += 16; // demo checksum if (memcmp(p, "PLAY", 4)) { CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname); Z_Free(buffer); return UINT8_MAX; } p += 4; // "PLAY" if (oldversion <= 0x0008) p++; // gamemap else p += 2; // gamemap p += 16; // mapmd5 flags = READUINT8(p); if (!(flags & aflags)) { CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname); Z_Free(buffer); return UINT8_MAX; } if (flags & DF_RECORDATTACK) { oldtime = READUINT32(p); oldscore = READUINT32(p); oldrings = READUINT16(p); } else if (flags & DF_NIGHTSATTACK) { oldtime = READUINT32(p); oldscore = READUINT32(p); oldrings = 0; } else // appease compiler return UINT8_MAX; Z_Free(buffer); c = 0; if (newtime < oldtime || (newtime == oldtime && (newscore > oldscore || newrings > oldrings))) c |= 1; // Better time if (newscore > oldscore || (newscore == oldscore && newtime < oldtime)) c |= 1<<1; // Better score if (newrings > oldrings || (newrings == oldrings && newtime < oldtime)) c |= 1<<2; // Better rings return c; } // // G_PlayDemo // void G_DeferedPlayDemo(const char *name) { COM_BufAddText("playdemo \""); COM_BufAddText(name); COM_BufAddText("\"\n"); } // // Start a demo from a .LMP file or from a wad resource // void G_DoPlayDemo(char *defdemoname) { UINT8 i; lumpnum_t l; char skin[17],color[MAXCOLORNAME+1],*n,*pdemoname; UINT8 version,subversion,charability,charability2,thrustfactor,accelstart,acceleration,cnamelen; pflags_t pflags; UINT32 randseed, followitem; fixed_t camerascale,shieldscale,actionspd,mindash,maxdash,normalspeed,runspeed,jumpfactor,height,spinheight; char msg[1024]; #ifdef OLD22DEMOCOMPAT boolean use_old_demo_vars = false; #endif skin[16] = '\0'; color[MAXCOLORNAME] = '\0'; n = defdemoname+strlen(defdemoname); while (*n != '/' && *n != '\\' && n != defdemoname) n--; if (n != defdemoname) n++; pdemoname = ZZ_Alloc(strlen(n)+1); strcpy(pdemoname,n); // Internal if no extension, external if one exists if (FIL_CheckExtension(defdemoname)) { //FIL_DefaultExtension(defdemoname, ".lmp"); if (!FIL_ReadFile(defdemoname, &demobuffer)) { snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname); CONS_Alert(CONS_ERROR, "%s", msg); gameaction = ga_nothing; M_StartMessage(msg, NULL, MM_NOTHING); return; } demo_p = demobuffer; } // load demo resource from WAD else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR) { snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname); CONS_Alert(CONS_ERROR, "%s", msg); gameaction = ga_nothing; M_StartMessage(msg, NULL, MM_NOTHING); return; } else // it's an internal demo demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC); // read demo header gameaction = ga_nothing; demoplayback = true; if (memcmp(demo_p, DEMOHEADER, 12)) { snprintf(msg, 1024, M_GetText("%s is not a SRB2 replay file.\n"), pdemoname); CONS_Alert(CONS_ERROR, "%s", msg); M_StartMessage(msg, NULL, MM_NOTHING); Z_Free(pdemoname); Z_Free(demobuffer); demoplayback = false; titledemo = false; return; } demo_p += 12; // DEMOHEADER version = READUINT8(demo_p); subversion = READUINT8(demo_p); demoversion = READUINT16(demo_p); switch(demoversion) { case 0x000d: case DEMOVERSION: // latest always supported cnamelen = MAXCOLORNAME; break; #ifdef OLD22DEMOCOMPAT // all that changed between then and now was longer color name case 0x000c: cnamelen = 16; use_old_demo_vars = true; break; #endif // too old, cannot support. default: snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname); CONS_Alert(CONS_ERROR, "%s", msg); M_StartMessage(msg, NULL, MM_NOTHING); Z_Free(pdemoname); Z_Free(demobuffer); demoplayback = false; titledemo = false; return; } demo_p += 16; // demo checksum if (memcmp(demo_p, "PLAY", 4)) { snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname); CONS_Alert(CONS_ERROR, "%s", msg); M_StartMessage(msg, NULL, MM_NOTHING); Z_Free(pdemoname); Z_Free(demobuffer); demoplayback = false; titledemo = false; return; } demo_p += 4; // "PLAY" gamemap = READINT16(demo_p); demo_p += 16; // mapmd5 demoflags = READUINT8(demo_p); modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT; CON_ToggleOff(); hu_demoscore = 0; hu_demotime = UINT32_MAX; hu_demorings = 0; switch (modeattacking) { case ATTACKING_NONE: // 0 break; case ATTACKING_RECORD: // 1 hu_demotime = READUINT32(demo_p); hu_demoscore = READUINT32(demo_p); hu_demorings = READUINT16(demo_p); break; case ATTACKING_NIGHTS: // 2 hu_demotime = READUINT32(demo_p); hu_demoscore = READUINT32(demo_p); break; default: // 3 modeattacking = ATTACKING_NONE; break; } // Random seed randseed = READUINT32(demo_p); // Player name M_Memcpy(player_names[0],demo_p,16); demo_p += 16; // Skin M_Memcpy(skin,demo_p,16); demo_p += 16; // Color M_Memcpy(color,demo_p,cnamelen); demo_p += cnamelen; charability = READUINT8(demo_p); charability2 = READUINT8(demo_p); actionspd = (fixed_t)READUINT8(demo_p)<color = players[0].skincolor; oldghost.x = players[0].mo->x; oldghost.y = players[0].mo->y; oldghost.z = players[0].mo->z; } // Set saved attribute values // No cheat checking here, because even if they ARE wrong... // it would only break the replay if we clipped them. players[0].camerascale = camerascale; players[0].shieldscale = shieldscale; players[0].charability = charability; players[0].charability2 = charability2; players[0].actionspd = actionspd; players[0].mindash = mindash; players[0].maxdash = maxdash; players[0].normalspeed = normalspeed; players[0].runspeed = runspeed; players[0].thrustfactor = thrustfactor; players[0].accelstart = accelstart; players[0].acceleration = acceleration; players[0].height = height; players[0].spinheight = spinheight; players[0].jumpfactor = jumpfactor; players[0].followitem = followitem; players[0].pflags = pflags; demo_start = true; } void G_AddGhost(char *defdemoname) { INT32 i; lumpnum_t l; char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname,md5[16]; UINT8 cnamelen; demoghost *gh; UINT8 flags; UINT8 *buffer,*p; mapthing_t *mthing; UINT16 count, ghostversion; name[16] = '\0'; skin[16] = '\0'; color[16] = '\0'; n = defdemoname+strlen(defdemoname); while (*n != '/' && *n != '\\' && n != defdemoname) n--; if (n != defdemoname) n++; pdemoname = ZZ_Alloc(strlen(n)+1); strcpy(pdemoname,n); // Internal if no extension, external if one exists if (FIL_CheckExtension(defdemoname)) { //FIL_DefaultExtension(defdemoname, ".lmp"); if (!FIL_ReadFileTag(defdemoname, &buffer, PU_LEVEL)) { CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), defdemoname); Z_Free(pdemoname); return; } p = buffer; } // load demo resource from WAD else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR) { CONS_Alert(CONS_ERROR, M_GetText("Failed to read lump '%s'.\n"), defdemoname); Z_Free(pdemoname); return; } else // it's an internal demo buffer = p = W_CacheLumpNum(l, PU_LEVEL); // read demo header if (memcmp(p, DEMOHEADER, 12)) { CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Not a SRB2 replay.\n"), pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } p += 12; // DEMOHEADER p++; // VERSION p++; // SUBVERSION ghostversion = READUINT16(p); switch(ghostversion) { case 0x000d: case DEMOVERSION: // latest always supported cnamelen = MAXCOLORNAME; break; // all that changed between then and now was longer color name case 0x000c: cnamelen = 16; break; // too old, cannot support. default: CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } M_Memcpy(md5, p, 16); p += 16; // demo checksum for (gh = ghosts; gh; gh = gh->next) if (!memcmp(md5, gh->checksum, 16)) // another ghost in the game already has this checksum? { // Don't add another one, then! CONS_Debug(DBG_SETUP, "Rejecting duplicate ghost %s (MD5 was matched)\n", pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } if (memcmp(p, "PLAY", 4)) { CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } p += 4; // "PLAY" if (ghostversion <= 0x0008) p++; // gamemap else p += 2; // gamemap p += 16; // mapmd5 (possibly check for consistency?) flags = READUINT8(p); if (!(flags & DF_GHOST)) { CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT) { case ATTACKING_NONE: // 0 break; case ATTACKING_RECORD: // 1 p += 10; // demo time, score, and rings break; case ATTACKING_NIGHTS: // 2 p += 8; // demo time left, score break; default: // 3 break; } p += 4; // random seed // Player name (TODO: Display this somehow if it doesn't match cv_playername!) M_Memcpy(name, p,16); p += 16; // Skin M_Memcpy(skin, p,16); p += 16; // Color M_Memcpy(color, p,cnamelen); p += cnamelen; // Ghosts do not have a player structure to put this in. p++; // charability p++; // charability2 p++; // actionspd p++; // mindash p++; // maxdash p++; // normalspeed p++; // runspeed p++; // thrustfactor p++; // accelstart p++; // acceleration p += (ghostversion < 0x000e) ? 2 : 2 * sizeof(fixed_t); // height and spinheight p++; // camerascale p++; // shieldscale p += 4; // jumpfactor p += 4; // followitem p++; // pflag data // net var data count = READUINT16(p); while (count--) { SKIPSTRING(p); SKIPSTRING(p); p++; } if (*p == DEMOMARKER) { CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname); Z_Free(pdemoname); Z_Free(buffer); return; } gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL); gh->next = ghosts; gh->buffer = buffer; M_Memcpy(gh->checksum, md5, 16); gh->p = p; ghosts = gh; gh->version = ghostversion; mthing = playerstarts[0]; I_Assert(mthing); { // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling. fixed_t z,f,c; fixed_t offset = mthing->z << FRACBITS; gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST); gh->mo->angle = FixedAngle(mthing->angle << FRACBITS); f = gh->mo->floorz; c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height; if (!!(mthing->options & MTF_AMBUSH) ^ !!(mthing->options & MTF_OBJECTFLIP)) { z = c - offset; if (z < f) z = f; } else { z = f + offset; if (z > c) z = c; } gh->mo->z = z; } gh->oldmo.x = gh->mo->x; gh->oldmo.y = gh->mo->y; gh->oldmo.z = gh->mo->z; // Set skin gh->mo->skin = &skins[0]; for (i = 0; i < numskins; i++) if (!stricmp(skins[i].name,skin)) { gh->mo->skin = &skins[i]; break; } gh->oldmo.skin = gh->mo->skin; // Set color gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor; for (i = 0; i < numskincolors; i++) if (!stricmp(skincolors[i].name,color)) { gh->mo->color = (UINT16)i; break; } gh->oldmo.color = gh->mo->color; gh->mo->state = states+S_PLAY_STND; gh->mo->sprite = gh->mo->state->sprite; gh->mo->sprite2 = (gh->mo->state->frame & FF_FRAMEMASK); //gh->mo->frame = tr_trans30<mo->flags2 |= MF2_DONTDRAW; gh->fadein = (9-3)*6; // fade from invisible to trans30 over as close to 35 tics as possible gh->mo->tics = -1; CONS_Printf(M_GetText("Added ghost %s from %s\n"), name, pdemoname); Z_Free(pdemoname); } // Clean up all ghosts void G_FreeGhosts(void) { while (ghosts) { demoghost *next = ghosts->next; Z_Free(ghosts); ghosts = next; } ghosts = NULL; } // // G_TimeDemo // NOTE: name is a full filename for external demos // static INT32 restorecv_vidwait; void G_TimeDemo(const char *name) { nodrawers = M_CheckParm("-nodraw"); noblit = M_CheckParm("-noblit"); restorecv_vidwait = cv_vidwait.value; if (cv_vidwait.value) CV_Set(&cv_vidwait, "0"); timingdemo = true; singletics = true; framecount = 0; demostarttime = I_GetTime(); G_DeferedPlayDemo(name); } void G_DoPlayMetal(void) { lumpnum_t l; mobj_t *mo = NULL; thinker_t *th; // it's an internal demo if ((l = W_CheckNumForName(va("%sMS",G_BuildMapName(gamemap)))) == LUMPERROR) { CONS_Alert(CONS_WARNING, M_GetText("No bot recording for this map.\n")); return; } else metalbuffer = metal_p = W_CacheLumpNum(l, PU_STATIC); // find metal sonic for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) { if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) continue; mo = (mobj_t *)th; if (mo->type != MT_METALSONIC_RACE) continue; break; } if (th == &thlist[THINK_MOBJ]) { CONS_Alert(CONS_ERROR, M_GetText("Failed to find bot entity.\n")); Z_Free(metalbuffer); return; } // read demo header metal_p += 12; // DEMOHEADER metal_p++; // VERSION metal_p++; // SUBVERSION metalversion = READUINT16(metal_p); switch(metalversion) { case DEMOVERSION: // latest always supported case 0x000d: // There are checks wheter the momentum is from older demo versions or not case 0x000c: // all that changed between then and now was longer color name break; // too old, cannot support. default: CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n")); Z_Free(metalbuffer); return; } metal_p += 16; // demo checksum if (memcmp(metal_p, "METL", 4)) { CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, wasn't recorded in Metal format.\n")); Z_Free(metalbuffer); return; } metal_p += 4; // "METL" // read initial tic memset(&oldmetal,0,sizeof(oldmetal)); oldmetal.x = mo->x; oldmetal.y = mo->y; oldmetal.z = mo->z; metalplayback = mo; } void G_DoneLevelLoad(void) { CONS_Printf(M_GetText("Loaded level in %f sec\n"), (double)(I_GetTime() - demostarttime) / TICRATE); framecount = 0; demostarttime = I_GetTime(); } /* =================== = = G_CheckDemoStatus = = Called after a death or level completion to allow demos to be cleaned up = Returns true if a new demo loop action will take place =================== */ // Writes the demo's checksum, or just random garbage if you can't do that for some reason. static void WriteDemoChecksum(void) { UINT8 *p = demobuffer+16; // checksum position #ifdef NOMD5 UINT8 i; for (i = 0; i < 16; i++, p++) *p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct. #else md5_buffer((char *)p+16, demo_p - (p+16), p); // make a checksum of everything after the checksum in the file. #endif } // Stops recording a demo. static void G_StopDemoRecording(void) { boolean saved = false; if (demo_p) { WRITEUINT8(demo_p, DEMOMARKER); // add the demo end marker WriteDemoChecksum(); saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file. } free(demobuffer); demorecording = false; if (modeattacking != ATTACKING_RECORD) { if (saved) CONS_Printf(M_GetText("Demo %s recorded\n"), demoname); else CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname); } } // Stops metal sonic's demo. Separate from other functions because metal + replays can coexist void G_StopMetalDemo(void) { // Metal Sonic finishing doesn't end the game, dammit. Z_Free(metalbuffer); metalbuffer = NULL; metalplayback = NULL; metal_p = NULL; } // Stops metal sonic recording. ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill) { boolean saved = false; if (demo_p) { WRITEUINT8(demo_p, (kill) ? METALDEATH : DEMOMARKER); // add the demo end (or metal death) marker WriteDemoChecksum(); saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuffer, demo_p - demobuffer); // finally output the file. } free(demobuffer); metalrecording = false; if (saved) I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap)); I_Error("Failed to save demo!"); } // Stops timing a demo. static void G_StopTimingDemo(void) { INT32 demotime; double f1, f2; demotime = I_GetTime() - demostarttime; if (!demotime) return; G_StopDemo(); timingdemo = false; f1 = (double)demotime; f2 = (double)framecount*TICRATE; CONS_Printf(M_GetText("timed %u gametics in %d realtics - %u frames\n%f seconds, %f avg fps\n"), leveltime,demotime,(UINT32)framecount,f1/TICRATE,f2/f1); // CSV-readable timedemo results, for external parsing if (timedemo_csv) { FILE *f; const char *csvpath = va("%s"PATHSEP"%s", srb2home, "timedemo.csv"); const char *header = "id,demoname,seconds,avgfps,leveltime,demotime,framecount,ticrate,rendermode,vidmode,vidwidth,vidheight,procbits\n"; const char *rowformat = "\"%s\",\"%s\",%f,%f,%u,%d,%u,%u,%u,%u,%u,%u,%u\n"; boolean headerrow = !FIL_FileExists(csvpath); UINT8 procbits = 0; // Bitness if (sizeof(void*) == 4) procbits = 32; else if (sizeof(void*) == 8) procbits = 64; f = fopen(csvpath, "a+"); if (f) { if (headerrow) fputs(header, f); fprintf(f, rowformat, timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits); fclose(f); CONS_Printf("Timedemo results saved to '%s'\n", csvpath); } else { // Just print the CSV output to console CON_LogMessage(header); CONS_Printf(rowformat, timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits); } } if (restorecv_vidwait != cv_vidwait.value) CV_SetValue(&cv_vidwait, restorecv_vidwait); D_AdvanceDemo(); } // reset engine variable set for the demos // called from stopdemo command, map command, and g_checkdemoStatus. void G_StopDemo(void) { Z_Free(demobuffer); demobuffer = NULL; demoplayback = false; titledemo = false; timingdemo = false; singletics = false; if (gamestate == GS_INTERMISSION) Y_EndIntermission(); // cleanup G_SetGamestate(GS_NULL); wipegamestate = GS_NULL; SV_StopServer(); SV_ResetServer(); } boolean G_CheckDemoStatus(void) { G_FreeGhosts(); // DO NOT end metal sonic demos here if (timingdemo) { G_StopTimingDemo(); return true; } if (demoplayback) { if (singledemo) I_Quit(); G_StopDemo(); if (modeattacking) M_EndModeAttackRun(); else D_AdvanceDemo(); return true; } if (demorecording) { G_StopDemoRecording(); return true; } return false; }