// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  p_user.c
/// \brief New stuff?
///        Player related stuff.
///        Bobbing POV/weapon, movement.
///        Pending weapon.

#include "d_player.h"
#include "doomdef.h"
#include "doomstat.h"
#include "i_system.h"
#include "d_event.h"
#include "d_net.h"
#include "g_game.h"
#include "info.h"
#include "p_local.h"
#include "p_mobj.h"
#include "r_fps.h"
#include "r_main.h"
#include "s_sound.h"
#include "r_skins.h"
#include "d_think.h"
#include "r_sky.h"
#include "p_setup.h"
#include "m_random.h"
#include "m_misc.h"
#include "i_video.h"
#include "i_gamepad.h"
#include "p_slopes.h"
#include "p_spec.h"
#include "r_splats.h"
#include "z_zone.h"
#include "w_wad.h"
#include "hu_stuff.h"
// We need to affect the NiGHTS hud
#include "st_stuff.h"
#include "lua_script.h"
#include "lua_hook.h"
// Objectplace
#include "m_cheat.h"
// Thok camera snap (ctrl-f "chalupa")
#include "g_input.h"
#include "m_menu.h" // menustack

// SRB2kart
#include "m_cond.h" // M_UpdateUnlockablesAndExtraEmblems
#include "k_kart.h"
#include "console.h" // CON_LogMessage
#include "k_bot.h"
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_terrain.h" // K_SpawnSplashForMobj
#include "k_color.h"
#include "k_follower.h"
#include "k_items.h"
#include "g_party.h"

#include "m_easing.h" // Arrow Bullet aura

#include "acs/interface.h"

#ifdef HWRENDER
#include "hardware/hw_light.h"
#include "hardware/hw_main.h"
#endif

#if 0
static void P_NukeAllPlayers(player_t *player);
#endif

// Some people like the more limited camerea from V1 so why not.
consvar_t cv_naturalcamera = CVAR_INIT ("naturalcamera", "Off", CV_SAVE, CV_OnOff, NULL);

//
// Jingle stuff.
//

jingle_t jingleinfo[NUMJINGLES] = {
	// {musname, looping, reset, nest}
	{""        , false}, // JT_NONE
	{""        , false}, // JT_OTHER
	{""        , false}, // JT_MASTER
	{"_1up"    , false},
	{"_shoes"  ,  true},
	{"_inv"    , false},
	{"_minv"   , false},
	{"_drown"  , false},
	{"_super"  ,  true},
	{"_gover"  , false},
	{"_ntime"  , false},  // JT_NIGHTSTIMEOUT
	{"_drown"  , false}   // JT_SSTIMEOUT
	// {"_clear"  , false},
	// {"_inter"  ,  true},
	// {"_conti"  ,  true}
};

//
// Movement.
//

// 16 pixels of bob
//#define MAXBOB (0x10 << FRACBITS)

static boolean onground;

//
// P_Thrust
// Moves the given origin along a given angle.
//
void P_Thrust(mobj_t *mo, angle_t angle, fixed_t move)
{
	angle >>= ANGLETOFINESHIFT;

	mo->momx += FixedMul(move, FINECOSINE(angle));
	mo->momy += FixedMul(move, FINESINE(angle));
}

#if 0
static inline void P_VectorInstaThrust(fixed_t xa, fixed_t xb, fixed_t xc, fixed_t ya, fixed_t yb, fixed_t yc,
	fixed_t za, fixed_t zb, fixed_t zc, fixed_t momentum, mobj_t *mo)
{
	fixed_t a1, b1, c1, a2, b2, c2, i, j, k;

	a1 = xb - xa;
	b1 = yb - ya;
	c1 = zb - za;
	a2 = xb - xc;
	b2 = yb - yc;
	c2 = zb - zc;
/*
	// Convert to unit vectors...
	a1 = FixedDiv(a1,FixedSqrt(FixedMul(a1,a1) + FixedMul(b1,b1) + FixedMul(c1,c1)));
	b1 = FixedDiv(b1,FixedSqrt(FixedMul(a1,a1) + FixedMul(b1,b1) + FixedMul(c1,c1)));
	c1 = FixedDiv(c1,FixedSqrt(FixedMul(c1,c1) + FixedMul(c1,c1) + FixedMul(c1,c1)));

	a2 = FixedDiv(a2,FixedSqrt(FixedMul(a2,a2) + FixedMul(c2,c2) + FixedMul(c2,c2)));
	b2 = FixedDiv(b2,FixedSqrt(FixedMul(a2,a2) + FixedMul(c2,c2) + FixedMul(c2,c2)));
	c2 = FixedDiv(c2,FixedSqrt(FixedMul(a2,a2) + FixedMul(c2,c2) + FixedMul(c2,c2)));
*/
	// Calculate the momx, momy, and momz
	i = FixedMul(momentum, FixedMul(b1, c2) - FixedMul(c1, b2));
	j = FixedMul(momentum, FixedMul(c1, a2) - FixedMul(a1, c2));
	k = FixedMul(momentum, FixedMul(a1, b2) - FixedMul(a1, c2));

	mo->momx = i;
	mo->momy = j;
	mo->momz = k;
}
#endif

//
// P_InstaThrust
// Moves the given origin along a given angle instantly.
//
// FIXTHIS: belongs in another file, not here
//
void P_InstaThrust(mobj_t *mo, angle_t angle, fixed_t move)
{
	angle >>= ANGLETOFINESHIFT;

	mo->momx = FixedMul(move, FINECOSINE(angle));
	mo->momy = FixedMul(move,FINESINE(angle));
}

// Returns a location (hard to explain - go see how it is used)
fixed_t P_ReturnThrustX(mobj_t *mo, angle_t angle, fixed_t move)
{
	(void)mo;
	angle >>= ANGLETOFINESHIFT;
	return FixedMul(move, FINECOSINE(angle));
}
fixed_t P_ReturnThrustY(mobj_t *mo, angle_t angle, fixed_t move)
{
	(void)mo;
	angle >>= ANGLETOFINESHIFT;
	return FixedMul(move, FINESINE(angle));
}

//
// P_AutoPause
// Returns true when gameplay should be halted even if the game isn't necessarily paused.
//
boolean P_AutoPause(void)
{
	// Don't pause even on menu-up or focus-lost in netgames or record attack
	if (netgame || modeattacking || gamestate == GS_TITLESCREEN)
		return false;

	return ((menustack[0] && !demo.playback) || ( window_notinfocus && cv_pauseifunfocused.value ));
}

//
// P_CalcHeight
// Calculate the walking / running height adjustment
//
void P_CalcHeight(player_t *player)
{
	fixed_t bob = 0;
	fixed_t pviewheight;
	mobj_t *mo = player->mo;
	fixed_t bobmul = FRACUNIT - FixedDiv(FixedHypot(player->rmomx, player->rmomy), K_GetKartSpeed(player, false, false));

	// Regular movement bobbing.
	// Should not be calculated when not on ground (FIXTHIS?)
	// OPTIMIZE: tablify angle
	// Note: a LUT allows for effects
	//  like a ramp with low health.

	if (bobmul > FRACUNIT) bobmul = FRACUNIT;
	if (bobmul < FRACUNIT/8) bobmul = FRACUNIT/8;

	player->bob = FixedMul(cv_movebob.value, bobmul);

	if (!P_IsObjectOnGround(mo) || player->spectator)
	{
		if (mo->eflags & MFE_VERTICALFLIP)
		{
			player->viewz = mo->z + mo->height - player->viewheight;
			if (player->viewz < mo->floorz + FixedMul(FRACUNIT, mo->scale))
				player->viewz = mo->floorz + FixedMul(FRACUNIT, mo->scale);
		}
		else
		{
			player->viewz = mo->z + player->viewheight;
			if (player->viewz > mo->ceilingz - FixedMul(FRACUNIT, mo->scale))
				player->viewz = mo->ceilingz - FixedMul(FRACUNIT, mo->scale);
		}
		return;
	}

	// First person move bobbing in Kart is more of a "move jitter", to match how a go-kart would feel :p
	if (leveltime & 1)
	{
		bob = player->bob;
	}

	// move viewheight
	pviewheight = P_GetPlayerViewHeight(player); // default eye view height

	if (player->playerstate == PST_LIVE)
	{
		player->viewheight += player->deltaviewheight;

		if (player->viewheight > pviewheight)
		{
			player->viewheight = pviewheight;
			player->deltaviewheight = 0;
		}

		if (player->viewheight < pviewheight/2)
		{
			player->viewheight = pviewheight/2;
			if (player->deltaviewheight <= 0)
				player->deltaviewheight = 1;
		}

		if (player->deltaviewheight)
		{
			player->deltaviewheight += FixedMul(FRACUNIT/4, mo->scale);
			if (!player->deltaviewheight)
				player->deltaviewheight = 1;
		}
	}

	if (player->mo->eflags & MFE_VERTICALFLIP)
		player->viewz = mo->z + mo->height - player->viewheight - bob;
	else
		player->viewz = mo->z + player->viewheight + bob;

	if (player->viewz > mo->ceilingz-FixedMul(4*FRACUNIT, mo->scale))
		player->viewz = mo->ceilingz-FixedMul(4*FRACUNIT, mo->scale);
	if (player->viewz < mo->floorz+FixedMul(4*FRACUNIT, mo->scale))
		player->viewz = mo->floorz+FixedMul(4*FRACUNIT, mo->scale);
}

/** Decides if a player is moving.
  * \param pnum The player number to test.
  * \return True if the player is considered to be moving.
  * \author Graue <graue@oceanbase.org>
  */
boolean P_PlayerMoving(INT32 pnum)
{
	player_t *p = &players[pnum];

	if (!Playing())
		return false;

	if (p->jointime < 5*TICRATE || p->playerstate == PST_DEAD || p->playerstate == PST_REBORN || p->spectator)
		return false;

	return gamestate == GS_LEVEL && p->mo && p->mo->health > 0
		&& (abs(p->rmomx) >= FixedMul(FRACUNIT/2, p->mo->scale)
			|| abs(p->rmomy) >= FixedMul(FRACUNIT/2, p->mo->scale)
			|| abs(p->mo->momz) >= FixedMul(FRACUNIT/2, p->mo->scale));
}

// P_GetNextEmerald
//
// Gets the number (0 based) of the next emerald to obtain
//
UINT8 P_GetNextEmerald(void)
{
	INT16 mapnum = gamemap-1;

	if (mapnum > nummapheaders || !mapheaderinfo[mapnum])
		return 0;

	if (!mapheaderinfo[mapnum]->cup || mapheaderinfo[mapnum]->cup->cachedlevels[CUPCACHE_SPECIAL] != mapnum)
		return 0;

	return mapheaderinfo[mapnum]->cup->emeraldnum;
}

//
// P_GiveEmerald
//
// Award an emerald upon completion
// of a special stage.
//
void P_GiveEmerald(boolean spawnObj)
{
	UINT8 em = P_GetNextEmerald();

	S_StartSound(NULL, sfx_cgot); // Got the emerald!
	emeralds |= (1 << em);
	stagefailed = false;

	if (spawnObj)
	{
		// The Chaos Emerald begins to orbit us!
		// Only visibly give it to ONE person!
		UINT8 i, pnum = ((playeringame[consoleplayer]) && (!players[consoleplayer].spectator) && (players[consoleplayer].mo)) ? consoleplayer : 255;
		for (i = 0; i < MAXPLAYERS; i++)
		{
			mobj_t *emmo;
			if (!playeringame[i])
				continue;
			if (players[i].spectator)
				continue;
			if (!players[i].mo)
				continue;

			emmo = P_SpawnMobjFromMobj(players[i].mo, 0, 0, players[i].mo->height, MT_GOTEMERALD);
			if (!emmo)
				continue;
			P_SetTarget(&emmo->target, players[i].mo);
			P_SetMobjState(emmo, mobjinfo[MT_GOTEMERALD].meleestate + em);

			// Make sure we're not being carried before our tracer is changed
			players[i].carry = CR_NONE;

			P_SetTarget(&players[i].mo->tracer, emmo);

			if (pnum == 255)
			{
				pnum = i;
				continue;
			}

			if (i == pnum)
				continue;

			emmo->flags2 |= RF_DONTDRAW;
		}
	}
}

//
// P_GiveFinishFlags
//
// Give the player visual indicators
// that they've finished the map.
//
void P_GiveFinishFlags(player_t *player)
{
	angle_t angle = FixedAngle(player->mo->angle << FRACBITS);
	UINT8 i;

	if (!player->mo)
		return;

	if (!(netgame||multiplayer))
		return;

	for (i = 0; i < 3; i++)
	{
		angle_t fa = (angle >> ANGLETOFINESHIFT) & FINEMASK;
		fixed_t xoffs = FINECOSINE(fa);
		fixed_t yoffs = FINESINE(fa);
		mobj_t* flag = P_SpawnMobjFromMobj(player->mo, xoffs, yoffs, 0, MT_FINISHFLAG);
		flag->angle = angle;
		angle += FixedAngle(120*FRACUNIT);

		P_SetTarget(&flag->target, player->mo);
	}
}

//
// P_FindLowestLap
//
// SRB2Kart, a similar function as above for finding the lowest lap
//
UINT8 P_FindLowestLap(void)
{
	INT32 i;
	UINT8 lowest = UINT8_MAX;

	if (!(gametypes[gametype]->rules & GTR_CIRCUIT))
		return 0;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
			continue;

		if (lowest == UINT8_MAX || players[i].laps < lowest)
		{
			lowest = players[i].laps;
		}
	}

	CONS_Debug(DBG_GAMELOGIC, "Lowest laps found: %d\n", lowest);

	return lowest;
}

//
// P_FindHighestLap
//
UINT8 P_FindHighestLap(void)
{
	INT32 i;
	UINT8 highest = 0;

	if (!(gametypes[gametype]->rules & GTR_CIRCUIT))
		return 0;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
			continue;

		if (players[i].laps > highest)
			highest = players[i].laps;
	}

	CONS_Debug(DBG_GAMELOGIC, "Highest laps found: %d\n", highest);

	return highest;
}

//
// P_PlayerInPain
//
// Is player in pain??
// Checks for painstate and flashing, if both found return true
//
boolean P_PlayerInPain(const player_t *player)
{
	if (K_IsPlayerDamaged(player) || player->squishedtimer)
		return true;

	return false;
}

//
// P_ResetPlayer
//
// Useful when you want to kill everything the player is doing.
void P_ResetPlayer(player_t *player)
{
	//player->pflags &= ~(PF_);

	// turns out PF_SLIDING is NOT reset by this function!
	if (mapnamespace != MNS_SRB2KART || player->carry != CR_SLIDING)
		player->carry = CR_NONE;
	player->onconveyor = 0;

	player->glanceDir = 0;
}

//
// P_GivePlayerRings
//
// Gives rings to the player, and does any special things required.
// Call this function when you want to increment the player's health.
// Returns the number of rings successfully given (or taken).
//

INT32 P_GivePlayerRings(player_t *player, INT32 num_rings)
{
	INT32 test;

	if (!player->mo)
		return 0;
	
	if ((K_RingsActive() == false)) // No rings when turned off
		return 0;

	test = player->rings + num_rings;
	if (test > player->ringmax) // Caps at 20 rings, sorry!
		num_rings -= (test-player->ringmax);
	else if (test < player->ringmin) // Chaotix ring debt!
		num_rings -= (test-player->ringmin);

	player->rings += num_rings;

	return num_rings;
}

//
// P_GivePlayerLives
//
// Gives the player an extra life.
// Call this function when you want to add lives to the player.
//
void P_GivePlayerLives(player_t *player, INT32 numlives)
{
	player->lives += numlives;

	if (player->lives > 9)
		player->lives = 9;
	else if (player->lives < 1)
		player->lives = 1;
}

// Adds to the player's score
void P_AddPlayerScore(player_t *player, UINT32 amount)
{
	if (!(gametypes[gametype]->rules & GTR_POINTS))
		return;

	if (player->exiting) // srb2kart
		return;

	// Don't go above MAXSCORE.
	if (player->roundscore + amount < MAXSCORE)
		player->roundscore += amount;
	else
		player->roundscore = MAXSCORE;
}

//
// P_PlayLivesJingle
//
void P_PlayLivesJingle(player_t *player)
{
	if (player && !P_IsLocalPlayer(player))
		return;

	if (use1upSound)
		S_StartSound(NULL, sfx_oneup);
	else
	{
		P_PlayJingle(player, JT_1UP);
		strlcpy(S_sfx[sfx_None].caption, "One-up", 7);
		S_StartCaption(sfx_None, -1, extralifetics+1);
	}
}

void P_PlayJingle(player_t *player, jingletype_t jingletype)
{
	const char *musname = jingleinfo[jingletype].musname;
	UINT16 musflags = 0;
	boolean looping = jingleinfo[jingletype].looping;

	char newmusic[7];
	strncpy(newmusic, musname, 7);
#ifdef HAVE_LUA_MUSICPLUS
 	if(LUAh_MusicJingle(jingletype, newmusic, &musflags, &looping))
 		return;
#endif
	newmusic[6] = 0;

	P_PlayJingleMusic(player, newmusic, musflags, looping, jingletype);
}

//
// P_PlayJingleMusic
//
void P_PlayJingleMusic(player_t *player, const char *musname, UINT16 musflags, boolean looping, UINT16 status)
{
	// If gamestate != GS_LEVEL, always play the jingle (1-up intermission)
	if (gamestate == GS_LEVEL && player && !P_IsLocalPlayer(player))
		return;

	S_RetainMusic(musname, musflags, looping, 0, status);
	S_StopMusic();
	S_ChangeMusicInternal(musname, looping);
}

