diff --git a/src/am_map.c b/src/am_map.c index fe0b5e9a5..f05f2def1 100644 --- a/src/am_map.c +++ b/src/am_map.c @@ -324,6 +324,7 @@ static void AM_initVariables(void) m_x = (plr->mo->x >> FRACTOMAPBITS) - m_w/2; m_y = (plr->mo->y >> FRACTOMAPBITS) - m_h/2; } + AM_changeWindowLoc(); // for saving & restoring diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 0d14733da..bc7582070 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2110,14 +2110,18 @@ INT32 D_LookupPlayer(const char *s) static INT32 FindPlayerByPlace(INT32 place) { INT32 playernum; + for (playernum = 0; playernum < MAXPLAYERS; ++playernum) - if (playeringame[playernum]) { + if (!playeringame[playernum]) + continue; + if (players[playernum].kartstuff[k_position] == place) { return playernum; } } + return -1; } @@ -5674,7 +5678,7 @@ static void Command_Archivetest_f(void) // assign mobjnum i = 1; for (th = thinkercap.next; th != &thinkercap; th = th->next) - if (th->function.acp1 == (actionf_p1)P_MobjThinker) + if (th->function == (actionf_p1)P_MobjThinker) ((mobj_t *)th)->mobjnum = i++; // allocate buffer diff --git a/src/d_think.h b/src/d_think.h index 8177f464d..1ef2577dc 100644 --- a/src/d_think.h +++ b/src/d_think.h @@ -28,18 +28,11 @@ // To compile this as "ANSI C with classes" we will need to handle the various // action functions cleanly. // -typedef void (*actionf_v)(); typedef void (*actionf_p1)(void *); -typedef union -{ - actionf_v acv; - actionf_p1 acp1; -} actionf_t; - // Historically, "think_t" is yet another function pointer to a routine // to handle an actor. -typedef actionf_t think_t; +typedef actionf_p1 think_t; // Doubly linked list of actors. typedef struct thinker_s diff --git a/src/dehacked.c b/src/dehacked.c index f0e2583d8..c5412e29f 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -1416,211 +1416,212 @@ static void readhuditem(MYFILE *f, INT32 num) // DO NOT FORGET TO SYNC THIS LIST WITH THE ACTIONNUM ENUM IN INFO.H actionpointer_t actionpointers[] = { - {{A_Explode}, "A_EXPLODE"}, - {{A_Pain}, "A_PAIN"}, - {{A_Fall}, "A_FALL"}, - {{A_MonitorPop}, "A_MONITORPOP"}, - {{A_Look}, "A_LOOK"}, - {{A_Chase}, "A_CHASE"}, - {{A_FaceStabChase}, "A_FACESTABCHASE"}, - {{A_FaceTarget}, "A_FACETARGET"}, - {{A_FaceTracer}, "A_FACETRACER"}, - {{A_Scream}, "A_SCREAM"}, - {{A_BossDeath}, "A_BOSSDEATH"}, - {{A_CustomPower}, "A_CUSTOMPOWER"}, - {{A_GiveWeapon}, "A_GIVEWEAPON"}, - {{A_RingShield}, "A_RINGSHIELD"}, - {{A_RingBox}, "A_RINGBOX"}, - {{A_Invincibility}, "A_INVINCIBILITY"}, - {{A_SuperSneakers}, "A_SUPERSNEAKERS"}, - {{A_BunnyHop}, "A_BUNNYHOP"}, - {{A_BubbleSpawn}, "A_BUBBLESPAWN"}, - {{A_FanBubbleSpawn}, "A_FANBUBBLESPAWN"}, - {{A_BubbleRise}, "A_BUBBLERISE"}, - {{A_BubbleCheck}, "A_BUBBLECHECK"}, - {{A_AwardScore}, "A_AWARDSCORE"}, - {{A_ExtraLife}, "A_EXTRALIFE"}, - {{A_BombShield}, "A_BOMBSHIELD"}, - {{A_JumpShield}, "A_JUMPSHIELD"}, - {{A_WaterShield}, "A_WATERSHIELD"}, - {{A_ForceShield}, "A_FORCESHIELD"}, - {{A_PityShield}, "A_PITYSHIELD"}, - {{A_GravityBox}, "A_GRAVITYBOX"}, - {{A_ScoreRise}, "A_SCORERISE"}, - {{A_ParticleSpawn}, "A_PARTICLESPAWN"}, - {{A_AttractChase}, "A_ATTRACTCHASE"}, - {{A_DropMine}, "A_DROPMINE"}, - {{A_FishJump}, "A_FISHJUMP"}, - {{A_ThrownRing}, "A_THROWNRING"}, - {{A_GrenadeRing}, "A_GRENADERING"}, // SRB2kart - {{A_SetSolidSteam}, "A_SETSOLIDSTEAM"}, - {{A_UnsetSolidSteam}, "A_UNSETSOLIDSTEAM"}, - {{A_SignPlayer}, "A_SIGNPLAYER"}, - {{A_OverlayThink}, "A_OVERLAYTHINK"}, - {{A_JetChase}, "A_JETCHASE"}, - {{A_JetbThink}, "A_JETBTHINK"}, - {{A_JetgThink}, "A_JETGTHINK"}, - {{A_JetgShoot}, "A_JETGSHOOT"}, - {{A_ShootBullet}, "A_SHOOTBULLET"}, - {{A_MinusDigging}, "A_MINUSDIGGING"}, - {{A_MinusPopup}, "A_MINUSPOPUP"}, - {{A_MinusCheck}, "A_MINUSCHECK"}, - {{A_ChickenCheck}, "A_CHICKENCHECK"}, - {{A_MouseThink}, "A_MOUSETHINK"}, - {{A_DetonChase}, "A_DETONCHASE"}, - {{A_CapeChase}, "A_CAPECHASE"}, - {{A_RotateSpikeBall}, "A_ROTATESPIKEBALL"}, - {{A_SlingAppear}, "A_SLINGAPPEAR"}, - {{A_MaceRotate}, "A_MACEROTATE"}, - {{A_UnidusBall}, "A_UNIDUSBALL"}, - {{A_RockSpawn}, "A_ROCKSPAWN"}, - {{A_SetFuse}, "A_SETFUSE"}, - {{A_CrawlaCommanderThink}, "A_CRAWLACOMMANDERTHINK"}, - {{A_SmokeTrailer}, "A_SMOKETRAILER"}, - {{A_RingExplode}, "A_RINGEXPLODE"}, - {{A_OldRingExplode}, "A_OLDRINGEXPLODE"}, - {{A_MixUp}, "A_MIXUP"}, - {{A_RecyclePowers}, "A_RECYCLEPOWERS"}, - {{A_Boss1Chase}, "A_BOSS1CHASE"}, - {{A_FocusTarget}, "A_FOCUSTARGET"}, - {{A_Boss2Chase}, "A_BOSS2CHASE"}, - {{A_Boss2Pogo}, "A_BOSS2POGO"}, - {{A_BossZoom}, "A_BOSSZOOM"}, - {{A_BossScream}, "A_BOSSSCREAM"}, - {{A_Boss2TakeDamage}, "A_BOSS2TAKEDAMAGE"}, - {{A_Boss7Chase}, "A_BOSS7CHASE"}, - {{A_GoopSplat}, "A_GOOPSPLAT"}, - {{A_Boss2PogoSFX}, "A_BOSS2POGOSFX"}, - {{A_Boss2PogoTarget}, "A_BOSS2POGOTARGET"}, - {{A_BossJetFume}, "A_BOSSJETFUME"}, - {{A_EggmanBox}, "A_EGGMANBOX"}, - {{A_TurretFire}, "A_TURRETFIRE"}, - {{A_SuperTurretFire}, "A_SUPERTURRETFIRE"}, - {{A_TurretStop}, "A_TURRETSTOP"}, - {{A_JetJawRoam}, "A_JETJAWROAM"}, - {{A_JetJawChomp}, "A_JETJAWCHOMP"}, - {{A_PointyThink}, "A_POINTYTHINK"}, - {{A_CheckBuddy}, "A_CHECKBUDDY"}, - {{A_HoodThink}, "A_HOODTHINK"}, - {{A_ArrowCheck}, "A_ARROWCHECK"}, - {{A_SnailerThink}, "A_SNAILERTHINK"}, - {{A_SharpChase}, "A_SHARPCHASE"}, - {{A_SharpSpin}, "A_SHARPSPIN"}, - {{A_VultureVtol}, "A_VULTUREVTOL"}, - {{A_VultureCheck}, "A_VULTURECHECK"}, - {{A_SkimChase}, "A_SKIMCHASE"}, - {{A_1upThinker}, "A_1UPTHINKER"}, - {{A_SkullAttack}, "A_SKULLATTACK"}, - {{A_LobShot}, "A_LOBSHOT"}, - {{A_FireShot}, "A_FIRESHOT"}, - {{A_SuperFireShot}, "A_SUPERFIRESHOT"}, - {{A_BossFireShot}, "A_BOSSFIRESHOT"}, - {{A_Boss7FireMissiles}, "A_BOSS7FIREMISSILES"}, - {{A_Boss1Laser}, "A_BOSS1LASER"}, - {{A_Boss4Reverse}, "A_BOSS4REVERSE"}, - {{A_Boss4SpeedUp}, "A_BOSS4SPEEDUP"}, - {{A_Boss4Raise}, "A_BOSS4RAISE"}, - {{A_SparkFollow}, "A_SPARKFOLLOW"}, - {{A_BuzzFly}, "A_BUZZFLY"}, - {{A_GuardChase}, "A_GUARDCHASE"}, - {{A_EggShield}, "A_EGGSHIELD"}, - {{A_SetReactionTime}, "A_SETREACTIONTIME"}, - {{A_Boss1Spikeballs}, "A_BOSS1SPIKEBALLS"}, - {{A_Boss3TakeDamage}, "A_BOSS3TAKEDAMAGE"}, - {{A_Boss3Path}, "A_BOSS3PATH"}, - {{A_LinedefExecute}, "A_LINEDEFEXECUTE"}, - {{A_PlaySeeSound}, "A_PLAYSEESOUND"}, - {{A_PlayAttackSound}, "A_PLAYATTACKSOUND"}, - {{A_PlayActiveSound}, "A_PLAYACTIVESOUND"}, - {{A_SpawnObjectAbsolute}, "A_SPAWNOBJECTABSOLUTE"}, - {{A_SpawnObjectRelative}, "A_SPAWNOBJECTRELATIVE"}, - {{A_ChangeAngleRelative}, "A_CHANGEANGLERELATIVE"}, - {{A_ChangeAngleAbsolute}, "A_CHANGEANGLEABSOLUTE"}, - {{A_PlaySound}, "A_PLAYSOUND"}, - {{A_FindTarget}, "A_FINDTARGET"}, - {{A_FindTracer}, "A_FINDTRACER"}, - {{A_SetTics}, "A_SETTICS"}, - {{A_SetRandomTics}, "A_SETRANDOMTICS"}, - {{A_ChangeColorRelative}, "A_CHANGECOLORRELATIVE"}, - {{A_ChangeColorAbsolute}, "A_CHANGECOLORABSOLUTE"}, - {{A_MoveRelative}, "A_MOVERELATIVE"}, - {{A_MoveAbsolute}, "A_MOVEABSOLUTE"}, - {{A_Thrust}, "A_THRUST"}, - {{A_ZThrust}, "A_ZTHRUST"}, - {{A_SetTargetsTarget}, "A_SETTARGETSTARGET"}, - {{A_SetObjectFlags}, "A_SETOBJECTFLAGS"}, - {{A_SetObjectFlags2}, "A_SETOBJECTFLAGS2"}, - {{A_RandomState}, "A_RANDOMSTATE"}, - {{A_RandomStateRange}, "A_RANDOMSTATERANGE"}, - {{A_DualAction}, "A_DUALACTION"}, - {{A_RemoteAction}, "A_REMOTEACTION"}, - {{A_ToggleFlameJet}, "A_TOGGLEFLAMEJET"}, - {{A_ItemPop}, "A_ITEMPOP"}, // SRB2kart - {{A_JawzChase}, "A_JAWZCHASE"}, // SRB2kart - {{A_JawzExplode}, "A_JAWZEXPLODE"}, // SRB2kart - {{A_SPBChase}, "A_SPBCHASE"}, // SRB2kart - {{A_MineExplode}, "A_MINEEXPLODE"}, // SRB2kart - {{A_BallhogExplode}, "A_BALLHOGEXPLODE"}, // SRB2kart - {{A_LightningFollowPlayer},"A_LIGHTNINGFOLLOWPLAYER"}, //SRB2kart - {{A_FZBoomFlash}, "A_FZBOOMFLASH"}, //SRB2kart - {{A_FZBoomSmoke}, "A_FZBOOMSMOKE"}, //SRB2kart - {{A_RandomShadowFrame}, "A_RANDOMSHADOWFRAME"}, //SRB2kart - {{A_RoamingShadowThinker}, "A_ROAMINGSHADOWTHINKER"}, //SRB2kart - {{A_MayonakaArrow}, "A_MAYONAKAARROW"}, //SRB2kart - {{A_ReaperThinker}, "A_REAPERTHINKER"}, //SRB2kart - {{A_MementosTPParticles}, "A_MEMENTOSTPPARTICLES"}, //SRB2kart - {{A_FlameParticle}, "A_FLAMEPARTICLE"}, // SRB2kart - {{A_OrbitNights}, "A_ORBITNIGHTS"}, - {{A_GhostMe}, "A_GHOSTME"}, - {{A_SetObjectState}, "A_SETOBJECTSTATE"}, - {{A_SetObjectTypeState}, "A_SETOBJECTTYPESTATE"}, - {{A_KnockBack}, "A_KNOCKBACK"}, - {{A_PushAway}, "A_PUSHAWAY"}, - {{A_RingDrain}, "A_RINGDRAIN"}, - {{A_SplitShot}, "A_SPLITSHOT"}, - {{A_MissileSplit}, "A_MISSILESPLIT"}, - {{A_MultiShot}, "A_MULTISHOT"}, - {{A_InstaLoop}, "A_INSTALOOP"}, - {{A_Custom3DRotate}, "A_CUSTOM3DROTATE"}, - {{A_SearchForPlayers}, "A_SEARCHFORPLAYERS"}, - {{A_CheckRandom}, "A_CHECKRANDOM"}, - {{A_CheckTargetRings}, "A_CHECKTARGETRINGS"}, - {{A_CheckRings}, "A_CHECKRINGS"}, - {{A_CheckTotalRings}, "A_CHECKTOTALRINGS"}, - {{A_CheckHealth}, "A_CHECKHEALTH"}, - {{A_CheckRange}, "A_CHECKRANGE"}, - {{A_CheckHeight}, "A_CHECKHEIGHT"}, - {{A_CheckTrueRange}, "A_CHECKTRUERANGE"}, - {{A_CheckThingCount}, "A_CHECKTHINGCOUNT"}, - {{A_CheckAmbush}, "A_CHECKAMBUSH"}, - {{A_CheckCustomValue}, "A_CHECKCUSTOMVALUE"}, - {{A_CheckCusValMemo}, "A_CHECKCUSVALMEMO"}, - {{A_SetCustomValue}, "A_SETCUSTOMVALUE"}, - {{A_UseCusValMemo}, "A_USECUSVALMEMO"}, - {{A_RelayCustomValue}, "A_RELAYCUSTOMVALUE"}, - {{A_CusValAction}, "A_CUSVALACTION"}, - {{A_ForceStop}, "A_FORCESTOP"}, - {{A_ForceWin}, "A_FORCEWIN"}, - {{A_SpikeRetract}, "A_SPIKERETRACT"}, - {{A_InfoState}, "A_INFOSTATE"}, - {{A_Repeat}, "A_REPEAT"}, - {{A_SetScale}, "A_SETSCALE"}, - {{A_RemoteDamage}, "A_REMOTEDAMAGE"}, - {{A_HomingChase}, "A_HOMINGCHASE"}, - {{A_TrapShot}, "A_TRAPSHOT"}, - {{A_VileTarget}, "A_VILETARGET"}, - {{A_VileAttack}, "A_VILEATTACK"}, - {{A_VileFire}, "A_VILEFIRE"}, - {{A_BrakChase}, "A_BRAKCHASE"}, - {{A_BrakFireShot}, "A_BRAKFIRESHOT"}, - {{A_BrakLobShot}, "A_BRAKLOBSHOT"}, - {{A_NapalmScatter}, "A_NAPALMSCATTER"}, - {{A_SpawnFreshCopy}, "A_SPAWNFRESHCOPY"}, + {A_Explode, "A_EXPLODE"}, + {A_Pain, "A_PAIN"}, + {A_Fall, "A_FALL"}, + {A_MonitorPop, "A_MONITORPOP"}, + {A_Look, "A_LOOK"}, + {A_Chase, "A_CHASE"}, + {A_FaceStabChase, "A_FACESTABCHASE"}, + {A_FaceTarget, "A_FACETARGET"}, + {A_FaceTracer, "A_FACETRACER"}, + {A_Scream, "A_SCREAM"}, + {A_BossDeath, "A_BOSSDEATH"}, + {A_CustomPower, "A_CUSTOMPOWER"}, + {A_GiveWeapon, "A_GIVEWEAPON"}, + {A_RingShield, "A_RINGSHIELD"}, + {A_RingBox, "A_RINGBOX"}, + {A_Invincibility, "A_INVINCIBILITY"}, + {A_SuperSneakers, "A_SUPERSNEAKERS"}, + {A_BunnyHop, "A_BUNNYHOP"}, + {A_BubbleSpawn, "A_BUBBLESPAWN"}, + {A_FanBubbleSpawn, "A_FANBUBBLESPAWN"}, + {A_BubbleRise, "A_BUBBLERISE"}, + {A_BubbleCheck, "A_BUBBLECHECK"}, + {A_AwardScore, "A_AWARDSCORE"}, + {A_ExtraLife, "A_EXTRALIFE"}, + {A_BombShield, "A_BOMBSHIELD"}, + {A_JumpShield, "A_JUMPSHIELD"}, + {A_WaterShield, "A_WATERSHIELD"}, + {A_ForceShield, "A_FORCESHIELD"}, + {A_PityShield, "A_PITYSHIELD"}, + {A_GravityBox, "A_GRAVITYBOX"}, + {A_ScoreRise, "A_SCORERISE"}, + {A_ParticleSpawn, "A_PARTICLESPAWN"}, + {A_AttractChase, "A_ATTRACTCHASE"}, + {A_DropMine, "A_DROPMINE"}, + {A_FishJump, "A_FISHJUMP"}, + {A_ThrownRing, "A_THROWNRING"}, + {A_GrenadeRing, "A_GRENADERING"}, // SRB2kart + {A_SetSolidSteam, "A_SETSOLIDSTEAM"}, + {A_UnsetSolidSteam, "A_UNSETSOLIDSTEAM"}, + {A_SignPlayer, "A_SIGNPLAYER"}, + {A_OverlayThink, "A_OVERLAYTHINK"}, + {A_JetChase, "A_JETCHASE"}, + {A_JetbThink, "A_JETBTHINK"}, + {A_JetgThink, "A_JETGTHINK"}, + {A_JetgShoot, "A_JETGSHOOT"}, + {A_ShootBullet, "A_SHOOTBULLET"}, + {A_MinusDigging, "A_MINUSDIGGING"}, + {A_MinusPopup, "A_MINUSPOPUP"}, + {A_MinusCheck, "A_MINUSCHECK"}, + {A_ChickenCheck, "A_CHICKENCHECK"}, + {A_MouseThink, "A_MOUSETHINK"}, + {A_DetonChase, "A_DETONCHASE"}, + {A_CapeChase, "A_CAPECHASE"}, + {A_RotateSpikeBall, "A_ROTATESPIKEBALL"}, + {A_SlingAppear, "A_SLINGAPPEAR"}, + {A_MaceRotate, "A_MACEROTATE"}, + {A_UnidusBall, "A_UNIDUSBALL"}, + {A_RockSpawn, "A_ROCKSPAWN"}, + {A_SetFuse, "A_SETFUSE"}, + {A_CrawlaCommanderThink, "A_CRAWLACOMMANDERTHINK"}, + {A_SmokeTrailer, "A_SMOKETRAILER"}, + {A_RingExplode, "A_RINGEXPLODE"}, + {A_OldRingExplode, "A_OLDRINGEXPLODE"}, + {A_MixUp, "A_MIXUP"}, + {A_RecyclePowers, "A_RECYCLEPOWERS"}, + {A_Boss1Chase, "A_BOSS1CHASE"}, + {A_FocusTarget, "A_FOCUSTARGET"}, + {A_Boss2Chase, "A_BOSS2CHASE"}, + {A_Boss2Pogo, "A_BOSS2POGO"}, + {A_BossZoom, "A_BOSSZOOM"}, + {A_BossScream, "A_BOSSSCREAM"}, + {A_Boss2TakeDamage, "A_BOSS2TAKEDAMAGE"}, + {A_Boss7Chase, "A_BOSS7CHASE"}, + {A_GoopSplat, "A_GOOPSPLAT"}, + {A_Boss2PogoSFX, "A_BOSS2POGOSFX"}, + {A_Boss2PogoTarget, "A_BOSS2POGOTARGET"}, + {A_BossJetFume, "A_BOSSJETFUME"}, + {A_EggmanBox, "A_EGGMANBOX"}, + {A_TurretFire, "A_TURRETFIRE"}, + {A_SuperTurretFire, "A_SUPERTURRETFIRE"}, + {A_TurretStop, "A_TURRETSTOP"}, + {A_JetJawRoam, "A_JETJAWROAM"}, + {A_JetJawChomp, "A_JETJAWCHOMP"}, + {A_PointyThink, "A_POINTYTHINK"}, + {A_CheckBuddy, "A_CHECKBUDDY"}, + {A_HoodThink, "A_HOODTHINK"}, + {A_ArrowCheck, "A_ARROWCHECK"}, + {A_SnailerThink, "A_SNAILERTHINK"}, + {A_SharpChase, "A_SHARPCHASE"}, + {A_SharpSpin, "A_SHARPSPIN"}, + {A_VultureVtol, "A_VULTUREVTOL"}, + {A_VultureCheck, "A_VULTURECHECK"}, + {A_SkimChase, "A_SKIMCHASE"}, + {A_1upThinker, "A_1UPTHINKER"}, + {A_SkullAttack, "A_SKULLATTACK"}, + {A_LobShot, "A_LOBSHOT"}, + {A_FireShot, "A_FIRESHOT"}, + {A_SuperFireShot, "A_SUPERFIRESHOT"}, + {A_BossFireShot, "A_BOSSFIRESHOT"}, + {A_Boss7FireMissiles, "A_BOSS7FIREMISSILES"}, + {A_Boss1Laser, "A_BOSS1LASER"}, + {A_Boss4Reverse, "A_BOSS4REVERSE"}, + {A_Boss4SpeedUp, "A_BOSS4SPEEDUP"}, + {A_Boss4Raise, "A_BOSS4RAISE"}, + {A_SparkFollow, "A_SPARKFOLLOW"}, + {A_BuzzFly, "A_BUZZFLY"}, + {A_GuardChase, "A_GUARDCHASE"}, + {A_EggShield, "A_EGGSHIELD"}, + {A_SetReactionTime, "A_SETREACTIONTIME"}, + {A_Boss1Spikeballs, "A_BOSS1SPIKEBALLS"}, + {A_Boss3TakeDamage, "A_BOSS3TAKEDAMAGE"}, + {A_Boss3Path, "A_BOSS3PATH"}, + {A_LinedefExecute, "A_LINEDEFEXECUTE"}, + {A_PlaySeeSound, "A_PLAYSEESOUND"}, + {A_PlayAttackSound, "A_PLAYATTACKSOUND"}, + {A_PlayActiveSound, "A_PLAYACTIVESOUND"}, + {A_SpawnObjectAbsolute, "A_SPAWNOBJECTABSOLUTE"}, + {A_SpawnObjectRelative, "A_SPAWNOBJECTRELATIVE"}, + {A_ChangeAngleRelative, "A_CHANGEANGLERELATIVE"}, + {A_ChangeAngleAbsolute, "A_CHANGEANGLEABSOLUTE"}, + {A_PlaySound, "A_PLAYSOUND"}, + {A_FindTarget, "A_FINDTARGET"}, + {A_FindTracer, "A_FINDTRACER"}, + {A_SetTics, "A_SETTICS"}, + {A_SetRandomTics, "A_SETRANDOMTICS"}, + {A_ChangeColorRelative, "A_CHANGECOLORRELATIVE"}, + {A_ChangeColorAbsolute, "A_CHANGECOLORABSOLUTE"}, + {A_MoveRelative, "A_MOVERELATIVE"}, + {A_MoveAbsolute, "A_MOVEABSOLUTE"}, + {A_Thrust, "A_THRUST"}, + {A_ZThrust, "A_ZTHRUST"}, + {A_SetTargetsTarget, "A_SETTARGETSTARGET"}, + {A_SetObjectFlags, "A_SETOBJECTFLAGS"}, + {A_SetObjectFlags2, "A_SETOBJECTFLAGS2"}, + {A_RandomState, "A_RANDOMSTATE"}, + {A_RandomStateRange, "A_RANDOMSTATERANGE"}, + {A_DualAction, "A_DUALACTION"}, + {A_RemoteAction, "A_REMOTEACTION"}, + {A_ToggleFlameJet, "A_TOGGLEFLAMEJET"}, + {A_ItemPop, "A_ITEMPOP"}, // SRB2kart + {A_JawzChase, "A_JAWZCHASE"}, // SRB2kart + {A_JawzExplode, "A_JAWZEXPLODE"}, // SRB2kart + {A_SPBChase, "A_SPBCHASE"}, // SRB2kart + {A_MineExplode, "A_MINEEXPLODE"}, // SRB2kart + {A_BallhogExplode, "A_BALLHOGEXPLODE"}, // SRB2kart + {A_LightningFollowPlayer,"A_LIGHTNINGFOLLOWPLAYER"}, //SRB2kart + {A_FZBoomFlash, "A_FZBOOMFLASH"}, //SRB2kart + {A_FZBoomSmoke, "A_FZBOOMSMOKE"}, //SRB2kart + {A_RandomShadowFrame, "A_RANDOMSHADOWFRAME"}, //SRB2kart + {A_RoamingShadowThinker, "A_ROAMINGSHADOWTHINKER"}, //SRB2kart + {A_MayonakaArrow, "A_MAYONAKAARROW"}, //SRB2kart + {A_ReaperThinker, "A_REAPERTHINKER"}, //SRB2kart + {A_MementosTPParticles, "A_MEMENTOSTPPARTICLES"}, //SRB2kart + {A_FlameParticle, "A_FLAMEPARTICLE"}, // SRB2kart + {A_OrbitNights, "A_ORBITNIGHTS"}, + {A_GhostMe, "A_GHOSTME"}, + {A_SetObjectState, "A_SETOBJECTSTATE"}, + {A_SetObjectTypeState, "A_SETOBJECTTYPESTATE"}, + {A_KnockBack, "A_KNOCKBACK"}, + {A_PushAway, "A_PUSHAWAY"}, + {A_RingDrain, "A_RINGDRAIN"}, + {A_SplitShot, "A_SPLITSHOT"}, + {A_MissileSplit, "A_MISSILESPLIT"}, + {A_MultiShot, "A_MULTISHOT"}, + {A_InstaLoop, "A_INSTALOOP"}, + {A_Custom3DRotate, "A_CUSTOM3DROTATE"}, + {A_SearchForPlayers, "A_SEARCHFORPLAYERS"}, + {A_CheckRandom, "A_CHECKRANDOM"}, + {A_CheckTargetRings, "A_CHECKTARGETRINGS"}, + {A_CheckRings, "A_CHECKRINGS"}, + {A_CheckTotalRings, "A_CHECKTOTALRINGS"}, + {A_CheckHealth, "A_CHECKHEALTH"}, + {A_CheckRange, "A_CHECKRANGE"}, + {A_CheckHeight, "A_CHECKHEIGHT"}, + {A_CheckTrueRange, "A_CHECKTRUERANGE"}, + {A_CheckThingCount, "A_CHECKTHINGCOUNT"}, + {A_CheckAmbush, "A_CHECKAMBUSH"}, + {A_CheckCustomValue, "A_CHECKCUSTOMVALUE"}, + {A_CheckCusValMemo, "A_CHECKCUSVALMEMO"}, + {A_SetCustomValue, "A_SETCUSTOMVALUE"}, + {A_UseCusValMemo, "A_USECUSVALMEMO"}, + {A_RelayCustomValue, "A_RELAYCUSTOMVALUE"}, + {A_CusValAction, "A_CUSVALACTION"}, + {A_ForceStop, "A_FORCESTOP"}, + {A_ForceWin, "A_FORCEWIN"}, + {A_SpikeRetract, "A_SPIKERETRACT"}, + {A_InfoState, "A_INFOSTATE"}, + {A_Repeat, "A_REPEAT"}, + {A_SetScale, "A_SETSCALE"}, + {A_RemoteDamage, "A_REMOTEDAMAGE"}, + {A_HomingChase, "A_HOMINGCHASE"}, + {A_TrapShot, "A_TRAPSHOT"}, + {A_VileTarget, "A_VILETARGET"}, + {A_VileAttack, "A_VILEATTACK"}, + {A_VileFire, "A_VILEFIRE"}, + {A_BrakChase, "A_BRAKCHASE"}, + {A_BrakFireShot, "A_BRAKFIRESHOT"}, + {A_BrakLobShot, "A_BRAKLOBSHOT"}, + {A_NapalmScatter, "A_NAPALMSCATTER"}, + {A_SpawnFreshCopy, "A_SPAWNFRESHCOPY"}, + //{A_InvincSparkleRotate, "A_INVINCSPARKLEROTATE"}, - {{NULL}, "NONE"}, + {NULL, "NONE"}, // This NULL entry must be the last in the list - {{NULL}, NULL}, + {NULL, NULL}, }; static void readframe(MYFILE *f, INT32 num) @@ -1702,7 +1703,7 @@ static void readframe(MYFILE *f, INT32 num) for (z = 0; actionpointers[z].name; z++) { - if (actionpointers[z].action.acv == states[num].action.acv) + if (actionpointers[z].action == states[num].action) { break; } @@ -1711,17 +1712,17 @@ static void readframe(MYFILE *f, INT32 num) z = 0; found = LUA_SetLuaAction(&states[num], actiontocompare); if (!found) - while (actionpointers[z].name) { - if (fastcmp(actiontocompare, actionpointers[z].name)) + while (actionpointers[z].name) { - states[num].action = actionpointers[z].action; - states[num].action.acv = actionpointers[z].action.acv; // assign - states[num].action.acp1 = actionpointers[z].action.acp1; - found = true; - break; + if (fastcmp(actiontocompare, actionpointers[z].name)) + { + states[num].action = actionpointers[z].action; + found = true; + break; + } + z++; } - z++; } if (!found) @@ -8524,13 +8525,13 @@ static inline int lib_freeslot(lua_State *L) // Arguments: mobj_t actor, int var1, int var2 static inline int lib_action(lua_State *L) { - actionf_t *action = lua_touserdata(L,lua_upvalueindex(1)); + actionf_p1 *action = lua_touserdata(L,lua_upvalueindex(1)); mobj_t *actor = *((mobj_t **)luaL_checkudata(L,1,META_MOBJ)); var1 = (INT32)luaL_optinteger(L,2,0); var2 = (INT32)luaL_optinteger(L,3,0); if (!actor) return LUA_ErrInvalid(L, "mobj_t"); - action->acp1(actor); + (*action)(actor); return 0; } @@ -9344,13 +9345,15 @@ int LUA_SOCLib(lua_State *L) const char *LUA_GetActionName(void *action) { - actionf_t *act = (actionf_t *)action; + actionf_p1 *act = (actionf_p1 *)action; size_t z; + for (z = 0; actionpointers[z].name; z++) { - if (actionpointers[z].action.acv == act->acv) + if (actionpointers[z].action == *act) return actionpointers[z].name; } + return NULL; } @@ -9363,8 +9366,6 @@ void LUA_SetActionByName(void *state, const char *actiontocompare) if (fasticmp(actiontocompare, actionpointers[z].name)) { st->action = actionpointers[z].action; - st->action.acv = actionpointers[z].action.acv; // assign - st->action.acp1 = actionpointers[z].action.acp1; return; } } diff --git a/src/g_game.c b/src/g_game.c index 76b076bba..5c394470c 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2562,7 +2562,7 @@ void G_ChangePlayerReferences(mobj_t *oldmo, mobj_t *newmo) // scan all thinkers for (th = thinkercap.next; th != &thinkercap; th = th->next) { - if (th->function.acp1 != (actionf_p1)P_MobjThinker) + if (th->function != (actionf_p1)P_MobjThinker) continue; mo2 = (mobj_t *)th; @@ -4222,6 +4222,3944 @@ INT32 G_FindMapByNameOrCode(const char *mapname, char **realmapnamep) return newmapnum; } +// +// DEMO RECORDING +// + +#define DEMOVERSION 0x0002 +#define DEMOHEADER "\xF0" "KartReplay" "\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! +#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_LUAVARS 0x20 // this demo contains extra lua vars; this is mostly used for backwards compability + +#define DF_ATTACKSHIFT 1 +#define DF_ENCORE 0x40 +#define DF_MULTIPLAYER 0x80 // This demo was recorded in multiplayer mode! + +#ifdef DEMO_COMPAT_100 +#define DF_FILELIST 0x08 // This demo contains an extra files list +#define DF_GAMETYPEMASK 0x30 +#define DF_GAMESHIFT 4 +#endif + +#define DEMO_SPECTATOR 0x40 + +// For demos +#define ZT_FWD 0x01 +#define ZT_SIDE 0x02 +#define ZT_ANGLE 0x04 +#define ZT_BUTTONS 0x08 +#define ZT_AIMING 0x10 +#define ZT_DRIFT 0x20 +#define ZT_LATENCY 0x40 +#define DEMOMARKER 0x80 // demoend + +UINT8 demo_extradata[MAXPLAYERS]; +UINT8 demo_writerng; // 0=no, 1=yes, 2=yes but on a timeout +static ticcmd_t oldcmd[MAXPLAYERS]; + +#define DW_END 0xFF // End of extradata block +#define DW_RNG 0xFE // Check RNG seed! + +#define DW_EXTRASTUFF 0xFE // Numbers below this are reserved for writing player slot data + +// Below consts are only used for demo extrainfo sections +#define DW_STANDING 0x00 + +// For Metal Sonic and time attack ghosts +#define GZT_XYZ 0x01 +#define GZT_MOMXY 0x02 +#define GZT_MOMZ 0x04 +#define GZT_ANGLE 0x08 +// Not used for Metal Sonic +#define GZT_SPRITE 0x10 // Animation frame +#define GZT_EXTRA 0x20 +#define GZT_NIGHTS 0x40 // NiGHTS Mode stuff! + +// 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_KART 0x80 // SRB2Kart: Changed current held item/quantity and bumpers for battle + +static mobj_t oldmetal, oldghost[MAXPLAYERS]; + +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); +} + +ticcmd_t *G_CopyTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n) +{ + return M_Memcpy(dest, src, n*sizeof(*src)); +} + +ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n) +{ + size_t i; + for (i = 0; i < n; i++) + { + dest[i].forwardmove = src[i].forwardmove; + dest[i].sidemove = src[i].sidemove; + dest[i].angleturn = SHORT(src[i].angleturn); + dest[i].aiming = (INT16)SHORT(src[i].aiming); + dest[i].buttons = (UINT16)SHORT(src[i].buttons); + dest[i].driftturn = (INT16)SHORT(src[i].driftturn); + dest[i].latency = (INT16)SHORT(src[i].latency); + } + return dest; +} + +// Finds a skin with the closest stats if the expected skin doesn't exist. +static INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight) +{ + INT32 i, closest_skin = 0; + UINT8 closest_stats = UINT8_MAX, stat_diff; + + for (i = 0; i < numskins; i++) + { + stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight); + if (stat_diff < closest_stats) + { + closest_stats = stat_diff; + closest_skin = i; + } + } + + return closest_skin; +} + +static void FindClosestSkinForStats(UINT32 p, UINT8 kartspeed, UINT8 kartweight) +{ + INT32 closest_skin = GetSkinNumClosestToStats(kartspeed, kartweight); + + //CONS_Printf("Using %s instead...\n", skins[closest_skin].name); + SetPlayerSkinByNum(p, closest_skin); +} + +void G_ReadDemoExtraData(void) +{ + INT32 p, extradata, i; + char name[17]; + + if (leveltime > starttime) + { + rewind_t *rewind = CL_SaveRewindPoint(demobuf.p - demobuf.buffer); + if (rewind) + { + memcpy(rewind->oldcmd, oldcmd, sizeof (oldcmd)); + memcpy(rewind->oldghost, oldghost, sizeof (oldghost)); + } + } + + memset(name, '\0', 17); + + p = READUINT8(demobuf.p); + + while (p < DW_EXTRASTUFF) + { + extradata = READUINT8(demobuf.p); + + if (extradata & DXD_RESPAWN) + { + if (players[p].mo) + P_DamageMobj(players[p].mo, NULL, NULL, DMG_INSTAKILL); // Is this how this should work..? + } + if (extradata & DXD_SKIN) + { + UINT8 kartspeed, kartweight; + + // Skin + M_Memcpy(name, demobuf.p, 16); + demobuf.p += 16; + SetPlayerSkin(p, name); + + kartspeed = READUINT8(demobuf.p); + kartweight = READUINT8(demobuf.p); + + if (stricmp(skins[players[p].skin].name, name) != 0) + FindClosestSkinForStats(p, kartspeed, kartweight); + + players[p].kartspeed = kartspeed; + players[p].kartweight = kartweight; + } + if (extradata & DXD_COLOR) + { + // Color + M_Memcpy(name, demobuf.p, 16); + demobuf.p += 16; + for (i = 0; i < MAXSKINCOLORS; i++) + if (!stricmp(KartColor_Names[i], name)) // SRB2kart + { + players[p].skincolor = i; + if (players[p].mo) + players[p].mo->color = i; + break; + } + } + if (extradata & DXD_NAME) + { + // Name + M_Memcpy(player_names[p],demobuf.p,16); + demobuf.p += 16; + } + if (extradata & DXD_PLAYSTATE) + { + extradata = READUINT8(demobuf.p); + + switch (extradata) { + case DXD_PST_PLAYING: + players[p].pflags |= PF_WANTSTOJOIN; // fuck you + break; + + case DXD_PST_SPECTATING: + players[p].pflags &= ~PF_WANTSTOJOIN; // double-fuck you + if (!playeringame[p]) + { + CL_ClearPlayer(p); + playeringame[p] = true; + G_AddPlayer(p); + players[p].spectator = true; + + // There's likely an off-by-one error in timing recording or playback of joins. This hacks around it so I don't have to find out where that is. \o/ + if (oldcmd[p].forwardmove) + P_RandomByte(); + } + else + { + players[p].spectator = true; + if (players[p].mo) + P_DamageMobj(players[p].mo, NULL, NULL, DMG_INSTAKILL); + else + players[p].playerstate = PST_REBORN; + } + break; + + case DXD_PST_LEFT: + CL_RemovePlayer(p, 0); + break; + } + + G_ResetViews(); + + // maybe these are necessary? + if (G_BattleGametype()) + K_CheckBumpers(); // SRB2Kart + else if (G_RaceGametype()) + P_CheckRacers(); // also SRB2Kart + } + + + p = READUINT8(demobuf.p); + } + + while (p != DW_END) + { + UINT32 rng; + UINT32 checkrng; + + switch (p) + { + case DW_RNG: + rng = READUINT32(demobuf.p); + checkrng = P_GetRandSeed(); + if (checkrng != rng) + { + if (demosynced) + { + CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (RNG)!\n")); + CONS_Printf("expected rng %d got %d\n", rng, checkrng); + } + demosynced = false; + + P_SetRandSeed(rng); + } + } + + p = READUINT8(demobuf.p); + } + + if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER) + { + // end of demo data stream + G_CheckDemoStatus(); + return; + } +} + +void G_WriteDemoExtraData(void) +{ + INT32 i; + char name[17]; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (demo_extradata[i]) + { + WRITEUINT8(demobuf.p, i); + WRITEUINT8(demobuf.p, demo_extradata[i]); + + //if (demo_extradata[i] & DXD_RESPAWN) has no extra data + if (demo_extradata[i] & DXD_SKIN) + { + // Skin + memset(name, 0, 16); + strncpy(name, skins[players[i].skin].name, 16); + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + + WRITEUINT8(demobuf.p, skins[players[i].skin].kartspeed); + WRITEUINT8(demobuf.p, skins[players[i].skin].kartweight); + } + if (demo_extradata[i] & DXD_COLOR) + { + // Color + memset(name, 0, 16); + strncpy(name, KartColor_Names[players[i].skincolor], 16); + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + } + if (demo_extradata[i] & DXD_NAME) + { + // Name + memset(name, 0, 16); + memcpy(name, player_names[i], 15); // Keeping 1 null byte for safety, sorry players with name containing more than 15 characters + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + } + if (demo_extradata[i] & DXD_PLAYSTATE) + { + demo_writerng = 1; + if (!playeringame[i]) + WRITEUINT8(demobuf.p, DXD_PST_LEFT); + else if ( + players[i].spectator && + !(players[i].pflags & PF_WANTSTOJOIN) // <= fuck you specifically + ) + WRITEUINT8(demobuf.p, DXD_PST_SPECTATING); + else + WRITEUINT8(demobuf.p, DXD_PST_PLAYING); + } + } + + demo_extradata[i] = 0; + } + + // May not be necessary, but might as well play it safe... + if ((leveltime & 255) == 128) + demo_writerng = 1; + + { + static UINT8 timeout = 0; + + if (timeout) timeout--; + + if (demo_writerng == 1 || (demo_writerng == 2 && timeout == 0)) + { + demo_writerng = 0; + timeout = 16; + WRITEUINT8(demobuf.p, DW_RNG); + WRITEUINT32(demobuf.p, P_GetRandSeed()); + } + } + + WRITEUINT8(demobuf.p, DW_END); +} + +void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum) +{ + UINT8 ziptic; + + if (!demobuf.p || !demo.deferstart) + return; + ziptic = READUINT8(demobuf.p); + + if (ziptic & ZT_FWD) + oldcmd[playernum].forwardmove = READSINT8(demobuf.p); + if (ziptic & ZT_SIDE) + oldcmd[playernum].sidemove = READSINT8(demobuf.p); + if (ziptic & ZT_ANGLE) + oldcmd[playernum].angleturn = READINT16(demobuf.p); + if (ziptic & ZT_BUTTONS) + oldcmd[playernum].buttons = READUINT16(demobuf.p); + if (ziptic & ZT_AIMING) + oldcmd[playernum].aiming = READINT16(demobuf.p); + if (ziptic & ZT_DRIFT) + oldcmd[playernum].driftturn = READINT16(demobuf.p); + if (ziptic & ZT_LATENCY) + oldcmd[playernum].latency = READUINT8(demobuf.p); + + G_CopyTiccmd(cmd, &oldcmd[playernum], 1); + + // SRB2kart: Copy-pasted from ticcmd building, removes that crappy demo cam + if (((players[displayplayers[0]].mo && players[displayplayers[0]].speed > 0) // Moving + || (leveltime > starttime && (cmd->buttons & BT_ACCELERATE && cmd->buttons & BT_BRAKE)) // Rubber-burn turn + || (players[displayplayers[0]].kartstuff[k_respawn]) // Respawning + || (players[displayplayers[0]].spectator || objectplacing)) // Not a physical player + && !(players[displayplayers[0]].kartstuff[k_spinouttimer] && players[displayplayers[0]].kartstuff[k_sneakertimer])) // Spinning and boosting cancels out spinout + localangle[0] += (cmd->angleturn<<16); + + if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER) + { + // end of demo data stream + G_CheckDemoStatus(); + return; + } +} + +void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum) +{ + char ziptic = 0; + UINT8 *ziptic_p; + + if (!demobuf.p) + return; + + ziptic_p = demobuf.p++; // the ziptic, written at the end of this function + + if (cmd->forwardmove != oldcmd[playernum].forwardmove) + { + WRITEUINT8(demobuf.p,cmd->forwardmove); + oldcmd[playernum].forwardmove = cmd->forwardmove; + ziptic |= ZT_FWD; + } + + if (cmd->sidemove != oldcmd[playernum].sidemove) + { + WRITEUINT8(demobuf.p,cmd->sidemove); + oldcmd[playernum].sidemove = cmd->sidemove; + ziptic |= ZT_SIDE; + } + + if (cmd->angleturn != oldcmd[playernum].angleturn) + { + WRITEINT16(demobuf.p,cmd->angleturn); + oldcmd[playernum].angleturn = cmd->angleturn; + ziptic |= ZT_ANGLE; + } + + if (cmd->buttons != oldcmd[playernum].buttons) + { + WRITEUINT16(demobuf.p,cmd->buttons); + oldcmd[playernum].buttons = cmd->buttons; + ziptic |= ZT_BUTTONS; + } + + if (cmd->aiming != oldcmd[playernum].aiming) + { + WRITEINT16(demobuf.p,cmd->aiming); + oldcmd[playernum].aiming = cmd->aiming; + ziptic |= ZT_AIMING; + } + + if (cmd->driftturn != oldcmd[playernum].driftturn) + { + WRITEINT16(demobuf.p,cmd->driftturn); + oldcmd[playernum].driftturn = cmd->driftturn; + ziptic |= ZT_DRIFT; + } + + if (cmd->latency != oldcmd[playernum].latency) + { + WRITEUINT8(demobuf.p,cmd->latency); + oldcmd[playernum].latency = cmd->latency; + ziptic |= ZT_LATENCY; + } + + *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(INT32 playernum) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_THOK; +} + +void G_GhostAddSpin(INT32 playernum) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_SPIN; +} + +void G_GhostAddRev(INT32 playernum) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + ghostext[playernum].flags = (ghostext[playernum].flags & ~EZT_THOKMASK) | EZT_REV; +} + +void G_GhostAddFlip(INT32 playernum) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + ghostext[playernum].flags |= EZT_FLIP; +} + +void G_GhostAddColor(INT32 playernum, ghostcolor_t color) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + if (ghostext[playernum].lastcolor == (UINT8)color) + { + ghostext[playernum].flags &= ~EZT_COLOR; + return; + } + + ghostext[playernum].flags |= EZT_COLOR; + ghostext[playernum].color = (UINT8)color; +} + +void G_GhostAddScale(INT32 playernum, fixed_t scale) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + if (ghostext[playernum].lastscale == scale) + { + ghostext[playernum].flags &= ~EZT_SCALE; + return; + } + + ghostext[playernum].flags |= EZT_SCALE; + ghostext[playernum].scale = scale; +} + +void G_GhostAddHit(INT32 playernum, mobj_t *victim) +{ + if (!demo.recording || !(demoflags & DF_GHOST)) + return; + + ghostext[playernum].flags |= EZT_HIT; + ghostext[playernum].hits++; + ghostext[playernum].hitlist = Z_Realloc(ghostext[playernum].hitlist, ghostext[playernum].hits * sizeof(mobj_t *), PU_LEVEL, &ghostext[playernum].hitlist); + P_SetTarget(ghostext[playernum].hitlist + (ghostext[playernum].hits-1), victim); +} + +void G_WriteAllGhostTics(void) +{ + if (!demobuf.p) + return; + + UINT8 *save_demo_p = demobuf.p; +#define CHECKSPACE(num) if (demobuf.p+(num) > demoend) { demobuf.p = save_demo_p; G_CheckDemoStatus(); return; } + + boolean toobig = false; + INT32 i, counter = leveltime; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator) + continue; + + if (P_MobjWasRemoved(players[i].mo)) + continue; + + counter++; + + if (multiplayer && ((counter % cv_netdemosyncquality.value) != 0)) // Only write 1 in this many ghost datas per tic to cut down on multiplayer replay size. + continue; + + if (((ghostext[i].flags && (ghostext[i].flags & EZT_HIT)) || ghostext[i].hits) + && !ghostext[i].hitlist) // hitlist might be freed during resynch, beware PU_LEVEL! + { + ghostext[i].hits = 0; + continue; + } + + CHECKSPACE(1); + + 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 >= demoend - (13 + 9 + 9)) + { + toobig = true; + break; + } + } + + CHECKSPACE(1); + WRITEUINT8(demobuf.p, 0xFF); + + if (toobig) + { + G_CheckDemoStatus(); // no more space + return; + } + +#undef CHECKSPACE +} + +void G_WriteGhostTic(mobj_t *ghost, INT32 playernum) +{ + char ziptic = 0; + UINT8 *ziptic_p; + UINT32 i; + UINT8 sprite; + UINT8 frame; + + if (!demobuf.p) + return; + + UINT8 *save_demo_p = demobuf.p; +#define CHECKSPACE(num) if (demobuf.p+(num) > demoend) { demobuf.p = save_demo_p; G_CheckDemoStatus(); return; } + + if (!(demoflags & DF_GHOST)) + return; // No ghost data to write. + + if (ghost->player && ghost->player->pflags & PF_NIGHTSMODE && ghost->tracer) + { + // We're talking about the NiGHTS thing, not the normal platforming thing! + ziptic |= GZT_NIGHTS; + ghost = ghost->tracer; + } + + ziptic_p = demobuf.p++; // the ziptic, written at the end of this function + +#define MAXMOM (0x7FFF<<8) + + // GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic. + if (abs(ghost->x-oldghost[playernum].x) > MAXMOM + || abs(ghost->y-oldghost[playernum].y) > MAXMOM + || abs(ghost->z-oldghost[playernum].z) > MAXMOM + || ((UINT8)(leveltime & 255) > 0 && (UINT8)(leveltime & 255) <= (UINT8)cv_netdemosyncquality.value)) // Hack to enable slightly nicer resyncing + { + oldghost[playernum].x = ghost->x; + oldghost[playernum].y = ghost->y; + oldghost[playernum].z = ghost->z; + ziptic |= GZT_XYZ; + + CHECKSPACE(sizeof(fixed_t)*3); + WRITEFIXED(demobuf.p, oldghost[playernum].x); + WRITEFIXED(demobuf.p, oldghost[playernum].y); + WRITEFIXED(demobuf.p, oldghost[playernum].z); + } + else + { + // For moving normally: + // Store one full byte of movement, plus one byte of fractional movement. + INT16 momx = (INT16)((ghost->x-oldghost[playernum].x + (1<<4))>>8); + INT16 momy = (INT16)((ghost->y-oldghost[playernum].y + (1<<4))>>8); + + if (momx != oldghost[playernum].momx + || momy != oldghost[playernum].momy) + { + oldghost[playernum].momx = momx; + oldghost[playernum].momy = momy; + ziptic |= GZT_MOMXY; + + CHECKSPACE(4); + + WRITEINT16(demobuf.p, momx); + WRITEINT16(demobuf.p, momy); + } + + momx = (INT16)((ghost->z-oldghost[playernum].z + (1<<4))>>8); + + if (momx != oldghost[playernum].momz) + { + oldghost[playernum].momz = momx; + ziptic |= GZT_MOMZ; + + CHECKSPACE(2); + + WRITEINT16(demobuf.p, momx); + } + + // This SHOULD set oldghost.x/y/z to match ghost->x/y/z + // but it keeps the fractional loss of one byte, + // so it will hopefully be made up for in future tics. + oldghost[playernum].x += oldghost[playernum].momx<<8; + oldghost[playernum].y += oldghost[playernum].momy<<8; + oldghost[playernum].z += oldghost[playernum].momz<<8; + } + +#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->angle>>24 != oldghost[playernum].angle) + { + oldghost[playernum].angle = ghost->angle>>24; + ziptic |= GZT_ANGLE; + + CHECKSPACE(1); + + WRITEUINT8(demobuf.p, oldghost[playernum].angle); + } + + // Store the sprite frame. + frame = ghost->frame & 0xFF; + if (frame != oldghost[playernum].frame) + { + oldghost[playernum].frame = frame; + ziptic |= GZT_SPRITE; + + CHECKSPACE(1); + + WRITEUINT8(demobuf.p, oldghost[playernum].frame); + } + + // Check for sprite set changes + sprite = ghost->sprite; + if (sprite != oldghost[playernum].sprite) + { + oldghost[playernum].sprite = sprite; + ghostext[playernum].flags |= EZT_SPRITE; + } + + if (ghost->player) + { + if ( + ghostext[playernum].kartitem != ghost->player->kartstuff[k_itemtype] || + ghostext[playernum].kartamount != ghost->player->kartstuff[k_itemamount] || + ghostext[playernum].kartbumpers != ghost->player->kartstuff[k_bumper] + ) + { + ghostext[playernum].flags |= EZT_KART; + ghostext[playernum].kartitem = ghost->player->kartstuff[k_itemtype]; + ghostext[playernum].kartamount = ghost->player->kartstuff[k_itemamount]; + ghostext[playernum].kartbumpers = ghost->player->kartstuff[k_bumper]; + + } + } + + if (ghostext[playernum].color == ghostext[playernum].lastcolor) + ghostext[playernum].flags &= ~EZT_COLOR; + + if (ghostext[playernum].scale == ghostext[playernum].lastscale) + ghostext[playernum].flags &= ~EZT_SCALE; + + if (ghostext[playernum].flags) + { + ziptic |= GZT_EXTRA; + WRITEUINT8(demobuf.p, ghostext[playernum].flags); + + if (ghostext[playernum].flags & EZT_COLOR) + { + CHECKSPACE(1); + WRITEUINT8(demobuf.p, ghostext[playernum].color); + ghostext[playernum].lastcolor = ghostext[playernum].color; + } + + if (ghostext[playernum].flags & EZT_SCALE) + { + CHECKSPACE(sizeof(fixed_t)); + WRITEFIXED(demobuf.p, ghostext[playernum].scale); + ghostext[playernum].lastscale = ghostext[playernum].scale; + } + + if (ghostext[playernum].flags & EZT_HIT) + { + CHECKSPACE(2); + WRITEUINT16(demobuf.p, ghostext[playernum].hits); + + for (i = 0; i < ghostext[playernum].hits; i++) + { + mobj_t *mo = ghostext[playernum].hitlist[i]; + + CHECKSPACE(4+4+2+sizeof(fixed_t)*3+sizeof(angle_t)); + + WRITEUINT32(demobuf.p, UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.) + WRITEUINT32(demobuf.p, mo->type); + WRITEUINT16(demobuf.p, (UINT16)mo->health); + WRITEFIXED(demobuf.p, mo->x); + WRITEFIXED(demobuf.p, mo->y); + WRITEFIXED(demobuf.p, mo->z); + WRITEANGLE(demobuf.p, mo->angle); + P_SetTarget(ghostext[playernum].hitlist+i, NULL); + } + + ghostext[playernum].hits = 0; + } + + if (ghostext[playernum].flags & EZT_SPRITE) + { + CHECKSPACE(1); + WRITEUINT8(demobuf.p, sprite); + } + + if (ghostext[playernum].flags & EZT_KART) + { + CHECKSPACE(12); + WRITEINT32(demobuf.p, ghostext[playernum].kartitem); + WRITEINT32(demobuf.p, ghostext[playernum].kartamount); + WRITEINT32(demobuf.p, ghostext[playernum].kartbumpers); + } + + ghostext[playernum].flags = 0; + } + + *ziptic_p = ziptic; +#undef CHECKSPACE +} + +void G_ConsAllGhostTics(void) +{ + UINT8 p; + + if (!demobuf.p || !demo.deferstart) + return; + + p = READUINT8(demobuf.p); + + while (p != 0xFF) + { + 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. +// This fixes desynchronising demos when fighting eggman. +void G_ConsGhostTic(INT32 playernum) +{ + UINT8 ziptic; + fixed_t px,py,pz,gx,gy,gz; + mobj_t *testmo; + fixed_t syncleeway; + boolean nightsfail = false; + + if (!(demoflags & DF_GHOST)) + return; // No ghost data to use. + + testmo = players[playernum].mo; + + // Grab ghost data. + ziptic = READUINT8(demobuf.p); + + if (ziptic & GZT_XYZ) + { + oldghost[playernum].x = READFIXED(demobuf.p); + oldghost[playernum].y = READFIXED(demobuf.p); + oldghost[playernum].z = READFIXED(demobuf.p); + syncleeway = 0; + } + else + { + if (ziptic & GZT_MOMXY) + { + oldghost[playernum].momx = READINT16(demobuf.p)<<8; + oldghost[playernum].momy = READINT16(demobuf.p)<<8; + } + + if (ziptic & GZT_MOMZ) + oldghost[playernum].momz = READINT16(demobuf.p)<<8; + + oldghost[playernum].x += oldghost[playernum].momx; + oldghost[playernum].y += oldghost[playernum].momy; + oldghost[playernum].z += oldghost[playernum].momz; + syncleeway = FRACUNIT; + } + + if (ziptic & GZT_ANGLE) + demobuf.p++; + + if (ziptic & GZT_SPRITE) + demobuf.p++; + + if (ziptic & GZT_NIGHTS) + { + if (!testmo || !testmo->player || !(testmo->player->pflags & PF_NIGHTSMODE) || !testmo->tracer) + nightsfail = true; + else + testmo = testmo->tracer; + } + + if (ziptic & GZT_EXTRA) + { // But wait, there's more! + ziptic = READUINT8(demobuf.p); + if (ziptic & EZT_COLOR) + demobuf.p++; + + if (ziptic & EZT_SCALE) + demobuf.p += sizeof(fixed_t); + + if (ziptic & EZT_HIT) + { // Resync mob damage. + UINT16 i, count = READUINT16(demobuf.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++) + { + demobuf.p += 4; // reserved. + type = READUINT32(demobuf.p); + health = READUINT16(demobuf.p); + x = READFIXED(demobuf.p); + y = READFIXED(demobuf.p); + z = READFIXED(demobuf.p); + demobuf.p += sizeof(angle_t); // angle, unnecessary for cons. + + mobj = NULL; + for (th = thinkercap.next; th != &thinkercap; th = th->next) + { + if (th->function != (actionf_p1)P_MobjThinker) + continue; + mobj = (mobj_t *)th; + if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z) + break; + mobj = NULL; // wasn't this one, keep searching. + } + + if (mobj && mobj->health != health) // Wasn't damaged?! This is desync! Fix it! + { + if (demosynced) + { + CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (health)!\n")); + CONS_Printf("expected health %d got %d\n", health, mobj->health); + } + + demosynced = false; + P_DamageMobj(mobj, players[0].mo, players[0].mo, 1); + } + } + } + + if (ziptic & EZT_SPRITE) + demobuf.p++; + + if (ziptic & EZT_KART) + { + ghostext[playernum].kartitem = READINT32(demobuf.p); + ghostext[playernum].kartamount = READINT32(demobuf.p); + ghostext[playernum].kartbumpers = READINT32(demobuf.p); + } + } + + if (testmo) + { + // Re-synchronise + px = testmo->x; + py = testmo->y; + pz = testmo->z; + gx = oldghost[playernum].x; + gy = oldghost[playernum].y; + gz = oldghost[playernum].z; + + if (nightsfail || abs(px-gx) > syncleeway || abs(py-gy) > syncleeway || abs(pz-gz) > syncleeway) + { + ghostext[playernum].desyncframes++; + + if (ghostext[playernum].desyncframes >= 2) + { + if (demosynced) + CONS_Alert(CONS_WARNING, "Demo playback has desynced (player %s)!\n", player_names[playernum]); + demosynced = false; + + P_UnsetThingPosition(testmo); + testmo->x = oldghost[playernum].x; + testmo->y = oldghost[playernum].y; + P_SetThingPosition(testmo); + testmo->z = oldghost[playernum].z; + + if (abs(testmo->z - testmo->floorz) < 4*FRACUNIT) + testmo->z = testmo->floorz; // Sync players to the ground when they're likely supposed to be there... + + ghostext[playernum].desyncframes = 2; + } + } + else + ghostext[playernum].desyncframes = 0; + + if ( +#ifdef DEMO_COMPAT_100 + demo.version != 0x0001 && +#endif + ( + players[playernum].kartstuff[k_itemtype] != ghostext[playernum].kartitem || + players[playernum].kartstuff[k_itemamount] != ghostext[playernum].kartamount || + players[playernum].kartstuff[k_bumper] != ghostext[playernum].kartbumpers + ) + ) + { + if (demosynced) + { + CONS_Alert(CONS_WARNING, "Demo playback has desynced (item/bumpers)!(player %s)!\n", player_names[playernum]); + CONS_Printf("expected item type %d got %d\n", ghostext[playernum].kartitem, players[playernum].kartstuff[k_itemtype]); + CONS_Printf("expected item amount %d got %d\n", ghostext[playernum].kartamount, players[playernum].kartstuff[k_itemamount]); + } + demosynced = false; + + players[playernum].kartstuff[k_itemtype] = ghostext[playernum].kartitem; + players[playernum].kartstuff[k_itemamount] = ghostext[playernum].kartamount; + players[playernum].kartstuff[k_bumper] = ghostext[playernum].kartbumpers; + } + } + + if (*demobuf.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); + +#ifdef DEMO_COMPAT_100 + if (g->version != 0x0001) + { +#endif + while (ziptic != DW_END) // Get rid of extradata stuff + { + if (ziptic == 0) // Only support player 0 info for now + { + ziptic = READUINT8(g->p); + if (ziptic & DXD_SKIN) + g->p += 18; // We _could_ read this info, but it shouldn't change anything in record attack... + if (ziptic & DXD_COLOR) + g->p += 16; // Same tbh + if (ziptic & DXD_NAME) + g->p += 16; // yea + if (ziptic & DXD_PLAYSTATE && READUINT8(g->p) != DXD_PST_PLAYING) + I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this + } + else if (ziptic == DW_RNG) + g->p += 4; // RNG seed + else + I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this + + ziptic = READUINT8(g->p); + } + + ziptic = READUINT8(g->p); // Back to actual ziptic stuff +#ifdef DEMO_COMPAT_100 + } +#endif + + 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; + if (ziptic & ZT_DRIFT) + g->p += 2; + if (ziptic & ZT_LATENCY) + g->p += 1; + + // Grab ghost data. + ziptic = READUINT8(g->p); + +#ifdef DEMO_COMPAT_100 + if (g->version != 0x0001) + { +#endif + if (ziptic == 0xFF) + goto skippedghosttic; // Didn't write ghost info this frame + else if (ziptic != 0) + I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this + ziptic = READUINT8(g->p); +#ifdef DEMO_COMPAT_100 + } +#endif + 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 = READINT16(g->p)<<8; + g->oldmo.momy = READINT16(g->p)<<8; + } + if (ziptic & GZT_MOMZ) + g->oldmo.momz = READINT16(g->p)<<8; + g->oldmo.x += g->oldmo.momx; + g->oldmo.y += g->oldmo.momy; + g->oldmo.z += g->oldmo.momz; + } + if (ziptic & GZT_ANGLE) + g->oldmo.angle = READUINT8(g->p)<<24; + if (ziptic & GZT_SPRITE) + g->oldmo.frame = 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->angle = g->oldmo.angle; + g->mo->frame = g->oldmo.frame | tr_trans30<p); + if (ziptic & EZT_COLOR) + { + g->color = READUINT8(g->p); + switch(g->color) + { + default: + 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; + } + } + if (ziptic & EZT_FLIP) + g->mo->eflags ^= MFE_VERTICALFLIP; + if (ziptic & EZT_SCALE) + { + g->mo->destscale = READFIXED(g->p); + if (g->mo->destscale != g->mo->scale) + P_SetScale(g->mo, g->mo->destscale); + } + if (ziptic & EZT_THOKMASK) + { // Let's only spawn ONE of these per frame, thanks. + mobj_t *mobj; + INT32 type = -1; + if (g->mo->skin) + { + switch (ziptic & EZT_THOKMASK) + { + case EZT_THOK: + type = (UINT32)mobjinfo[MT_PLAYER].painchance; + break; + case EZT_SPIN: + type = (UINT32)mobjinfo[MT_PLAYER].damage; + break; + case EZT_REV: + type = (UINT32)mobjinfo[MT_PLAYER].raisestate; + break; + } + } + if (type != -1) + { + 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->x, g->mo->y, g->mo->z - 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<tics = -1; // nope. + mobj->color = g->mo->color; + if (g->mo->eflags & MFE_VERTICALFLIP) + { + mobj->flags2 |= MF2_OBJECTFLIP; + mobj->eflags |= MFE_VERTICALFLIP; + } + P_SetScale(mobj, g->mo->scale); + mobj->destscale = g->mo->scale; + } + 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); + mobj->fuse = 8; + P_SetTarget(&mobj->target, g->mo); + } + } + if (ziptic & 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 + g->p += 4; // backwards compat., type used to be here + health = READUINT16(g->p); + x = READFIXED(g->p); + y = READFIXED(g->p); + z = READFIXED(g->p); + angle = READANGLE(g->p); + if (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 (ziptic & EZT_SPRITE) + g->mo->sprite = READUINT8(g->p); + if (ziptic & EZT_KART) + g->p += 12; // kartitem, kartamount, kartbumpers + } + +#ifdef DEMO_COMPAT_100 + if (g->version != 0x0001) + { +#endif + if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here. + I_Error("Ghost is not a record attack ghost"); //@TODO lmao don't blow up like this +#ifdef DEMO_COMPAT_100 + } +#endif + +skippedghosttic: + // Tick ghost colors (Super and Mario Invincibility flashing) + switch(g->color) + { + case GHC_SUPER: // Super Sonic (P_DoSuperStuff) + g->mo->color = SKINCOLOR_SUPER1; + g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4); + break; + case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer) + g->mo->color = (UINT8)(leveltime % MAXSKINCOLORS); + break; + default: + break; + } + + // Demo ends after ghost data. + if (*g->p == DEMOMARKER) + { + g->mo->momx = g->mo->momy = g->mo->momz = 0; + if (p) + p->next = g->next; + else + ghosts = g->next; + Z_Free(g); + continue; + } + p = g; + } +} + +// Demo rewinding functions +typedef struct rewindinfo_s { + tic_t leveltime; + + struct { + boolean ingame; + player_t player; + mobj_t mobj; + } playerinfo[MAXPLAYERS]; + + struct rewindinfo_s *prev; +} rewindinfo_t; + +static tic_t currentrewindnum; +static rewindinfo_t *rewindhead = NULL; // Reverse chronological order + +void G_InitDemoRewind(void) +{ + CL_ClearRewinds(); + + while (rewindhead) + { + rewindinfo_t *p = rewindhead->prev; + Z_Free(rewindhead); + rewindhead = p; + } + + currentrewindnum = 0; +} + +void G_StoreRewindInfo(void) +{ + static UINT8 timetolog = 8; + rewindinfo_t *info; + size_t i; + + if (timetolog-- > 0) + return; + timetolog = 8; + + info = Z_Calloc(sizeof(rewindinfo_t), PU_STATIC, NULL); + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator) + { + info->playerinfo[i].ingame = false; + continue; + } + + info->playerinfo[i].ingame = true; + memcpy(&info->playerinfo[i].player, &players[i], sizeof(player_t)); + if (players[i].mo) + memcpy(&info->playerinfo[i].mobj, players[i].mo, sizeof(mobj_t)); + } + + info->leveltime = leveltime; + info->prev = rewindhead; + rewindhead = info; +} + +void G_PreviewRewind(tic_t previewtime) +{ + SINT8 i; + size_t j; + fixed_t tweenvalue = 0; + rewindinfo_t *info = rewindhead, *next_info = rewindhead; + + if (!info) + return; + + while (info->leveltime > previewtime && info->prev) + { + next_info = info; + info = info->prev; + } + if (info != next_info) + tweenvalue = FixedDiv(previewtime - info->leveltime, next_info->leveltime - info->leveltime); + + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator) + { + if (info->playerinfo[i].player.mo) + { + //@TODO spawn temp object to act as a player display + } + + continue; + } + + if (!info->playerinfo[i].ingame || !info->playerinfo[i].player.mo) + { + if (players[i].mo) + players[i].mo->flags2 |= MF2_DONTDRAW; + + continue; + } + + if (!players[i].mo) + continue; //@TODO spawn temp object to act as a player display + + players[i].mo->flags2 &= ~MF2_DONTDRAW; + + P_UnsetThingPosition(players[i].mo); +#define TWEEN(pr) info->playerinfo[i].mobj.pr + FixedMul((INT32) (next_info->playerinfo[i].mobj.pr - info->playerinfo[i].mobj.pr), tweenvalue) + players[i].mo->x = TWEEN(x); + players[i].mo->y = TWEEN(y); + players[i].mo->z = TWEEN(z); + players[i].mo->angle = TWEEN(angle); +#undef TWEEN + P_SetThingPosition(players[i].mo); + + players[i].frameangle = info->playerinfo[i].player.frameangle + FixedMul((INT32) (next_info->playerinfo[i].player.frameangle - info->playerinfo[i].player.frameangle), tweenvalue); + + players[i].mo->sprite = info->playerinfo[i].mobj.sprite; + players[i].mo->frame = info->playerinfo[i].mobj.frame; + + players[i].realtime = info->playerinfo[i].player.realtime; + for (j = 0; j < NUMKARTSTUFF; j++) + players[i].kartstuff[j] = info->playerinfo[i].player.kartstuff[j]; + } + + for (i = splitscreen; i >= 0; i--) + P_ResetCamera(&players[displayplayers[i]], &camera[i]); +} + +void G_ConfirmRewind(tic_t rewindtime) +{ + SINT8 i; + tic_t j; + boolean oldmenuactive = menuactive, oldsounddisabled = sound_disabled; + + INT32 olddp1 = displayplayers[0], olddp2 = displayplayers[1], olddp3 = displayplayers[2], olddp4 = displayplayers[3]; + UINT8 oldss = splitscreen; + + menuactive = false; // Prevent loops + + CV_StealthSetValue(&cv_renderview, 0); + + if (rewindtime <= starttime) + { + demo.rewinding = false; + G_DoPlayDemo(NULL); // Restart the current demo + } + else + { + rewind_t *rewind; + sound_disabled = true; // Prevent sound spam + demo.rewinding = true; + + rewind = CL_RewindToTime(rewindtime); + + if (rewind) + { + demobuf.p = demobuf.buffer + rewind->demopos; + memcpy(oldcmd, rewind->oldcmd, sizeof (oldcmd)); + memcpy(oldghost, rewind->oldghost, sizeof (oldghost)); + paused = false; + } + else + { + demo.rewinding = true; + G_DoPlayDemo(NULL); // Restart the current demo + } + } + + for (j = 0; j < rewindtime && leveltime < rewindtime; j++) + { + G_Ticker((j % NEWTICRATERATIO) == 0); + } + + demo.rewinding = false; + menuactive = oldmenuactive; // Bring the menu back up + sound_disabled = oldsounddisabled; // Re-enable SFX + + wipegamestate = gamestate; // No fading back in! + + COM_BufInsertText("renderview on\n"); + + splitscreen = oldss; + displayplayers[0] = olddp1; + displayplayers[1] = olddp2; + displayplayers[2] = olddp3; + displayplayers[3] = olddp4; + R_ExecuteSetViewSize(); + G_ResetViews(); + + for (i = splitscreen; i >= 0; i--) + P_ResetCamera(&players[displayplayers[i]], &camera[i]); +} + +void G_ReadMetalTic(mobj_t *metal) +{ + UINT8 ziptic; + UINT16 speed; + UINT8 statetype; + + if (!metal_p) + return; + ziptic = READUINT8(metal_p); + + // Read changes from the tic + if (ziptic & GZT_XYZ) + { + oldmetal.x = READFIXED(metal_p); + oldmetal.y = READFIXED(metal_p); + oldmetal.z = READFIXED(metal_p); + P_MoveOrigin(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 = READINT16(metal_p)<<8; + oldmetal.momy = READINT16(metal_p)<<8; + } + if (ziptic & GZT_MOMZ) + oldmetal.momz = READINT16(metal_p)<<8; + oldmetal.x += oldmetal.momx; + oldmetal.y += oldmetal.momy; + oldmetal.z += oldmetal.momz; + } + if (ziptic & GZT_ANGLE) + oldmetal.angle = READUINT8(metal_p)<<24; + if (ziptic & GZT_SPRITE) + metal_p++; // Currently unused. (Metal Sonic figures out what he's doing his own damn self.) + + // 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->angle = oldmetal.angle; + + if (ziptic & GZT_EXTRA) + { // But wait, there's more! + ziptic = READUINT8(metal_p); + if (ziptic & EZT_FLIP) + metal->eflags ^= MFE_VERTICALFLIP; + if (ziptic & EZT_SCALE) + { + metal->destscale = READFIXED(metal_p); + if (metal->destscale != metal->scale) + P_SetScale(metal, metal->destscale); + } + } + + // Calculates player's speed based on distance-of-a-line formula + speed = FixedDiv(P_AproxDistance(oldmetal.momx, oldmetal.momy), metal->scale)>>FRACBITS; + + // Use speed to decide an appropriate state + if (speed > 20) // default skin runspeed + statetype = 2; + else if (speed > 1) // stopspeed + statetype = 1; + else + statetype = 0; + + // Set state + if (statetype != metal->threshold) + { + switch (statetype) + { + case 2: // run + P_SetMobjState(metal,metal->info->meleestate); + break; + case 1: // walk + P_SetMobjState(metal,metal->info->seestate); + break; + default: // stand + P_SetMobjState(metal,metal->info->spawnstate); + break; + } + metal->threshold = statetype; + } + + // TODO: Modify state durations based on movement speed, similar to players? + + if (*metal_p == DEMOMARKER) + { + // end of demo data stream + G_StopMetalDemo(); + return; + } +} + +void G_WriteMetalTic(mobj_t *metal) +{ + UINT8 ziptic = 0; + UINT8 *ziptic_p; + + if (!demobuf.p) // demobuf.p will be NULL until the race start linedef executor is triggered! + return; + + ziptic_p = demobuf.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; + WRITEFIXED(demobuf.p,oldmetal.x); + WRITEFIXED(demobuf.p,oldmetal.y); + WRITEFIXED(demobuf.p,oldmetal.z); + ziptic |= GZT_XYZ; + } + else + { + // For moving normally: + // Store one full byte of movement, plus one byte of fractional movement. + INT16 momx = (INT16)((metal->x-oldmetal.x)>>8); + INT16 momy = (INT16)((metal->y-oldmetal.y)>>8); + if (momx != oldmetal.momx + || momy != oldmetal.momy) + { + oldmetal.momx = momx; + oldmetal.momy = momy; + WRITEINT16(demobuf.p,momx); + WRITEINT16(demobuf.p,momy); + ziptic |= GZT_MOMXY; + } + momx = (INT16)((metal->z-oldmetal.z)>>8); + if (momx != oldmetal.momz) + { + oldmetal.momz = momx; + WRITEINT16(demobuf.p,momx); + ziptic |= GZT_MOMZ; + } + + // This SHOULD set oldmetal.x/y/z to match metal->x/y/z + // but it keeps the fractional loss of one byte, + // so it will hopefully be made up for in future tics. + oldmetal.x += oldmetal.momx<<8; + oldmetal.y += oldmetal.momy<<8; + oldmetal.z += oldmetal.momz<<8; + } + + #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->angle>>24 != oldmetal.angle) + { + oldmetal.angle = metal->angle>>24; + WRITEUINT8(demobuf.p,oldmetal.angle); + ziptic |= GZT_ANGLE; + } + + // Metal Sonic does not need our state changes. + // ... currently. + + { + UINT8 *exttic_p = NULL; + UINT8 exttic = 0; + if ((metal->eflags & MFE_VERTICALFLIP) != (oldmetal.eflags & MFE_VERTICALFLIP)) + { + if (!exttic_p) + exttic_p = demobuf.p++; + exttic |= EZT_FLIP; + oldmetal.eflags ^= MFE_VERTICALFLIP; + } + if (metal->scale != oldmetal.scale) + { + if (!exttic_p) + exttic_p = demobuf.p++; + exttic |= EZT_SCALE; + WRITEFIXED(demobuf.p,metal->scale); + oldmetal.scale = metal->scale; + } + if (exttic_p) + { + *exttic_p = exttic; + ziptic |= GZT_EXTRA; + } + } + + *ziptic_p = ziptic; + + // attention here for the ticcmd size! + // latest demos with mouse aiming byte in ticcmd + if (demobuf.p >= demoend - 32) + { + G_StopMetalRecording(); // no more space + return; + } +} + +// +// G_RecordDemo +// +void G_RecordDemo(const char *name) +{ + INT32 maxsize; + + demobuf.p = NULL; + G_ResetDemoRecording(); + demoend = NULL; + + if (cv_recordmultiplayerdemos.value) + { + CONS_Printf("Recording demo %s.lmp\n", name); + + strcpy(demoname, name); + strcat(demoname, ".lmp"); + + maxsize = cv_maxdemosize.value*1024*1024; + + demobuf.buffer = Z_Malloc(maxsize + 100*1024, PU_STATIC, NULL); + demoend = demobuf.buffer + maxsize; + + if (demobuf.buffer) + demo.recording = true; + else + CONS_Alert(CONS_ERROR, "Failed to allocate demo buffer\n"); + } +} + +void G_RecordMetal(void) +{ + INT32 maxsize; + maxsize = cv_maxdemosize.value*1024*1024; + if (demobuf.buffer) + Z_Free(demobuf.buffer); + demobuf.p = NULL; + metalrecording = false; + demobuf.buffer = Z_Malloc(maxsize + 100*1024, PU_STATIC, NULL); + demoend = demobuf.buffer + maxsize; + + if (demobuf.buffer) + metalrecording = true; + else + CONS_Alert(CONS_ERROR, "Failed to allocate demo buffer\n"); +} + +void G_BeginRecording(void) +{ + UINT8 i, p; + char name[17]; + player_t *player = &players[consoleplayer]; + + char *filename; + UINT8 totalfiles; + UINT8 *m; + + if (!cv_recordmultiplayerdemos.value) + { + G_ResetDemoRecording(); + return; + } + + if (demobuf.buffer == NULL) + { + CONS_Alert(CONS_ERROR, "No demo buffer allocated\n"); + G_ResetDemoRecording(); + return; + } + + if (demobuf.p) + { + G_ResetDemoRecording(); + return; + } + + memset(name,0,sizeof(name)); + + demobuf.p = demobuf.buffer; + demoflags = DF_GHOST|(multiplayer ? DF_MULTIPLAYER : (modeattacking<important) + { + nameonly(( filename = va("%s", wadfiles[i]->filename) )); + WRITESTRINGL(demobuf.p, filename, MAX_WADPATH); + WRITEMEM(demobuf.p, wadfiles[i]->md5sum, 16); + + totalfiles++; + } + + WRITEUINT8(m, totalfiles); + + switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT) + { + case ATTACKING_NONE: // 0 + break; + case ATTACKING_RECORD: // 1 + demotime_p = demobuf.p; + WRITEUINT32(demobuf.p,UINT32_MAX); // time + WRITEUINT32(demobuf.p,UINT32_MAX); // lap + break; + default: // 3 + break; + } + + WRITEUINT32(demobuf.p, P_GetInitSeed()); + + // Reserved for extrainfo location from start of file + demoinfo_p = demobuf.p; + WRITEUINT32(demobuf.p, 0); + + // Save netvars + CV_SaveNetVars(&demobuf.p, true); + + // Now store some info for each in-game player + for (p = 0; p < MAXPLAYERS; p++) + { + if (!playeringame[p]) + continue; + + player = &players[p]; + + WRITEUINT8(demobuf.p, p | (player->spectator ? DEMO_SPECTATOR : 0)); + + // Name + memset(name, 0, 16); + memcpy(name, player_names[p], 15); + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + + // Skin + memset(name, 0, 16); + strncpy(name, skins[player->skin].name, 16); + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + + // Color + memset(name, 0, 16); + strncpy(name, KartColor_Names[player->skincolor], 16); + M_Memcpy(demobuf.p,name,16); + demobuf.p += 16; + + // Score, since Kart uses this to determine where you start on the map + WRITEUINT32(demobuf.p, player->score); + + // Kart speed and weight + WRITEUINT8(demobuf.p, skins[player->skin].kartspeed); + WRITEUINT8(demobuf.p, skins[player->skin].kartweight); + } + + WRITEUINT8(demobuf.p, 0xFF); // Denote the end of the player listing + + // player lua vars, always saved even if empty... Unless it's record attack. + if (demoflags & DF_LUAVARS) + LUA_Archive(&demobuf, false); + + memset(&oldcmd,0,sizeof(oldcmd)); + memset(&oldghost,0,sizeof(oldghost)); + memset(&ghostext,0,sizeof(ghostext)); + + for (i = 0; i < MAXPLAYERS; i++) + { + ghostext[i].lastcolor = ghostext[i].color = GHC_NORMAL; + ghostext[i].lastscale = ghostext[i].scale = FRACUNIT; + + if (players[i].mo) + { + oldghost[i].x = players[i].mo->x; + oldghost[i].y = players[i].mo->y; + oldghost[i].z = players[i].mo->z; + oldghost[i].angle = players[i].mo->angle; + + // preticker started us gravity flipped + if (players[i].mo->eflags & MFE_VERTICALFLIP) + ghostext[i].flags |= EZT_FLIP; + } + } +} + +void G_BeginMetal(void) +{ + mobj_t *mo = players[consoleplayer].mo; + + if (demobuf.buffer == NULL) + { + CONS_Alert(CONS_ERROR, "No metal demo buffer allocated\n"); + metalrecording = false; + return; + } + + if (demobuf.p) + { + Z_Free(demobuf.buffer); + demobuf.buffer = NULL; + metalrecording = false; + return; + } + + demobuf.p = demobuf.buffer; + + // Write header. + M_Memcpy(demobuf.p, DEMOHEADER, 12); demobuf.p += 12; + WRITEUINT8(demobuf.p,VERSION); + WRITEUINT8(demobuf.p,SUBVERSION); + WRITEUINT16(demobuf.p,DEMOVERSION); + + // demo checksum + demobuf.p += 16; + + M_Memcpy(demobuf.p, "METL", 4); demobuf.p += 4; + + // 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; +} + +void G_WriteStanding(UINT8 ranking, char *name, INT32 skinnum, UINT8 color, UINT32 val) +{ + char temp[17]; + + if (!demobuf.p) + return; + + if (demoinfo_p && *(UINT32 *)demoinfo_p == 0 && demobuf.buffer != NULL) + { + WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker + *(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer; + } + + WRITEUINT8(demobuf.p, DW_STANDING); + WRITEUINT8(demobuf.p, ranking); + + // Name + memset(temp, 0, 16); + strncpy(temp, name, 16); + M_Memcpy(demobuf.p,temp,16); + demobuf.p += 16; + + // Skin + memset(temp, 0, 16); + strncpy(temp, skins[skinnum].name, 16); + M_Memcpy(demobuf.p,temp,16); + demobuf.p += 16; + + // Color + memset(temp, 0, 16); + strncpy(temp, KartColor_Names[color], 16); + M_Memcpy(demobuf.p,temp,16); + demobuf.p += 16; + + // Score/time/whatever + WRITEUINT32(demobuf.p, val); +} + +void G_SetDemoTime(UINT32 ptime, UINT32 plap) +{ + if (!demo.recording || !demotime_p) + return; + if (demoflags & DF_RECORDATTACK) + { + WRITEUINT32(demotime_p, ptime); + WRITEUINT32(demotime_p, plap); + demotime_p = NULL; + } +} + +static void G_LoadDemoExtraFiles(UINT8 **pp) +{ + UINT8 totalfiles; + char filename[MAX_WADPATH]; + UINT8 md5sum[16]; + filestatus_t ncs = FS_NOTFOUND; + boolean toomany = false; + boolean alreadyloaded; + UINT8 i, j; + + totalfiles = READUINT8((*pp)); + for (i = 0; i < totalfiles; ++i) + { + if (toomany) + SKIPSTRING((*pp)); + else + { + strlcpy(filename, (char *)(*pp), sizeof filename); + SKIPSTRING((*pp)); + } + READMEM((*pp), md5sum, 16); + + if (!toomany) + { + alreadyloaded = false; + + for (j = 0; j < numwadfiles; ++j) + { + if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0) + { + alreadyloaded = true; + break; + } + } + + if (alreadyloaded) + continue; + + if (numwadfiles >= MAX_WADFILES) + toomany = true; + else + ncs = findfile(filename, md5sum, false); + + if (toomany) + { + CONS_Alert(CONS_WARNING, M_GetText("Too many files loaded to add anymore for demo playback\n")); + if (!CON_Ready()) + M_StartMessage(M_GetText("There are too many files loaded to add this demo's addons.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING); + } + else if (ncs != FS_FOUND) + { + if (ncs == FS_NOTFOUND) + CONS_Alert(CONS_NOTICE, M_GetText("You do not have a copy of %s\n"), filename); + else if (ncs == FS_MD5SUMBAD) + CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), filename); + else + CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), filename); + + if (!CON_Ready()) + M_StartMessage(M_GetText("There were errors trying to add this demo's addons. Check the console for more information.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING); + } + else + { + P_PartialAddWadFile(filename, false); + } + } + } + + if (P_PartialAddGetStage() >= 0) + P_MultiSetupWadFiles(true); // in case any partial adds were done +} + +static void G_SkipDemoExtraFiles(UINT8 **pp) +{ + UINT8 totalfiles; + UINT8 i; + + totalfiles = READUINT8((*pp)); + for (i = 0; i < totalfiles; ++i) + { + SKIPSTRING((*pp));// file name + (*pp) += 16;// md5 + } +} + +// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's. +// Enabling quick prevents filesystem checks to see if needed files are available to load. +static UINT8 G_CheckDemoExtraFiles(UINT8 **pp, boolean quick) +{ + UINT8 totalfiles, filesloaded, nmusfilecount; + char filename[MAX_WADPATH]; + UINT8 md5sum[16]; + boolean toomany = false; + boolean alreadyloaded; + UINT8 i, j; + UINT8 error = 0; + + totalfiles = READUINT8((*pp)); + filesloaded = 0; + for (i = 0; i < totalfiles; ++i) + { + if (toomany) + SKIPSTRING((*pp)); + else + { + strlcpy(filename, (char *)(*pp), sizeof filename); + SKIPSTRING((*pp)); + } + READMEM((*pp), md5sum, 16); + + if (!toomany) + { + alreadyloaded = false; + nmusfilecount = 0; + + for (j = 0; j < numwadfiles; ++j) + { + if (wadfiles[j]->important && j > mainwads) + nmusfilecount++; + else + continue; + + if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0) + { + alreadyloaded = true; + + if (i != nmusfilecount-1 && error < DFILE_ERROR_OUTOFORDER) + error |= DFILE_ERROR_OUTOFORDER; + + break; + } + } + + if (alreadyloaded) + { + filesloaded++; + continue; + } + + if (numwadfiles >= MAX_WADFILES) + error = DFILE_ERROR_CANNOTLOAD; + else if (!quick && findfile(filename, md5sum, false) != FS_FOUND) + error = DFILE_ERROR_CANNOTLOAD; + else if (error < DFILE_ERROR_INCOMPLETEOUTOFORDER) + error |= DFILE_ERROR_NOTLOADED; + } else + error = DFILE_ERROR_CANNOTLOAD; + } + + // Get final file count + nmusfilecount = 0; + + for (j = 0; j < numwadfiles; ++j) + if (wadfiles[j]->important && j > mainwads) + nmusfilecount++; + + if (!error && filesloaded < nmusfilecount) + error = DFILE_ERROR_EXTRAFILES; + + return error; +} + +// 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, oldlap, newlap; + UINT16 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 == DEMOVERSION); + p += 64; // full demo title + p += 16; // demo checksum + I_Assert(!memcmp(p, "PLAY", 4)); + p += 4; // PLAY + p += 2; // gamemap + p += 16; // map md5 + flags = READUINT8(p); // demoflags + p++; // gametype + G_SkipDemoExtraFiles(&p); + + aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK); + I_Assert(aflags); + if (flags & DF_RECORDATTACK) + { + newtime = READUINT32(p); + newlap = READUINT32(p); + } + 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 + p += 64; // full demo title + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + // Old replays gotta go :] + CONS_Alert(CONS_NOTICE, M_GetText("File '%s' outdated version. It will be overwritten. Nyeheheh.\n"), oldname); + Z_Free(buffer); + return UINT8_MAX; +#endif + // 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" + p += 2; // gamemap + p += 16; // mapmd5 + flags = READUINT8(p); + p++; // gametype + G_SkipDemoExtraFiles(&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); + oldlap = READUINT32(p); + } + else // appease compiler + return UINT8_MAX; + + Z_Free(buffer); + + c = 0; + if (newtime < oldtime + || (newtime == oldtime && (newlap < oldlap))) + c |= 1; // Better time + if (newlap < oldlap + || (newlap == oldlap && newtime < oldtime)) + c |= 1<<1; // Better lap time + return c; +} + +void G_LoadDemoInfo(menudemo_t *pdemo) +{ + UINT8 *infobuffer, *info_p, *extrainfo_p; + UINT8 version, subversion, pdemoflags; + UINT16 pdemoversion, count; + + if (!FIL_ReadFile(pdemo->filepath, &infobuffer)) + { + CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), pdemo->filepath); + pdemo->type = MD_INVALID; + sprintf(pdemo->title, "INVALID REPLAY"); + + return; + } + + info_p = infobuffer; + + if (memcmp(info_p, DEMOHEADER, 12)) + { + CONS_Alert(CONS_ERROR, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemo->filepath); + pdemo->type = MD_INVALID; + sprintf(pdemo->title, "INVALID REPLAY"); + Z_Free(infobuffer); + return; + } + + pdemo->type = MD_LOADED; + + info_p += 12; // DEMOHEADER + + version = READUINT8(info_p); + subversion = READUINT8(info_p); + pdemoversion = READUINT16(info_p); + + memset(pdemo->version, 0, sizeof(pdemo->version)); + snprintf(pdemo->version, sizeof(pdemo->version), "v%d.%d", version, subversion); + + switch(pdemoversion) + { + case DEMOVERSION: // latest always supported + // demo title + M_Memcpy(pdemo->title, info_p, 64); + info_p += 64; + + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + pdemo->type = MD_OUTDATED; + sprintf(pdemo->title, "Legacy Replay"); + break; +#endif + // too old, cannot support. + default: + CONS_Alert(CONS_ERROR, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemo->filepath); + pdemo->type = MD_INVALID; + sprintf(pdemo->title, "INVALID REPLAY"); + Z_Free(infobuffer); + return; + } + + if (version != VERSION || subversion != SUBVERSION) + pdemo->type = MD_OUTDATED; + + info_p += 16; // demo checksum + if (memcmp(info_p, "PLAY", 4)) + { + CONS_Alert(CONS_ERROR, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemo->filepath); + pdemo->type = MD_INVALID; + sprintf(pdemo->title, "INVALID REPLAY"); + Z_Free(infobuffer); + return; + } + info_p += 4; // "PLAY" + pdemo->map = READINT16(info_p); + info_p += 16; // mapmd5 + + pdemoflags = READUINT8(info_p); + + // temp? + if (!(pdemoflags & DF_MULTIPLAYER)) + { + CONS_Alert(CONS_ERROR, M_GetText("%s is not a multiplayer replay and can't be listed on this menu fully yet.\n"), pdemo->filepath); + Z_Free(infobuffer); + return; + } +#ifdef DEMO_COMPAT_100 + else if (pdemoversion == 0x0001) + { + CONS_Alert(CONS_ERROR, M_GetText("%s is a legacy multiplayer replay and cannot be played.\n"), pdemo->filepath); + pdemo->type = MD_INVALID; + sprintf(pdemo->title, "INVALID REPLAY"); + Z_Free(infobuffer); + return; + } +#endif + + pdemo->gametype = READUINT8(info_p); + + pdemo->addonstatus = G_CheckDemoExtraFiles(&info_p, true); + info_p += 4; // RNG seed + + extrainfo_p = infobuffer + READUINT32(info_p); + + // Pared down version of CV_LoadNetVars to find the kart speed + pdemo->kartspeed = 1; // Default to normal speed + count = READUINT16(info_p); + while (count--) + { + UINT16 netid; + char *svalue; + + netid = READUINT16(info_p); + svalue = (char *)info_p; + SKIPSTRING(info_p); + info_p++; // stealth + + if (netid == cv_kartspeed.netid) + { + UINT8 j; + for (j = 0; kartspeed_cons_t[j].strvalue; j++) + if (!stricmp(kartspeed_cons_t[j].strvalue, svalue)) + pdemo->kartspeed = kartspeed_cons_t[j].value; + } + else if (netid == cv_basenumlaps.netid && pdemo->gametype == GT_RACE) + pdemo->numlaps = atoi(svalue); + } + + if (pdemoflags & DF_ENCORE) + pdemo->kartspeed |= DF_ENCORE; + + // Read standings! + count = 0; + + while (READUINT8(extrainfo_p) == DW_STANDING) // Assume standings are always first in the extrainfo + { + INT32 i; + char temp[16]; + + pdemo->standings[count].ranking = READUINT8(extrainfo_p); + + // Name + M_Memcpy(pdemo->standings[count].name, extrainfo_p, 16); + extrainfo_p += 16; + + // Skin + M_Memcpy(temp,extrainfo_p,16); + extrainfo_p += 16; + pdemo->standings[count].skin = UINT8_MAX; + for (i = 0; i < numskins; i++) + if (stricmp(skins[i].name, temp) == 0) + { + pdemo->standings[count].skin = i; + break; + } + + // Color + M_Memcpy(temp,extrainfo_p,16); + extrainfo_p += 16; + for (i = 0; i < MAXSKINCOLORS; i++) + if (!stricmp(KartColor_Names[i],temp)) // SRB2kart + { + pdemo->standings[count].color = i; + break; + } + + // Score/time/whatever + pdemo->standings[count].timeorscore = READUINT32(extrainfo_p); + + count++; + + if (count >= MAXPLAYERS) + break; //@TODO still cycle through the rest of these if extra demo data is ever used + } + + // I think that's everything we need? + Z_Free(infobuffer); +} + +#if defined (_WIN32) +// return the file creation time +// useful for demos that were renamed +static long G_GetCreationTime(char *filepath) +{ + struct stat fileinfo; + + if (stat(filepath, &fileinfo) == 0) + return fileinfo.st_ctime; + + return 0; +} +#endif + +static char *G_GetDemoDate(menudemo_t *pdemo) +{ + char *datetime; + datetime = malloc(sizeof(pdemo->date)); // mallocma balls + + // no mallocma balls... :c + if (!datetime) + { + return NULL; + } + + time_t file_time = 0; + + // get le filepath + char *filename; + filename = strdup(pdemo->filepath); + +#if defined (_WIN32) + if (!filename) + { + // if we cant get a filename try just getting the file create time + file_time = G_GetCreationTime(pdemo->filepath); + goto skipfilenametime; + } +#else + if (!filename) + { + free(datetime); + return NULL; + } +#endif + + // get the actual filename Zzz... + nameonly(filename); + + // convert it to long Zzz.... + file_time = strtol(filename, NULL, 10); + free(filename); // dont need this anymore a + +#if defined (_WIN32) +skipfilenametime: +#endif + + // then throw it into localtime to get an actual human readable format lmao + struct tm *tm_buf = NULL; + tm_buf = localtime(&file_time); + + // cant believe we ended up in 1970 + if (tm_buf == NULL || tm_buf->tm_year <= 110) + { +#if defined (_WIN32) + // uh ohh, we got an invalid time + // try one more time getting the creation time + file_time = G_GetCreationTime(pdemo->filepath); + tm_buf = localtime(&file_time); + + if (tm_buf == NULL || tm_buf->tm_year <= 110) + { + free(datetime); + return NULL; + } + + goto gotcreationtime; +#else + free(datetime); + return NULL; +#endif + } + +#if defined (_WIN32) +gotcreationtime: +#endif + + const char *format; + + // US ppl are special (: + if (cv_demodateformat.value == 2) + format = "%m.%d.%Y"; + else if (cv_demodateformat.value == 1) + format = "%d.%m.%Y"; + else + format = strstr(setlocale(LC_TIME, NULL), "en_US") ? "%m.%d.%Y" : "%d.%m.%Y"; + + strftime(datetime, sizeof(pdemo->date), format, tm_buf); + + return datetime; +} + +void G_LoadDemoTitle(menudemo_t *pdemo) +{ + UINT8 infobuffer[96], *info_p; + UINT16 pdemoversion; + size_t count; + + FILE *handle = fopen(pdemo->filepath, "rb"); + + if (!handle) + { + CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), pdemo->filepath); + sprintf(pdemo->title, "INVALID REPLAY"); + return; + } + + count = fread(infobuffer, 1, 96, handle); + fclose(handle); + info_p = infobuffer; + + // First check isn't too accurate technically, but well, valid replays should be larger than that anyway + if (count < 96 || memcmp(info_p, DEMOHEADER, 12)) + { + CONS_Alert(CONS_ERROR, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemo->filepath); + sprintf(pdemo->title, "INVALID REPLAY"); + return; + } + + info_p += 12; // DEMOHEADER + + READUINT8(info_p); + READUINT8(info_p); + pdemoversion = READUINT16(info_p); + + memset(pdemo->date, 0, sizeof(pdemo->date)); + + switch(pdemoversion) + { + case DEMOVERSION: // latest always supported + // demo title + M_Memcpy(pdemo->title, info_p, 64); + + // demo date + char *demodate; + demodate = G_GetDemoDate(pdemo); + if (demodate) + strncpy(pdemo->date, demodate, sizeof(pdemo->date)); + free(demodate); + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + sprintf(pdemo->title, "Legacy Replay"); + break; +#endif + // too old, cannot support. + default: + CONS_Alert(CONS_ERROR, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemo->filepath); + sprintf(pdemo->title, "INVALID REPLAY"); + } +} + +// +// G_PlayDemo +// +void G_DeferedPlayDemo(const char *name) +{ + COM_BufAddText("playdemo \""); + COM_BufAddText(name); + COM_BufAddText("\" -addfiles\n"); +} + +// +// Start a demo from a .LMP file or from a wad resource +// +#define SKIPERRORS +void G_DoPlayDemo(char *defdemoname) +{ + UINT8 i, p; + lumpnum_t l; + char skin[17],color[17],*n,*pdemoname; + UINT8 version,subversion; + UINT32 randseed; + char msg[1024]; +#if defined(SKIPERRORS) && !defined(DEVELOP) + boolean skiperrors = false; +#endif + boolean spectator; + UINT8 slots[MAXPLAYERS], kartspeed[MAXPLAYERS], kartweight[MAXPLAYERS], numslots = 0; + + G_InitDemoRewind(); + + skin[16] = '\0'; + color[16] = '\0'; + + // No demo name means we're restarting the current demo + if (defdemoname == NULL) + { + demobuf.p = demobuf.buffer; + pdemoname = ZZ_Alloc(1); // Easier than adding checks for this everywhere it's freed + } + else + { + n = defdemoname+strlen(defdemoname); + while (*n != '/' && *n != '\\' && n != defdemoname) + n--; + if (n != defdemoname) + n++; + pdemoname = ZZ_Alloc(strlen(n)+1); + strcpy(pdemoname,n); + + M_SetPlaybackMenuPointer(); + + // Internal if no extension, external if one exists + if (FIL_CheckExtension(defdemoname)) + { + //FIL_DefaultExtension(defdemoname, ".lmp"); + if (!FIL_ReadFile(defdemoname, &demobuf.buffer)) + { + 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; + } + demobuf.p = demobuf.buffer; + } + // 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 + { + demobuf.buffer = demobuf.p = W_CacheLumpNum(l, PU_STATIC); +#if defined(SKIPERRORS) && !defined(DEVELOP) + skiperrors = true; // SRB2Kart: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes... +#endif + } + } + + // read demo header + gameaction = ga_nothing; + demo.playback = true; + if (memcmp(demobuf.p, DEMOHEADER, 12)) + { + snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + demobuf.p += 12; // DEMOHEADER + + version = READUINT8(demobuf.p); + subversion = READUINT8(demobuf.p); + demo.version = READUINT16(demobuf.p); + switch(demo.version) + { + case DEMOVERSION: // latest always supported + // demo title + M_Memcpy(demo.titlename, demobuf.p, 64); + demobuf.p += 64; + + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + 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); + G_ResetDemoPlayback(pdemoname); + return; + } + demobuf.p += 16; // demo checksum + if (memcmp(demobuf.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); + G_ResetDemoPlayback(pdemoname); + return; + } + demobuf.p += 4; // "PLAY" + gamemap = READINT16(demobuf.p); + demobuf.p += 16; // mapmd5 + + demoflags = READUINT8(demobuf.p); +#ifdef DEMO_COMPAT_100 + if (demo.version == 0x0001) + { + if (demoflags & DF_MULTIPLAYER) + { + snprintf(msg, 1024, M_GetText("%s is an alpha multiplayer replay and cannot be played.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + } + else + { +#endif + gametype = READUINT8(demobuf.p); + + if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running. + G_SkipDemoExtraFiles(&demobuf.p); + else if (demo.loadfiles) + G_LoadDemoExtraFiles(&demobuf.p); + else if (demo.ignorefiles) + G_SkipDemoExtraFiles(&demobuf.p); + else + { + UINT8 error = G_CheckDemoExtraFiles(&demobuf.p, false); + + if (error) + { + switch (error) + { + case DFILE_ERROR_NOTLOADED: + snprintf(msg, 1024, + M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"), + pdemoname); + break; + + case DFILE_ERROR_OUTOFORDER: + snprintf(msg, 1024, + M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + break; + + case DFILE_ERROR_INCOMPLETEOUTOFORDER: + snprintf(msg, 1024, + M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"), + pdemoname); + break; + + case DFILE_ERROR_CANNOTLOAD: + snprintf(msg, 1024, + M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + break; + + case DFILE_ERROR_EXTRAFILES: + snprintf(msg, 1024, + M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + break; + } + + CONS_Alert(CONS_ERROR, "%s", msg); + if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out. + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + } +#ifdef DEMO_COMPAT_100 + } +#endif + + modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT; + multiplayer = !!(demoflags & DF_MULTIPLAYER); + CON_ToggleOff(); + + hu_demotime = UINT32_MAX; + hu_demolap = UINT32_MAX; + + switch (modeattacking) + { + case ATTACKING_NONE: // 0 + break; + case ATTACKING_RECORD: // 1 + hu_demotime = READUINT32(demobuf.p); + hu_demolap = READUINT32(demobuf.p); + break; + default: // 3 + modeattacking = ATTACKING_NONE; + break; + } + + // Random seed + randseed = READUINT32(demobuf.p); +#ifdef DEMO_COMPAT_100 + if (demo.version != 0x0001) +#endif + demobuf.p += 4; // Extrainfo location + +#ifdef DEMO_COMPAT_100 + if (demo.version == 0x0001) + { + // Player name + M_Memcpy(player_names[0],demobuf.p,16); + demobuf.p += 16; + + // Skin + M_Memcpy(skin,demobuf.p,16); + demobuf.p += 16; + + // Color + M_Memcpy(color,demobuf.p,16); + demobuf.p += 16; + + demobuf.p += 5; // Backwards compat - some stats + // SRB2kart + kartspeed[0] = READUINT8(demobuf.p); + kartweight[0] = READUINT8(demobuf.p); + // + demobuf.p += 9; // Backwards compat - more stats + + // Skin not loaded? + if (!SetPlayerSkin(0, skin)) + { + snprintf(msg, 1024, M_GetText("%s features a character that is not currently loaded.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + + // ...*map* not loaded? + if (!gamemap || (gamemap > NUMMAPS) || !mapheaderinfo[gamemap-1] || !(mapheaderinfo[gamemap-1]->menuflags & LF2_EXISTSHACK)) + { + snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + + // Set color + for (i = 0; i < MAXSKINCOLORS; i++) + if (!stricmp(KartColor_Names[i],color)) // SRB2kart + { + players[0].skincolor = i; + break; + } + + // net var data + CV_LoadNetVars(&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(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + + Z_Free(pdemoname); + + memset(&oldcmd,0,sizeof(oldcmd)); + memset(&oldghost,0,sizeof(oldghost)); + memset(&ghostext,0,sizeof(ghostext)); + + CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n")); + + // console warning messages +#if defined(SKIPERRORS) && !defined(DEVELOP) + demosynced = (!skiperrors); +#else + demosynced = true; +#endif + + // didn't start recording right away. + demo.deferstart = false; + + consoleplayer = 0; + memset(displayplayers, 0, sizeof(displayplayers)); + memset(playeringame, 0, sizeof(playeringame)); + playeringame[0] = true; + + goto post_compat; + } +#endif + + // net var data + CV_LoadNetVars(&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(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + + Z_Free(pdemoname); + + memset(&oldcmd,0,sizeof(oldcmd)); + memset(&oldghost,0,sizeof(oldghost)); + memset(&ghostext,0,sizeof(ghostext)); + +#if defined(SKIPERRORS) && !defined(DEVELOP) + if ((VERSION != version || SUBVERSION != subversion) && !skiperrors) +#else + if (VERSION != version || SUBVERSION != subversion) +#endif + CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n")); + + // console warning messages +#if defined(SKIPERRORS) && !defined(DEVELOP) + demosynced = (!skiperrors); +#else + demosynced = true; +#endif + + // didn't start recording right away. + demo.deferstart = false; + + //LUA_HookInt(gamemap, HOOK(MapChange)); + + consoleplayer = 0; + memset(playeringame, 0, sizeof(playeringame)); + memset(displayplayers, 0, sizeof(displayplayers)); + memset(camera, 0, sizeof(camera)); // reset freecam + + // Load players that were in-game when the map started + p = READUINT8(demobuf.p); + + while (p != 0xFF) + { + spectator = false; + if (p & DEMO_SPECTATOR) + { + spectator = true; + p &= ~DEMO_SPECTATOR; + + if (modeattacking) + { + snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with spectators, and is thus invalid.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + } + slots[numslots] = p; numslots++; + + if (modeattacking && numslots > 1) + { + snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with multiple players, and is thus invalid.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage(msg, NULL, MM_NOTHING); + G_ResetDemoPlayback(pdemoname); + return; + } + + if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator) + displayplayers[0] = consoleplayer = serverplayer = p; + + playeringame[p] = true; + players[p].spectator = spectator; + + // Name + M_Memcpy(player_names[p],demobuf.p,16); + demobuf.p += 16; + + // Skin + M_Memcpy(skin,demobuf.p,16); + demobuf.p += 16; + SetPlayerSkin(p, skin); + + // Color + M_Memcpy(color,demobuf.p,16); + demobuf.p += 16; + for (i = 0; i < MAXSKINCOLORS; i++) + if (!stricmp(KartColor_Names[i],color)) // SRB2kart + { + players[p].skincolor = i; + break; + } + + // Score, since Kart uses this to determine where you start on the map + players[p].score = READUINT32(demobuf.p); + + // Kart stats, temporarily + kartspeed[p] = READUINT8(demobuf.p); + kartweight[p] = READUINT8(demobuf.p); + + if (stricmp(skins[players[p].skin].name, skin) != 0) + FindClosestSkinForStats(p, kartspeed[p], kartweight[p]); + + // Look for the next player + p = READUINT8(demobuf.p); + } + + // end of player read (the 0xFF marker) + // so this is where we are to read our lua variables (if possible!) + if (demoflags & DF_LUAVARS) // again, used for compability, lua shit will be saved to replays regardless of if it's even been loaded + { + if (!gL) // No Lua state! ...I guess we'll just start one... + LUA_ClearState(); + + // No modeattacking check, DF_LUAVARS won't be present here. + LUA_UnArchive(&demobuf, false); + } + + splitscreen = 0; + + if (demo.title) + { + splitscreen = M_RandomKey(6)-1; + splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views + + for (p = 0; p <= splitscreen; p++) + G_ResetView(p+1, slots[M_RandomKey(numslots)], false); + } + + R_ExecuteSetViewSize(); + +#ifdef DEMO_COMPAT_100 +post_compat: +#endif + + P_SetRandSeed(randseed); + G_InitNew(demoflags & DF_ENCORE, G_BuildMapName(gamemap), true, true); // Doesn't matter whether you reset or not here, given changes to resetplayer. + + for (i = 0; i < MAXPLAYERS; i++) + { + if (players[i].mo) + { + players[i].mo->color = players[i].skincolor; + oldghost[i].x = players[i].mo->x; + oldghost[i].y = players[i].mo->y; + oldghost[i].z = players[i].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[i].kartspeed = kartspeed[i]; + players[i].kartweight = kartweight[i]; + } + + if (cv_director.value) + CV_SetValue(&cv_director, 0); + + demo.deferstart = true; +} +#undef SKIPERRORS + +void G_AddGhost(char *defdemoname) +{ + INT32 i; + lumpnum_t l; + char name[17],skin[17],color[17],*n,*pdemoname,md5[16]; + demoghost *gh; + UINT8 flags; + UINT8 *buffer,*p; + mapthing_t *mthing; + UINT16 count, ghostversion; + skin_t *ghskin = &skins[0]; + UINT8 kartspeed = UINT8_MAX, kartweight = UINT8_MAX; + + 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 SRB2Kart replay.\n"), pdemoname); + Z_Free(pdemoname); + Z_Free(buffer); + return; + } + + p += 12; // DEMOHEADER + p++; // VERSION + p++; // SUBVERSION + + ghostversion = READUINT16(p); + switch(ghostversion) + { + case DEMOVERSION: // latest always supported + p += 64; // title + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + break; +#endif + // 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" + 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; + } + +#ifdef DEMO_COMPAT_100 + if (ghostversion != 0x0001) +#endif + p++; // gametype + +#ifdef DEMO_COMPAT_100 + if (ghostversion != 0x0001) +#endif + G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts. + switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT) + { + case ATTACKING_NONE: // 0 + break; + case ATTACKING_RECORD: // 1 + p += 8; // demo time, lap + break; + default: // 3 + break; + } + + p += 4; // random seed + +#ifdef DEMO_COMPAT_100 + if (ghostversion == 0x0001) + { + // 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,16); + p += 16; + + // Ghosts do not have a player structure to put this in. + p++; // charability + p++; // charability2 + p++; // actionspd + p++; // mindash + p++; // maxdash + // SRB2kart + p++; // kartspeed + p++; // kartweight + // + p++; // normalspeed + p++; // runspeed + p++; // thrustfactor + p++; // accelstart + p++; // acceleration + p += 4; // jumpfactor + } + else +#endif + p += 4; // Extra data location reference + + // net var data + count = READUINT16(p); + while (count--) + { + p += 2; + 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; + } + +#ifdef DEMO_COMPAT_100 + if (ghostversion != 0x0001) + { +#endif + if (READUINT8(p) != 0) + { + CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname); + Z_Free(pdemoname); + Z_Free(buffer); + return; + } + + // 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, 16); + p += 16; + + p += 4; // score + + kartspeed = READUINT8(p); + kartweight = READUINT8(p); + + if (READUINT8(p) != 0xFF) + { + CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname); + Z_Free(pdemoname); + Z_Free(buffer); + return; + } +#ifdef DEMO_COMPAT_100 + } +#endif + + for (i = 0; i < numskins; i++) + if (!stricmp(skins[i].name,skin)) + { + ghskin = &skins[i]; + break; + } + + if (i == numskins) + { + if (kartspeed != UINT8_MAX && kartweight != UINT8_MAX) + ghskin = &skins[GetSkinNumClosestToStats(kartspeed, kartweight)]; + + CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Invalid character. Falling back to %s.\n"), pdemoname, ghskin->name); + } + + 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; + gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST); + gh->mo->angle = FixedAngle(mthing->angle*FRACUNIT); + f = gh->mo->floorz; + c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height; + if (!!(mthing->options & MTF_AMBUSH) ^ !!(mthing->options & MTF_OBJECTFLIP)) + { + z = c; + if (mthing->options >> ZSHIFT) + z -= ((mthing->options >> ZSHIFT) << FRACBITS); + if (z < f) + z = f; + } + else + { + z = f; + if (mthing->options >> ZSHIFT) + z += ((mthing->options >> ZSHIFT) << FRACBITS); + if (z > c) + z = c; + } + gh->mo->z = z; + } + gh->mo->state = states+S_KART_STND1; // SRB2kart - was S_PLAY_STND + gh->mo->sprite = gh->mo->state->sprite; + gh->mo->frame = (gh->mo->state->frame & FF_FRAMEMASK) | tr_trans20<mo->tics = -1; + + gh->oldmo.x = gh->mo->x; + gh->oldmo.y = gh->mo->y; + gh->oldmo.z = gh->mo->z; + + // Set skin + gh->mo->skin = gh->oldmo.skin = ghskin; + + // Set color + gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor; + for (i = 0; i < MAXSKINCOLORS; i++) + if (!stricmp(KartColor_Names[i],color)) // SRB2kart + { + gh->mo->color = (UINT8)i; + break; + } + gh->oldmo.color = gh->mo->color; + + CONS_Printf(M_GetText("Added ghost %s from %s\n"), name, pdemoname); + Z_Free(pdemoname); +} + +// A simplified version of G_AddGhost... +void G_UpdateStaffGhostName(lumpnum_t l) +{ + UINT8 *buffer,*p; + UINT16 ghostversion; + UINT8 flags; + + buffer = p = W_CacheLumpNum(l, PU_CACHE); + + // read demo header + if (memcmp(p, DEMOHEADER, 12)) + { + goto fail; + } + + p += 12; // DEMOHEADER + p++; // VERSION + p++; // SUBVERSION + + ghostversion = READUINT16(p); + switch(ghostversion) + { + case DEMOVERSION: // latest always supported + p += 64; // full demo title + break; + +#ifdef DEMO_COMPAT_100 + case 0x0001: + break; +#endif + + // too old, cannot support. + default: + goto fail; + } + + p += 16; // demo checksum + + if (memcmp(p, "PLAY", 4)) + { + goto fail; + } + + p += 4; // "PLAY" + p += 2; // gamemap + p += 16; // mapmd5 (possibly check for consistency?) + + flags = READUINT8(p); + if (!(flags & DF_GHOST)) + { + goto fail; // we don't NEED to do it here, but whatever + } + +#ifdef DEMO_COMPAT_100 + if (ghostversion != 0x0001) +#endif + p++; // Gametype + +#ifdef DEMO_COMPAT_100 + if (ghostversion != 0x0001) +#endif + G_SkipDemoExtraFiles(&p); + + switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT) + { + case ATTACKING_NONE: // 0 + break; + case ATTACKING_RECORD: // 1 + p += 8; // demo time, lap + break; + default: // 3 + break; + } + + p += 4; // random seed + + +#ifdef DEMO_COMPAT_100 + if (ghostversion == 0x0001) + { + // Player name + M_Memcpy(dummystaffname, p,16); + dummystaffname[16] = '\0'; + goto fail; // Not really a failure but whatever + } +#endif + + p += 4; // Extrainfo location marker + + // Ehhhh don't need ghostversion here (?) so I'll reuse the var here + ghostversion = READUINT16(p); + while (ghostversion--) + { + p += 2; + SKIPSTRING(p); + p++; // stealth + } + + // Assert first player is in and then read name + if (READUINT8(p) != 0) + goto fail; + M_Memcpy(dummystaffname, p,16); + dummystaffname[16] = '\0'; + + // Ok, no longer any reason to care, bye +fail: + Z_Free(buffer); + return; +} + +// +// 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"); + demo.timing = 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 = thinkercap.next; th != &thinkercap; th = th->next) + { + if (th->function != (actionf_p1)P_MobjThinker) + continue; + + mo = (mobj_t *)th; + if (mo->type == MT_METALSONIC_RACE) + break; + } + if (!mo) + { + 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 + break; +#ifdef DEMO_COMPAT_100 + case 0x0001: + I_Error("You need to implement demo compat here, doofus! %s:%d", __FILE__, __LINE__); +#endif + // 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; + oldmetal.angle = mo->angle; + 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 +=================== +*/ + +// 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(void) +{ + boolean saved = false; + if (demobuf.p && demobuf.buffer != NULL) + { + UINT8 *p = demobuf.buffer+16; // checksum position +#ifdef NOMD5 + UINT8 i; + WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker + for (i = 0; i < 16; i++, p++) + *p = P_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct. +#else + WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker + md5_buffer((char *)p+16, demobuf.p - (p+16), (void *)p); // make a checksum of everything after the checksum in the file. +#endif + saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuf.buffer, demobuf.p - demobuf.buffer); // finally output the file. + } + Z_Free(demobuf.buffer); + demobuf.buffer = NULL; + metalrecording = false; + if (saved) + I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap)); + I_Error("Failed to save demo!"); +} + +// reset engine variable set for the demos +// called from stopdemo command, map command, and g_checkdemoStatus. +void G_StopDemo(void) +{ + Z_Free(demobuf.buffer); + demobuf.buffer = NULL; + demo.playback = false; + if (demo.title) + modeattacking = ATTACKING_NONE; + demo.title = false; + demo.timing = false; + singletics = false; + + UINT8 i; + for (i = 0; i < MAXSPLITSCREENPLAYERS; ++i) + { + camera[i].freecam = false; + camera[i].localangle = 0; + camera[i].localaiming = 0; + } + + CV_SetValue(&cv_playbackspeed, 1); + demo.rewinding = false; + CL_ClearRewinds(); + + if (gamestate == GS_LEVEL && rendermode != render_none) + { + V_SetPaletteLump("PLAYPAL"); // Reset the palette + R_ReInitColormaps(0, LUMPERROR); + } + if (gamestate == GS_INTERMISSION) + Y_EndIntermission(); // cleanup + if (gamestate == GS_VOTING) + Y_EndVote(); + + M_ClearMenus(true); + + G_SetGamestate(GS_NULL); + wipegamestate = GS_NULL; + SV_StopServer(); + SV_ResetServer(); +} + +// Stops timing a demo. +static void G_StopTimingDemo(void) +{ + INT32 demotime; + double f1, f2; + demotime = I_GetTime() - demostarttime; + if (!demotime) + return; + G_StopDemo(); + demo.timing = 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); + + if (restorecv_vidwait != cv_vidwait.value) + CV_SetValue(&cv_vidwait, restorecv_vidwait); + + D_StartTitle(); +} + + +boolean G_CheckDemoStatus(void) +{ + while (ghosts) + { + demoghost *next = ghosts->next; + Z_Free(ghosts); + ghosts = next; + } + ghosts = NULL; + + // DO NOT end metal sonic demos here + + if (demo.timing) + { + G_StopTimingDemo(); + return true; + } + + if (demo.playback) + { + if (demo.quitafterplaying) + I_Quit(); + + if (multiplayer && !demo.title) + { + G_ExitLevel(); + } + else if (modeattacking && !demo.title) // nooo dont crash our titledemos + { + G_StopDemo(); + M_EndModeAttackRun(); + } + else + { + G_StopDemo(); + D_StartTitle(); + } + + return true; + } + + if (!demo.recording) + return false; + + if (modeattacking || demo.savemode != DSM_NOTSAVING) + { + G_SaveDemo(); + return true; + } + + G_ResetDemoRecording(); + + return false; +} + +void G_ResetDemoRecording(void) +{ + if (demobuf.buffer) + Z_Free(demobuf.buffer); + demobuf.buffer = NULL; + demo.recording = false; +} + +static void G_ResetDemoPlayback(char *pdemoname) +{ + if (pdemoname) + Z_Free(pdemoname); + if (demobuf.buffer) + Z_Free(demobuf.buffer); + demobuf.buffer = NULL; + demo.playback = false; + if (demo.title) + modeattacking = ATTACKING_NONE; + demo.title = false; +} + +void G_SaveDemo(void) +{ + if (!demobuf.p) + { + CONS_Alert(CONS_ERROR, "Failed to save Demo. No Demo pointer exists!\n"); + // reset the demo buffer + G_ResetDemoRecording(); + return; + } + + if (demobuf.buffer == NULL) + { + CONS_Alert(CONS_ERROR, "Failed to save Demo. No Demo buffer allocated!\n"); + // reset the demo buffer + G_ResetDemoRecording(); + return; + } + + UINT8 *p = demobuf.buffer+16; // after version + UINT32 length; + + // Ensure extrainfo pointer is always available, even if no info is present. + if (demoinfo_p && *(UINT32 *)demoinfo_p == 0) + { + WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker + *(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer; + } + WRITEUINT8(demobuf.p, DW_END); // Mark end of demo extra data. + + M_Memcpy(p, demo.titlename, 64); // Write demo title here + p += 64; + + if (multiplayer) + { + // Change the demo's name to be a slug of the title + char demo_slug[128]; + char *writepoint; + size_t i, strindex = 0; + boolean dash = true; + + for (i = 0; demo.titlename[i] && i < 127; i++) + { + if ((demo.titlename[i] >= 'a' && demo.titlename[i] <= 'z') || + (demo.titlename[i] >= '0' && demo.titlename[i] <= '9')) + { + demo_slug[strindex] = demo.titlename[i]; + strindex++; + dash = false; + } + else if (demo.titlename[i] >= 'A' && demo.titlename[i] <= 'Z') + { + demo_slug[strindex] = demo.titlename[i] + 'a' - 'A'; + strindex++; + dash = false; + } + else if (!dash) + { + demo_slug[strindex] = '-'; + strindex++; + dash = true; + } + } + + demo_slug[strindex] = 0; + if (dash) demo_slug[strindex-1] = 0; + + writepoint = strstr(demoname, "-") + 1; + demo_slug[128 - (writepoint - demoname) - 4] = 0; + sprintf(writepoint, "%s.lmp", demo_slug); + } + + length = *(UINT32 *)demoinfo_p; + WRITEUINT32(demoinfo_p, length); +#ifdef NOMD5 + for (UINT8 k = 0; k < 16; k++, p++) + *p = M_RandomByte(); // This MD5 was chosen by fair dice roll and most likely < 50% correct. +#else + // Make a checksum of everything after the checksum in the file up to the end of the standard data. Extrainfo is freely modifiable. + md5_buffer((char *)p+16, (demobuf.buffer + length) - (p+16), p); +#endif + + + if (FIL_WriteFile(va(pandf, srb2home, demoname), demobuf.buffer, demobuf.p - demobuf.buffer)) // finally output the file. + demo.savemode = DSM_SAVED; + + G_ResetDemoRecording(); + + if (modeattacking != ATTACKING_RECORD) + { + if (demo.savemode == DSM_SAVED) + CONS_Printf(M_GetText("Demo %s recorded\n"), demoname); + else + CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname); + } +} + +boolean G_DemoTitleResponder(event_t *ev) +{ + INT32 ch; + + if (ev->type != ev_keydown) + return false; + + ch = (INT32)ev->data1; + + // Only ESC and non-keyboard keys abort connection + if (ch == KEY_ESCAPE) + { + demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING; + return true; + } + + if (ch == KEY_ENTER || ch >= KEY_MOUSE1) + { + demo.savemode = DSM_WILLSAVE; + return true; + } + + M_TextInputHandle(&demo.titlenameinput, ch); + + return true; +} + // // G_SetGamestate // diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c index 36f6a57a5..3d52df320 100644 --- a/src/hardware/hw_cache.c +++ b/src/hardware/hw_cache.c @@ -792,7 +792,7 @@ static void HWR_PrecacheLevelSprites(void) for (th = thinkercap.next; th != &thinkercap; th = th->next) { - if (th->function.acp1 != (actionf_p1)P_MobjThinker) + if (th->function != (actionf_p1)P_MobjThinker) continue; mo = (mobj_t *)th; diff --git a/src/info.c b/src/info.c index 7afc76bf6..1d2326cec 100644 --- a/src/info.c +++ b/src/info.c @@ -78,3336 +78,3279 @@ state_t states[NUMSTATES] = // (or tr_trans10<