// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2025 by Kart Krew. // Copyright (C) 2020 by Sonic Team Junior. // Copyright (C) 2000 by DooM Legacy Team. // Copyright (C) 1996 by id Software, Inc. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file p_tick.c /// \brief Archiving: SaveGame I/O, Thinker, Ticker #include "d_think.h" #include "doomstat.h" #include "d_main.h" #include "g_game.h" #include "g_input.h" #include "p_local.h" #include "z_zone.h" #include "s_sound.h" #include "st_stuff.h" #include "p_polyobj.h" #include "m_random.h" #include "m_cond.h" // gamedata->playtime #include "lua_script.h" #include "lua_hook.h" #include "m_perfstats.h" #include "i_system.h" // I_GetPreciseTime #include "i_video.h" // rendermode #include "r_main.h" #include "r_fps.h" #include "d_clisrv.h" // UpdateChallenges #include "p_link.h" // Object place #include "m_cheat.h" // SRB2Kart #include "k_kart.h" #include "k_race.h" #include "k_battle.h" #include "k_boss.h" #include "k_waypoint.h" #include "k_director.h" #include "k_specialstage.h" #include "acs/interface.h" #include "k_objects.h" #include "music.h" #include "k_dialogue.h" #include "m_easing.h" #include "k_hud.h" // messagetimer #include "k_endcam.h" #include "lua_profile.h" #ifdef PARANOIA #include "deh_tables.h" // MOBJTYPE_LIST #endif tic_t leveltime; boolean thinkersCompleted; UINT32 thinker_era = 0; static boolean g_freezeCheat; static boolean g_freezeLevel; boolean P_LevelIsFrozen(void) { return (g_freezeLevel || g_freezeCheat || K_EndCameraIsFreezing()); } boolean P_FreezeCheat(void) { return (g_freezeLevel || g_freezeCheat || K_EndCameraIsFreezing()); } void P_SetFreezeCheat(boolean value) { g_freezeCheat = value; } void P_SetFreezeLevel(boolean value) { g_freezeLevel = value; } boolean P_MobjIsFrozen(mobj_t *mobj) { if (g_freezeCheat == true) { // freeze cheat switch (mobj->type) { case MT_PLAYER: { break; } default: { return true; } } } if (g_freezeLevel == true || K_EndCameraIsFreezing()) { // level totally frozen return true; } if ((mobj->eflags & MFE_PAUSED) == MFE_PAUSED) { // hitlag return true; } // manual return mobj->frozen; } INT32 P_AltFlip(INT32 n, tic_t tics) { return leveltime % (2 * tics) < tics ? n : -(n); } // Please read p_tick.h INT32 P_LerpFlip(INT32 n, tic_t tics) { const tic_t w = 2 * tics; return P_AltFlip(((leveltime % w) - tics) * n, w); } // // THINKERS // All thinkers should be allocated by Z_Calloc // so they can be operated on uniformly. // The actual structures will vary in size, // but the first element must be thinker_t. // // The entries will behave like both the head and tail of the lists. thinker_t thlist[NUM_THINKERLISTS]; void Command_Numthinkers_f(void) { INT32 num; INT32 count = 0; actionf_p1 action; thinker_t *think; thinklistnum_t start = 0; thinklistnum_t end = NUM_THINKERLISTS - 1; thinklistnum_t i; if (G_GamestateUsesLevel() == false) { CONS_Printf(M_GetText("You must be in a level to use this.\n")); return; } if (COM_Argc() < 2) { CONS_Printf(M_GetText("numthinkers <#>: Count number of thinkers\n")); CONS_Printf( "\t1: P_MobjThinker\n" "\t2: P_NullPrecipThinker\n" "\t3: T_Friction\n" "\t4: T_Pusher\n" "\t5: P_RemoveThinkerDelayed\n"); return; } num = atoi(COM_Argv(1)); switch (num) { case 1: start = end = THINK_MOBJ; action = (actionf_p1)P_MobjThinker; CONS_Printf(M_GetText("Number of %s: "), "P_MobjThinker"); break; case 2: start = end = THINK_PRECIP; action = (actionf_p1)P_NullPrecipThinker; CONS_Printf(M_GetText("Number of %s: "), "P_NullPrecipThinker"); break; case 3: start = end = THINK_MAIN; action = (actionf_p1)T_Friction; CONS_Printf(M_GetText("Number of %s: "), "T_Friction"); break; case 4: start = end = THINK_MAIN; action = (actionf_p1)T_Pusher; CONS_Printf(M_GetText("Number of %s: "), "T_Pusher"); break; case 5: action = (actionf_p1)P_RemoveThinkerDelayed; CONS_Printf(M_GetText("Number of %s: "), "P_RemoveThinkerDelayed"); break; default: CONS_Printf(M_GetText("That is not a valid number.\n")); return; } for (i = start; i <= end; i++) { for (think = thlist[i].next; think != &thlist[i]; think = think->next) { if (think->function.acp1 != action) continue; count++; } } CONS_Printf("%d\n", count); } void Command_CountMobjs_f(void) { thinker_t *th; mobjtype_t i; INT32 count; if (G_GamestateUsesLevel() == false) { CONS_Printf(M_GetText("You must be in a level to use this.\n")); return; } if (COM_Argc() >= 2) { size_t j; for (j = 1; j < COM_Argc(); j++) { i = atoi(COM_Argv(j)); if (i >= NUMMOBJTYPES) { CONS_Printf(M_GetText("Object number %d out of range (max %d).\n"), i, NUMMOBJTYPES-1); continue; } count = 0; for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) { if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) continue; if (((mobj_t *)th)->type == i) count++; } const char *name; if (i >= MT_FIRSTFREESLOT) name = FREE_MOBJS[i-MT_FIRSTFREESLOT]; else name = MOBJTYPE_LIST[i]; CONS_Printf(M_GetText("There are %d objects of type %d (%s) currently in the level.\n"), count, i, name); } return; } CONS_Printf(M_GetText("Count of active objects in level:\n")); for (i = 0; i < NUMMOBJTYPES; i++) { count = 0; for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) { if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) continue; if (((mobj_t *)th)->type == i) count++; } if (count > 0) // Don't bother displaying if there are none of this type! { const char *name; if (i >= MT_FIRSTFREESLOT) name = FREE_MOBJS[i-MT_FIRSTFREESLOT]; else name = MOBJTYPE_LIST[i]; CONS_Printf(" * %d (%s): %d\n", i, name, count); } } } // // P_InitThinkers // void P_InitThinkers(void) { UINT8 i; P_InvalidateThinkersWithoutInit(); for (i = 0; i < NUM_THINKERLISTS; i++) { thlist[i].prev = thlist[i].next = &thlist[i]; } iquehead = iquetail = 0; waypointcap = NULL; trackercap = NULL; titlemapcam.mobj = NULL; for (i = 0; i <= 15; i++) { skyboxcenterpnts[i] = skyboxviewpnts[i] = NULL; } P_InitMobjPointers(); } // // P_InvalidateThinkersWithoutInit // void P_InvalidateThinkersWithoutInit(void) { thinker_era++; } // Adds a new thinker at the end of the list. void P_AddThinker(const thinklistnum_t n, thinker_t *thinker) { #ifdef PARANOIA I_Assert(n < NUM_THINKERLISTS); #endif thlist[n].prev->next = thinker; thinker->next = &thlist[n]; thinker->prev = thlist[n].prev; thlist[n].prev = thinker; thinker->references = 0; // killough 11/98: init reference counter to 0 #ifdef PARANOIA thinker->debug_mobjtype = MT_NULL; #endif } #ifdef PARANOIA static const char *MobjTypeName(const mobj_t *mobj) { actionf_p1 p1 = mobj->thinker.function.acp1; if (p1 == (actionf_p1)P_MobjThinker) { return MOBJTYPE_LIST[mobj->type]; } else if (p1 == (actionf_p1)P_RemoveThinkerDelayed) { if (mobj->thinker.debug_mobjtype != MT_NULL) { return MOBJTYPE_LIST[mobj->thinker.debug_mobjtype]; } } return ""; } static const char *MobjThinkerName(const mobj_t *mobj) { actionf_p1 p1 = mobj->thinker.function.acp1; if (p1 == (actionf_p1)P_MobjThinker) { return "P_MobjThinker"; } else if (p1 == (actionf_p1)P_RemoveThinkerDelayed) { return "P_RemoveThinkerDelayed"; } return ""; } #endif // // killough 11/98: // // Make currentthinker external, so that P_RemoveThinkerDelayed // can adjust currentthinker when thinkers self-remove. static thinker_t *currentthinker; // // P_RemoveThinkerDelayed() // // Called automatically as part of the thinker loop in P_RunThinkers(), // on nodes which are pending deletion. // // If this thinker has no more pointers referencing it indirectly, // remove it, and set currentthinker to one node preceeding it, so // that the next step in P_RunThinkers() will get its successor. // void P_RemoveThinkerDelayed(thinker_t *thinker) { if (thinker->references != 0) { #ifdef PARANOIA // Removed mobjs can be the target of another mobj. In // that case, the other mobj will manage its reference // to the removed mobj in P_MobjThinker. However, if // the removed mobj is removed after the other object // thinks, the reference management is delayed by one // tic (or two?) const UINT8 delay = 2; if (thinker->debug_time > leveltime) { thinker->debug_time = leveltime + delay + 1; // do not print errors again } else if ((thinker->debug_time + delay) <= leveltime) { CONS_Printf( "PARANOIA/P_RemoveThinkerDelayed: %p %s references=%d\n", (void*)thinker, MobjTypeName((mobj_t*)thinker), thinker->references ); thinker->debug_time = leveltime + delay + 1; // do not print this error again } #endif return; } R_DestroyLevelInterpolators(thinker); /* Note that currentthinker is guaranteed to point to us, * and since we're freeing our memory, we had better change that. So * point it to thinker->prev, so the iterator will correctly move on to * thinker->prev->next = thinker->next */ currentthinker = thinker->prev; /* Remove from main thinker list */ P_UnlinkThinker(thinker); } // // P_UnlinkThinker() // // Actually removes thinker from the list and frees its memory. // void P_UnlinkThinker(thinker_t *thinker) { thinker_t *next = thinker->next; I_Assert(thinker->references == 0); (next->prev = thinker->prev)->next = next; if (thinker->alloctype == TAT_LEVELPOOL) { Z_LevelPoolFree(thinker, thinker->size); } else { Z_Free(thinker); } } // // P_RemoveThinker // // Deallocation is lazy -- it will not actually be freed // until its thinking turn comes up. // // killough 4/25/98: // // Instead of marking the function with -1 value cast to a function pointer, // set the function to P_RemoveThinkerDelayed(), so that later, it will be // removed automatically as part of the thinker process. // void P_RemoveThinker(thinker_t *thinker) { LUA_InvalidateUserdata(thinker); thinker->function.acp1 = (actionf_p1)P_RemoveThinkerDelayed; } /* * P_SetTarget * * This function is used to keep track of pointer references to mobj thinkers. * In Doom, objects such as lost souls could sometimes be removed despite * their still being referenced. In Boom, 'target' mobj fields were tested * during each gametic, and any objects pointed to by them would be prevented * from being removed. But this was incomplete, and was slow (every mobj was * checked during every gametic). Now, we keep a count of the number of * references, and delay removal until the count is 0. */ mobj_t *P_SetTarget2(mobj_t **mop, mobj_t *targ #ifdef PARANOIA , const char *source_file, int source_line #endif ) { if (*mop) // If there was a target already, decrease its refcount { (*mop)->thinker.references--; #ifdef PARANOIA if ((*mop)->thinker.references < 0) { CONS_Printf( "PARANOIA/P_SetTarget: %p %s %s references=%d, references go negative! (%s:%d)\n", (void*)*mop, MobjTypeName(*mop), MobjThinkerName(*mop), (*mop)->thinker.references, source_file, source_line ); } (*mop)->thinker.debug_time = leveltime; #endif } if (targ != NULL) // Set new target and if non-NULL, increase its counter { targ->thinker.references++; #ifdef PARANOIA targ->thinker.debug_time = leveltime; #endif } *mop = targ; return targ; } // // P_RunThinkers // // killough 4/25/98: // // Fix deallocator to stop using "next" pointer after node has been freed // (a Doom bug). // // Process each thinker. For thinkers which are marked deleted, we must // load the "next" pointer prior to freeing the node. In Doom, the "next" // pointer was loaded AFTER the thinker was freed, which could have caused // crashes. // // But if we are not deleting the thinker, we should reload the "next" // pointer after calling the function, in case additional thinkers are // added at the end of the list. // // killough 11/98: // // Rewritten to delete nodes implicitly, by making currentthinker // external and using P_RemoveThinkerDelayed() implicitly. // static void P_RunThinkers(void) { size_t i; for (i = 0; i < NUM_ACTIVETHINKERLISTS; i++) { ps_thlist_times[i] = I_GetPreciseTime(); for (currentthinker = thlist[i].next; currentthinker != &thlist[i]; currentthinker = currentthinker->next) { #ifdef PARANOIA I_Assert(currentthinker->function.acp1 != NULL); #endif currentthinker->function.acp1(currentthinker); } ps_thlist_times[i] = I_GetPreciseTime() - ps_thlist_times[i]; } if (gametyperules & GTR_CIRCUIT) K_RunFinishLineBeam(); if (gametyperules & GTR_PAPERITEMS) K_RunPaperItemSpawners(); if ((gametyperules & GTR_OVERTIME) && battleovertime.enabled) K_RunBattleOvertime(); ps_acs_time = I_GetPreciseTime(); ACS_Tick(); ps_acs_time = I_GetPreciseTime() - ps_acs_time; } static inline void P_DeviceRumbleTick(void) { UINT8 i; for (i = 0; i <= splitscreen; i++) { player_t *player = &players[g_localplayers[i]]; UINT16 low = 0; UINT16 high = 0; if (player->mo != NULL && !player->exiting) { if ((player->mo->eflags & MFE_DAMAGEHITLAG) && player->mo->hitlag) { low = high = 65536 / 2; } else if (player->sneakertimer > (sneakertime-(TICRATE/2)) || player->panelsneakertimer > (sneakertime-(TICRATE/2)) || player->weaksneakertimer > (sneakertime-(TICRATE/2))) { low = high = 65536 / (3+player->numsneakers+player->numpanelsneakers+player->numweaksneakers); } else if (((player->boostpower < FRACUNIT) || (player->stairjank > 8)) && P_IsObjectOnGround(player->mo) && player->speed != 0) { low = high = 65536 / 32; } } G_PlayerDeviceRumble(i, low, high); } } void P_RunChaseCameras(void) { UINT8 i; for (i = 0; i <= r_splitscreen; i++) { if (camera[i].chase) { player_t *p = &players[displayplayers[i]]; camera_t *cam = &camera[i]; if (p->mo && p->cmd.throwdir != 0) { if (p->speed < 6 * p->mo->scale && abs(cam->dpad_y_held) < 2*TICRATE) cam->dpad_y_held += intsign(p->cmd.throwdir); } else cam->dpad_y_held = 0; P_MoveChaseCamera(p, cam, false); } } K_EndCameraGC(); } static fixed_t P_GetDarkness(tic_t start, tic_t end) { if (leveltime <= end) { tic_t fade = end - DARKNESS_FADE_TIME; tic_t t; if (leveltime < fade) // dark or darkening { t = leveltime - start; t = min(t, DARKNESS_FADE_TIME); } else // lightening { t = end - leveltime; } return Easing_Linear((t * FRACUNIT) / DARKNESS_FADE_TIME, 0, (gametyperules & GTR_BUMPERS) ? FRACUNIT/3 : FRACUNIT/7); } return 0; } static void P_TickDarkness(void) { const fixed_t globalValue = P_GetDarkness(g_darkness.start, g_darkness.end); UINT8 i; for (i = 0; i <= r_splitscreen; ++i) { const player_t *p = &players[displayplayers[i]]; fixed_t value = P_GetDarkness(p->darkness_start, p->darkness_end); g_darkness.value[i] = value ? value : globalValue; } } static void P_TickMusicFade(void) { if (leveltime >= g_musicfade.start && leveltime <= g_musicfade.end) { INT32 half = (g_musicfade.end - g_musicfade.start) / 2; INT32 fade = max(1, g_musicfade.fade); INT32 mid = half - fade; INT32 t = abs((INT32)leveltime - (INT32)(g_musicfade.start + half)); Music_LevelVolume((max(t, mid) - mid) * 100 / fade); } else if (!g_musicfade.ticked) Music_LevelVolume(100); g_musicfade.ticked = true; } static void P_StartLevelMusic(void) { if (!Music_Playing("level_nosync")) { // Do not stop level_nosync Music_Play(P_UseContinuousLevelMusic() ? "level_nosync" : "level"); } } // // P_Ticker // void P_Ticker(boolean run) { quake_t *quake = NULL; INT32 i; thinkersCompleted = false; // Increment jointime and quittime even if paused for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i]) { players[i].jointime++; } } if (objectplacing) { if (OP_FreezeObjectplace()) { P_MapStart(); R_UpdateMobjInterpolators(); OP_ObjectplaceMovement(&players[0]); P_MoveChaseCamera(&players[0], &camera[0], false); R_UpdateViewInterpolation(); P_MapEnd(); return; } } // Check for pause or menu up in single player if (paused || P_AutoPause()) { P_RunChaseCameras(); // special case: allow freecam to MOVE during pause! return; } for (i = 0; i <= r_splitscreen; i++) postimgtype[i] = postimg_none; P_MapStart(); if (run) { R_UpdateMobjInterpolators(); if (demo.recording) { if (!G_ConsiderEndingDemoWrite()) { G_WriteDemoExtraData(); for (i = 0; i < MAXPLAYERS; i++) if (playeringame[i]) G_WriteDemoTiccmd(&players[i].cmd, i); } } if (demo.playback && !demo.waitingfortally) { if (!G_ConsiderEndingDemoRead()) { G_ReadDemoExtraData(); for (i = 0; i < MAXPLAYERS; i++) if (playeringame[i]) G_ReadDemoTiccmd(&players[i].cmd, i); } } LUA_ResetTicTimers(); ps_lua_mobjhooks = 0; ps_checkposition_calls = 0; LUA_HOOK(PreThinkFrame); ps_playerthink_time = I_GetPreciseTime(); K_UpdateAllPlayerPositions(); // OK! Now that we got all of that sorted, players can think! for (i = 0; i < MAXPLAYERS; i++) { if (!(playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))) continue; P_PlayerThink(&players[i]); K_KartPlayerHUDUpdate(&players[i]); } ps_playerthink_time = I_GetPreciseTime() - ps_playerthink_time; if (gamedata && gamestate == GS_LEVEL && !demo.playback) { mapheader_t *mapheader; mapheader = mapheaderinfo[gamemap - 1]; // Keep track of how long they've been playing! gamedata->totalplaytime++; // Map playtime if (mapheader) { mapheader->records.timeplayed++; } // Netgame total time if (netgame) { gamedata->totalnetgametime++; if (mapheader) { mapheader->records.netgametimeplayed++; } } // Per-skin total playtime for all machine-local players for (i = 0; i < MAXPLAYERS; i++) { skin_t *playerskin; if (!P_IsMachineLocalPlayer(&players[i])) { continue; } if (!(playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))) { continue; } if (players[i].skin >= numskins) { continue; } playerskin = skins[players[i].skin]; playerskin->records.timeplayed++; } if (gametype != GT_TUTORIAL) { INT32 mode = M_GameDataGameType(gametype, battleprisons); // Gamedata mode playtime if (mode >= 0 && mode < GDGT_MAX) { gamedata->modeplaytime[mode]++; if (mapheader) { mapheader->records.modetimeplayed[mode]++; } } // Attacking mode playtime if (modeattacking != ATTACKING_NONE) { if (encoremode) // ((modeattacking & ATTACKING_SPB) != 0) { gamedata->spbattackingtotaltime++; if (mapheader) { mapheader->records.spbattacktimeplayed++; } } //else { gamedata->timeattackingtotaltime++; if (mapheader) { mapheader->records.timeattacktimeplayed++; } } } // Per-skin mode playtime for (i = 0; i < MAXPLAYERS; i++) { skin_t *playerskin; if (!P_IsMachineLocalPlayer(&players[i])) { continue; } if (!(playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))) { continue; } if (players[i].skin >= numskins) { continue; } playerskin = skins[players[i].skin]; playerskin->records.modetimeplayed[mode]++; } } // TODO would this be laggy with more conditions in play... if ( (leveltime > introtime && M_UpdateUnlockablesAndExtraEmblems(true, false)) || gamedata->deferredsave ) { G_SaveGameData(); } } } if (run) { ps_thinkertime = I_GetPreciseTime(); P_RunThinkers(); ps_thinkertime = I_GetPreciseTime() - ps_thinkertime; thinkersCompleted = true; // Run any "after all the other thinkers" stuff { player_t *finishingPlayers[MAXPLAYERS]; UINT8 numingame = 0, numFinishingPlayers = 0; for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo)) { P_PlayerAfterThink(&players[i]); if (players[i].spectator == true) continue; numingame++; // Check for the number of ties for first place after every player has thunk run for this tic if (players[i].exiting == 1 && players[i].position == 1 && (players[i].pflags & (PF_HITFINISHLINE|PF_NOCONTEST)) == PF_HITFINISHLINE) { finishingPlayers[numFinishingPlayers++] = &players[i]; } } } if ((netgame) // Antigrief is supposed to apply? && !(K_Cooperative() || timelimitintics > 0 || g_pointlimit > 0) // There are rules that will punish a griefing player && (gametyperules & GTR_CIRCUIT) && (leveltime > starttime) && K_GetNumWaypoints()) // The following only detects race griefing { for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo)) { if (players[i].spectator == true) continue; if (players[i].exiting || (players[i].pflags & PF_NOCONTEST)) continue; if (players[i].bot == true) continue; P_CheckRaceGriefing(&players[i], (numingame > 1)); } } } // Apply rumble to local players if (!demo.playback) { P_DeviceRumbleTick(); } if (numFinishingPlayers > 1) { for (i = 0; i < numFinishingPlayers; i++) { P_SetupSignExit(finishingPlayers[i], true); } } else if (numFinishingPlayers == 1) { P_SetupSignExit(finishingPlayers[0], false); } } if (musiccountdown > 0) { // Music is controlled by completion sequence } else if (K_CheckBossIntro() == true) { // Bosses have a punchy start, so no position. if (leveltime == 1) { P_StartLevelMusic(); } } else if (leveltime < starttime + TICRATE) { if (leveltime == (starttime + (TICRATE/2))) { // Plays the music after the starting countdown. P_StartLevelMusic(); } else if (starttime != introtime) { // Start countdown/music handling if (leveltime == starttime-(3*TICRATE)) { S_StartSound(NULL, sfx_s3ka7); // 3, } else if ((leveltime == starttime-(2*TICRATE)) || (leveltime == starttime-TICRATE)) { S_StartSound(NULL, sfx_s3ka7); // 2, 1, } else if (leveltime == starttime) { S_StartSound(NULL, sfx_s3kad); // GO! } // POSITION!! music if (modeattacking == ATTACKING_NONE || !P_UseContinuousLevelMusic()) { P_StartPositionMusic(true); // exact times only } } } if (modeattacking != ATTACKING_NONE && P_UseContinuousLevelMusic()) { if (leveltime == 4) { P_StartLevelMusic(); } } ps_lua_thinkframe_time = I_GetPreciseTime(); LUA_HookThinkFrame(); ps_lua_thinkframe_time = I_GetPreciseTime() - ps_lua_thinkframe_time; } if (run) UpdateChallenges(); // Run shield positioning P_RunOverlays(); P_UpdateSpecials(); P_RespawnSpecials(); // Lightning, rain sounds, etc. P_PrecipitationEffects(); if (run) { leveltime++; if (starttime > introtime && leveltime == starttime) { ACS_RunPositionScript(); } if ((gametyperules & GTR_OVERTIME) && !battleprisons && timelimitintics > 0 && leveltime == (timelimitintics + starttime + 1)) { ACS_RunOvertimeScript(); } } if (g_fast_forward == 0 || demo.simplerewind) { timeinmap++; } if (run) { if (racecountdown > 1) racecountdown--; P_TickDarkness(); P_TickMusicFade(); if (exitcountdown >= 1) { boolean run_exit_countdown = true; for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == false) { continue; } player_t *player = &players[i]; if (player->spectator == false && K_PlayerTallyActive(player) == true && player->tally.done == false) { run_exit_countdown = false; break; } } if (run_exit_countdown == true) { if (exitcountdown > 1) { exitcountdown--; } if (exitcountdown == 1) { if (demo.playback) G_FinishExitLevel(); else if (server) SendNetXCmd(XD_EXITLEVEL, NULL, 0); } } } K_RunItemCooldowns(); K_BossInfoTicker(); K_TickSpecialStage(); if ((gametyperules & GTR_POINTLIMIT)) { if (wantedcalcdelay && --wantedcalcdelay <= 0) K_CalculateBattleWanted(); } if (bombflashtimer) bombflashtimer--; // Bomb seizure prevention // Tick quake effects quake = g_quakes; while (quake != NULL) { if (quake->time <= 0) { // Time out, remove this effect quake_t *remove = quake; quake = quake->next; P_FreeQuake(remove); continue; } quake->time--; if (quake->epicenter != NULL && quake->mobj != NULL && P_MobjWasRemoved(quake->mobj) == false) { quake->epicenter->x = quake->mobj->x; quake->epicenter->y = quake->mobj->y; quake->epicenter->z = quake->mobj->z + (quake->mobj->height / 2); } quake = quake->next; } if (demo.recording) { if (cv_recordmultiplayerdemos.value && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime) G_CheckDemoTitleEntry(); if (!G_ConsiderEndingDemoWrite()) { G_WriteAllGhostTics(); } } else if (demo.playback && !demo.waitingfortally) { if (!G_ConsiderEndingDemoRead()) { // Use Ghost data for consistency checks. G_ConsAllGhostTics(); } } if (modeattacking) { if (!demo.waitingfortally) G_GhostTicker(); if (!demo.playback) G_TickTimeStickerMedals(); } if (mapreset > 1 && --mapreset <= 1 && server) // Remember: server uses it for mapchange, but EVERYONE ticks down for the animation D_MapChange(gamemap, gametype, encoremode, true, 0, false, false); if (cv_kartdebugwaypoints.value != 0) { K_DebugWaypointsVisualise(); } } if (gamestate == GS_LEVEL) { // Move the camera during levels. K_UpdateDirector(); P_RunChaseCameras(); } if (run && !levelloading && leveltime) { K_TickDialogue(); K_TickMessages(); } if (run) { LUA_HOOK(PostThinkFrame); R_UpdateLevelInterpolators(); // Hack: ensure newview is assigned every tic. // Ensures view interpolation is T-1 to T in poor network conditions // We need a better way to assign view state decoupled from game logic if (rendermode != render_none) { for (i = 0; i <= r_splitscreen; i++) { player_t *player = &players[displayplayers[i]]; if (!player->mo) continue; const boolean skybox = (player->skybox.viewpoint && cv_skybox.value); // True if there's a skybox object and skyboxes are on if (skybox) { R_SkyboxFrame(i); } R_SetupFrame(i); } } } P_MapEnd(); for (i = 0; i < MAXPLAYERS; i++) { G_CopyTiccmd(&players[i].oldcmd, &players[i].cmd, 1); } if (D_IsDeferredStartTitle()) { D_StartTitle(); } // Z_CheckMemCleanup(); }