boolean P_EvaluateMusicStatus(UINT16 status, const char *musname)
{
	// \todo lua hook
	int i;
	boolean result = false;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!P_IsLocalPlayer(&players[i]))
			continue;

		switch(status)
		{
			case JT_1UP: // Extra life
				result = false; //(players[i].powers[pw_extralife] > 1);
				break;

			case JT_SHOES:  // Speed shoes
				/*if (players[i].powers[pw_sneakers] > 1 && !players[i].powers[pw_super])
				{
					//strlcpy(S_sfx[sfx_None].caption, "Speed shoes", 12);
					//S_StartCaption(sfx_None, -1, players[i].powers[pw_sneakers]);
					result = true;
				}
				else*/
					result = false;
				break;

			case JT_INV: // Invincibility
			case JT_MINV: // Mario Invincibility
				/*if (players[i].powers[pw_invulnerability] > 1)
				{
					//strlcpy(S_sfx[sfx_None].caption, "Invincibility", 14);
					//S_StartCaption(sfx_None, -1, players[i].powers[pw_invulnerability]);
					result = true;
				}
				else*/
					result = false;
				break;

			case JT_DROWN:  // Drowning
				result = false; //(players[i].powers[pw_underwater] && players[i].powers[pw_underwater] <= 11*TICRATE + 1);
				break;

			case JT_SUPER:  // Super Sonic
				result = false; //(players[i].powers[pw_super]);
				break;

			case JT_GOVER: // Game Over
				result = (players[i].lives <= 0);
				break;

			case JT_NIGHTSTIMEOUT: // NiGHTS Time Out (10 seconds)
			case JT_SSTIMEOUT:
				result = false; //(players[i].nightstime && players[i].nightstime <= 10*TICRATE);
				break;

			case JT_OTHER:  // Other state
				result = LUA_HookShouldJingleContinue(&players[i], musname);
				break;

			case JT_NONE:   // Null state
			case JT_MASTER: // Main level music
			default:
				result = true;
		}

		if (result)
			break;
 	}

	return result;
}

void P_PlayRinglossSound(mobj_t *source)
{
	if ((K_RingsActive() == false))
		return;
	
	if (source->player && K_GetShieldFromPlayer(source->player) != KSHIELD_NONE)
		S_StartSound(source, sfx_s1a3); // Shield hit (no ring loss)
	else if (source->player && source->player->rings <= 0)
		S_StartSound(source, sfx_s1a6); // Ring debt (lessened ring loss)
	else
		S_StartSound(source, sfx_s1c6); // Normal ring loss sound
}

void P_PlayDeathSound(mobj_t *source)
{
	S_StartSound(source, sfx_s3k35);
}

void P_PlayVictorySound(mobj_t *source)
{
	if (cv_kartvoices.value)
		S_StartSound(source, sfx_kwin);
}

//
// P_EndingMusic
//
// Consistently sets ending music!
//
boolean P_EndingMusic(player_t *player)
{
	char buffer[9];
	boolean looping = true;
	INT32 bestlocalpos, test;
	player_t *bestlocalplayer;

	if (!P_IsLocalPlayer(player)) // Only applies to a local player
		return false;

	if (multiplayer && demo.playback) // Don't play this in multiplayer replays
		return false;

	// Event - Level Finish
	// Check for if this is valid or not
#define getplayerpos(p) \
	(((players[p].position < 1) || (players[p].pflags & PF_NOCONTEST)) \
		? MAXPLAYERS+1 \
		: players[p].position);

	if (r_splitscreen)
	{
		const UINT8 *localplayertable = G_PartyArray(consoleplayer);

		if (!((players[localplayertable[0]].exiting || (players[localplayertable[0]].pflags & PF_NOCONTEST))
			|| (players[localplayertable[1]].exiting || (players[localplayertable[1]].pflags & PF_NOCONTEST))
			|| ((r_splitscreen > 1) && (players[localplayertable[2]].exiting || (players[localplayertable[2]].pflags & PF_NOCONTEST)))
			|| ((r_splitscreen > 2) && (players[localplayertable[3]].exiting || (players[localplayertable[3]].pflags & PF_NOCONTEST)))))
			return false;

		bestlocalplayer = &players[localplayertable[0]];
		bestlocalpos = getplayerpos(localplayertable[0]);
#define setbests(p) \
	test = getplayerpos(p); \
	if (test < bestlocalpos) \
	{ \
		bestlocalplayer = &players[p]; \
		bestlocalpos = test; \
	}
		setbests(localplayertable[1]);
		if (r_splitscreen > 1)
			setbests(localplayertable[2]);
		if (r_splitscreen > 2)
			setbests(localplayertable[3]);
#undef setbests
	}
	else
	{
		if (!(player->exiting || (player->pflags & PF_NOCONTEST)))
			return false;

		bestlocalplayer = player;
		bestlocalpos = getplayerpos((player-players));
	}

#undef getplayerpos

	if ((gametypes[gametype]->rules & GTR_CIRCUIT) && bestlocalpos == MAXPLAYERS+1)
		sprintf(buffer, "k*fail"); // F-Zero death results theme
	else
	{
		if (K_IsPlayerLosing(bestlocalplayer))
			sprintf(buffer, "k*lose");
		else if (bestlocalpos == 1)
			sprintf(buffer, "k*win");
		else
			sprintf(buffer, "k*ok");
	}

	S_SpeedMusic(1.0f);

	if ((gametypes[gametype]->rules & GTR_CIRCUIT))
		buffer[1] = 'r';
	else if ((gametypes[gametype]->rules & GTR_BUMPERS))
	{
		buffer[1] = 'b';
		looping = false;
	}

	S_ChangeMusicInternal(buffer, looping);

	return true;
}

//
// P_RestoreMusic
//
// Restores music after some special music change
//
void P_RestoreMusic(player_t *player)
{
	UINT32 position;

	if (!P_IsLocalPlayer(player)) // Only applies to a local player
		return;

	S_SpeedMusic(1.0f);

	// TO-DO: Use jingle system for Kart's stuff

	// Event - HERE COMES A NEW CHALLENGER
	if (mapreset)
	{
		S_ChangeMusicInternal("chalng", false); //S_StopMusic();
		return;
	}

	// Event - Level Ending
	if (P_EndingMusic(player))
		return;

	// Event - Level Start
	if (bossinfo.boss == false && (leveltime < (starttime + (TICRATE/2)))) // see also where time overs are handled
		return;

	{
		INT32 wantedmus = 0; // 0 is level music, 1 is invincibility, 2 is grow

		if (r_splitscreen)
		{
			INT32 bestlocaltimer = 1;
			const UINT8 *localplayertable = G_PartyArray(consoleplayer);

#define setbests(p) \
	if (players[p].playerstate == PST_LIVE) \
	{ \
		if (players[p].growshrinktimer > bestlocaltimer) \
		{ wantedmus = 1; bestlocaltimer = players[p].growshrinktimer; } \
		else if (players[p].invincibilitytimer > bestlocaltimer) \
		{ wantedmus = 2; bestlocaltimer = players[p].invincibilitytimer; } \
		else if ((players[p].smonitortimer > bestlocaltimer) && (!players[p].smonitorwarning))\
		{ wantedmus = 2; bestlocaltimer = players[p].smonitortimer; } \
		else if ((K_IsKartItemAlternate(KITEM_SHRINK)) && \
		    (K_GetShrinkTime(&players[p]) > bestlocaltimer)) \
		{ wantedmus = 3; bestlocaltimer = K_GetShrinkTime(&players[p]); } \
	}
			setbests(localplayertable[0]);
			setbests(localplayertable[1]);
			if (r_splitscreen > 1)
				setbests(localplayertable[2]);
			if (r_splitscreen > 2)
				setbests(localplayertable[3]);
#undef setbests
		}
		else
		{
			if (player->playerstate == PST_LIVE)
			{
				if (player->growshrinktimer > 1)
					wantedmus = 1;
				else if (player->invincibilitytimer > 1)
					wantedmus = 2;
				else if (((player->smonitortimer > 1) && (!player->smonitorwarning)))
					wantedmus = 2;
				else if ((K_IsKartItemAlternate(KITEM_SHRINK)) &&
				    (K_GetShrinkTime(player) > 1))
					wantedmus = 3;
				
			}
		}

		// Item - Grow
		if ((wantedmus == 1) && cv_growmusic.value == 1)
		{
			S_ChangeMusicInternal("kgrow", true);
			S_SetRestoreMusicFadeInCvar(&cv_growmusicfade);
		}
		// Item - Invincibility
		else if ((wantedmus == 2) && cv_supermusic.value == 1)
		{
			S_ChangeMusicInternal(K_PlayFullInvinTheme() ? "kinvnf" : "kinvnc", true);
			S_SetRestoreMusicFadeInCvar(&cv_invincmusicfade);
		}
		// Item - Shrink (Alternative)
		else if ((wantedmus == 3) && cv_altshrinkmusic.value == 1)
		{
			S_ChangeMusicInternal("kshrnk", true);
			S_SetRestoreMusicFadeInCvar(&cv_altshrinkmusicfade);
		}
		else
		{
#if 0
			// Event - Final Lap
			// Still works for GME, but disabled for consistency
			if ((gametypes[gametype]->rules & GTR_CIRCUIT) && player->laps >= numlaps)
				S_SpeedMusic(1.2f);
#endif
			if (mapmusresume && cv_resume.value)
				position = mapmusresume;
			else
				position = mapmusposition;

			S_ChangeMusicEx(mapmusname, mapmusflags, true, position, 0,
					S_GetRestoreMusicFadeIn());
			S_ClearRestoreMusicFadeInCvar();
			mapmusresume = 0;
		}
	}
}

//
// P_IsObjectInGoop
//
// Returns true if the object is inside goop water.
// (Spectators and objects otherwise without gravity cannot have goop gravity!)
//
boolean P_IsObjectInGoop(const mobj_t *mo)
{
	if (mo->player && mo->player->spectator)
		return false;

	if (mo->flags & MF_NOGRAVITY)
		return false;

	return ((mo->eflags & (MFE_UNDERWATER|MFE_GOOWATER)) == (MFE_UNDERWATER|MFE_GOOWATER));
}

//
// P_IsObjectOnGround
//
// Returns true if the player is
// on the ground. Takes reverse
// gravity and FOFs into account.
//
boolean P_IsObjectOnGround(const mobj_t *mo)
{
	if (P_IsObjectInGoop(mo))
	{
/*
		// It's a crazy hack that checking if you're on the ground
		// would actually CHANGE your position and momentum,
		if (mo->z < mo->floorz)
		{
			mo->z = mo->floorz;
			mo->momz = 0;
		}
		else if (mo->z + mo->height > mo->ceilingz)
		{
			mo->z = mo->ceilingz - mo->height;
			mo->momz = 0;
		}
*/
		// but I don't want you to ever 'stand' while submerged in goo.
		// You're in constant vertical momentum, even if you get stuck on something.
		// No exceptions.
		return false;
	}

	if (mo->eflags & MFE_VERTICALFLIP)
	{
		if (mo->z+mo->height >= mo->ceilingz)
			return true;
	}
	else
	{
		if (mo->z <= mo->floorz)
			return true;
	}

	return false;
}

//
// P_IsObjectOnGroundIn
//
// Returns true if the player is
// on the ground in a specific sector. Takes reverse
// gravity and FOFs into account.
//
boolean P_IsObjectOnGroundIn(const mobj_t *mo, const sector_t *sec)
{
	ffloor_t *rover;

	// Is the object in reverse gravity?
	if (mo->eflags & MFE_VERTICALFLIP)
	{
		// Detect if the player is on the ceiling.
		if (mo->z+mo->height >= P_GetSpecialTopZ(mo, sec, sec))
			return true;
		// Otherwise, detect if the player is on the bottom of a FOF.
		else
		{
			for (rover = sec->ffloors; rover; rover = rover->next)
			{
				// If the FOF doesn't exist, continue.
				if (!(rover->fofflags & FOF_EXISTS))
					continue;

				// If the FOF is configured to let the object through, continue.
				if (!((rover->fofflags & FOF_BLOCKPLAYER && mo->player)
					|| (rover->fofflags & FOF_BLOCKOTHERS && !mo->player)))
					continue;

				// If the the platform is intangible from below, continue.
				if (rover->fofflags & FOF_PLATFORM)
					continue;

				// If the FOF is a water block, continue. (Unnecessary check?)
				if (rover->fofflags & FOF_SWIMMABLE)
					continue;

				// Actually check if the player is on the suitable FOF.
				if (mo->z+mo->height == P_GetSpecialBottomZ(mo, sectors + rover->secnum, sec))
					return true;
			}
		}
	}
	// Nope!
	else
	{
		// Detect if the player is on the floor.
		if (mo->z <= P_GetSpecialBottomZ(mo, sec, sec))
			return true;
		// Otherwise, detect if the player is on the top of a FOF.
		else
		{
			for (rover = sec->ffloors; rover; rover = rover->next)
			{
				// If the FOF doesn't exist, continue.
				if (!(rover->fofflags & FOF_EXISTS))
					continue;

				// If the FOF is configured to let the object through, continue.
				if (!((rover->fofflags & FOF_BLOCKPLAYER && mo->player)
					|| (rover->fofflags & FOF_BLOCKOTHERS && !mo->player)))
					continue;

				// If the the platform is intangible from above, continue.
				if (rover->fofflags & FOF_REVERSEPLATFORM)
					continue;

				// If the FOF is a water block, continue. (Unnecessary check?)
				if (rover->fofflags & FOF_SWIMMABLE)
					continue;

				// Actually check if the player is on the suitable FOF.
				if (mo->z == P_GetSpecialTopZ(mo, sectors + rover->secnum, sec))
					return true;
			}
		}
	}

	return false;
}

//
// P_IsObjectOnRealGround
//
// Helper function for T_EachTimeThinker
// Like P_IsObjectOnGroundIn, except ONLY THE REAL GROUND IS CONSIDERED, NOT FOFS
// I'll consider whether to make this a more globally accessible function or whatever in future
// -- Monster Iestyn
//
// Really simple, but personally I think it's also incredibly helpful. I think this is fine in p_user.c
// -- Sal

boolean P_IsObjectOnRealGround(const mobj_t *mo, const sector_t *sec)
{
	// Is the object in reverse gravity?
	if (mo->eflags & MFE_VERTICALFLIP)
	{
		// Detect if the player is on the ceiling.
		if (mo->z+mo->height >= P_GetSpecialTopZ(mo, sec, sec))
			return true;
	}
	// Nope!
	else
	{
		// Detect if the player is on the floor.
		if (mo->z <= P_GetSpecialBottomZ(mo, sec, sec))
			return true;
	}
	return false;
}

//
// P_SetObjectMomZ
//
// Sets the player momz appropriately.
// Takes reverse gravity into account.
//
void P_SetObjectMomZ(mobj_t *mo, fixed_t value, boolean relative)
{
	if (mo->eflags & MFE_VERTICALFLIP)
		value = -value;

	if (mo->scale != FRACUNIT)
		value = FixedMul(value, mo->scale);

	if (relative)
		mo->momz += value;
	else
		mo->momz = value;
}

//
// P_GetPlayerHeight
//
// Returns the height
// of the player.
//
fixed_t P_GetPlayerHeight(player_t *player)
{
	return FixedMul(player->mo->info->height, player->mo->scale);
}

//
// P_GetPlayerSpinHeight
//
// Returns the 'spin height'
// of the player.
//
fixed_t P_GetPlayerSpinHeight(player_t *player)
{
	return FixedMul(FixedMul(player->mo->info->height, player->mo->scale),2*FRACUNIT/3);
}

//
// P_IsMachineLocalPlayer
//
// Returns true if player is
// ACTUALLY on the local machine
//
boolean P_IsMachineLocalPlayer(player_t *player)
{
	UINT8 i;

	if (player == NULL)
	{
		return false;
	}

	for (i = 0; i <= r_splitscreen; i++)
	{
		if (player == &players[g_localplayers[i]])
			return true;
	}

	return false;
}

//
// P_IsLocalPlayer
//
// Returns true if player is
// on the local machine
// (or simulated party)
//
boolean P_IsLocalPlayer(const player_t *player)
{
	if (player == NULL)
	{
		return false;
	}

	// nobody is ever local when watching something back - you're a spectator there, even if your g_localplayers might say otherwise
	if (demo.playback)
		return false;

	// handles both online parties and local players (no need to call P_IsMachineLocalPlayer here)
	return G_IsPartyLocal(player-players);
}

//
// P_IsDisplayPlayer
//
// Returns true if player is
// currently being watched.
//
boolean P_IsDisplayPlayer(player_t *player)
{
	UINT8 i;

	if (player == NULL)
	{
		return false;
	}

	for (i = 0; i <= r_splitscreen; i++) // DON'T skip P1
	{
		if (camera[i].freecam)
		{
			// Freecam still techically has a player in
			// displayplayers. But since the camera is
			// detached, it would be weird if sounds were
			// heard from that player's perspective.
			continue;
		}

		if (player == &players[displayplayers[i]])
			return true;
	}

	return false;
}

//
// P_SpawnGhostMobjEX
//
// Spawns a ghost object on the player. Has an option for "legacy mode".
//
mobj_t *P_SpawnGhostMobjEX(mobj_t *mobj, boolean legacy)
{
	mobj_t *ghost = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_GHOST);

	P_SetTarget(&ghost->target, mobj);

	P_SetScale(ghost, mobj->scale);
	ghost->destscale = mobj->scale;

	if (mobj->eflags & MFE_VERTICALFLIP)
	{
		ghost->eflags |= MFE_VERTICALFLIP;
		ghost->z += mobj->height - ghost->height;
	}

	ghost->color = mobj->color;
	ghost->colorized = mobj->colorized; // Kart: they should also be colorized if their origin is

	ghost->angle = (mobj->player ? mobj->player->drawangle : mobj->angle);
	ghost->roll = mobj->roll;
	ghost->pitch = mobj->pitch;
	ghost->sprite = mobj->sprite;
	ghost->sprite2 = mobj->sprite2;
	ghost->frame = mobj->frame;
	ghost->tics = -1;
	ghost->renderflags = (mobj->renderflags & ~RF_TRANSMASK)|RF_TRANS50;
	ghost->fuse = ghost->info->damage;
	ghost->skin = mobj->skin;
	ghost->standingslope = mobj->standingslope;

	ghost->sprxoff = mobj->sprxoff;
	ghost->spryoff = mobj->spryoff;
	ghost->sprzoff = mobj->sprzoff;

	// baked offsets
	ghost->bakexoff = mobj->bakexoff;
	ghost->bakeyoff = mobj->bakeyoff;
	ghost->bakezoff = mobj->bakezoff;
	ghost->bakexpiv = mobj->bakexpiv;
	ghost->bakeypiv = mobj->bakeypiv;
	ghost->bakezpiv = mobj->bakezpiv;

	ghost->rollangle = mobj->rollangle;

	ghost->spritexscale = mobj->spritexscale;
	ghost->spriteyscale = mobj->spriteyscale;
	ghost->spritexoffset = mobj->spritexoffset;
	ghost->spriteyoffset = mobj->spriteyoffset;

	if (mobj->flags2 & MF2_OBJECTFLIP)
		ghost->flags2 |= MF2_OBJECTFLIP;

	if (!(mobj->flags & MF_DONTENCOREMAP))
		ghost->flags &= ~MF_DONTENCOREMAP;

	if (mobj->player)
	{
		if(mobj->player->followmobj)
		{
			mobj_t *ghost2 = P_SpawnGhostMobj(mobj->player->followmobj);
			P_SetTarget(&ghost2->tracer, ghost);
			P_SetTarget(&ghost->tracer, ghost2);
			ghost2->flags2 |= (mobj->player->followmobj->flags2 & MF2_LINKDRAW);
		}
	}

	// Copy interpolation data :)
	ghost->old_x = mobj->old_x2;
	ghost->old_y = mobj->old_y2;
	ghost->old_z = mobj->old_z2;
	ghost->old_angle = (mobj->player ? mobj->player->old_drawangle2 : mobj->old_angle2);
	ghost->old_pitch = mobj->old_pitch2;
	ghost->old_roll = mobj->old_roll2;
	ghost->old_sloperoll = mobj->old_sloperoll2;
	ghost->old_slopepitch = mobj->old_slopepitch2;
	ghost->old_scale = mobj->old_scale2;
	ghost->old_spritexscale = mobj->old_spritexscale2;
	ghost->old_spriteyscale = mobj->old_spriteyscale2;
	ghost->old_spritexoffset = mobj->old_spritexoffset2;
	ghost->old_spriteyoffset = mobj->old_spriteyoffset2;

	// Legacy mode
	if (legacy)
	{
		ghost->extravalue3 = 1;
	}

	return ghost;
}

