Files
SRB2Kart-Saturn/src/p_tick.c
T
2024-12-09 10:53:08 +01:00

802 lines
18 KiB
C

// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2018 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file p_tick.c
/// \brief Archiving: SaveGame I/O, Thinker, Ticker
#include "doomstat.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_setup.h"
#include "p_polyobj.h"
#include "m_random.h"
#include "lua_script.h"
#include "lua_hook.h"
#include "k_director.h"
#include "k_kart.h"
#include "i_system.h"
#include "r_main.h"
#include "r_fps.h"
#include "i_video.h" // rendermode
#include "m_perfstats.h"
// Object place
#include "m_cheat.h"
// Dynamic slopes
#include "p_slopes.h"
tic_t leveltime;
//
// 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.
//
// Both the head and tail of the thinker list.
thinker_t thinkercap;
void Command_Numthinkers_f(void)
{
INT32 num;
INT32 count = 0;
actionf_p1 action;
thinker_t *think;
if (gamestate != GS_LEVEL)
{
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:
action = (actionf_p1)P_MobjThinker;
CONS_Printf(M_GetText("Number of %s: "), "P_MobjThinker");
break;
case 2:
action = (actionf_p1)P_NullPrecipThinker;
CONS_Printf(M_GetText("Number of %s: "), "P_NullPrecipThinker");
break;
case 3:
action = (actionf_p1)T_Friction;
CONS_Printf(M_GetText("Number of %s: "), "T_Friction");
break;
case 4:
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 (think = thinkercap.next; think != &thinkercap; 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 (gamestate != GS_LEVEL)
{
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 = thinkercap.next; th != &thinkercap; th = th->next)
{
if (th->function.acp1 != (actionf_p1)P_MobjThinker)
continue;
if (((mobj_t *)th)->type == i)
count++;
}
CONS_Printf(M_GetText("There are %d objects of type %d currently in the level.\n"), count, i);
}
return;
}
CONS_Printf(M_GetText("Count of active objects in level:\n"));
for (i = 0; i < NUMMOBJTYPES; i++)
{
count = 0;
for (th = thinkercap.next; th != &thinkercap; th = th->next)
{
if (th->function.acp1 != (actionf_p1)P_MobjThinker)
continue;
if (((mobj_t *)th)->type == i)
count++;
}
if (count > 0) // Don't bother displaying if there are none of this type!
CONS_Printf(" * %d: %d\n", i, count);
}
}
//
// P_InitThinkers
//
void P_InitThinkers(void)
{
thinkercap.prev = thinkercap.next = &thinkercap;
waypointcap = NULL;
}
//
// P_AddThinker
// Adds a new thinker at the end of the list.
//
void P_AddThinker(thinker_t *thinker)
{
thinkercap.prev->next = thinker;
thinker->next = &thinkercap;
thinker->prev = thinkercap.prev;
thinkercap.prev = thinker;
thinker->references = 0; // killough 11/98: init reference counter to 0
thinker->cachable = (thinker->function.acp1 == (actionf_p1)P_MobjThinker);
}
//
// 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)
{
#ifdef PARANOIA
if (thinker->next)
thinker->next = NULL;
else if (thinker->references) // Usually gets cleared up in one frame; what's going on here, then?
CONS_Printf("Number of potentially faulty references: %d\n", thinker->references);
#endif
if (thinker->references)
return;
/* Remove from main thinker list */
thinker_t *next = thinker->next;
/* 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 */
(next->prev = currentthinker = thinker->prev)->next = next;
R_DestroyLevelInterpolators(thinker);
if (thinker->cachable == true)
{
// put cachable thinkers in the mobj cache, so we can avoid allocations
((mobj_t *)thinker)->hnext = mobjcache;
mobjcache = (mobj_t *)thinker;
}
else
{
Z_Free(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->cachable == true)
{
// put cachable thinkers in the mobj cache, so we can avoid allocations
((mobj_t *)thinker)->hnext = mobjcache;
mobjcache = (mobj_t *)thinker;
}
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_SetTarget(mobj_t **mop, mobj_t *targ)
{
if (*mop) // If there was a target already, decrease its refcount
(*mop)->thinker.references--;
if ((*mop = targ) != NULL) // Set new target and if non-NULL, increase its counter
targ->thinker.references++;
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 inline void P_RunThinkers(void)
{
for (currentthinker = thinkercap.next; currentthinker != &thinkercap; currentthinker = currentthinker->next)
{
if (currentthinker->function.acp1 == (actionf_p1)P_NullPrecipThinker)
continue;
#ifdef PARANOIA
I_Assert(currentthinker->function.acp1 != NULL)
#endif
currentthinker->function.acp1(currentthinker);
}
}
static void P_DeviceRumbleTick(void)
{
UINT8 i;
if (I_NumJoys() == 0 || (cv_rumble[0].value == 0 && cv_rumble[1].value == 0 && cv_rumble[2].value == 0 && cv_rumble[3].value == 0))
{
return;
}
for (i = 0; i <= splitscreen; i++)
{
player_t *player = &players[displayplayers[i]];
UINT16 low = 0;
UINT16 high = 0;
if (!P_IsLocalPlayer(player))
continue;
if (cv_usejoystick[i].value == 0)
continue;
if (!playeringame[displayplayers[i]] || player->spectator)
continue;
if (player->mo == NULL)
continue;
if (player->exiting)
{
G_PlayerDeviceRumble(i, low, high, 0);
continue;
}
if (player->kartstuff[k_spinouttimer])
{
low = high = 65536 / 4;
}
else if (player->kartstuff[k_sneakertimer] > (sneakertime-(TICRATE/2)))
{
low = high = 65536 / 8;
}
else if ((player->kartstuff[k_offroad] && !player->kartstuff[k_hyudorotimer])
&& P_IsObjectOnGround(player->mo) && player->speed != 0)
{
low = high = 65536 / 64;
}
else if (player->kartstuff[k_brakedrift])
{
low = 0;
high = 65536 / 256;
}
if (low == 0 && high == 0)
continue;
G_PlayerDeviceRumble(i, low, high, 57); // hack alert! i just dont want this think constantly resetting the rumble lol
}
}
void P_RunChaseCameras(void)
{
UINT8 i;
for (i = 0; i <= splitscreen; i++)
{
if (camera[i].chase)
P_MoveChaseCamera(&players[displayplayers[i]], &camera[i], false);
}
}
static void P_RunQuakes(void)
{
fixed_t ir;
if (quake.time <= 0)
{
quake.x = quake.y = quake.z = quake.roll = 0;
return;
}
ir = quake.intensity>>1;
/// \todo Calculate distance from epicenter if set and modulate the intensity accordingly based on radius.
quake.x = M_RandomRange(-ir,ir);
quake.y = M_RandomRange(-ir,ir);
quake.z = M_RandomRange(-ir,ir);
ir >>= 2;
ir = M_RandomRange(-ir,ir);
if (ir < 0)
ir = ANGLE_MAX - FixedAngle(-ir);
else
ir = FixedAngle(ir);
quake.roll = ir;
--quake.time;
}
//
// P_Ticker
//
void P_Ticker(boolean run)
{
INT32 i;
// Increment jointime even if paused
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i])
{
players[i].jointime++;
}
}
if (run)
{
// Update old view state BEFORE ticking so resetting
// the old interpolation state from game logic works.
R_UpdateViewInterpolation();
}
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())
{
if (demo.rewinding && leveltime > 0)
{
leveltime = (leveltime-1) & ~3;
if (timeinmap > 0)
timeinmap = (timeinmap-1) & ~3;
G_PreviewRewind(leveltime);
}
else if (demo.freecam && democam.cam) // special case: allow freecam to MOVE during pause!
P_DemoCameraMovement(democam.cam);
return;
}
P_MapStart();
if (run)
{
R_UpdateMobjInterpolators();
if (demo.recording)
{
G_WriteDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
G_WriteDemoTiccmd(&players[i].cmd, i);
}
if (demo.playback)
{
#ifdef DEMO_COMPAT_100
if (demo.version == 0x0001)
{
G_ReadDemoTiccmd(&players[consoleplayer].cmd, 0);
}
else
{
#endif
G_ReadDemoExtraData();
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i])
{
//@TODO all this throwdir stuff shouldn't be here! But it's added to maintain 1.0.4 compat for now...
// Remove for 1.1!
if (players[i].cmd.buttons & BT_FORWARD)
players[i].kartstuff[k_throwdir] = 1;
else if (players[i].cmd.buttons & BT_BACKWARD)
players[i].kartstuff[k_throwdir] = -1;
else
players[i].kartstuff[k_throwdir] = 0;
G_ReadDemoTiccmd(&players[i].cmd, i);
}
#ifdef DEMO_COMPAT_100
}
#endif
}
ps_lua_mobjhooks.value.i = 0;
ps_checkposition_calls.value.i = 0;
PS_START_TIMING(ps_lua_prethinkframe_time);
LUA_HookPreThinkFrame();
PS_STOP_TIMING(ps_lua_prethinkframe_time);
PS_START_TIMING(ps_playerthink_time);
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
P_PlayerThink(&players[i]);
PS_STOP_TIMING(ps_playerthink_time);
}
// Keep track of how long they've been playing!
if (!demo.playback) // Don't increment if a demo is playing.
totalplaytime++;
if (run)
{
// Dynamic slopeness
if (midgamejoin) // only run here if we joined midgame to fix some desynchs
P_RunDynamicSlopes();
PS_START_TIMING(ps_thinkertime);
P_RunThinkers();
PS_STOP_TIMING(ps_thinkertime);
// Run any "after all the other thinkers" stuff
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
P_PlayerAfterThink(&players[i]);
// Apply rumble to local players
if (!demo.playback)
{
P_DeviceRumbleTick();
}
PS_START_TIMING(ps_lua_thinkframe_time);
LUA_HookThinkFrame();
PS_STOP_TIMING(ps_lua_thinkframe_time);
}
// Run shield positioning
//P_RunShields();
P_RunOverlays();
P_RunShadows();
P_UpdateSpecials();
P_RespawnSpecials();
// Lightning, rain sounds, etc.
P_PrecipitationEffects();
if (run)
leveltime++;
// as this is mostly used for HUD stuff, add the record attack specific hack to it as well!
if (!(modeattacking && !demo.playback) || leveltime >= starttime - TICRATE*4)
timeinmap++;
if (run)
{
if (countdowntimer && --countdowntimer <= 0)
{
countdowntimer = 0;
countdowntimeup = true;
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
continue;
if (!players[i].mo)
continue;
P_DamageMobj(players[i].mo, NULL, NULL, 10000);
}
}
if (racecountdown > 1)
racecountdown--;
if (exitcountdown > 1)
exitcountdown--;
if (indirectitemcooldown > 0)
indirectitemcooldown--;
if (hyubgone > 0)
hyubgone--;
if (G_RaceGametype())
{
K_UpdateSpectateGrief();
}
if (G_BattleGametype())
{
if (wantedcalcdelay && --wantedcalcdelay <= 0)
K_CalculateBattleWanted();
}
P_RunQuakes();
if (metalplayback)
G_ReadMetalTic(metalplayback);
if (metalrecording)
G_WriteMetalTic(players[consoleplayer].mo);
if (demo.recording)
{
INT32 axis = JoyAxis(AXISLOOKBACK, 1);
G_WriteAllGhostTics();
if (cv_recordmultiplayerdemos.value)
{
if (demo.savemode == DSM_NOTSAVING || demo.savemode == DSM_WILLAUTOSAVE)
if (demo.savebutton && demo.savebutton + 3*TICRATE < leveltime && (InputDown(gc_lookback, 1) || (cv_usejoystick[0].value && axis > 0)))
demo.savemode = DSM_TITLEENTRY;
// if there are no players left at all, stop demo recording
// Demos that that dont have any players crash during playback, which can happen with dedicated servers
if (demo.savemode == DSM_WILLAUTOSAVE && !D_NumPlayers())
G_ResetDemoRecording();
}
}
else if (demo.playback) // Use Ghost data for consistency checks.
{
#ifdef DEMO_COMPAT_100
if (demo.version == 0x0001)
G_ConsGhostTic(0);
else
#endif
G_ConsAllGhostTics();
}
if (modeattacking)
G_GhostTicker();
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);
PS_START_TIMING(ps_lua_postthinkframe_time);
LUA_HookPostThinkFrame();
PS_STOP_TIMING(ps_lua_postthinkframe_time);
}
K_UpdateDirector();
// Always move the camera.
P_RunChaseCameras();
if (run)
{
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 <= splitscreen; i++)
{
player_t *player = &players[displayplayers[i]];
if (!player->mo)
continue;
const boolean skybox = (skyboxmo[0] && cv_skybox.value);
if (skyVisiblePerPlayer[i] && skybox)
{
R_SkyboxFrame(i);
}
R_SetupFrame(i, skybox);
}
}
}
P_MapEnd();
if (demo.playback)
G_StoreRewindInfo();
}
// Abbreviated ticker for pre-loading, calls thinkers and assorted things
void P_PreTicker(INT32 frames)
{
INT32 i;
ticcmd_t temptic;
hook_defrosting = frames;
while (hook_defrosting)
{
P_MapStart();
R_UpdateMobjInterpolators();
LUA_HOOK(PreThinkFrame);
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
{
// stupid fucking cmd hack
// if it isn't for this, players can move in preticker time
// (and disrupt demo recording and other things !!)
memcpy(&temptic, &players[i].cmd, sizeof(ticcmd_t));
memset(&players[i].cmd, 0, sizeof(ticcmd_t));
// correct angle on spawn...
players[i].cmd.angleturn = temptic.angleturn;
P_PlayerThink(&players[i]);
memcpy(&players[i].cmd, &temptic, sizeof(ticcmd_t));
}
// Dynamic slopeness
if (midgamejoin)
P_RunDynamicSlopes();
P_RunThinkers();
// Run any "after all the other thinkers" stuff
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
P_PlayerAfterThink(&players[i]);
LUA_HookThinkFrame();
P_RunOverlays();
P_RunShadows();
P_UpdateSpecials();
P_RespawnSpecials();
LUA_HOOK(PostThinkFrame);
R_UpdateLevelInterpolators();
R_UpdateViewInterpolation();
R_ResetViewInterpolation(0);
P_MapEnd();
hook_defrosting--;
}
}