//
// P_DoPlayerExit
//
// Player exits the map via sector trigger
void P_DoPlayerExit(player_t *player, pflags_t flags)
{
	if (player->exiting || mapreset)
		return;

	player->pflags |= flags;

	if (P_IsLocalPlayer(player) && (!player->spectator && !demo.playback))
		legitimateexit = true;

	if (player->griefstrikes > 0)
		player->griefstrikes--; // Remove a strike for finishing a race normally

	if (!player->spectator)
	{
		boolean losing;
		if ((gametypes[gametype]->rules & GTR_CIRCUIT)) // If in Race Mode, allow
		{
			player->exiting = raceexittime+2;
			K_UpdateAllPlayerPositions();
			losing = K_IsPlayerLosing(player); // HEY!!!! Set it AFTER K_UpdateAllPlayerPositions!!!!

			if (cv_kartvoices.value)
			{
				//const INT32 sfx_id = (losing ? sfx_klose : sfx_kwin);
				skin_t *playerskin = &skins[player->skin];

				const kartvoice_t *playervoice = (!P_MobjWasRemoved(player->mo) ? P_GetMobjVoice(player->mo) : &playerskin->voices[0]);

				if (playervoice)
				{
					S_StartSound(player->mo, (losing ? playervoice->lose : playervoice->win));
				}
			}

			// See Y_StartIntermission timer handling
			if (!K_CanChangeRules(false) || cv_inttime.value > 0)
				P_EndingMusic(player);

			if (P_CheckRacers())
			{
				player->exiting = raceexittime+1;
				G_BeginLevelExit();
			}
		}
		else if ((gametypes[gametype]->rules & GTR_BUMPERS)) // Battle Mode exiting
		{
			player->exiting = battleexittime+1;
			G_BeginLevelExit();
			P_EndingMusic(player);
		}
		else // Accidental death safeguard???
		{
			player->exiting = raceexittime+2;
			G_BeginLevelExit();
		}

		losing = K_IsPlayerLosing(player);

		if (grandprixinfo.gp == true)
		{
			if (player->bot)
			{
				// Bots are going to get harder... :)
				K_IncreaseBotDifficulty(player);
			}
			else if (!losing)
			{
				const UINT8 lifethreshold = 20;
				UINT8 extra = 0;

				// YOU WIN
				grandprixinfo.wonround = true;

				// Increase your total rings
				if (RINGTOTAL(player) > 0)
				{
					player->totalring += RINGTOTAL(player);

					extra = player->totalring / lifethreshold;

					if (extra > player->xtralife)
					{
						P_GivePlayerLives(player, extra - player->xtralife);
						S_StartSound(NULL, sfx_cdfm73);
						player->xtralife = extra;
					}
				}
			}
		}

		if (G_GametypeUsesLives() && losing)
		{
			// Remove a life from the losing player
			K_PlayerLoseLife(player);
		}
	}
	
	player->karthud[khud_cardanimation] = 0; // srb2kart: reset battle animation

	if (player == &players[consoleplayer])
		demo.savebutton = leveltime;

	ACS_RunPlayerFinishScript(player);
}

//
// P_DoAllPlayersExit
//
// All players exit the map via event
void P_DoAllPlayersExit(pflags_t flags, boolean trygivelife)
{
	UINT8 i;
	boolean givenlife = false;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || players[i].spectator)
		{
			continue;
		}
		if (players[i].exiting)
		{
			continue;
		}

		P_DoPlayerExit(&players[i], flags);

		if (trygivelife == false)
		{
			continue;
		}

		P_GivePlayerLives(&players[i], 1);
		givenlife = true;
	}

	if (givenlife)
	{
		// Life sound
		S_StartSound(NULL, sfx_cdfm73);
	}
}

//
// P_PlayerHitFloor
//
// Handles player hitting floor surface.
// Returns whether to clip momz.
boolean P_PlayerHitFloor(player_t *player, boolean fromAir)
{
	boolean clipmomz;

	I_Assert(player->mo != NULL);

	clipmomz = !(P_CheckDeathPitCollide(player->mo));

	if (player->airtime > 10 && player->prevonground == false && fromAir == true && clipmomz == true)
	{
		K_SpawnSplashForMobj(player->mo, abs(player->mo->momz));
	}
	
	if (fromAir == true && clipmomz == true)
	{
		// Cut momentum in half when you hit the ground and
		// aren't pressing any controls.
		if (!(player->cmd.forwardmove || player->cmd.sidemove) && !player->cmomx && !player->cmomy
			&& !K_IsPlayerDamaged(player))
		{
			player->mo->momx = player->mo->momx/2;
			player->mo->momy = player->mo->momy/2;
							
			if (player->cmd.buttons & BT_BRAKE && !(player->cmd.forwardmove)) // FURTHER slowdown if you're braking.
			{
				player->mo->momx = player->mo->momx/2;
				player->mo->momy = player->mo->momy/2;
			}
		}
	}

	return clipmomz;
}

boolean P_InQuicksand(const mobj_t *mo) // Returns true if you are in quicksand
{
	sector_t *sector = mo->subsector->sector;
	fixed_t topheight, bottomheight;

	fixed_t flipoffset = ((mo->eflags & MFE_VERTICALFLIP) ? (mo->height/2) : 0);

	if (sector->ffloors)
	{
		ffloor_t *rover;

		for (rover = sector->ffloors; rover; rover = rover->next)
		{
			if (!(rover->fofflags & FOF_EXISTS))
				continue;

			if (!(rover->fofflags & FOF_QUICKSAND))
				continue;

			topheight    = P_GetFFloorTopZAt   (rover, mo->x, mo->y);
			bottomheight = P_GetFFloorBottomZAt(rover, mo->x, mo->y);

			if (mo->z + flipoffset > topheight)
				continue;

			if (mo->z + (mo->height/2) + flipoffset < bottomheight)
				continue;

			return true;
		}
	}

	return false; // No sand here, Captain!
}

static boolean P_PlayerCanBust(player_t *player, ffloor_t *rover)
{
	// TODO: Make these act like the Lua SA2 boxes.
	// (void)player;
	(void)rover;
	(void)player;

	if (!(rover->fofflags & FOF_EXISTS))
		return false;

	if (!(rover->fofflags & FOF_BUSTUP))
		return false;

	return true;
}

static void P_CheckBustableBlocks(player_t *player)
{
	msecnode_t *node;
	fixed_t oldx;
	fixed_t oldy;

	if ((netgame || multiplayer) && player->spectator)
		return;

	oldx = player->mo->x;
	oldy = player->mo->y;

	for (node = player->mo->touching_sectorlist; node; node = node->m_sectorlist_next)
	{
		ffloor_t *rover;
		fixed_t topheight, bottomheight;

		if (!node->m_sector)
			break;

		if (!node->m_sector->ffloors)
			continue;

		for (rover = node->m_sector->ffloors; rover; rover = rover->next)
		{
			if (!P_PlayerCanBust(player, rover))
				continue;

			topheight = P_GetFOFTopZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
			bottomheight = P_GetFOFBottomZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);

			// Height checks
			if (rover->bustflags & FB_ONLYBOTTOM)
			{
				if (player->mo->z + player->mo->momz + player->mo->height < bottomheight)
					continue;

				if (player->mo->z + player->mo->height > bottomheight)
					continue;
			}
			else
			{
				switch (rover->busttype)
				{
				case BT_TOUCH:
					if (player->mo->z + player->mo->momz > topheight)
						continue;

					if (player->mo->z + player->mo->momz + player->mo->height < bottomheight)
						continue;

					break;
				case BT_SPINBUST:
					if (player->mo->z + player->mo->momz > topheight)
						continue;

					if (player->mo->z + player->mo->height < bottomheight)
						continue;

					break;
				default:
					if (player->mo->z >= topheight)
						continue;

					if (player->mo->z + player->mo->height < bottomheight)
						continue;

					break;
				}
			}

			// Impede the player's fall a bit
			if (((rover->busttype == BT_TOUCH) || (rover->busttype == BT_SPINBUST)) && player->mo->z >= topheight)
				player->mo->momz >>= 1;
			else if (rover->busttype == BT_TOUCH)
			{
				player->mo->momx >>= 1;
				player->mo->momy >>= 1;
			}

			//if (metalrecording)
			//	G_RecordBustup(rover);

			EV_CrumbleChain(node->m_sector, rover); // node->m_sector

			// Run a linedef executor??
			if (rover->bustflags & FB_EXECUTOR)
				P_LinedefExecute(rover->busttag, player->mo, node->m_sector);

			goto bustupdone;
		}
	}
bustupdone:
	P_UnsetThingPosition(player->mo);
	player->mo->x = oldx;
	player->mo->y = oldy;
	P_SetThingPosition(player->mo);
}

static void P_CheckBouncySectors(player_t *player)
{
	msecnode_t *node;
	fixed_t oldx;
	fixed_t oldy;
	fixed_t oldz;
	vector3_t momentum;

	oldx = player->mo->x;
	oldy = player->mo->y;
	oldz = player->mo->z;

	P_UnsetThingPosition(player->mo);
	player->mo->x += player->mo->momx;
	player->mo->y += player->mo->momy;
	player->mo->z += player->mo->momz;
	P_SetThingPosition(player->mo);

	for (node = player->mo->touching_sectorlist; node; node = node->m_sectorlist_next)
	{
		ffloor_t *rover;

		if (!node->m_sector)
			break;

		if (!node->m_sector->ffloors)
			continue;

		for (rover = node->m_sector->ffloors; rover; rover = rover->next)
		{
			fixed_t topheight, bottomheight;

			if (!(rover->fofflags & FOF_EXISTS))
				continue; // FOFs should not be bouncy if they don't even "exist"

			// Handle deprecated bouncy FOF sector type
			if (!udmf && GETSECSPECIAL(rover->master->frontsector->special, 1) == 15)
			{
				rover->fofflags |= FOF_BOUNCY;
				rover->bouncestrength = P_AproxDistance(rover->master->dx, rover->master->dy)/100;
			}

			if (!(rover->fofflags & FOF_BOUNCY))
				continue;

			topheight = P_GetFOFTopZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
			bottomheight = P_GetFOFBottomZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);

			if (player->mo->z > topheight)
				continue;

			if (player->mo->z + player->mo->height < bottomheight)
				continue;

			if (oldz < P_GetFOFTopZ(player->mo, node->m_sector, rover, oldx, oldy, NULL)
					&& oldz + player->mo->height > P_GetFOFBottomZ(player->mo, node->m_sector, rover, oldx, oldy, NULL))
			{
				player->mo->momx = -FixedMul(player->mo->momx,rover->bouncestrength);
				player->mo->momy = -FixedMul(player->mo->momy,rover->bouncestrength);

				// Kill heavy airdrop too so you don't get softlocked.
				K_KillAirDrop(player, PAF_AIRDROP_HEAVY);
			}
			else
			{
				pslope_t *slope = (abs(oldz - topheight) < abs(oldz + player->mo->height - bottomheight)) ? *rover->t_slope : *rover->b_slope;

				momentum.x = player->mo->momx;
				momentum.y = player->mo->momy;
				momentum.z = player->mo->momz*2;

				if (slope)
					P_ReverseQuantizeMomentumToSlope(&momentum, slope);

				momentum.z = -FixedMul(momentum.z,rover->bouncestrength)/2;

				if (abs(momentum.z) < (rover->bouncestrength*2))
					goto bouncydone;

				if ((mapnamespace == MNS_SRB2KART) && !(rover->master->flags & ML_NOTBOUNCY))
				{
					if (momentum.z > 0)
					{
						if (momentum.z < 8*FRACUNIT)
							momentum.z = 8*FRACUNIT;
					}
					else if (momentum.z > -8*FRACUNIT && momentum.z != 0)
						momentum.z = -8*FRACUNIT;
				}

				if (momentum.z > FixedMul(24*FRACUNIT, player->mo->scale)) //half of the default player height
					momentum.z = FixedMul(24*FRACUNIT, player->mo->scale);
				else if (momentum.z < -FixedMul(24*FRACUNIT, player->mo->scale))
					momentum.z = -FixedMul(24*FRACUNIT, player->mo->scale);

				if (slope)
					P_QuantizeMomentumToSlope(&momentum, slope);

				player->mo->momx = momentum.x;
				player->mo->momy = momentum.y;
				player->mo->momz = momentum.z;

				// Kill heavy airdrop too so you don't get softlocked.
				K_KillAirDrop(player, PAF_AIRDROP_HEAVY);

			}
			goto bouncydone;
		}
	}

bouncydone:
	P_UnsetThingPosition(player->mo);
	player->mo->x = oldx;
	player->mo->y = oldy;
	player->mo->z = oldz;
	P_SetThingPosition(player->mo);
}

static void P_CheckQuicksand(player_t *player)
{
	ffloor_t *rover;
	fixed_t sinkspeed, friction;
	fixed_t topheight, bottomheight;

	if (!(player->mo->subsector->sector->ffloors && player->mo->momz <= 0))
		return;

	for (rover = player->mo->subsector->sector->ffloors; rover; rover = rover->next)
	{
		if (!(rover->fofflags & FOF_EXISTS)) continue;

		if (!(rover->fofflags & FOF_QUICKSAND))
			continue;

		topheight    = P_GetFFloorTopZAt   (rover, player->mo->x, player->mo->y);
		bottomheight = P_GetFFloorBottomZAt(rover, player->mo->x, player->mo->y);

		if (topheight >= player->mo->z && bottomheight < player->mo->z + player->mo->height)
		{
			sinkspeed = abs(rover->master->v1->x - rover->master->v2->x)>>1;

			sinkspeed = FixedDiv(sinkspeed,TICRATE*FRACUNIT);

			if (player->mo->eflags & MFE_VERTICALFLIP)
			{
				fixed_t ceilingheight = P_GetCeilingZ(player->mo, player->mo->subsector->sector, player->mo->x, player->mo->y, NULL);

				player->mo->z += sinkspeed;

				if (player->mo->z + player->mo->height >= ceilingheight)
					player->mo->z = ceilingheight - player->mo->height;

				if (player->mo->momz <= 0)
					P_PlayerHitFloor(player, false);
			}
			else
			{
				fixed_t floorheight = P_GetFloorZ(player->mo, player->mo->subsector->sector, player->mo->x, player->mo->y, NULL);

				player->mo->z -= sinkspeed;

				if (player->mo->z <= floorheight)
					player->mo->z = floorheight;

				if (player->mo->momz >= 0)
					P_PlayerHitFloor(player, false);
			}

			friction = abs(rover->master->v1->y - rover->master->v2->y)>>6;

			player->mo->momx = FixedMul(player->mo->momx, friction);
			player->mo->momy = FixedMul(player->mo->momy, friction);
		}
	}
}

//
// P_CheckInvincibilityTimer
//
// Restores music from invincibility, and handles invincibility sparkles
//
static void P_CheckInvincibilityTimer(player_t *player)
{
	if (!player->powers[pw_invulnerability] && !player->invincibilitytimer && !player->smonitortimer)
		return;

	// Resume normal music stuff.
	if (player->invincibilitytimer == 1
		|| player->powers[pw_invulnerability] == 1
		|| (player->smonitorwarning == 1)
		|| player->smonitortimer == 1)
	{
		player->smonitorwarning = 2; // Don't attempt to restore music again.

		//K_KartResetPlayerColor(player); -- this gets called every tic anyways
		G_GhostAddColor((INT32) (player - players), GHC_NORMAL);

		P_RestoreMusic(player);
		return;
	}
}

//
// P_DoBubbleBreath
//
// Handles bubbles spawned by the player
//
static void P_DoBubbleBreath(player_t *player)
{
	fixed_t x = player->mo->x;
	fixed_t y = player->mo->y;
	fixed_t z = player->mo->z;
	mobj_t *bubble = NULL;

	if (!(player->mo->eflags & MFE_UNDERWATER) || player->spectator || (K_GetShieldFromPlayer(player) == KSHIELD_BUBBLE))
		return;

	if (player->charflags & SF_MACHINE)
	{
		if (P_RandomChance(FRACUNIT/5))
		{
			fixed_t r = player->mo->radius>>FRACBITS;
			x += (P_RandomRange(r, -r)<<FRACBITS);
			y += (P_RandomRange(r, -r)<<FRACBITS);
			z += (P_RandomKey(player->mo->height>>FRACBITS)<<FRACBITS);
			bubble = P_SpawnMobj(x, y, z, MT_WATERZAP);
			S_StartSound(bubble, sfx_beelec);
		}
	}
	else
	{
		if (player->mo->eflags & MFE_VERTICALFLIP)
			z += player->mo->height - FixedDiv(player->mo->height,5*(FRACUNIT/4));
		else
			z += FixedDiv(player->mo->height,5*(FRACUNIT/4));

		if (P_RandomChance(FRACUNIT/16))
			bubble = P_SpawnMobj(x, y, z, MT_SMALLBUBBLE);
		else if (P_RandomChance(3*FRACUNIT/256))
			bubble = P_SpawnMobj(x, y, z, MT_MEDIUMBUBBLE);
	}

	if (bubble)
	{
		bubble->threshold = 42;
		bubble->destscale = player->mo->scale;
		P_SetScale(bubble, bubble->destscale);
	}
}

//#define OLD_MOVEMENT_CODE 1
static void P_3dMovement(player_t *player)
{
	ticcmd_t *cmd;
	angle_t movepushangle, movepushsideangle; // Analog
	fixed_t movepushforward = 0, movepushside = 0;
	angle_t dangle; // replaces old quadrants bits
	fixed_t oldMagnitude, newMagnitude;
	vector3_t totalthrust;
	UINT16 buttons = K_GetKartButtons(player);

	totalthrust.x = totalthrust.y = 0; // I forget if this is needed
	totalthrust.z = FRACUNIT*P_MobjFlip(player->mo)/3; // A bit of extra push-back on slopes

	if (K_SlopeResistance(player) == true)
	{
		totalthrust.z = -(totalthrust.z);
	}
	
	cmd = &player->cmd;
	
	cmd->forwardmove = K_GetForwardMove(player);
	cmd->sidemove = K_GetSideMove(player);

	// Get the old momentum; this will be needed at the end of the function! -SH
	oldMagnitude = R_PointToDist2(player->mo->momx - player->cmomx, player->mo->momy - player->cmomy, 0, 0);

	if (player->drift != 0 && player->driftlock == 0)
	{
		movepushangle = player->mo->angle - (ANGLE_45/5) * player->drift;
	}
	else if (K_IsPlayerDamaged(player) || player->wipeoutslow) // if spun out, use the boost angle
	{
		movepushangle = (angle_t)player->boostangle;
	}
	else
	{
		movepushangle = player->mo->angle;
	}

	movepushsideangle = movepushangle-ANGLE_90;

	// cmomx/cmomy stands for the conveyor belt speed.
	if (player->onconveyor == 2) // Wind/Current
	{
		//if (player->mo->z > player->mo->watertop || player->mo->z + player->mo->height < player->mo->waterbottom)
		if (!(player->mo->eflags & (MFE_UNDERWATER|MFE_TOUCHWATER)))
			player->cmomx = player->cmomy = 0;
	}
	else if (player->onconveyor == 4 && !P_IsObjectOnGround(player->mo)) // Actual conveyor belt
		player->cmomx = player->cmomy = 0;
	else if (player->onconveyor != 2 && player->onconveyor != 4 && player->onconveyor != 1)
		player->cmomx = player->cmomy = 0;

	player->rmomx = player->mo->momx - player->cmomx;
	player->rmomy = player->mo->momy - player->cmomy;

	// Calculates player's speed based on distance-of-a-line formula
	player->speed = R_PointToDist2(0, 0, player->rmomx, player->rmomy);

	// Monster Iestyn - 04-11-13
	// Quadrants are stupid, excessive and broken, let's do this a much simpler way!
	// Get delta angle from rmom angle and player angle first
	dangle = R_PointToAngle2(0,0, player->rmomx, player->rmomy) - player->mo->angle;
	if (dangle > ANGLE_180) //flip to keep to one side
	{
		dangle = InvAngle(dangle);
	}

	// When sliding, don't allow forward/back
	if (player->carry == CR_SLIDING)
		cmd->forwardmove = 0;

	// Do not let the player control movement if not onground.
	// SRB2Kart: pogo spring and speed bumps are supposed to control like you're on the ground
	onground = (P_IsObjectOnGround(player->mo) || (player->pogospring));

	player->aiming = cmd->aiming<<FRACBITS;

	// Forward movement
	if (!((player->exiting || mapreset) || (P_PlayerInPain(player) && !onground)))
	{
		movepushforward = K_3dKartMovement(player, onground, cmd->forwardmove);

		// allow very small movement while in air for gameplay
		if (!onground)
			movepushforward >>= 2; // proper air movement

		if (player->mo->movefactor != FRACUNIT) // Friction-scaled acceleration...
			movepushforward = FixedMul(movepushforward, player->mo->movefactor);

		if (buttons & BT_BRAKE && (player->cmd.forwardmove == 0)) // SRB2kart - braking isn't instant
			movepushforward /= 64;

		if (player->cmd.forwardmove > 0)
			player->brakestop = 0;
		else if (player->brakestop < 6) // Don't start reversing with brakes until you've made a stop first
		{
			if (player->speed < 8*FRACUNIT)
				player->brakestop++;
			movepushforward = 0;
		}

		totalthrust.x += P_ReturnThrustX(player->mo, movepushangle, movepushforward);
		totalthrust.y += P_ReturnThrustY(player->mo, movepushangle, movepushforward);
	}
	else if (!K_IsPlayerDamaged(player))
	{
		K_MomentumToFacing(player);
	}
	
	// Sideways movement
	if (cmd->sidemove != 0 && !((player->exiting || mapreset) || K_IsPlayerDamaged(player)))
	{
		if (cmd->sidemove > 0)
			movepushside = (cmd->sidemove * FRACUNIT/128) + FixedDiv(player->speed, K_GetKartSpeed(player, true, true));
		else
			movepushside = (cmd->sidemove * FRACUNIT/128) - FixedDiv(player->speed, K_GetKartSpeed(player, true, true));

		totalthrust.x += P_ReturnThrustX(player->mo, movepushsideangle, movepushside);
		totalthrust.y += P_ReturnThrustY(player->mo, movepushsideangle, movepushside);
	}

	if ((totalthrust.x || totalthrust.y)
		&& player->mo->standingslope != NULL
		&& (!(player->mo->standingslope->flags & SL_NOPHYSICS))
		&& abs(player->mo->standingslope->zdelta) > FRACUNIT/2)
	{
		// Factor thrust to slope, but only for the part pushing up it!
		// The rest is unaffected.
		angle_t thrustangle = R_PointToAngle2(0, 0, totalthrust.x, totalthrust.y) - player->mo->standingslope->xydirection;

		if (player->mo->standingslope->zdelta < 0)
		{
			// Direction goes down, so thrustangle needs to face toward
			if (thrustangle < ANGLE_90 || thrustangle > ANGLE_270)
			{
				P_QuantizeMomentumToSlope(&totalthrust, player->mo->standingslope);
			}
		}
		else
		{
			// Direction goes up, so thrustangle needs to face away
			if (thrustangle > ANGLE_90 && thrustangle < ANGLE_270)
			{
				P_QuantizeMomentumToSlope(&totalthrust, player->mo->standingslope);
			}
		}
	}

	player->mo->momx += totalthrust.x;
	player->mo->momy += totalthrust.y;

	// Time to ask three questions:
	// 1) Are we over topspeed?
	// 2) If "yes" to 1, were we moving over topspeed to begin with?
	// 3) If "yes" to 2, are we now going faster?

	// Time to ask three questions:
	// 1) Are we over topspeed?
	// 2) If "yes" to 1, were we moving over topspeed to begin with?
	// 3) If "yes" to 2, are we now going faster?

	// If "yes" to 3, normalize to our initial momentum; this will allow thoks to stay as fast as they normally are.
	// If "no" to 3, ignore it; the player might be going too fast, but they're slowing down, so let them.
	// If "no" to 2, normalize to topspeed, so we can't suddenly run faster than it of our own accord.
	// If "no" to 1, we're not reaching any limits yet, so ignore this entirely!
	// -Shadow Hog
	newMagnitude = R_PointToDist2(player->mo->momx - player->cmomx, player->mo->momy - player->cmomy, 0, 0);
	if (player->offroad > 0 || (player->loop.radius == 0 && player->outruntime == 0))
	{
		if (newMagnitude > K_GetKartSpeed(player, true, true)) //topspeed)
		{
			fixed_t tempmomx, tempmomy;
			if (oldMagnitude > K_GetKartSpeed(player, true, true) && onground) // SRB2Kart: onground check for air speed cap
			{
				if (newMagnitude > oldMagnitude)
				{
					tempmomx = FixedMul(FixedDiv(player->mo->momx - player->cmomx, newMagnitude), oldMagnitude);
					tempmomy = FixedMul(FixedDiv(player->mo->momy - player->cmomy, newMagnitude), oldMagnitude);
					player->mo->momx = tempmomx + player->cmomx;
					player->mo->momy = tempmomy + player->cmomy;
				}
				// else do nothing
			}
			else if (player->airdriftspeed == 0)
			{
				tempmomx = FixedMul(FixedDiv(player->mo->momx - player->cmomx, newMagnitude), K_GetKartSpeed(player, true, true)); //topspeed)
				tempmomy = FixedMul(FixedDiv(player->mo->momy - player->cmomy, newMagnitude), K_GetKartSpeed(player, true, true)); //topspeed)
				player->mo->momx = tempmomx + player->cmomx;
				player->mo->momy = tempmomy + player->cmomy;
			}
		}
	}
}

boolean P_CanPlayerTurn(player_t *player, ticcmd_t *cmd)
{
	if (player->spectator || objectplacing)
	{
		// Spectators come first.
		return true;
	}

	if (((gametypes[gametype]->rules & GTR_NOCOUNTDOWN) || (leveltime > starttime))
		&& (cmd->buttons & BT_ACCELERATE)
		&& (cmd->buttons & BT_BRAKE))
	{
		// You are rubberburn turning.
		return true;
	}

	if (player->respawn)
	{
		// You are Respawning.
		return true;
	}

	if (player->dashRingPullTics || player->dashRingPushTics)
	{
		// Allow for turning adjustments while using dash rings.
		return true;
	}

	if (player->mo && player->speed == 0)
	{
		// You need to be moving.
		return false;
	}

	return true;
}

static void P_UpdatePlayerAiming(player_t *player)
{
	ticcmd_t *cmd = &player->cmd;
	int i;

	if (!cv_allowmlook.value || player->spectator == false)
	{
		player->aiming = 0;
	}
	else
	{
		player->aiming += (cmd->aiming << TICCMD_REDUCE);
		player->aiming = G_ClipAimingPitch((INT32*) &player->aiming);
	}

	for (i = 0; i <= r_splitscreen; i++)
	{
		if (player == &players[displayplayers[i]])
		{
			localaiming[i] = player->aiming;
			break;
		}
	}
}

//
// P_UpdatePlayerAngle
//
// Updates player angleturn with cmd->turning
//
static void P_UpdatePlayerAngle(player_t *player)
{
	INT16 angle_diff, max_left_turn, max_right_turn;
	boolean add_delta = true;
	ticcmd_t *cmd = &player->cmd;
	angle_t anglechange = player->angleturn;

	// Kart: store the current turn range for later use
	if (P_CanPlayerTurn(player, cmd))
	{
		player->lturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, KART_FULLTURN)+1;
		player->rturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, -KART_FULLTURN)-1;
	} else {
		player->lturn_max[leveltime%MAXPREDICTTICS] = player->rturn_max[leveltime%MAXPREDICTTICS] = 0;
	}
	
	if ((gametypes[gametype]->rules & GTR_NOCOUNTDOWN) || leveltime >= starttime)
	{
		// KART: Don't directly apply angleturn! It may have been either A) forged by a malicious client, or B) not be a smooth turn due to a player dropping frames.
		// Instead, turn the player only up to the amount they're supposed to turn accounting for latency. Allow exactly 1 extra turn unit to try to keep old replays synced.
		angle_diff = cmd->angle - (player->mo->angle>>16);
		max_left_turn = player->lturn_max[(leveltime + MAXPREDICTTICS - cmd->latency) % MAXPREDICTTICS];
		max_right_turn = player->rturn_max[(leveltime + MAXPREDICTTICS - cmd->latency) % MAXPREDICTTICS];

		//CONS_Printf("----------------\nangle diff: %d - turning options: %d to %d - ", angle_diff, max_left_turn, max_right_turn);

		if (angle_diff > max_left_turn)
			angle_diff = max_left_turn;
		else if (angle_diff < max_right_turn)
			angle_diff = max_right_turn;
		else
		{
			// Try to keep normal turning as accurate to 1.0.1 as possible to reduce replay desyncs.
			anglechange = cmd->angle<<TICCMD_REDUCE;
			add_delta = false;
		}
		//CONS_Printf("applied turn: %d\n", angle_diff);

		if (add_delta) {
			anglechange += angle_diff<<TICCMD_REDUCE;
			anglechange &= ~0xFFFF; // Try to keep the turning somewhat similar to how it was before?
			/*CONS_Printf("leftover turn (%s): %5d or %4d%%\n",
							player_names[player-players],
							(INT16) (cmd->angle - (player->mo->angle>>TICCMD_REDUCE)),
							(INT16) (cmd->angle - (player->mo->angle>>TICCMD_REDUCE)) * 100 / (angle_diff ? angle_diff : 1));*/
		}
	}


	// CONS_Printf("Playerid:%d\n",p);
	player->angleturn = anglechange;
	player->mo->angle = player->angleturn;

	P_UpdatePlayerAiming(player);
}

#define MINAURAPCT (cv_kartaltshrink_arrowbulletthres.value - (FRACUNIT / 4))
#define AURA_DIFF (max(1, (FRACUNIT << 1) - MINAURAPCT))
fixed_t P_KartAltShrinkAuraScalar(player_t *player)
{
	if ((!player) || (!player->mo) || (P_MobjWasRemoved(player->mo)))
		return 0;

	return FixedDiv(min(max(0, K_GetSpeedPercentage(player) - MINAURAPCT), (FRACUNIT << 1)), AURA_DIFF);
}

//
// P_MovePlayer
void P_MovePlayer(player_t *player)
{
	ticcmd_t *cmd;
	//INT32 i;

	fixed_t runspd;

	cmd = &player->cmd;
	runspd = 14*player->mo->scale; //srb2kart

	// Let's have some movement speed fun on low-friction surfaces, JUST for players...
	// (high friction surfaces shouldn't have any adjustment, since the acceleration in
	// this game is super high and that ends up cheesing high-friction surfaces.)
	runspd = FixedMul(runspd, player->mo->movefactor);

	// Control relinquishing stuff!
	if (player->nocontrol)
		player->pflags |= PF_STASIS;

	// note: don't unset stasis here

	if (player->spectator)
	{
		player->mo->eflags &= ~MFE_VERTICALFLIP; // deflip...
		return;
	}

	//////////////////////
	// MOVEMENT CODE	//
	//////////////////////

	P_UpdatePlayerAngle(player);

	ticruned++;
	if (!(cmd->flags & TICCMD_RECEIVED))
		ticmiss++;

	P_3dMovement(player);

	if (player->squishedtimer > 0)
	{
		P_SetPlayerMobjState(player->mo, S_KART_SPINOUT);
		player->mo->spriteyscale = (FRACUNIT / 4);
		if (player->squishedtimer == 1)
			player->mo->spriteyscale = FRACUNIT;
	}
	else if (player->carry == CR_SLIDING)
	{
		P_SetPlayerMobjState(player->mo, S_KART_SPINOUT);
		player->drawangle -= ANGLE_22h;
		player->mo->rollangle = 0;
		player->glanceDir = 0;
		player->pflags &= ~PF_GAINAX;
	}
	else if (player->flipovertimer > 0)
	{
		player->mo->rollangle = (angle_t)(player->flipovertimer - 1) * FLIPOVERANG;
	}
	else if ((player->spinouttimer > 0))
	{
		UINT16 speed = player->spinouttimer/8;
		if (speed > 8)
			speed = 8;
		else if (speed < 1)
			speed = 1;

		P_SetPlayerMobjState(player->mo, S_KART_SPINOUT);

		if (speed == 1 && abs((signed)(player->mo->angle - player->drawangle)) < ANGLE_22h)
			player->drawangle = player->mo->angle; // Face forward at the end of the animation
		else
			player->drawangle -= (ANGLE_11hh * speed);

		player->mo->rollangle = 0;
	}
	else if (player->nocontrol && player->pflags & PF_FAILEDSTART)
	{
		P_SetPlayerMobjState(player->mo, S_KART_SPINOUT);

		if (((player->nocontrol + 5) % 20) < 10)
			player->drawangle += ANGLE_11hh;
		else
			player->drawangle -= ANGLE_11hh;
	}
	else
	{
		K_KartMoveAnimation(player);

		if (player->pogospring)
			player->drawangle += ANGLE_22h;
		else
			player->drawangle = player->mo->angle + K_GetDriftAngleOffset(player);

		player->mo->rollangle = 0;
	}
	player->mo->movefactor = FRACUNIT; // We're not going to do any more with this, so let's change it back for the next frame.

	//{ SRB2kart

	// Drifting sound
	// Start looping the sound now.
	if (leveltime % 50 == 0 && onground && (player->drift != 0 || (player->pflags & PF_RECOVERYSPIN)))
		S_StartSound(player->mo, sfx_drift);
	// Leveltime being 50 might take a while at times. We'll start it up once, isntantly.
	else if (!S_SoundPlaying(player->mo, sfx_drift) && onground && (player->drift != 0 || (player->pflags & PF_RECOVERYSPIN)))
		S_StartSound(player->mo, sfx_drift);
	// Ok, we'll stop now.
	else if ((player->drift == 0 && !(player->pflags & PF_RECOVERYSPIN)) || (!onground))
		S_StopSoundByID(player->mo, sfx_drift);

	K_MoveKartPlayer(player, onground);
	//}

//////////////////
//GAMEPLAY STUFF//
//////////////////

	////////////////////////////
	//SPINNING AND SPINDASHING//
	////////////////////////////

	// SRB2kart - Drifting smoke and fire
	if ((player->sneakertimer || player->flamestore)
		&& onground && (leveltime & 1))
		K_SpawnBoostTrail(player);

	if (((player->invincibilitytimer) ||
	     ((player->smonitortimer > 0) &&
	      (((leveltime %
		 max(1, 10 - FixedMul(9, K_SMonitorGradient(player->smonitortimer)))) == 0)))))
	{
		K_SpawnSparkleTrail(player->mo);
	}

	// The Arrow Bullet aura should spawn before you actually enter the Arrow Bullet state.
	if (cv_kartaltshrink_arrowbullet.value && K_IsAltShrunk(player) && (K_GetSpeedPercentage(player) >= MINAURAPCT))
	{
		fixed_t aura_scalar = P_KartAltShrinkAuraScalar(player);

		INT32 auravis = 10 - FixedMul(7, Easing_OutCubic(aura_scalar, 0, FRACUNIT));
		auravis = min(9, max(1, auravis)) << FF_TRANSSHIFT;

		fixed_t aura_scale = Easing_OutCubic(aura_scalar, FRACHALF, FRACUNIT + FRACHALF);

		if ((!player->arrowbullet) || (P_MobjWasRemoved(player->arrowbullet)))
		{
			mobj_t *newbullet = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_OVERLAY);

			P_SetMobjState(newbullet, S_ALTSHRINK_ARROWBULLET);

            newbullet->movefactor = aura_scale;
			newbullet->threshold |= OV_DONTCOPYANGLE;

			P_SetTarget(&newbullet->target, player->mo);

			P_SetTarget(&player->arrowbullet, newbullet);

			player->arrowbullet->angle = K_MomentumAngle(player->mo);
			player->arrowbullet->sloperoll = player->mo->sloperoll;
			player->arrowbullet->slopepitch = player->mo->slopepitch;

			player->arrowbullet->frame &= ~FF_TRANSMASK;
			player->arrowbullet->frame |= auravis;

			player->arrowbullet->dispoffset = min(2, player->mo->dispoffset + 1);
		}
		else
		{
			player->arrowbullet->angle = K_MomentumAngle(player->mo);
			player->arrowbullet->sloperoll = player->mo->sloperoll;
			player->arrowbullet->slopepitch = player->mo->slopepitch;

			player->arrowbullet->movefactor = aura_scale;

			player->arrowbullet->frame &= ~FF_TRANSMASK;
			player->arrowbullet->frame |= auravis;

			player->arrowbullet->dispoffset = min(2, player->mo->dispoffset + 1);
		}
	}
	else
	{
		if ((player->arrowbullet) && (!P_MobjWasRemoved(player->arrowbullet)))
		{
			P_SetTarget(&player->arrowbullet->target, NULL);
			P_RemoveMobj(player->arrowbullet);
			P_SetTarget(&player->arrowbullet, NULL);
		}
	}

	if (K_AltShrinkArrowBulletCondition(player))
	{
		K_SpawnNormalSpeedLines(player);
	}
#undef MINAURAPCT
#undef AURA_DIFF

	if (player->wipeoutslow > 1 && (leveltime & 1))
		K_SpawnWipeoutTrail(player->mo, false);

	K_DriftDustHandling(player->mo);

	// Crush test...
	if ((player->mo->ceilingz - player->mo->floorz < player->mo->height) && !(player->mo->flags & MF_NOCLIP))
	{
		if (player->spectator)
			P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SPECTATOR); // Respawn crushed spectators
		else
			P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SQUISH); // SRB2kart - we don't kill when squished, we squish when squished.

		if (player->playerstate == PST_DEAD)
			return;
	}

#ifdef FLOORSPLATS
	if (cv_shadow.value && rendermode == render_soft)
		R_AddFloorSplat(player->mo->subsector, player->mo, "SHADOW", player->mo->x,
			player->mo->y, player->mo->floorz, SPLATDRAWMODE_OPAQUE);
#endif

	// Look for blocks to bust up
	// Because of FF_SHATTER, we should look for blocks constantly,
	// not just when spinning or playing as Knuckles
	if (CheckForBustableBlocks)
		P_CheckBustableBlocks(player);

	// Check for a BOUNCY sector!
	if (CheckForBouncySector)
		P_CheckBouncySectors(player);

	// Look for Quicksand!
	if (CheckForQuicksand)
		P_CheckQuicksand(player);
}

static void P_DoZoomTube(player_t *player)
{
	fixed_t speed;
	mobj_t *waypoint = NULL;
	fixed_t dist;
	boolean reverse;

	if (player->speed > 0)
		reverse = false;
	else
		reverse = true;

	player->flashing = 1;

	speed = abs(player->speed);

	// change slope
	dist = P_AproxDistance(P_AproxDistance(player->mo->tracer->x - player->mo->x, player->mo->tracer->y - player->mo->y), player->mo->tracer->z - player->mo->z);

	if (dist < 1)
		dist = 1;

	player->mo->momx = FixedMul(FixedDiv(player->mo->tracer->x - player->mo->x, dist), (speed));
	player->mo->momy = FixedMul(FixedDiv(player->mo->tracer->y - player->mo->y, dist), (speed));
	player->mo->momz = FixedMul(FixedDiv(player->mo->tracer->z - player->mo->z, dist), (speed));

	// Calculate the distance between the player and the waypoint
	// 'dist' already equals this.

	// Will the player go past the waypoint?
	if (speed > dist)
	{
		speed -= dist;
		// If further away, set XYZ of player to waypoint location
		P_UnsetThingPosition(player->mo);
		player->mo->x = player->mo->tracer->x;
		player->mo->y = player->mo->tracer->y;
		player->mo->z = player->mo->tracer->z;
		P_SetThingPosition(player->mo);

		// ugh, duh!!
		player->mo->floorz = player->mo->subsector->sector->floorheight;
		player->mo->ceilingz = player->mo->subsector->sector->ceilingheight;

		CONS_Debug(DBG_GAMELOGIC, "Looking for next waypoint...\n");

		// Find next waypoint
		waypoint = reverse ? P_GetPreviousTubeWaypoint(player->mo->tracer, false) : P_GetNextTubeWaypoint(player->mo->tracer, false);

		if (waypoint)
		{
			CONS_Debug(DBG_GAMELOGIC, "Found waypoint (sequence %d, number %d).\n", waypoint->threshold, waypoint->health);

			P_SetTarget(&player->mo->tracer, waypoint);

			// calculate MOMX/MOMY/MOMZ for next waypoint

			// change slope
			dist = P_AproxDistance(P_AproxDistance(player->mo->tracer->x - player->mo->x, player->mo->tracer->y - player->mo->y), player->mo->tracer->z - player->mo->z);

			if (dist < 1)
				dist = 1;

			player->mo->momx = FixedMul(FixedDiv(player->mo->tracer->x - player->mo->x, dist), (speed));
			player->mo->momy = FixedMul(FixedDiv(player->mo->tracer->y - player->mo->y, dist), (speed));
			player->mo->momz = FixedMul(FixedDiv(player->mo->tracer->z - player->mo->z, dist), (speed));
		}
		else
		{
			P_SetTarget(&player->mo->tracer, NULL); // Else, we just let them fly.
			player->carry = CR_NONE;

			CONS_Debug(DBG_GAMELOGIC, "Next waypoint not found, releasing from track...\n");
		}
	}

	// change angle
	if (player->mo->tracer)
	{
		player->mo->angle = R_PointToAngle2(player->mo->x, player->mo->y, player->mo->tracer->x, player->mo->tracer->y);
		P_SetPlayerAngle(player, player->mo->angle);
	}
}

#if 0
//
// P_NukeAllPlayers
//
// Hurts all players
// source = guy who gets the credit
//
static void P_NukeAllPlayers(player_t *player)
{
	mobj_t *mo;
	UINT8 i;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i])
			continue;
		if (players[i].spectator)
			continue;
		if (!players[i].mo)
			continue;
		if (players[i].mo == player->mo)
			continue;
		if (players[i].mo->health <= 0)
			continue;

		P_DamageMobj(players[i].mo, player->mo, player->mo, 1, DMG_NORMAL);
	}

	CONS_Printf(M_GetText("%s caused a world of pain.\n"), player_names[player-players]);

	return;
}
#endif

//
// P_NukeEnemies
// Looks for something you can hit - Used for bomb shield
//
void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius)
{
	mobj_t *mo;
	thinker_t *think;

	radius = FixedMul(radius, mapobjectscale);

	for (think = thlist[THINK_MOBJ].next; think != &thlist[THINK_MOBJ]; think = think->next)
	{
		if (think->function == (actionf_p1)P_RemoveThinkerDelayed)
			continue;

		mo = (mobj_t *)think;

		if (!(mo->flags & MF_SHOOTABLE) && (mo->type != MT_SPB)) // Don't want to give SPB MF_SHOOTABLE, to ensure it's undamagable through other means
			continue;

		if (mo->flags & MF_MONITOR)
			continue; // Monitors cannot be 'nuked'.

		if (abs(inflictor->x - mo->x) > radius || abs(inflictor->y - mo->y) > radius || abs(inflictor->z - mo->z) > radius)
			continue; // Workaround for possible integer overflow in the below -Red

		if (P_AproxDistance(P_AproxDistance(inflictor->x - mo->x, inflictor->y - mo->y), inflictor->z - mo->z) > radius)
			continue;

		if (mo->type == MT_SPB) // If you destroy a SPB, you don't get the luxury of a cooldown.
		{
			spbplace = -1;
			K_SetIndirectItemCooldown(0);
		}

		if (mo->flags & MF_BOSS) //don't OHKO bosses nor players!
			P_DamageMobj(mo, inflictor, source, 1, DMG_NORMAL|DMG_CANTHURTSELF);
		else if (mo->type == MT_PLAYER)	// Thunder shield: Combo players.
			P_DamageMobj(mo, inflictor, source, 1, DMG_NORMAL|DMG_CANTHURTSELF);
		else
			P_DamageMobj(mo, inflictor, source, 1000, DMG_NORMAL|DMG_CANTHURTSELF);
	}
}

//
// P_ConsiderAllGone
// Shamelessly lifted from TD. Thanks, Sryder!
//

// SRB2Kart: Use for GP?
/*
static void P_ConsiderAllGone(void)
{
	INT32 i, lastdeadplayer = -1, deadtimercheck = INT32_MAX;

	if (countdown2)
		return;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i])
			continue;

		if (players[i].playerstate != PST_DEAD && !players[i].spectator && players[i].mo && players[i].mo->health)
			break;

		if (players[i].spectator)
		{
			if (lastdeadplayer == -1)
				lastdeadplayer = i;
		}
		else if (players[i].lives > 0)
		{
			lastdeadplayer = i;
			if (players[i].deadtimer < deadtimercheck)
				deadtimercheck = players[i].deadtimer;
		}
	}

	if (i == MAXPLAYERS && lastdeadplayer != -1 && deadtimercheck > 2*TICRATE) // the last killed player will reset the level in G_DoReborn
	{
		//players[lastdeadplayer].spectator = true;
		players[lastdeadplayer].outofcoop = true;
		players[lastdeadplayer].playerstate = PST_REBORN;
	}
}
*/

//
// P_DeathThink
// Fall on your face when dying.
// Decrease POV height to floor height.
//
static void P_DeathThink(player_t *player)
{
	boolean playerGone = false;

	player->deltaviewheight = 0;

	if (player->deadtimer < INT32_MAX)
		player->deadtimer++;

	if ((player->pflags & PF_NOCONTEST))
	{
		player->karthud[khud_timeovercam]++;

		if (player->mo)
		{
			player->mo->flags |= (MF_NOGRAVITY|MF_NOCLIP);
			if (skins[player->skin].flags & SF_OLDDEATH)
				player->mo->renderflags |= RF_DONTDRAW;
		}
	}
	else
		player->karthud[khud_timeovercam] = 0;

	K_KartPlayerHUDUpdate(player);

	if (player->pflags & PF_NOCONTEST)
	{
		playerGone = true;
	}
	else if (player->bot == false)
	{
		if (G_GametypeUsesLives() == true && player->lives == 0)
		{
			playerGone = true;
		}
	}
	else if (player->bot && K_BotDefaultSpectator())
	{
		playerGone = true;
	}

	if (playerGone == false)
	{
		// SRB2kart - spawn automatically after 1 second
		if (player->deadtimer > ((netgame || multiplayer)
			? cv_respawntime.value*TICRATE
			: TICRATE)) // don't let them change it in record attack
				player->playerstate = PST_REBORN;
	}

	// Keep time rolling
	if (!(player->exiting || mapreset) && !(player->pflags & PF_NOCONTEST) && !stoppedclock)
	{
		if (leveltime >= starttime)
		{
			player->realtime = leveltime - starttime;
			if (player->spectator || !circuitmap)
				player->laptime[LAP_CUR] = 0;
			else if (player->laptime[LAP_CUR] != UINT32_MAX)
				player->laptime[LAP_CUR]++; // This is too complicated to sync to realtime, just sorta hope for the best :V
		}
		else
		{
			player->realtime = 0;
			player->laptime[LAP_CUR] = 0;
		}
	}

	if (!player->mo)
		return;

	//K_KartResetPlayerColor(player); -- called at death, don't think we need to re-establish

	P_CalcHeight(player);
}

//
// P_MoveCamera: make sure the camera is not outside the world and looks at the player avatar
//

camera_t camera[MAXSPLITSCREENPLAYERS] = {}; // Four cameras, three for splitscreen

static void CV_CamRotate_OnChange(void)
{
	if (cv_cam_rotate[0].value < 0)
		CV_SetValue(&cv_cam_rotate[0], cv_cam_rotate[0].value + 360);
	else if (cv_cam_rotate[0].value > 359)
		CV_SetValue(&cv_cam_rotate[0], cv_cam_rotate[0].value % 360);
}

static void CV_CamRotate2_OnChange(void)
{
	if (cv_cam_rotate[1].value < 0)
		CV_SetValue(&cv_cam_rotate[1], cv_cam_rotate[1].value + 360);
	else if (cv_cam_rotate[1].value > 359)
		CV_SetValue(&cv_cam_rotate[1], cv_cam_rotate[1].value % 360);
}

static void CV_CamRotate3_OnChange(void)
{
	if (cv_cam_rotate[2].value < 0)
		CV_SetValue(&cv_cam_rotate[2], cv_cam_rotate[2].value + 360);
	else if (cv_cam_rotate[2].value > 359)
		CV_SetValue(&cv_cam_rotate[2], cv_cam_rotate[2].value % 360);
}

static void CV_CamRotate4_OnChange(void)
{
	if (cv_cam_rotate[3].value < 0)
		CV_SetValue(&cv_cam_rotate[3], cv_cam_rotate[3].value + 360);
	else if (cv_cam_rotate[3].value > 359)
		CV_SetValue(&cv_cam_rotate[3], cv_cam_rotate[3].value % 360);
}

static CV_PossibleValue_t CV_CamSpeed[] = {{0, "MIN"}, {1*FRACUNIT, "MAX"}, {0, NULL}};
static CV_PossibleValue_t CV_CamRotate[] = {{-720, "MIN"}, {720, "MAX"}, {0, NULL}};

consvar_t cv_cam_dist[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("cam_dist", "190", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam2_dist", "190", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam3_dist", "190", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam4_dist", "190", CV_FLOAT|CV_SAVE, CV_Signed, NULL)
};

consvar_t cv_cam_height[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("cam_height", "75", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam2_height", "75", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam3_height", "75", CV_FLOAT|CV_SAVE, CV_Signed, NULL),
	CVAR_INIT ("cam4_height", "75", CV_FLOAT|CV_SAVE, CV_Signed, NULL)
};

consvar_t cv_cam_still[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("cam_still", "Off", 0, CV_OnOff, NULL),
	CVAR_INIT ("cam2_still", "Off", 0, CV_OnOff, NULL),
	CVAR_INIT ("cam3_still", "Off", 0, CV_OnOff, NULL),
	CVAR_INIT ("cam4_still", "Off", 0, CV_OnOff, NULL)
};

consvar_t cv_cam_speed[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("cam_speed", "0.4", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL),
	CVAR_INIT ("cam2_speed", "0.4", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL),
	CVAR_INIT ("cam3_speed", "0.4", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL),
	CVAR_INIT ("cam4_speed", "0.4", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL)
};

consvar_t cv_cam_rotate[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("cam_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate_OnChange),
	CVAR_INIT ("cam2_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate2_OnChange),
	CVAR_INIT ("cam3_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate3_OnChange),
	CVAR_INIT ("cam4_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate4_OnChange)
};

consvar_t cv_tilting = CVAR_INIT ("tilting", "On", CV_SAVE, CV_OnOff, NULL);

consvar_t cv_actionmovie = CVAR_INIT ("actionmovie", "On", CV_SAVE, CV_OnOff, NULL);

fixed_t t_cam_dist[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};
fixed_t t_cam_height[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};
fixed_t t_cam_rotate[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};

void P_DemoCameraMovement(camera_t *cam, UINT8 num)
{
	ticcmd_t *cmd;
	angle_t thrustangle;
	player_t *lastp;

	// first off we need to get button input
	cmd = D_LocalTiccmd(num);

	if (cmd->aiming != 0)
	{
		cam->aiming += cmd->aiming << TICCMD_REDUCE;

		cam->reset_aiming = false;
	}

	cam->angle += cmd->turning << TICCMD_REDUCE;

	// camera movement:
	if (!cam->button_a_held)
	{
		if (cmd->buttons & BT_ACCELERATE)
		{
			cam->z += 32*mapobjectscale;
		}
		else if (cmd->buttons & BT_BRAKE)
		{
			cam->z -= 32*mapobjectscale;
		}
	}

	if (!(cmd->buttons & (BT_ACCELERATE | BT_DRIFT)) && cam->button_a_held)
	{
		cam->button_a_held--;
	}

	// if you hold item, you will lock on to displayplayer. (The last player you were ""f12-ing"")
	if (cam->freecam && cmd->buttons & BT_ATTACK)
	{
		lastp = &players[displayplayers[0]];	// Fun fact, I was trying displayplayers[0]->mo as if it was Lua like an absolute idiot.
		cam->angle = R_PointToAngle2(cam->x, cam->y, lastp->mo->x, lastp->mo->y);
		cam->aiming = R_PointToAngle2(0, cam->z, R_PointToDist2(cam->x, cam->y, lastp->mo->x, lastp->mo->y), lastp->mo->z + lastp->mo->scale*128*P_MobjFlip(lastp->mo));	// This is still unholy. Aim a bit above their heads.

		cam->reset_aiming = false;
	}

	// After switching to democam, the vertical angle of
	// chasecam is inherited. This is intentional because it
	// creates a smooth transition. However, moving
	// forward/back will have a slope. So, as long as democam
	// controls haven't been used to alter the vertical angle,
	// slowly reset it to flat.
	if (cam->reset_aiming || ((cmd->buttons & BT_DRIFT) && !cam->button_a_held))
	{
		INT32 aiming = cam->aiming;
		INT32 smooth = FixedMul(ANGLE_11hh / 4, FCOS(cam->aiming));

		if (abs(smooth) < abs(aiming))
		{
			cam->aiming -= smooth * intsign(aiming);
		}
		else
		{
			cam->aiming = 0;
			cam->reset_aiming = false; // completely smoothed out
		}
	}

	G_FinalClipAimingPitch((INT32 *)&cam->aiming, NULL, false);

	cam->momx = cam->momy = cam->momz = 0;

	if (cmd->forwardmove != 0)
	{
		thrustangle = cam->angle >> ANGLETOFINESHIFT;

		cam->x += FixedMul(cmd->forwardmove*mapobjectscale, FINECOSINE(thrustangle));
		cam->y += FixedMul(cmd->forwardmove*mapobjectscale, FINESINE(thrustangle));

		if (!cam->reset_aiming)
		{
			cam->z += FixedMul(cmd->forwardmove*mapobjectscale, AIMINGTOSLOPE(cam->aiming));
		}
		// momentums are useless here, directly add to the coordinates

		// this.......... doesn't actually check for floors and walls and whatnot but the function to do that is a pure mess so fuck that.
		// besides freecam going inside walls sounds pretty cool on paper.
	}

	// update subsector to avoid crashes;
	cam->subsector = R_PointInSubsectorFast(cam->x, cam->y);
}

void P_ToggleDemoCamera(UINT8 viewnum)
{
	camera_t *cam = &camera[viewnum];

	if (!cam->freecam)	// toggle on
	{
		cam->freecam = true;
		cam->button_a_held = 2;
		cam->reset_aiming = true;
	}
	else	// toggle off
	{
		cam->freecam = false;
	}
}

void P_ResetCamera(player_t *player, camera_t *thiscam)
{
	tic_t tries = 0;
	fixed_t x, y, z;

	if (thiscam->freecam)
		return;	// do not reset the camera there.

	if (!player->mo)
		return;

	if (thiscam->chase && player->mo->health <= 0)
		return;

	thiscam->chase = !player->spectator;
	x = player->mo->x - P_ReturnThrustX(player->mo, thiscam->angle, player->mo->radius);
	y = player->mo->y - P_ReturnThrustY(player->mo, thiscam->angle, player->mo->radius);
	if (player->mo->eflags & MFE_VERTICALFLIP)
		z = player->mo->z + player->mo->height - P_GetPlayerViewHeight(player) - 16*FRACUNIT;
	else
		z = player->mo->z + P_GetPlayerViewHeight(player);

	// set bits for the camera
	thiscam->x = x;
	thiscam->y = y;
	thiscam->z = z;

	thiscam->angle = player->mo->angle;
	thiscam->aiming = 0;
	thiscam->relativex = 0;

	thiscam->subsector = R_PointInSubsectorFast(thiscam->x,thiscam->y);

	thiscam->radius = 20*FRACUNIT;
	thiscam->height = 16*FRACUNIT;
	thiscam->reset_aiming = true;

	while (!P_MoveChaseCamera(player,thiscam,true) && ++tries < 2*TICRATE);
}

boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcalled)
{
	static boolean lookbackactive[MAXSPLITSCREENPLAYERS];
	static UINT8 lookbackdelay[MAXSPLITSCREENPLAYERS];
	UINT8 num;
	angle_t angle = 0, focusangle = 0, focusaiming = 0, pitch = 0;
	fixed_t x, y, z, dist, distxy, distz, viewpointx, viewpointy, camspeed, camdist, camheight, pviewheight;
	fixed_t pan, xpan, ypan;
	INT32 camrotate;
	boolean camstill, lookback, lookbackdown;
	UINT8 timeover;
	mobj_t *mo;
	fixed_t f1, f2;
	fixed_t speed;

	sonicloopcamvars_t *loop = &player->loop.camera;
	tic_t loop_out = leveltime - loop->enter_tic;
	tic_t loop_in = max(leveltime, loop->exit_tic) - loop->exit_tic;
	boolean affected_by_loop = (loop_out <=
		(loop->zoom_in_speed + loop->zoom_out_speed) && leveltime > introtime);

	thiscam->old_x = thiscam->x;
	thiscam->old_y = thiscam->y;
	thiscam->old_z = thiscam->z;
	thiscam->old_angle = thiscam->angle;
	thiscam->old_aiming = thiscam->aiming;

	// We probably shouldn't move the camera if there is no player or player mobj somehow
	if (!player || !player->mo)
		return true;

	// This can happen when joining
	if (thiscam->subsector == NULL || thiscam->subsector->sector == NULL)
		return true;

	if (thiscam == &camera[1]) // Camera 2
	{
		num = 1;
	}
	else if (thiscam == &camera[2]) // Camera 3
	{
		num = 2;
	}
	else if (thiscam == &camera[3]) // Camera 4
	{
		num = 3;
	}
	else // Camera 1
	{
		num = 0;
	}

	if (thiscam->freecam || player->spectator)
	{
		UINT8 camnum = G_PartyPosition(g_localplayers[num]);
		P_DemoCameraMovement(thiscam, camnum);
		P_CalcChasePostImg(player, thiscam);
		return true;
	}

	if (paused || P_AutoPause())
		return true;

	mo = player->mo;

	if (player->playerstate != PST_LIVE && !(skins[player->skin].flags & SF_OLDDEATH))
	{
		// Do not move the camera while dead!
		if (thiscam->chase)
			P_CalcChasePostImg(player, thiscam);

		return true;
	}

	if ((player->pflags & PF_NOCONTEST)) // 1 for momentum keep, 2 for turnaround
		timeover = (player->karthud[khud_timeovercam] > 2*TICRATE ? 2 : 1);
	else
		timeover = 0;

	if (!(player->playerstate == PST_DEAD || player->exiting || leveltime < introtime))
	{
		if (player->spectator) // force cam off for spectators
			return true;

		if (!cv_chasecam[num].value && thiscam == &camera[num])
			return true;
	}

	if (!thiscam->chase && !resetcalled)
	{
		focusangle = ((P_IsLocalPlayer(player)) ? localangle[num] : mo->angle);
		camrotate = ((thiscam == &camera[num]) ? cv_cam_rotate[num].value : 0);

		if (leveltime < introtime) // Whoooshy camera!
		{
			const INT32 introcam = (introtime - leveltime);
			camrotate += introcam*5;
		}

		thiscam->angle = focusangle + FixedAngle(camrotate*FRACUNIT);
		P_ResetCamera(player, thiscam);
		return true;
	}

	thiscam->radius = 20*mapobjectscale;
	thiscam->height = 16*mapobjectscale;

	// Don't run while respawning from a starpost
	// Inu 4/8/13 Why not?!
//	if (leveltime > 0 && timeinmap <= 0)
//		return true;

	if (demo.playback)
	{
		focusangle = mo->angle;
		focusaiming = 0;
	}
	else if (P_IsMachineLocalPlayer(player))
	{
		// Players-turned-bots outside of end of race contexts (Lua)
		// don't update their local camera angle, so it should be updated
		// somewhere - I choose here because it makes the most sense.
		if (K_PlayerUsesBotMovement(player) && !player->bot)
			P_ForceLocalAngle(player, mo->angle, true);

		focusangle = localangle[num];
		focusaiming = localaiming[num];
	}
	else
	{
		focusangle = mo->angle;
		focusaiming = player->aiming;
	}

	if (abs(thiscam->dpad_y_held) >= 2*TICRATE)
	{
		focusaiming += ANGLE_45 * intsign(thiscam->dpad_y_held) * P_MobjFlip(mo);
	}

	if (P_CameraThinker(player, thiscam, resetcalled))
		return true;

	lookback = ( player->cmd.buttons & BT_LOOKBACK );

	camspeed = cv_cam_speed[num].value;
	camstill = cv_cam_still[num].value;
	camrotate = cv_cam_rotate[num].value;
	camdist = FixedMul(cv_cam_dist[num].value, mapobjectscale);
	camheight = FixedMul(cv_cam_height[num].value, mapobjectscale);

	if (leveltime > introtime)
	{
		if (loop_in < loop->zoom_in_speed)
		{
			fixed_t f = loop_out < loop->zoom_out_speed
				? (loop_out * FRACUNIT) / loop->zoom_out_speed
				: FRACUNIT - ((loop_in * FRACUNIT) / loop->zoom_in_speed);

			camspeed -= FixedMul(f, camspeed - (FRACUNIT/10));
			camdist += FixedMul(f, loop->dist);
		}

		if (loop_in < max(loop->pan_back, 1))
		{
			fixed_t f = (loop_in * FRACUNIT) / max(loop->pan_back, 1);

			fixed_t dx = mo->x - thiscam->x;
			fixed_t dy = mo->y - thiscam->y;

			angle_t th = R_PointToAngle2(0, 0, dx, dy);
			fixed_t d = AngleFixed(focusangle - th);

			if (d > 180*FRACUNIT)
			{
				d -= (360*FRACUNIT);
			}

			focusangle = th + FixedAngle(FixedMul(f, d));

			if (loop_in == 0)
			{
				focusaiming = R_PointToAngle2(0, thiscam->z, FixedHypot(dx, dy), mo->z);
			}
		}

		if (loop_in == 0)
		{
			tic_t accel = max(loop->pan_accel, 1);
			fixed_t f = (min(loop_out, accel) * FRACUNIT) / accel;

			INT32 turn = AngleDeltaSigned(focusangle, player->loop.yaw - loop->pan);
			INT32 turnspeed = FixedAngle(FixedMul(f, loop->pan_speed));

			if (turn > turnspeed)
			{
				// TODO: this code let the panning angle flip
				// depending on your camera angle when entering
				// the loop.
				// It caused just about every loop to look weird
				// sometimes, since the camera could move
				// differently than configured.
				// I don't know if this behavior should ever come
				// back, but in case it should, I'm leaving this
				// comment here.
#if 0
				if (turn < ANGLE_90)
				{
					turnspeed = -(turnspeed);
				}
#endif

				focusangle += turnspeed;
			}
		}
	}

	if (timeover)
	{
		const INT32 timeovercam = max(0, min(180, (player->karthud[khud_timeovercam] - 2*TICRATE)*15));
		camrotate += timeovercam;
	}
	else if (leveltime < introtime && !(modeattacking && !demo.playback)) // Whoooshy camera! (don't do this in RA when we PLAY, still do it in replays however~)
	{
		const INT32 introcam = (introtime - leveltime);
		camrotate += introcam*5;
		camdist += (introcam * mapobjectscale)*3;
		camheight += (introcam * mapobjectscale)*2;
	}
	else if (player->exiting) // SRB2Kart: Leave the camera behind while exiting, for dramatic effect!
		camstill = true;
	else if ((lookback || lookbackdelay[num]) && !affected_by_loop)
	{
		// SRB2Kart -- Camera flip when looking backwards
#define MAXLOOKBACKDELAY 2
		camspeed = FRACUNIT;
		if (lookback)
		{
			camrotate += 180;
			lookbackdelay[num] = MAXLOOKBACKDELAY;
		}
		else
			lookbackdelay[num]--;
	}
	else if (player->respawn)
	{
		camspeed = 3*FRACUNIT/4;
	}
	lookbackdown = (lookbackdelay[num] == MAXLOOKBACKDELAY) != lookbackactive[num];
	lookbackactive[num] = (lookbackdelay[num] == MAXLOOKBACKDELAY);
#undef MAXLOOKBACKDELAY

	if (mo->eflags & MFE_VERTICALFLIP)
		camheight += thiscam->height;

	if (camspeed > FRACUNIT)
		camspeed = FRACUNIT;

	if (timeover)
		angle = mo->angle + FixedAngle(camrotate*FRACUNIT);
	else if (leveltime < introtime)
		angle = focusangle + FixedAngle(camrotate*FRACUNIT);
	else if (camstill || resetcalled || player->playerstate == PST_DEAD)
		angle = thiscam->angle;
	else
	{
		if (camspeed == FRACUNIT)
			angle = focusangle + FixedAngle(camrotate<<FRACBITS);
		else
		{
			angle_t input = focusangle + FixedAngle(camrotate<<FRACBITS) - thiscam->angle;
			boolean invert = (input > ANGLE_180);
			if (invert)
				input = InvAngle(input);

			input = FixedAngle(FixedMul(AngleFixed(input), camspeed));
			if (invert)
				input = InvAngle(input);

			angle = thiscam->angle + input;
		}
	}

	if (!resetcalled && (leveltime >= introtime && timeover != 2)
		&& (t_cam_rotate[num] != -42))
	{
		angle = FixedAngle(camrotate*FRACUNIT);
		thiscam->angle = angle;
	}

	// sets ideal cam pos
	dist = camdist;

	if (player->loop.radius)
	{
		speed = player->speed;
	}
	else
	{
		speed = P_AproxDistance(P_AproxDistance(mo->momx, mo->momy), mo->momz / 16);
	}

	if (cv_naturalcamera.value)
	{
		const fixed_t speedthreshold = 48*mapobjectscale;
		const fixed_t olddist = P_AproxDistance(mo->x - thiscam->x, mo->y - thiscam->y);

		fixed_t lag, distoffset;

		if (player->karthud[khud_boostcam])
		{
			dist -= FixedMul(11*dist/16, player->karthud[khud_boostcam]);
		}

		lag = FRACUNIT - ((FixedDiv(speed, speedthreshold) - FRACUNIT) * 2);

		if (lag > FRACUNIT)
		{
			lag = FRACUNIT;
		}

		if (lag < camspeed)
		{
			lag = camspeed;
		}

		distoffset = dist - olddist;
		dist = olddist + FixedMul(distoffset, lag);

		if (dist < 0)
		{
			dist = 0;
		}
	}
	else
	{
		if (speed > K_GetKartSpeed(player, false, true))
			dist += 4*(speed - K_GetKartSpeed(player, false, true));
		dist += abs(thiscam->momz)/4;

		if (player->karthud[khud_boostcam])
		{
			dist -= FixedMul(11*dist/16, player->karthud[khud_boostcam]);
		}
	}

	if (mo->standingslope)
	{
		pitch = (angle_t)FixedMul(P_ReturnThrustX(mo, thiscam->angle - mo->standingslope->xydirection, FRACUNIT), (fixed_t)mo->standingslope->zangle);
		if (mo->eflags & MFE_VERTICALFLIP)
		{
			if (pitch >= ANGLE_180)
				pitch = 0;
		}
		else
		{
			if (pitch < ANGLE_180)
				pitch = 0;
		}
	}
	pitch = thiscam->pitch + (angle_t)FixedMul(pitch - thiscam->pitch, camspeed/4);

#ifdef HWRENDER
	if (rendermode == render_opengl && !cv_glshearing.value)
		distxy = FixedMul(dist, FINECOSINE((pitch>>ANGLETOFINESHIFT) & FINEMASK));
	else
#endif
		distxy = dist;
	distz = -FixedMul(dist, FINESINE((pitch>>ANGLETOFINESHIFT) & FINEMASK));

	x = mo->x - FixedMul(FINECOSINE((angle>>ANGLETOFINESHIFT) & FINEMASK), distxy);
	y = mo->y - FixedMul(FINESINE((angle>>ANGLETOFINESHIFT) & FINEMASK), distxy);

	// SRB2Kart: set camera panning
	if (camstill || resetcalled || player->playerstate == PST_DEAD || player->loop.radius)
		pan = xpan = ypan = 0;
	else
	{
		if (player->drift != 0)
		{
			fixed_t panmax = (dist/5);
			INT32 driftval = K_GetKartDriftSparkValue(player);
			INT32 dc = player->driftcharge;

			if (dc > driftval || dc < 0)
				dc = driftval;

			pan = FixedDiv(FixedMul((fixed_t)dc, panmax), driftval);

			if (pan > panmax)
				pan = panmax;
			if (player->drift < 0)
				pan *= -1;
		}
		else
			pan = 0;

		pan = thiscam->pan + FixedMul(pan - thiscam->pan, camspeed/4);

		xpan = FixedMul(FINECOSINE(((angle+ANGLE_90)>>ANGLETOFINESHIFT) & FINEMASK), pan);
		ypan = FixedMul(FINESINE(((angle+ANGLE_90)>>ANGLETOFINESHIFT) & FINEMASK), pan);

		x += xpan;
		y += ypan;
	}

	pviewheight = FixedMul(32<<FRACBITS, mo->scale);

	if (mo->eflags & MFE_VERTICALFLIP)
	{
		distz = min(-camheight, distz);
		z = mo->z + mo->height - pviewheight + distz;
	}
	else
	{
		distz = max(camheight, distz);
		z = mo->z + pviewheight + distz;
	}

	z += player->cameraOffset;
	z -= 2*(player->karthud[khud_heavydropcam] + player->karthud[khud_postdropcam])*FRACUNIT*P_MobjFlip(mo);

	// point viewed by the camera
	// this point is just 64 unit forward the player
	dist = 64*mapobjectscale;
	viewpointx = mo->x + FixedMul(FINECOSINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist) + xpan;
	viewpointy = mo->y + FixedMul(FINESINE((angle>>ANGLETOFINESHIFT) & FINEMASK), dist) + ypan;

	if (timeover)
		thiscam->angle = angle;
	else if (!camstill && !resetcalled && !paused && timeover != 1)
		thiscam->angle = R_PointToAngle2(thiscam->x, thiscam->y, viewpointx, viewpointy);

	if (timeover == 1)
	{
		thiscam->momx = P_ReturnThrustX(NULL, mo->angle, 32*mo->scale); // Push forward
		thiscam->momy = P_ReturnThrustY(NULL, mo->angle, 32*mo->scale);
		thiscam->momz = 0;
	}
	else if (player->exiting || timeover == 2)
		thiscam->momx = thiscam->momy = thiscam->momz = 0;
	else if (leveltime < introtime)
	{
		thiscam->momx = FixedMul(x - thiscam->x, camspeed);
		thiscam->momy = FixedMul(y - thiscam->y, camspeed);
		thiscam->momz = FixedMul(z - thiscam->z, camspeed);
	}
	else
	{
		thiscam->momx = x - thiscam->x;
		thiscam->momy = y - thiscam->y;
		if (lookback && lookbackdelay[num] && !affected_by_loop) {
			// when looking back, camera's momentum
			// should inherit the momentum of the player
			// plus extra
			thiscam->momx += 2*mo->momx;
			thiscam->momy += 2*mo->momy;
		}
		
		thiscam->momz = FixedMul(z - thiscam->z, camspeed*3/5);
	}

	thiscam->pan = pan;
	thiscam->pitch = pitch;

	// compute aming to look the viewed point
	f1 = viewpointx-thiscam->x;
	f2 = viewpointy-thiscam->y;
	dist = FixedHypot(f1, f2);

	if (mo->eflags & MFE_VERTICALFLIP)
	{
		angle = R_PointToAngle2(0, thiscam->z + thiscam->height, dist, mo->z + mo->height - player->mo->height + player->cameraOffset + 2*player->karthud[khud_heavydropcam]*FRACUNIT);
		if (thiscam->pitch < ANGLE_180 && thiscam->pitch > angle)
			angle += (thiscam->pitch - angle)/2;
	}
	else
	{
		angle = R_PointToAngle2(0, thiscam->z, dist, mo->z + player->mo->height + player->cameraOffset - 2*player->karthud[khud_heavydropcam]*FRACUNIT);
		if (thiscam->pitch >= ANGLE_180 && thiscam->pitch < angle)
			angle -= (angle - thiscam->pitch)/2;
	}

	if (player->playerstate != PST_DEAD)
		angle += (focusaiming < ANGLE_180 ? focusaiming/2 : InvAngle(InvAngle(focusaiming)/2)); // overcomplicated version of '((signed)focusaiming)/2;'

	if (!camstill && !timeover) // Keep the view still...
	{
		G_ClipAimingPitch((INT32 *)&angle);

		if (camspeed == FRACUNIT)
			thiscam->aiming = angle;
		else
		{
			angle_t input;
			boolean invert;

			input = thiscam->aiming - angle;
			invert = (input > ANGLE_180);
			if (invert)
				input = InvAngle(input);

			input = FixedAngle(FixedMul(AngleFixed(input), (5*camspeed)/16));
			if (invert)
				input = InvAngle(input);

			thiscam->aiming -= input;
		}
	}

	if (!resetcalled && (player->playerstate == PST_DEAD || player->playerstate == PST_REBORN))
	{
		// Don't let the camera match your movement.
		thiscam->momz = 0;
		if (player->spectator)
			thiscam->aiming = 0;
		// Only let the camera go a little bit downwards.
		else if (!(mo->eflags & MFE_VERTICALFLIP) && thiscam->aiming < ANGLE_337h && thiscam->aiming > ANGLE_180)
			thiscam->aiming = ANGLE_337h;
		else if (mo->eflags & MFE_VERTICALFLIP && thiscam->aiming > ANGLE_22h && thiscam->aiming < ANGLE_180)
			thiscam->aiming = ANGLE_22h;
	}

	if (lookbackdown)
	{
		P_MoveChaseCamera(player, thiscam, false);
		R_ResetViewInterpolation(num + 1);
	}

	return (x == thiscam->x && y == thiscam->y && z == thiscam->z && angle == thiscam->aiming);

}

boolean P_SpectatorJoinGame(player_t *player)
{
	INT32 changeto = 0;
	const char *text = NULL;

	// Team changing isn't allowed.
	if (!cv_allowteamchange.value)
		return false;

	// Team changing in Team Match and CTF
	// Pressing fire assigns you to a team that needs players if allowed.
	// Partial code reproduction from p_tick.c autobalance code.
	// a surprise tool that will help us later...
	if (G_GametypeHasTeams())
	{
		INT32 z, numplayersred = 0, numplayersblue = 0;

		//find a team by num players, score, or random if all else fails.
		for (z = 0; z < MAXPLAYERS; ++z)
			if (playeringame[z])
			{
				if (players[z].ctfteam == 1)
					++numplayersred;
				else if (players[z].ctfteam == 2)
					++numplayersblue;
			}
		// for z

		if (numplayersblue > numplayersred)
			changeto = 1;
		else if (numplayersred > numplayersblue)
			changeto = 2;
		else if (bluescore > redscore)
			changeto = 1;
		else if (redscore > bluescore)
			changeto = 2;
		else
			changeto = (P_RandomFixed() & 1) + 1;

		if (!LUA_HookTeamSwitch(player, changeto, true, false, false))
			return false;
	}

	// no conditions that could cause the gamejoin to fail below this line

	if (player->mo)
	{
		P_RemoveMobj(player->mo);
		P_SetTarget(&player->mo, NULL);
	}
	player->spectator = false;
	player->pflags &= ~PF_WANTSTOJOIN;
	player->spectatewait = 0;
	player->ctfteam = changeto;
	player->playerstate = PST_REBORN;
	player->enteredGame = true;

	// Reset away view (some code referenced from Got_Teamchange)
	if (G_IsPartyLocal(player - players))
	{
		LUA_HookViewpointSwitch(player, player, true);
		displayplayers[G_PartyPosition(player - players)] = (player-players);
	}

	// a surprise tool that will help us later...
	if (changeto == 1)
		text = va("\x82*%s switched to the %c%s%c team.\n", player_names[player-players], '\x85', "RED", '\x82');
	else if (changeto == 2)
		text = va("\x82*%s switched to the %c%s%c team.\n", player_names[player-players], '\x85', "BLU", '\x82');
	else
		text = va("\x82*%s entered the game.", player_names[player-players]);

	HU_AddChatText(text, false);
	return true; // no more player->mo, cannot continue.
}

static boolean P_CameraCheckHeatFirstperson(player_t *player, sector_t *sector, fixed_t pviewheight)
{
	if (sector->flags & MSF_HEATWAVE)
		return true;

	if (sector->ffloors)
	{
		ffloor_t *rover;

		for (rover = sector->ffloors; rover; rover = rover->next)
		{
			if (!(rover->fofflags & FOF_EXISTS))
				continue;

			if (pviewheight >= P_GetFFloorTopZAt(rover, player->mo->x, player->mo->y))
				continue;
			if (pviewheight <= P_GetFFloorBottomZAt(rover, player->mo->x, player->mo->y))
				continue;

			if (rover->master->frontsector->flags & MSF_HEATWAVE)
				return true;
		}
	}

	return false;
}

static boolean P_CameraCheckWaterFirstperson(player_t *player, sector_t *sector, fixed_t pviewheight)
{
	if (sector->ffloors)
	{
		ffloor_t *rover;

		for (rover = sector->ffloors; rover; rover = rover->next)
		{
			if (!(rover->fofflags & FOF_EXISTS) || !(rover->fofflags & FOF_SWIMMABLE) || rover->fofflags & FOF_BLOCKPLAYER)
				continue;

			if (pviewheight >= P_GetFFloorTopZAt(rover, player->mo->x, player->mo->y))
				continue;
			if (pviewheight <= P_GetFFloorBottomZAt(rover, player->mo->x, player->mo->y))
				continue;

			return true;
		}
	}

	return false;
}

// the below is first person only, if you're curious. check out P_CalcChasePostImg in p_mobj.c for chasecam
static void P_CalcPostImg(player_t *player, camera_t *thiscam)
{
	sector_t *sector = NULL;
	UINT8 postimgtype = 0;
	//INT32 *param;
	fixed_t pviewheight = 0;

	/*for (i = 0; i <= splitscreen; i++)
	{
		if (player == &players[displayplayers[i]])
		{
			param = &postimgparam[i];
			break;
		}
	}*/

	if (encoremode) // srb2kart
		postimgtype |= POSTIMG_MIRROR;

	if (player->mo->eflags & MFE_VERTICALFLIP)
		postimgtype |= POSTIMG_FLIP;

#ifdef HWRENDER
	if (rendermode == render_opengl && splitscreen)
	{
		thiscam->postimgflags = postimgtype;
		return;
	}
#endif

	sector = player->mo->subsector->sector;

	if (player->mo->eflags & MFE_VERTICALFLIP)
		pviewheight = player->mo->z + player->mo->height - player->viewheight;
	else
		pviewheight = player->mo->z + player->viewheight;

	if (player->awayviewtics && player->awayviewmobj && !P_MobjWasRemoved(player->awayviewmobj))
	{
		sector = player->awayviewmobj->subsector->sector;
		pviewheight = player->awayviewmobj->z + 20*FRACUNIT;
	}

	// see if we are in water (water trumps heat)
	if (P_CameraCheckWaterFirstperson(player, sector, pviewheight))
		postimgtype |= POSTIMG_WATER;
	// see if we are in heat (no, not THAT kind of heat...)
	else if (P_CameraCheckHeatFirstperson(player, sector, pviewheight))
		postimgtype |= POSTIMG_HEAT;

	// Motion blur
	// unused
	/*if (player->speed > (35<<FRACBITS))
	{
		postimgtype |= POSTIMG_MOTION;
		*param = (player->speed - 32)/4;

		if (*param > 5)
			*param = 5;
	}*/

	thiscam->postimgflags = postimgtype;
}

void P_DoTimeOver(player_t *player)
{
	if (player->pflags & PF_NOCONTEST)
	{
		// NO! Don't do this!
		return;
	}

	if (P_IsLocalPlayer(player) && !demo.playback)
	{
		legitimateexit = true; // SRB2kart: losing a race is still seeing it through to the end :p
	}

	if (netgame && !player->bot && !bossinfo.boss)
	{
		CON_LogMessage(va(M_GetText("%s ran out of time.\n"), player_names[player-players]));
	}

	player->pflags |= PF_NOCONTEST;
	K_UpdatePowerLevelsOnFailure(player);

	if (G_GametypeUsesLives())
	{
		K_PlayerLoseLife(player);
	}

	if (player->mo)
	{
		S_StopSound(player->mo);
		P_DamageMobj(player->mo, NULL, NULL, 1, DMG_TIMEOVER);
	}

	P_EndingMusic(player);

	if (!exitcountdown)
	{
		G_BeginLevelExit();
		exitcountdown = 5*TICRATE;
	}
}

// SRB2Kart: These are useful functions, but we aren't using them yet.
#if 0

// Get an axis of a certain ID number
static mobj_t *P_GetAxis(INT32 num)
{
	thinker_t *th;
	mobj_t *mobj;

	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
	{
		if (th->function == (actionf_p1)P_RemoveThinkerDelayed)
			continue;

		mobj = (mobj_t *)th;

		// NiGHTS axes spawn before anything else. If this mobj doesn't have MF2_AXIS, it means we reached the axes' end.
		if (!(mobj->flags2 & MF2_AXIS))
			break;

		// Skip if this axis isn't the one we want.
		if (mobj->health != num)
			continue;

		return mobj;
	}

	CONS_Alert(CONS_WARNING, "P_GetAxis: Track segment %d is missing!\n", num);
	return NULL;
}

// Auxiliary function. For a given position and axis, it calculates the nearest "valid" snap-on position.
static void P_GetAxisPosition(fixed_t x, fixed_t y, mobj_t *amo, fixed_t *newx, fixed_t *newy, angle_t *targetangle, angle_t *grind)
{
	fixed_t ax = amo->x;
	fixed_t ay = amo->y;
	angle_t ang;
	angle_t gr = 0;

	if (amo->type == MT_AXISTRANSFERLINE)
	{
		ang = amo->angle;
		// Extra security for cardinal directions.
		if (ang == ANGLE_90 || ang == ANGLE_270) // Vertical lines
			x = ax;
		else if (ang == 0 || ang == ANGLE_180) // Horizontal lines
			y = ay;
		else // Diagonal lines
		{
			fixed_t distance = R_PointToDist2(ax, ay, x, y);
			angle_t fad = ((R_PointToAngle2(ax, ay, x, y) - ang) >> ANGLETOFINESHIFT) & FINEMASK;
			fixed_t cosine = FINECOSINE(fad);
			angle_t fa = (ang >> ANGLETOFINESHIFT) & FINEMASK;
			distance = FixedMul(distance, cosine);
			x = ax + FixedMul(distance, FINECOSINE(fa));
			y = ay + FixedMul(distance, FINESINE(fa));
		}
	}
	else // Keep minecart to circle
	{
		fixed_t rad = amo->radius;
		fixed_t distfactor = FixedDiv(rad, R_PointToDist2(ax, ay, x, y));

		gr = R_PointToAngle2(ax, ay, x, y);
		ang = gr + ANGLE_90;
		x = ax + FixedMul(x - ax, distfactor);
		y = ay + FixedMul(y - ay, distfactor);
	}

	*newx = x;
	*newy = y;
	*targetangle = ang;
	*grind = gr;
}

static void P_ParabolicMove(mobj_t *mo, fixed_t x, fixed_t y, fixed_t z, fixed_t g, fixed_t speed)
{
	fixed_t dx = x - mo->x;
	fixed_t dy = y - mo->y;
	fixed_t dz = z - mo->z;
	fixed_t dh = P_AproxDistance(dx, dy);
	fixed_t c = FixedDiv(dx, dh);
	fixed_t s = FixedDiv(dy, dh);
	fixed_t fixConst = FixedDiv(speed, g);

	mo->momx = FixedMul(c, speed);
	mo->momy = FixedMul(s, speed);
	mo->momz = FixedDiv(dh, 2*fixConst) + FixedDiv(dz, FixedDiv(dh, fixConst/2));
}

#endif

	/* gaysed script from me, based on Golden's sprite slope roll */

// holy SHIT
static INT32
Quaketilt (player_t *player)
{
	angle_t tilt;
	fixed_t lowb; // this threshold for speed
	angle_t moma = R_PointToAngle2(0, 0, player->mo->momx, player->mo->momy);
	INT32 delta = (INT32)( player->mo->angle - moma );
	fixed_t speed;

	boolean sliptiding = K_Sliptiding(player);

	if (delta == (INT32)ANGLE_180)/* FUCK YOU HAVE A HACK */
	{
		return 0;
	}

	// Hi! I'm "not a math guy"!
	if (abs(delta) > ANGLE_90)
		delta = (INT32)(( moma + ANGLE_180 ) - player->mo->angle );
	if (P_IsObjectOnGround(player->mo))
	{
		if (sliptiding && cv_sliptidetilt.value)
		{
			tilt = ANGLE_45;
			lowb = 20*FRACUNIT;
		}
		else
		{
			tilt = ANGLE_11hh/2;
			lowb = 60*FRACUNIT;
		}
	}
	else
	{
		tilt = ANGLE_22h;
		lowb = 40*FRACUNIT;
	}
	lowb = FixedMul(lowb, player->mo->scale);
	moma = FixedMul(FixedDiv(delta, ANGLE_90), tilt);
	speed = player->speed;
	if (speed < lowb)
	{
		// ease out tilt as we slow...
		moma = FixedMul(moma, FixedDiv(speed, lowb));
	}
	return moma;
}

static angle_t P_GetCameraPitchRollAngle(mobj_t *mobj, player_t *viewPlayer)
{
	angle_t viewingAngle, rollOrPitch;
	angle_t sloperolll = mobj->slopepitch;
	angle_t slopepitch = mobj->sloperoll;
	fixed_t pitchMul, rollMul;

	if (mobj->player && cv_sloperoll.value)
	{
		// We are the player, use camera for air tilting.
		viewingAngle = R_PointToAnglePlayer(viewPlayer, mobj->x, mobj->y);
	}
	else
	{
		// For regular objects, use the camera; just not the *player's* camera.
		//... Unless you are a Nights bumber...
		if (mobj->type == MT_NIGHTSBUMPER)
			viewingAngle = mobj->angle;
		else
			viewingAngle = R_PointToAngleFloat(mobj->x, mobj->y);
	}

	pitchMul = -FINESINE(viewingAngle >> ANGLETOFINESHIFT);
	rollMul = FINECOSINE(viewingAngle >> ANGLETOFINESHIFT);

	rollOrPitch = FixedMul(mobj->pitch + sloperolll, pitchMul) + FixedMul(mobj->roll + slopepitch, rollMul);

	return rollOrPitch;
}

static void
DoABarrelRoll (player_t *player)
{
	angle_t slope;
	angle_t delta;

	fixed_t smoothing;

	if (player->loop.radius)
	{
		return;
	}
	
	if (player->respawn)
	{
		player->tilt = 0;
		return;
	}

	if (player->exiting)
	{
		return;
	}

	slope = InvAngle(P_GetCameraPitchRollAngle(player->mo, player));

	if (AbsAngle(slope) < ANGLE_11hh)
	{
		slope = 0;
	}

	if (AbsAngle(slope) > ANGLE_45)
	{
		slope = slope & ANGLE_180 ? InvAngle(ANGLE_45) : ANGLE_45;
	}

	slope -= Quaketilt(player);

	delta = slope - player->tilt;
	smoothing = FixedDiv(AbsAngle(slope), ANGLE_45);

	delta = FixedDiv(delta, 33 *
			FixedDiv(FRACUNIT, FRACUNIT + smoothing));

	if (delta)
		player->tilt += delta;
	else
		player->tilt  = slope;
}

#define SPINOUTROTSPEED (24 * FRACUNIT)
#define MAXSPINROT (360 * FRACUNIT)

//
// P_PlayerThink
//

void P_PlayerThink(player_t *player)
{
	ticcmd_t *cmd;
	const size_t playeri = (size_t)(player - players);

#ifdef PARANOIA
	if (!player->mo)
		I_Error("p_playerthink: players[%s].mo == NULL", sizeu1(playeri));
#endif

	player->driftlevel = 0; // idk where to put this

	// todo: Figure out what is actually causing these problems in the first place...
	if (player->mo->health <= 0 && player->playerstate == PST_LIVE) //you should be DEAD!
	{
		CONS_Debug(DBG_GAMELOGIC, "P_PlayerThink: Player %s in PST_LIVE with 0 health. (\"Zombie bug\")\n", sizeu1(playeri));
		player->playerstate = PST_DEAD;
	}

	player->old_drawangle = player->drawangle;

	if (player->awayviewmobj && P_MobjWasRemoved(player->awayviewmobj))
	{
		P_SetTarget(&player->awayviewmobj, NULL); // remove awayviewmobj asap if invalid
		player->awayviewtics = 0; // reset to zero
	}

	if (player->flashcount)
		player->flashcount--;

	if (player->awayviewtics && player->awayviewtics != -1)
		player->awayviewtics--;

	// Track airtime
	if (P_IsObjectOnGround(player->mo)
		&& !P_PlayerInPain(player)) // This isn't airtime, but it's control loss all the same.
	{
		player->airtime = 0;
	}
	else
	{
		player->airtime++;
	}

	// BlanKart: Track drifting time.
	if (player->drift != 0)
	{
		player->driftelapsed++;
	}
	else
	{
		player->driftelapsed = 0;
	}

	cmd = &player->cmd;

	// SRB2kart
	// Save the dir the player is holding
	//  to allow items to be thrown forward or backward.
	{
		const INT16 threshold = 0; //(KART_FULLTURN / 2);
		if (cmd->throwdir > threshold)
		{
			player->throwdir = 1;
		}
		else if (cmd->throwdir < -threshold)
		{
			player->throwdir = -1;
		}
		else
		{
			player->throwdir = 0;
		}
	}

	// Accessibility - kickstart your acceleration
	if (!(player->pflags & PF_KICKSTARTACCEL) || leveltime < starttime)
	{
		player->kickstartaccel = 0;
	}
	else if (cmd->buttons & BT_ACCELERATE)
	{
		if (!player->exiting && !(player->oldcmd.buttons & BT_ACCELERATE))
		{
			player->kickstartaccel = 0;
		}
		else if (player->kickstartaccel < ACCEL_KICKSTART)
		{
			player->kickstartaccel++;
			if ((player->kickstartaccel == ACCEL_KICKSTART) && P_IsLocalPlayer(player))
			{
				S_StartSound(NULL, sfx_ding);
			}
		}
		else // for HUD
		{
			player->kickstartaccel = ACCEL_KICKSTART+1;
		}
	}
	else if (player->kickstartaccel < ACCEL_KICKSTART)
	{
		player->kickstartaccel = 0;
	}
	else // for HUD
	{
		player->kickstartaccel = ACCEL_KICKSTART+1;
	}

	// MKWii-styled icon spinouts: do a full 360 rotation before stopping.
	if (P_PlayerInPain(player))
	{
		if (player->spinoutrot < MAXSPINROT)
		{
			if ((player->spinoutrot + SPINOUTROTSPEED) >= MAXSPINROT)
			{
				player->spinoutrot = (player->spinoutrot + SPINOUTROTSPEED) % MAXSPINROT;
			}
			else
			{
				player->spinoutrot += SPINOUTROTSPEED;
			}
		}
	}
	else
	{
		if ((player->spinoutrot) && (player->spinoutrot < MAXSPINROT))
		{
			if ((player->spinoutrot + SPINOUTROTSPEED) >= MAXSPINROT)
			{
				player->spinoutrot = 0;
			}
			else
			{
				player->spinoutrot += SPINOUTROTSPEED;
			}
		}
	}

#ifdef PARANOIA
	if (player->playerstate == PST_REBORN)
		I_Error("player %s is in PST_REBORN\n", sizeu1(playeri));
#endif

	if (!mapreset)
	{
		if (gametypes[gametype]->rules & GTR_CIRCUIT)
		{
			INT32 i;

			// Check if all the players in the race have finished. If so, end the level.
			for (i = 0; i < MAXPLAYERS; i++)
			{
				if (playeringame[i] && !players[i].spectator)
				{
					if (!players[i].exiting && !(players[i].pflags & PF_NOCONTEST) && players[i].lives > 0)
						break;
				}
			}

			if (i == MAXPLAYERS && player->exiting == raceexittime+2) // finished
				player->exiting = raceexittime+1;

#if 0
			// If 10 seconds are left on the timer,
			// begin the drown music for countdown!
			// SRB2Kart: despite how perfect this is, it's disabled FOR A REASON
			if (racecountdown == 11*TICRATE - 1)
			{
				if (P_IsLocalPlayer(player))
					S_ChangeMusicInternal("drown", false);
			}
#endif

			// If you've hit the countdown and you haven't made
			//  it to the exit, you're a goner!
			if (racecountdown == 1 && !player->spectator && !player->exiting && !(player->pflags & PF_NOCONTEST) && player->lives > 0)
			{
				P_DoTimeOver(player);

				if (player->playerstate == PST_DEAD)
				{
					LUA_HookPlayer(player, HOOK(PlayerThink));
					return;
				}
			}
		}

		// If it is set, start subtracting
		// Don't allow it to go back to 0
		if (player->exiting > 1 && (player->exiting < raceexittime+2 || !(gametypes[gametype]->rules & GTR_CIRCUIT))) // SRB2kart - "&& player->exiting > 1"
			player->exiting--;

		if (player->exiting && exitcountdown)
			player->exiting = 99; // SRB2kart

		if (player->exiting == 2 || exitcountdown == 2)
		{
			if (server)
			{
				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
			}
		}
	}

	// Make sure spectators always have a score and ring count of 0.
	if (player->spectator)
	{
		//player->score = 0;
		player->rings = 0;
		player->mo->health = 1;
	}

	// SRB2kart 010217
	if (!(gametypes[gametype]->rules & GTR_NOCOUNTDOWN))
	{
		if (leveltime < starttime)
		{
			player->nocontrol = 2;
		}
	}
	else
	{
		if (leveltime < introtime)
		{
			player->nocontrol = 2;
		}
	}

	// Synchronizes the "real" amount of time spent in the level.
	if (!(player->exiting || mapreset) && !(player->pflags & PF_NOCONTEST) && !stoppedclock)
	{
		if (leveltime >= starttime)
		{
			player->realtime = leveltime - starttime;

			if (player->spectator || !circuitmap)
				player->laptime[LAP_CUR] = 0;
			else if (player->laptime[LAP_CUR] != UINT32_MAX)
				player->laptime[LAP_CUR]++; // This is too complicated to sync to realtime, just sorta hope for the best :V
		}
		else
		{
			player->realtime = 0;
			player->laptime[LAP_CUR] = 0;
		}
	}

	if (netgame && cv_antigrief.value != 0 && (gametypes[gametype]->rules & GTR_CIRCUIT))
	{
		INT32 i;
		for (i = 0; i < MAXPLAYERS; i++)
		{
			if (!playeringame[i] || players[i].spectator || players[i].bot)
				continue;
			if (&players[i] == player)
				continue;
			break;
		}

		if (i < MAXPLAYERS && !player->spectator && !player->exiting && !player->bot && !(player->pflags & PF_NOCONTEST))
		{
			const tic_t griefval = cv_antigrief.value * TICRATE;
			const UINT8 n = player - players;

			{
				if (player->grieftime > griefval)
				{
					player->griefstrikes++;
					player->grieftime = 0;

					if (server)
					{
						if ((player->griefstrikes > 2)
#ifndef DEVELOP
							&& !IsPlayerAdmin(n)
#endif
							&& !P_IsLocalPlayer(player)) // P_IsMachineLocalPlayer for DRRR
						{
							// Send kick
							SendKick(n, KICK_MSG_GRIEF);
						}
						else
						{
							// Send spectate
							changeteam_union NetPacket;
							UINT16 usvalue;

							NetPacket.value.l = NetPacket.value.b = 0;
							NetPacket.packet.newteam = 0;
							NetPacket.packet.playernum = n;
							NetPacket.packet.verification = true;

							usvalue = SHORT(NetPacket.value.l|NetPacket.value.b);
							SendNetXCmd(XD_TEAMCHANGE, &usvalue, sizeof(usvalue));
						}
					}
				}
				else if (player->flashing == 0)
				{
					player->grieftime++;
				}
			}
		}
	}

	if (cmd->flags & TICCMD_TYPING)
	{
		/*
		typing_duration is slow to start and slow to stop.

		typing_timer counts down a grace period before the player is not
		actually considered typing anymore.
		*/
		if (cmd->flags & TICCMD_KEYSTROKE)
		{
			/* speed up if we are typing quickly! */
			if (player->typing_duration > 0 && player->typing_timer > 12)
			{
				if (player->typing_duration < 16)
				{
					player->typing_duration = 24;
				}
				else
				{
					/* slows down a tiny bit as it approaches the next dot */
					const UINT8 step = (((player->typing_duration + 15) & ~15) -
							player->typing_duration) / 2;
					player->typing_duration += max(step, 4);
				}
			}

			player->typing_timer = 15;
		}
		else if (player->typing_timer > 0)
		{
			player->typing_timer--;
		}

		/* if we are in the grace period (including currently typing) */
		if (player->typing_timer + player->typing_duration > 0)
		{
			/* always end the cycle on two dots */
			if (player->typing_timer == 0 &&
					(player->typing_duration < 16 || player->typing_duration == 40))
			{
				player->typing_duration = 0;
			}
			else if (player->typing_duration < 63)
			{
				player->typing_duration++;
			}
			else
			{
				player->typing_duration = 16;
			}
		}
	}
	else
	{
		player->typing_timer = 0;
		player->typing_duration = 0;
	}

	player->pflags &= ~PF_HITFINISHLINE;
	
	// experiment: process the drift input *before* player movement instead of after
	if (player->driftmode == DRIFTMODE_INSTANT)
	{
		if (cmd->buttons & BT_DRIFT)
		{
			player->pflags |= PF_DRIFTINPUT;
		}
		else
		{
			player->pflags &= ~PF_DRIFTINPUT;
		}
	}

	// check water content, set stuff in mobj
	P_MobjCheckWater(player->mo);

#ifndef SECTORSPECIALSAFTERTHINK
	if (player->onconveyor != 1 || !P_IsObjectOnGround(player->mo))
		player->onconveyor = 0;
	// check special sectors : damage & secrets

	if (!player->spectator)
		P_PlayerInSpecialSector(player);
#endif

	if (player->playerstate == PST_DEAD)
	{
		if (player->spectator)
			player->mo->renderflags |= RF_GHOSTLY;
		else
			player->mo->renderflags &= ~RF_GHOSTLYMASK;
		P_DeathThink(player);
		LUA_HookPlayer(player, HOOK(PlayerThink));
		return;
	}

	if ((netgame || multiplayer) && player->spectator && !player->bot && cmd->buttons & BT_ATTACK && !player->flashing)
	{
		player->pflags ^= PF_WANTSTOJOIN;
		player->flashing = TICRATE/2 + 1;
		/*if (P_SpectatorJoinGame(player))
			return; // player->mo was removed.*/
		//CONS_Printf("player %s wants to join on tic %d\n", player_names[player-players], leveltime);
	}

	if (player->linktimer /*&& !player->powers[pw_nights_linkfreeze]*/)
	{
		if (--player->linktimer <= 0) // Link timer
			player->linkcount = 0;
	}

	if (player->mo->reactiontime)
	{
		// Reactiontime is used to prevent movement
		// for a bit after a teleport.
		player->mo->reactiontime--;
	}
	else if (player->carry == CR_ZOOMTUBE && player->mo->tracer && player->mo->tracer->type == MT_TUBEWAYPOINT)
	{
		P_DoZoomTube(player);
		player->rmomx = player->rmomy = 0;
	}
	else if (player->loop.radius != 0)
	{
		P_PlayerOrbit(player);
		player->rmomx = player->mo->momx;
		player->rmomy = player->mo->momy;
	}
	else
	{
		// Move around.
		P_MovePlayer(player);
	}

	// Unset statis flag after moving.
	// In other words, if you manually set stasis via code,
	// it lasts for one tic.
	player->pflags &= ~PF_STASIS;

	if (player->onconveyor == 1)
		player->onconveyor = 3;
	else if (player->onconveyor == 3)
		player->cmomy = player->cmomx = 0;

	P_DoBubbleBreath(player); // Spawn Sonic's bubbles
	P_CheckInvincibilityTimer(player); // Spawn Invincibility Sparkles
	
	// "Blur" a bit when you have speed shoes and are going fast enough
	if ((player->powers[pw_super]
		|| player->powers[pw_sneakers]
		|| player->driftboost
		|| player->sneakertimer
		|| player->startboost
		|| player->ringboost)
		&& !(player->invincibilitytimer || player->smonitortimer) // SRB2kart
		&& (player->speed + abs(player->mo->momz)) > FixedMul(20*FRACUNIT,player->mo->scale))
	{
		UINT8 i;
		mobj_t *gmobj = P_SpawnGhostMobj(player->mo);

		gmobj->fuse = 2;
		if (leveltime & 1)
		{
			gmobj->frame &= ~FF_TRANSMASK;
			gmobj->frame |= tr_trans70<<FF_TRANSSHIFT;
		}

		// Hide the mobj from our sights if we're the displayplayer and chasecam is off.
		// Why not just not spawn the mobj?  Well, I'd rather only flirt with
		// consistency so much...
		for (i = 0; i <= splitscreen; i++)
		{
			if (player == &players[displayplayers[i]] && !camera[i].chase)
			{
				gmobj->renderflags |= RF_DONTDRAW;
				break;
			}
		}
	}

	// Counters, time dependent power ups.
	// Time Bonus & Ring Bonus count settings

	if (player->bumpertime)
		player->bumpertime--;

	// Strength counts up to diminish fade
	if (player->powers[pw_sneakers] && player->powers[pw_sneakers] < UINT16_MAX)
		player->powers[pw_sneakers]--;

	if (player->powers[pw_invulnerability] && player->powers[pw_invulnerability] < UINT16_MAX)
		player->powers[pw_invulnerability]--;

	if (player->flashing && player->flashing < UINT16_MAX &&
		(player->spectator || player->flashing < K_GetKartFlashing(player)))
		player->flashing--;

	if (player->nocontrol && player->nocontrol < UINT16_MAX)
	{
		if (!(--player->nocontrol))
			player->pflags &= ~PF_FAILEDSTART;
	}
	else
		player->nocontrol = 0;

	// Flash player after being hit.
	if (!(player->hyudorotimer // SRB2kart - fixes Hyudoro not flashing when it should.
		|| player->growshrinktimer > 0 // Grow doesn't flash either.
		|| (player->respawn) // Respawn timer (for drop dash effect)
		|| (player->pflags & PF_NOCONTEST) // NO CONTEST explosion
		|| ((gametypes[gametype]->rules & GTR_BUMPERS) && player->bumper <= 0 && player->karmadelay)))
	{
		if (player->flashing > 0 && player->flashing < K_GetKartFlashing(player)
			&& (leveltime & 1))
			player->mo->renderflags |= RF_DONTDRAW;
		else
			player->mo->renderflags &= ~RF_DONTDRAW;
	}

	K_KartPlayerThink(player, cmd); // SRB2kart

	DoABarrelRoll(player);

	if (player->carry == CR_SLIDING)
		player->carry = CR_NONE;

	LUA_HookPlayer(player, HOOK(PlayerThink));
}

//
// P_PlayerAfterThink
//
// Thinker for player after all other thinkers have run
//
void P_PlayerAfterThink(player_t *player)
{
	camera_t *thiscam = NULL; // if not one of the displayed players, just don't bother
	UINT8 i;

#ifdef PARANOIA
	if (!player->mo)
	{
		const size_t playeri = (size_t)(player - players);
		I_Error("P_PlayerAfterThink: players[%s].mo == NULL", sizeu1(playeri));
	}
#endif

	if (P_IsObjectOnGround(player->mo) == true)
	{
		player->outrun = 0;
	}

#ifdef SECTORSPECIALSAFTERTHINK
	if (player->onconveyor != 1 || !P_IsObjectOnGround(player->mo))
		player->onconveyor = 0;
	// check special sectors : damage & secrets

	if (!player->spectator)
		P_PlayerInSpecialSector(player);
#endif

	for (i = 0; i <= r_splitscreen; i++)
	{
		if (player == &players[displayplayers[i]])
		{
			thiscam = &camera[i];
			break;
		}
	}

	if (player->shieldtracer && P_MobjWasRemoved(player->shieldtracer))
		P_SetTarget(&player->shieldtracer, NULL);

	if (player->playerstate == PST_DEAD)
	{
		// Followers need handled while dead.
		K_HandleFollower(player);

		if (player->followmobj)
		{
			P_RemoveMobj(player->followmobj);
			P_SetTarget(&player->followmobj, NULL);
		}

		return;
	}

	if (thiscam)
	{
		if (!thiscam->chase) // bob view only if looking through the player's eyes
		{
			P_CalcHeight(player);
			P_CalcPostImg(player, thiscam);
		}
		else
		{
			// defaults to make sure 1st person cam doesn't do anything weird on startup
			player->deltaviewheight = 0;
			player->viewheight = P_GetPlayerViewHeight(player);

			if (player->mo->eflags & MFE_VERTICALFLIP)
				player->viewz = player->mo->z + player->mo->height - player->viewheight;
			else
				player->viewz = player->mo->z + player->viewheight;
		}
	}

	// spectator invisibility and nogravity.
	if ((netgame || multiplayer) && player->spectator)
	{
		player->mo->renderflags |= RF_DONTDRAW;
		player->mo->flags |= MF_NOGRAVITY;
	}

	K_KartPlayerAfterThink(player);

	if (player->followmobj && (player->spectator || player->mo->health <= 0 || player->followmobj->type != player->followitem))
	{
		P_RemoveMobj(player->followmobj);
		P_SetTarget(&player->followmobj, NULL);
	}

	if (!player->spectator && player->mo->health && player->followitem)
	{
		if (!player->followmobj || P_MobjWasRemoved(player->followmobj))
		{
			P_SetTarget(&player->followmobj, P_SpawnMobjFromMobj(player->mo, 0, 0, 0, player->followitem));
			P_SetTarget(&player->followmobj->tracer, player->mo);
			switch (player->followmobj->type)
			{
				default:
					player->followmobj->flags2 |= MF2_LINKDRAW;
					break;
			}
		}

		if (player->followmobj)
		{
			if (LUA_HookFollowMobj(player, player->followmobj) || P_MobjWasRemoved(player->followmobj))
				{;}
			else
			{
				switch (player->followmobj->type)
				{
					default:
						var1 = 1;
						var2 = 0;
						A_CapeChase(player->followmobj);
						break;
				}
			}
		}
	}

	// Run followers in AfterThink, after the players have moved,
	// so a lag value of 1 is exactly attached to the player.
	K_HandleFollower(player);

	if (K_PlayerUsesBotMovement(player))
	{
		K_UpdateBotGameplayVars(player);
	}

	if (thiscam)
	{
		// Store before it gets 0'd out
		thiscam->pmomz = player->mo->pmomz;
	}

	if (P_IsObjectOnGround(player->mo))
		player->mo->pmomz = 0;
}

void P_SetPlayerAngle(player_t *player, angle_t angle)
{
	P_ForceLocalAngle(player, angle, true);
	player->angleturn = angle;
}

angle_t P_GetLocalAngle(player_t *player)
{
	// this function is from vanilla srb2. can you tell?
	// (hint: they have separate variables for all of this shit instead of arrays)
	UINT8 i;

	for (i = 0; i <= r_splitscreen; i++)
	{
		if (player == &players[displayplayers[i]])
			return localangle[i];
	}

	return 0;
}

void P_ForceLocalAngle(player_t *player, angle_t angle, boolean interp)
{
	UINT8 i;

	angle = angle & ~UINT16_MAX;

	for (i = 0; i <= r_splitscreen; i++)
	{
		if (player == &players[displayplayers[i]])
		{
			localangle[i] = angle;
			if (!interp)
			{
				if (camera[i].chase)
					P_ResetCamera(player, &camera[i]);

				R_ResetViewInterpolation(i + 1);
			}
			break;
		}
	}
}

boolean P_PlayerFullbright(player_t *player)
{
	return (player->invincibilitytimer > 0) || (player->smonitortimer > 0);
}

void P_ResetPlayerCheats(void)
{
	INT32 i;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		player_t *player = &players[i];
		mobj_t *thing = player->mo;

		if (!playeringame[i])
			continue;

		player->pflags &= ~(PF_GODMODE);

		if (P_MobjWasRemoved(thing))
			continue;

		thing->flags &= ~(MF_NOCLIP);

		thing->destscale = mapobjectscale;
		P_SetScale(thing, thing->destscale);
	}
}
