// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
// Copyright (C) 2024 by Kart Krew
//
// 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  k_bot.cpp
/// \brief Bot logic & ticcmd generation code

#include <algorithm>

#include <tracy/tracy/Tracy.hpp>

#include "cxxutil.hpp"

#include "doomdef.h"
#include "d_player.h"
#include "g_game.h"
#include "r_main.h"
#include "p_local.h"
#include "k_bot.h"
#include "lua_hook.h"
#include "byteptr.h"
#include "d_net.h" // nodetoplayer
#include "k_kart.h"
#include "z_zone.h"
#include "i_system.h"
#include "p_maputl.h"
#include "d_ticcmd.h"
#include "m_random.h"
#include "r_things.h" // numskins
#include "m_perfstats.h"
#include "m_easing.h"
#include "d_clisrv.h"
#include "k_grandprix.h" // K_CanChangeRules
#include "hu_stuff.h" // HU_AddChatText
#ifdef HAVE_DISCORDRPC
#include "discord.h" // DRPC_UpdatePresence
#endif
#include "i_net.h" // doomcom
#include "blan/b_soc.h"

consvar_t cv_forcebots = CVAR_INIT ("kartforcebots", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);
consvar_t cv_botcontrol = CVAR_INIT ("kartbotcontrol", "On", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);

/*--------------------------------------------------
	void K_SetNameForBot(UINT8 playerNum, const char *realname)

		See header file for description.
--------------------------------------------------*/
void K_SetNameForBot(UINT8 newplayernum, const char *realname)
{
	UINT8 ix = MAXPLAYERS;

	// These names are generally sourced from skins.
	I_Assert(MAXPLAYERNAME >= SKINNAMESIZE+2);

	if (netgame == true)
	{
		// Check if a player is currently using the name, case-insensitively.
		// We only do this if online, because it doesn't matter if there are multiple Eggrobo *off*line.
		// See also EnsurePlayerNameIsGood
		for (ix = 0; ix < MAXPLAYERS; ix++)
		{
			if (ix == newplayernum)
				continue;
			if (playeringame[ix] == false)
				continue;
			if (strcasecmp(realname, player_names[ix]) != 0)
				continue;

			break;
		}
	}

	if (ix == MAXPLAYERS)
	{
		// No conflict detected!
		sprintf(player_names[newplayernum], "%s", realname);
		return;
	}

	// Ok, now we append on the end for duplicates...
	char namebuffer[MAXPLAYERNAME+1];
	sprintf(namebuffer, "%s %c", realname, 'A'+newplayernum);

	// ...and use the actual function, to handle more devious duplication.
	if (!EnsurePlayerNameIsGood(namebuffer, newplayernum))
	{
		// we can't bail from adding the bot...
		// this hopefully uncontroversial pick is all we CAN do
		sprintf(namebuffer, "Bot %u", newplayernum+1);
	}

	// And finally write.
	sprintf(player_names[newplayernum], "%s", namebuffer);
}

/*--------------------------------------------------
	void K_SetBot(UINT8 playerNum, UINT16 skinnum, UINT8 difficulty, botStyle_e style)

		See header file for description.
--------------------------------------------------*/
void K_SetBot(UINT8 newplayernum, UINT16 skinnum, UINT8 difficulty, botStyle_e style)
{
	CONS_Debug(DBG_NETPLAY, "addbot: %d\n", newplayernum);

	G_AddPlayer(newplayernum, newplayernum);

	if (newplayernum+1 > doomcom->numslots)
		doomcom->numslots = (INT16)(newplayernum+1);

	playernode[newplayernum] = servernode;

	players[newplayernum].splitscreenindex = 0;
	players[newplayernum].bot = true;
	players[newplayernum].botvars.difficulty = difficulty;
	players[newplayernum].botvars.style = style;
	players[newplayernum].lives = 9;

	// The bot may immediately become a spectator AT THE START of a GP.
	// For each subsequent round of GP, K_UpdateGrandPrixBots will handle this.
	players[newplayernum].spectator = grandprixinfo.gp && grandprixinfo.initalize;

	skincolornum_t color = static_cast<skincolornum_t>(skins[skinnum].prefcolor);
	const char *realname = skins[skinnum].realname;
	players[newplayernum].skincolor = color;
	K_SetNameForBot(newplayernum, realname);

	SetPlayerSkinByNum(newplayernum, skinnum);

	for (UINT8 i = 0; i < PWRLV_NUMTYPES; i++)
	{
		clientpowerlevels[newplayernum][i] = 0;
	}

	if (netgame)
	{
		HU_AddChatText(va("\x82*Bot %d has been added to the game", newplayernum+1), false);
	}

	LUA_HookInt(newplayernum, HOOK(PlayerJoin));
}

/*--------------------------------------------------
	boolean K_AddBot(UINT16 skin, UINT8 difficulty, botStyle_e style, UINT8 *p)

		See header file for description.
--------------------------------------------------*/
boolean K_AddBot(UINT16 skin, UINT8 difficulty, botStyle_e style, UINT8 *p)
{
	UINT8 newplayernum = *p;

	for (; newplayernum < MAXPLAYERS; newplayernum++)
	{
		if (playeringame[newplayernum] == false)
		{
			// free player slot
			break;
		}
	}

	if (newplayernum >= MAXPLAYERS)
	{
		// nothing is free
		*p = MAXPLAYERS;
		return false;
	}

	K_SetBot(newplayernum, skin, difficulty, style);
	DEBFILE(va("Everyone added bot %d\n", newplayernum));

	// use the next free slot
	*p = newplayernum+1;

	return true;
}

/*--------------------------------------------------
	void K_UpdateMatchRaceBots(void)

		See header file for description.
--------------------------------------------------*/
void K_UpdateMatchRaceBots(void)
{
	const UINT16 defaultbotskin = K_BotDefaultSkin();
	UINT8 difficulty;
	UINT8 pmax = (dedicated ? MAXPLAYERS-1 : MAXPLAYERS);
	UINT8 numplayers = 0;
	UINT8 numbots = 0;
	UINT8 numwaiting = 0;
	SINT8 wantedbots = 0;
	UINT16 usableskins = 0, skincount =  numskins;
	UINT16 grabskins[MAXSKINS+1];
	UINT16 i;

	// Init usable bot skins list
	for (i = 0; i < skincount; i++)
	{
		grabskins[usableskins++] = i;
	}
	grabskins[usableskins] = MAXSKINS;

	if (gamestate == GS_TITLESCREEN)
	{
		difficulty = 0;
	}
	else if ((gametyperules & GTR_BOTS) == 0 && !cv_forcebots.value)
	{
		difficulty = 0;
	}
	else if (K_CanChangeRules() == false)
	{
		difficulty = 0;
	}
	else
	{
		difficulty = cv_kartbot.value;
		if (netgame)
		{
			pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxplayers.value));
		}
		if (cv_ingamecap.value > 0)
		{
			pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_ingamecap.value));
		}
	}

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[i])
		{
			if (!players[i].spectator)
			{
				grabskins[players[i].skin] = MAXSKINS;

				if (players[i].bot)
				{
					numbots++;

					// While we're here, we should update bot difficulty to the proper value.
					players[i].botvars.difficulty = difficulty;

					// Enforce normal style for Match Race
					players[i].botvars.style = BOT_STYLE_NORMAL;
				}
				else
				{
					numplayers++;
				}
			}
			else if (players[i].pflags & PF_WANTSTOJOIN)
			{
				numwaiting++;
			}
		}
	}

	if (difficulty == 0)
	{
		// Remove bots if there are any.
		wantedbots = 0;
	}
	else
	{
		// Add bots to fill up MAXPLAYERS
		wantedbots = pmax - numplayers - numwaiting;

		if (wantedbots < 0)
		{
			wantedbots = 0;
		}
	}

	auto clear_bots = [&numbots](UINT8 max)
	{
		UINT8 i = MAXPLAYERS;
		while (numbots > max && i > 0)
		{
			i--;

			if (playeringame[i] && players[i].bot)
			{
				CL_RemovePlayer(i, KR_LEAVE);
				numbots--;
			}
		}
	};

	if (numbots < wantedbots)
	{
		// We require MORE bots!
		UINT8 newplayernum = 0;

		if (dedicated)
		{
			newplayernum = 1;
		}

		for (i = 0; i < usableskins; i++)
		{
			if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i])))
			{
				continue;
			}

			while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins])))
			{
				usableskins--;
			}

			grabskins[i] = grabskins[usableskins];
			grabskins[usableskins] = MAXSKINS;
		}

		while (numbots < wantedbots)
		{
			UINT16 skinnum = defaultbotskin;

			if (usableskins > 0)
			{
				UINT16 index = P_RandomKey(usableskins);
				skinnum = grabskins[index];
				if (((cv_ingamecap.value > 0) && (usableskins+1 >= cv_ingamecap.value)) || (usableskins+1 >= cv_maxplayers.value))
				{
					grabskins[index] = grabskins[--usableskins];
				}
			}

			if (!K_AddBot(skinnum, difficulty, BOT_STYLE_NORMAL, &newplayernum))
			{
				// Not enough player slots to add the bot, break the loop.
				break;
			}

			numbots++;
		}
	}
	else if (numbots > wantedbots)
	{
		clear_bots(wantedbots);
	}

	// We should have enough bots now :)

#ifdef HAVE_DISCORDRPC
	// Player count change was possible, so update presence
	DRPC_UpdatePresence();
#endif
}

/*--------------------------------------------------
	boolean K_PlayerUsesBotMovement(const player_t *player)

		See header file for description.
--------------------------------------------------*/
boolean K_PlayerUsesBotMovement(const player_t *player)
{
	if (player->bot)
		return true;

	return false;
}

/*--------------------------------------------------
	boolean K_BotCanTakeCut(player_t *player)

		See header file for description.
--------------------------------------------------*/
boolean K_BotCanTakeCut(player_t *player)
{
	if (
		(K_TripwirePassConditions(player) != TRIPWIRE_NONE || K_ApplyOffroad(player) == false)
		|| player->itemtype == KITEM_SNEAKER
		|| player->itemtype == KITEM_ROCKETSNEAKER
		|| player->itemtype == KITEM_INVINCIBILITY
		)
	{
		return true;
	}

	return false;
}

/*--------------------------------------------------
	static fixed_t K_BotSpeedScaled(const player_t *player, fixed_t speed)

		What the bot "thinks" their speed is, for predictions.
		Mainly to make bots brake earlier when on friction sectors.

	Input Arguments:-
		player - The bot player to calculate speed for.
		speed - Raw speed value.

	Return:-
		The bot's speed value for calculations.
--------------------------------------------------*/
static fixed_t K_BotSpeedScaled(const player_t *player, fixed_t speed)
{
	fixed_t result = speed;

	if (!player->pogospring && P_IsObjectOnGround(player->mo) == false)
	{
		// You have no air control, so don't predict too far ahead.
		return 0;
	}

	if (player->mo->movefactor != FRACUNIT)
	{
		fixed_t moveFactor = player->mo->movefactor;

		if (moveFactor == 0)
		{
			moveFactor = 1;
		}

		// Reverse against friction. Allows for bots to
		// acknowledge they'll be moving faster on ice,
		// and to steer harder / brake earlier.
		moveFactor = FixedDiv(FRACUNIT, moveFactor);

		// The full value is way too strong, reduce it.
		moveFactor -= (moveFactor - FRACUNIT)*3/4;

		result = FixedMul(result, moveFactor);
	}

	if (player->mo->standingslope != nullptr)
	{
		const pslope_t *slope = player->mo->standingslope;

		if (!(slope->flags & SL_NOPHYSICS) && abs(slope->zdelta) >= FRACUNIT/21)
		{
			fixed_t slopeMul = FRACUNIT;
			angle_t angle = K_MomentumAngle(player->mo) - slope->xydirection;

			if (P_MobjFlip(player->mo) * slope->zdelta < 0)
				angle ^= ANGLE_180;

			// Going uphill: 0
			// Going downhill: FRACUNIT*2
			slopeMul = FRACUNIT + FINECOSINE(angle >> ANGLETOFINESHIFT);

			// Range: 0.5 to 1.5
			result = FixedMul(result, (FRACUNIT>>1) + (slopeMul >> 1));
		}
	}

	return result;
}

/*--------------------------------------------------
	const botcontroller_t *K_GetBotController(const mobj_t *mobj)

		See header file for description.
--------------------------------------------------*/
const botcontroller_t *K_GetBotController(mobj_t *mobj)
{
	botcontroller_t *ret = nullptr;

	if (P_MobjWasRemoved(mobj) == true)
	{
		return nullptr;
	}

	if (mobj->subsector == nullptr || mobj->subsector->sector == nullptr)
	{
		return nullptr;
	}

	ret = &mobj->subsector->sector->botController;

	ffloor_t *rover = nullptr;
	for (rover = mobj->subsector->sector->ffloors; rover; rover = rover->next)
	{
		if ((rover->fofflags & FOF_EXISTS) == 0)
		{
			continue;
		}

		fixed_t topheight = P_GetFOFTopZ(mobj, mobj->subsector->sector, rover, mobj->x, mobj->y, nullptr);
		fixed_t bottomheight = P_GetFOFBottomZ(mobj, mobj->subsector->sector, rover, mobj->x, mobj->y, nullptr);

		if (mobj->z > topheight || mobj->z + mobj->height < bottomheight)
		{
			continue;
		}

		botcontroller_t *roverController = &rover->master->frontsector->botController;
		if (roverController->flags != 0)
		{
			ret = roverController;
		}
	}

	return ret;
}

/*--------------------------------------------------
	fixed_t K_BotMapModifier(void)

		See header file for description.
--------------------------------------------------*/
fixed_t K_BotMapModifier(void)
{
	constexpr INT32 complexity_scale = 10000;
	fixed_t modifier_max = K_TrackModifierMax();

	if (K_CanChangeRules() == false)
	{
		modifier_max = FRACUNIT;
	}

	const fixed_t complexity_value = std::clamp<fixed_t>(
		FixedDiv(K_GetTrackComplexity(), complexity_scale),
		-FixedDiv(FRACUNIT, modifier_max),
		modifier_max
	);

	return FRACUNIT + complexity_value;
}

/*--------------------------------------------------
	static UINT32 K_BotRubberbandDistance(const player_t *player)

		Calculates the distance away from 1st place that the
		bot should rubberband to.

	Input Arguments:-
		player - Player to compare.

	Return:-
		Distance to add, as an integer.
--------------------------------------------------*/
static UINT32 K_BotRubberbandDistance(const player_t *player)
{
	const UINT32 spacing = FixedDiv(640 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)) / FRACUNIT;
	const UINT8 portpriority = player - players;
	UINT8 pos = 1;
	UINT8 i;

	if (player->botvars.rival)
	{
		// The rival should always try to be the front runner for the race.
		return 0;
	}

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (i == portpriority)
		{
			continue;
		}

		if (!playeringame[i] || players[i].spectator)
		{
			continue;
		}

		if (!players[i].bot)
		{
			continue;
		}

		// First check difficulty levels, then score, then settle it with port priority!
		if (player->botvars.difficulty < players[i].botvars.difficulty)
		{
			pos += 3;
		}
		else if (player->score < players[i].score)
		{
			pos += 2;
		}
		else if (i < portpriority)
		{
			pos += 1;
		}
	}

	return (pos * spacing);
}

/*--------------------------------------------------
	fixed_t K_BotRubberband(const player_t *player)

		See header file for description.
--------------------------------------------------*/
fixed_t K_BotRubberband(const player_t *player)
{
	if (player->exiting)
	{
		// You're done, we don't need to rubberband anymore.
		return FRACUNIT;
	}

	const botcontroller_t *botController = K_GetBotController(player->mo);
	if (botController != nullptr && (botController->flags & TMBOT_NORUBBERBAND) == TMBOT_NORUBBERBAND) // Disable rubberbanding
	{
		return FRACUNIT;
	}

	fixed_t difficultyEase = ((player->botvars.difficulty - 1) * FRACUNIT) / (MAXBOTDIFFICULTY - 1);

	// Lv.   1: x0.65 avg
	// Lv. MAX: x1.05 avg
	const fixed_t rubberBase = Easing_OutSine(
		difficultyEase,
		FRACUNIT * 65 / 100,
		FRACUNIT * 105 / 100
	);

	// +/- x0.35
	const fixed_t rubberStretchiness = FixedMul(
		FixedDiv(
			35 * FRACUNIT / 100,
			K_GetKartGameSpeedScalar(gamespeed)
		),
		K_BotMapModifier()
	);

	// Lv.   1: x0.4 min
	// Lv. MAX: x0.85 min
	constexpr fixed_t rubberSlowMin = FRACUNIT / 2;
	const fixed_t rubberSlow = std::max<fixed_t>( rubberBase - rubberStretchiness, rubberSlowMin );

	// Lv.   1: x0.9 max
	// Lv. MAX: x1.35 max
	constexpr fixed_t rubberFastMax = FRACUNIT * 3 / 2;
	const fixed_t rubberFast = std::min<fixed_t>( rubberBase + rubberStretchiness, rubberFastMax );

	fixed_t rubberband = FRACUNIT >> 1;
	player_t *firstplace = nullptr;
	size_t i = SIZE_MAX;

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

		// Don't rubberband to ourselves...
		if (player == &players[i])
		{
			continue;
		}

#if 0
		// Only rubberband up to players.
		if (players[i].bot)
		{
			continue;
		}
#endif

		if (firstplace == nullptr || players[i].distancetofinish < firstplace->distancetofinish)
		{
			firstplace = &players[i];
		}
	}

	if (firstplace != nullptr)
	{
		const UINT32 spacing = FixedDiv(10240 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)) / FRACUNIT;
		const UINT32 wanteddist = firstplace->distancetofinish + K_BotRubberbandDistance(player);
		const INT32 distdiff = player->distancetofinish - wanteddist;

		rubberband = FixedDiv(distdiff + spacing, spacing * 2);

		if (player->boostpower < FRACUNIT)
		{
			// Do not let bots cheese offroad as much.
			rubberband = FixedMul(rubberband, player->boostpower);
		}

		if (P_MobjWasRemoved(player->mo) == false && player->mo->movefactor < FRACUNIT)
		{
			// Do not let bots speed up on ice too much.
			rubberband = FixedMul(rubberband, player->mo->movefactor);
		}

		if (rubberband > FRACUNIT)
		{
			rubberband = FRACUNIT;
		}
		else if (rubberband < 0)
		{
			rubberband = 0;
		}
	}

	UINT32 scaled_dist = player->distancetofinish;
	if (mapobjectscale != FRACUNIT)
	{
		// Bring back to normal scale.
		scaled_dist = FixedDiv(scaled_dist, mapobjectscale);
	}

	return Easing_Linear(rubberband, rubberSlow, rubberFast);
}

/*--------------------------------------------------
	fixed_t K_UpdateRubberband(player_t *player)

		See header file for description.
--------------------------------------------------*/
fixed_t K_UpdateRubberband(player_t *player)
{
	fixed_t dest = K_BotRubberband(player);
	fixed_t ret = player->botvars.rubberband;

	// Ease into the new value.
	ret += (dest - player->botvars.rubberband) / 8;

	return ret;
}

/*--------------------------------------------------
	fixed_t K_DistanceOfLineFromPoint(fixed_t v1x, fixed_t v1y, fixed_t v2x, fixed_t v2y, fixed_t cx, fixed_t cy)

		See header file for description.
--------------------------------------------------*/
fixed_t K_DistanceOfLineFromPoint(fixed_t v1x, fixed_t v1y, fixed_t v2x, fixed_t v2y, fixed_t px, fixed_t py)
{
	// Copy+paste from P_ClosestPointOnLine :pensive:
	fixed_t startx = v1x;
	fixed_t starty = v1y;
	fixed_t dx = v2x - v1x;
	fixed_t dy = v2y - v1y;

	fixed_t cx, cy;
	fixed_t vx, vy;
	fixed_t magnitude;
	fixed_t t;

	cx = px - startx;
	cy = py - starty;

	vx = dx;
	vy = dy;

	magnitude = R_PointToDist2(v2x, v2y, startx, starty);
	vx = FixedDiv(vx, magnitude);
	vy = FixedDiv(vy, magnitude);

	t = (FixedMul(vx, cx) + FixedMul(vy, cy));

	vx = FixedMul(vx, t);
	vy = FixedMul(vy, t);

	return R_PointToDist2(px, py, startx + vx, starty + vy);
}

/*--------------------------------------------------
	static void K_GetBotWaypointRadius(waypoint_t *waypoint, fixed_t *smallestRadius, fixed_t *smallestScaled)

		Calculates a new waypoint radius size to use, making it
		thinner depending on how harsh the turn is.

	Input Arguments:-
		waypoint - Waypoint to retrieve the radius of.

	Return:-
		N/A
--------------------------------------------------*/
static void K_GetBotWaypointRadius(waypoint_t *const waypoint, fixed_t *smallestRadius, fixed_t *smallestScaled)
{
	static const fixed_t maxReduce = FRACUNIT/32;
	static const angle_t maxDelta = ANGLE_22h;

	fixed_t radius = waypoint->mobj->radius;
	fixed_t reduce = FRACUNIT;
	angle_t delta = 0;

	size_t i, j;

	for (i = 0; i < waypoint->numnextwaypoints; i++)
	{
		const waypoint_t *next = waypoint->nextwaypoints[i];
		const angle_t nextAngle = R_PointToAngle2(
			waypoint->mobj->x, waypoint->mobj->y,
			next->mobj->x, next->mobj->y
		);

		for (j = 0; j < waypoint->numprevwaypoints; j++)
		{
			const waypoint_t *prev = waypoint->prevwaypoints[j];
			const angle_t prevAngle = R_PointToAngle2(
				prev->mobj->x, prev->mobj->y,
				waypoint->mobj->x, waypoint->mobj->y
			);

			delta = std::max<angle_t>(delta, AngleDelta(nextAngle, prevAngle));
		}
	}

	if (delta > maxDelta)
	{
		delta = maxDelta;
	}

	reduce = FixedDiv(delta, maxDelta);
	reduce = FRACUNIT + FixedMul(reduce, maxReduce - FRACUNIT);

	*smallestRadius = std::min<fixed_t>(*smallestRadius, radius);
	*smallestScaled = std::min<fixed_t>(*smallestScaled, FixedMul(radius, reduce));
}

static fixed_t K_ScaleWPDistWithSlope(fixed_t disttonext, angle_t angletonext, const pslope_t *slope, SINT8 flip)
{
	if (slope == nullptr)
	{
		return disttonext;
	}

	if ((slope->flags & SL_NOPHYSICS) == 0 && abs(slope->zdelta) >= FRACUNIT/21)
	{
		// Displace the prediction to go with the slope physics.
		fixed_t slopeMul = FRACUNIT;
		angle_t angle = angletonext - slope->xydirection;

		if (flip * slope->zdelta < 0)
		{
			angle ^= ANGLE_180;
		}

		// Going uphill: 0
		// Going downhill: FRACUNIT*2
		slopeMul = FRACUNIT + FINECOSINE(angle >> ANGLETOFINESHIFT);

		// Range: 0.25 to 1.75
		return FixedMul(disttonext, (FRACUNIT >> 2) + ((slopeMul * 3) >> 2));
	}

	return disttonext;
}

/*--------------------------------------------------
	static botprediction_t *K_CreateBotPrediction(const player_t *player)

		Calculates a point further along the track to attempt to drive towards.

	Input Arguments:-
		player - Player to compare.

	Return:-
		Bot prediction struct.
--------------------------------------------------*/
static botprediction_t *K_CreateBotPrediction(player_t *player)
{
	ZoneScoped;

	const precise_t time = I_GetPreciseTime();

	const INT16 handling = K_GetKartTurnValue(player, KART_FULLTURN); // Reduce prediction based on how fast you can turn

	const tic_t futuresight = (TICRATE * KART_FULLTURN) / std::max<INT16>(1, handling); // How far ahead into the future to try and predict
	const fixed_t speed = K_BotSpeedScaled(player, P_AproxDistance(player->mo->momx, player->mo->momy));

	const INT32 startDist = 0; //(DEFAULT_WAYPOINT_RADIUS * mapobjectscale) / FRACUNIT;
	const INT32 maxDist = (DEFAULT_WAYPOINT_RADIUS * 3 * mapobjectscale) / FRACUNIT; // This function gets very laggy when it goes far distances, and going too far isn't very helpful anyway.
	const INT32 distance = std::min<INT32>(((speed / FRACUNIT) * static_cast<INT32>(futuresight)) + startDist, maxDist);

	// Halves radius when encountering a wall on your way to your destination.
	fixed_t radReduce = FRACUNIT;

	fixed_t radius = INT32_MAX;
	fixed_t radiusScaled = INT32_MAX;

	INT32 distanceleft = distance;
	angle_t angletonext = ANGLE_MAX;
	INT32 disttonext = INT32_MAX;
	INT32 distscaled = INT32_MAX;
	pslope_t *nextslope = player->mo->standingslope;

	waypoint_t *wp = player->nextwaypoint;
	mobj_t *prevwpmobj = player->mo;

	const boolean useshortcuts = K_BotCanTakeCut(player);
	const boolean huntbackwards = false;
	boolean pathfindsuccess = false;
	path_t pathtofinish = {0};

	botprediction_t *predict = nullptr;
	size_t i;

	if (wp == nullptr || P_MobjWasRemoved(wp->mobj) == true)
	{
		// Can't do any of this if we don't have a waypoint.
		return nullptr;
	}

	predict = static_cast<botprediction_t *>(Z_Calloc(sizeof(botprediction_t), PU_LEVEL, nullptr));

	// Init defaults in case of pathfind failure
	angletonext = R_PointToAngle2(prevwpmobj->x, prevwpmobj->y, wp->mobj->x, wp->mobj->y);
	disttonext = P_AproxDistance(prevwpmobj->x - wp->mobj->x, prevwpmobj->y - wp->mobj->y);
	nextslope = wp->mobj->standingslope;
	distscaled = K_ScaleWPDistWithSlope(disttonext, angletonext, nextslope, P_MobjFlip(wp->mobj)) / FRACUNIT;

	pathfindsuccess = K_PathfindThruCircuit(
		wp, (unsigned)distanceleft,
		&pathtofinish,
		useshortcuts, huntbackwards
	);

	// Go through the waypoints until we've traveled the distance we wanted to predict ahead!
	if (pathfindsuccess == true)
	{
		for (i = 0; i < pathtofinish.numnodes; i++)
		{
			wp = (waypoint_t *)pathtofinish.array[i].nodedata;

			if (i == 0)
			{
				prevwpmobj = player->mo;
			}
			else
			{
				prevwpmobj = ((waypoint_t *)pathtofinish.array[ i - 1 ].nodedata)->mobj;
			}

			angletonext = R_PointToAngle2(prevwpmobj->x, prevwpmobj->y, wp->mobj->x, wp->mobj->y);
			disttonext = P_AproxDistance(prevwpmobj->x - wp->mobj->x, prevwpmobj->y - wp->mobj->y);
			nextslope = wp->mobj->standingslope;
			distscaled = K_ScaleWPDistWithSlope(disttonext, angletonext, nextslope, P_MobjFlip(wp->mobj)) / FRACUNIT;

			if (P_TraceBotTraversal(player->mo, wp->mobj) == false)
			{
				// If we can't get a direct path to this waypoint, reduce our prediction drastically.
				distscaled *= 4;
				radReduce = FRACUNIT >> 1;
			}

			K_GetBotWaypointRadius(wp, &radius, &radiusScaled);
			distanceleft -= distscaled;

			if (distanceleft <= 0)
			{
				// We're done!!
				break;
			}
		}

		Z_Free(pathtofinish.array);
	}

	// Set our predicted point's coordinates,
	// and use the smallest radius of all of the waypoints in the chain!
	predict->x = wp->mobj->x;
	predict->y = wp->mobj->y;

	predict->baseRadius = radius;
	predict->radius = FixedMul(radiusScaled, radReduce);

	// Set the prediction coordinates between the 2 waypoints if there's still distance left.
	if (distanceleft > 0)
	{
		// Scaled with the leftover anglemul!
		predict->x += P_ReturnThrustX(nullptr, angletonext, std::min<fixed_t>(disttonext, distanceleft) * FRACUNIT);
		predict->y += P_ReturnThrustY(nullptr, angletonext, std::min<fixed_t>(disttonext, distanceleft) * FRACUNIT);
	}

	ps_bots[player - players].prediction += I_GetPreciseTime() - time;
	return predict;
}

/*--------------------------------------------------
	static void K_DrawPredictionDebug(botprediction_t *predict, const player_t *player)

		Draws objects to show where the viewpoint bot is trying to go.

	Input Arguments:-
		predict - The prediction to visualize.
		player - The bot player this prediction is for.

	Return:-
		None
--------------------------------------------------*/
static void K_DrawPredictionDebug(botprediction_t *predict, const player_t *player)
{
	mobj_t *debugMobj = nullptr;
	angle_t sideAngle = ANGLE_MAX;
	UINT8 i = UINT8_MAX;

	I_Assert(predict != nullptr);
	I_Assert(player != nullptr);
	I_Assert(player->mo != nullptr && P_MobjWasRemoved(player->mo) == false);

	sideAngle = player->mo->angle + ANGLE_90;

	debugMobj = P_SpawnMobj(predict->x, predict->y, player->mo->z, MT_SPARK);
	P_SetMobjState(debugMobj, S_THOK);

	debugMobj->frame &= ~FF_TRANSMASK;
	debugMobj->frame |= FF_TRANS20|FF_FULLBRIGHT;

	debugMobj->color = SKINCOLOR_ORANGE;
	P_SetScale(debugMobj, debugMobj->destscale * 2);

	debugMobj->tics = 2;

	for (i = 0; i < 2; i++)
	{
		mobj_t *radiusMobj = nullptr;
		fixed_t radiusX = predict->x, radiusY = predict->y;

		if (i & 1)
		{
			radiusX -= FixedMul(predict->radius, FINECOSINE(sideAngle >> ANGLETOFINESHIFT));
			radiusY -= FixedMul(predict->radius, FINESINE(sideAngle >> ANGLETOFINESHIFT));
		}
		else
		{
			radiusX += FixedMul(predict->radius, FINECOSINE(sideAngle >> ANGLETOFINESHIFT));
			radiusY += FixedMul(predict->radius, FINESINE(sideAngle >> ANGLETOFINESHIFT));
		}

		radiusMobj = P_SpawnMobj(radiusX, radiusY, player->mo->z, MT_SPARK);
		P_SetMobjState(radiusMobj, S_THOK);

		radiusMobj->frame &= ~FF_TRANSMASK;
		radiusMobj->frame |= FF_TRANS20|FF_FULLBRIGHT;

		radiusMobj->color = SKINCOLOR_YELLOW;
		P_SetScale(debugMobj, debugMobj->destscale / 2);

		radiusMobj->tics = 2;
	}
}

/*--------------------------------------------------
	fixed_t K_BotDetermineDriftSkill(player_t *player)

		Calculates drift skill for a player based on stats.

	Input Arguments:-
		player - Player to get drift skill for.

	Return:-
		Calculated drift skill.
--------------------------------------------------*/
fixed_t K_BotDetermineDriftSkill(player_t *player)
{
	return ((FRACUNIT * (player->kartspeed + player->kartweight)) / 18);
}

static void K_WaypointGetDirectionVector(waypoint_t *wp1, waypoint_t *wp2, vector3_t *a_o)
{
	vector3_t v1, v2;

	v1.x = wp1->mobj->x;
	v1.y = wp1->mobj->y;
	v1.x = wp1->mobj->z;

	v2.x = wp2->mobj->x;
	v2.y = wp2->mobj->y;
	v2.x = wp2->mobj->z;

	FV3_SubEx(&v1,&v2,a_o);
	FV3_Normalize(a_o);
}

/*--------------------------------------------------
	void K_BotSetDriftState(player_t *player, botdrift_t newstate, tic_t lockout)

		See header file for description.
--------------------------------------------------*/
void K_BotSetDriftState(player_t *player, botdrift_t newstate, tic_t lockout)
{
	if (newstate != player->botvars.driftstate)
	{
		player->botvars.driftstate = newstate;
		player->botvars.drifttime = 0;
	}

	if (lockout)
		player->botvars.driftlockout = lockout;
}

#define MINBOTDRIFT (KART_FULLTURN * 2) / 3 // 0.66

/*--------------------------------------------------
	static INT32 K_BotStartDrift(player_t* player)

		Begins and ends "forced" drifts on a per-waypoint basis.

	Input Arguments:-
		player - Player to begin the drift for.

	Return:-
		Override value for turn amount.
--------------------------------------------------*/
static void K_BotStartDrift(player_t* player)
{
	// Handle DRIFTING towards waypoints!
	boolean shouldDrift;
	fixed_t botDriftSpeed;
	driftSetting_e driftsetting = DRIFT_NONE;
	fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false));

	if (speedfactor < FRACUNIT/2)
	{
		// don't bother if we're going too slow
		K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT);
		return;
	}

	if (speedfactor > (6-gamespeed)*FRACUNIT/3)
	{
		// likewise, don't bother if we're going too fast
		K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT/2);
		return;
	}

	if (player->botvars.driftlockout)
	{
		// things are not working out in our favor
		player->botvars.driftlockout--;
		return;
	}

	// check for waypoints ahead of us with drift settings, based on our current speed
	path_t path = {0};
	INT32 maxdist = FixedInt(cv_test1.value) + gamespeed*50;
	maxdist = FixedMul(maxdist, speedfactor * (player->botvars.driftstate == DRIFTSTATE_ACTIVE ? 1 : 2));

	if (maxdist >= 0 && K_PathfindThruCircuit(player->currentwaypoint, maxdist, &path, false, false))
	{
		for (size_t i = 0; i < path.numnodes; i++)
		{
			waypoint_t *wp =  static_cast<waypoint_t *>(path.array[i].nodedata);
			if (wp->driftsettings)
				driftsetting = static_cast<driftSetting_e>(wp->driftsettings);

			// don't break on DRIFT_END waypoints,
			// we could miss a drift waypoint right in front of it!
			if (driftsetting != DRIFT_NONE && driftsetting != DRIFT_END)
				break;
		}
		Z_Free(path.array);
	}

	if (driftsetting == DRIFT_NONE)
	{
		// No waypoints, nothing we can do here.
		return;
	}

	shouldDrift = false;
	botDriftSpeed = FixedMul(K_GetKartSpeed(player, false, false),
							 FixedPercentage(BOTDRIFTPERCENT));

	if (driftsetting == DRIFT_END)
	{
		if (player->botvars.driftstate != DRIFTSTATE_AUTO)
			K_BotSetDriftState(player, DRIFTSTATE_ENDING, 0);
	}
	else if (driftsetting > DRIFT_NONE && driftsetting < DRIFT_END
	         && player->botvars.driftstate == DRIFTSTATE_AUTO)
	{
		// Randomly decide to drift based on our skill at drifting,
		// and how fast we're moving.
		fixed_t driftpotential = P_RandomKey(MAXDRIFTSKILL);

		if ((driftpotential <= player->botvars.driftskill) &&
			(botDriftSpeed <= player->speed))
		{
			shouldDrift = true;
		}

		if (shouldDrift)
		{
			// Start our drift based on the waypoint's drift settings.
			SINT8 driftturn = 0;

			switch (driftsetting)
			{
			case DRIFT_PWRSLIDE_L:
				driftturn = -2;
				break;
			case DRIFT_LEFT:
				driftturn = -1;
				break;
			case DRIFT_PWRSLIDE_R:
				driftturn = 2;
				break;
			case DRIFT_RIGHT:
				driftturn = 1;
				break;
			default:
				break;
			}

			player->botvars.driftturn = driftturn;
			K_BotSetDriftState(player, DRIFTSTATE_STARTING, 0);
		}
	}
}

/*--------------------------------------------------
	static INT32 K_HandleBotTrack(const player_t *player, ticcmd_t *cmd, botprediction_t *predict)

		Determines inputs for standard track driving.

	Input Arguments:-
		player - Player to generate the ticcmd for.
		cmd - The player's ticcmd to modify.
		predict - Pointer to the bot's prediction.

	Return:-
		New value for turn amount.
--------------------------------------------------*/
static INT32 K_HandleBotTrack(player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle)
{
	ZoneScoped;

	// Handle steering towards waypoints!
	INT32 turnamt = 0;
	SINT8 turnsign = 0;
	angle_t moveangle;
	INT32 anglediff, anglediff2;
	fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false));

	I_Assert(predict != nullptr);

	moveangle = player->mo->angle;
	anglediff = AngleDeltaSigned(moveangle, destangle);

	// line up for an incoming drift
	if (player->botvars.driftstate == DRIFTSTATE_STARTING)
	{
		anglediff += FixedMul(ANG10-ANG2, speedfactor) * player->botvars.driftturn;
	}

	if (anglediff < 0)
	{
		turnsign = 1;
	}
	else
	{
		turnsign = -1;
	}

	anglediff2 = anglediff;
	anglediff = abs(anglediff);
	turnamt = KART_FULLTURN * turnsign;

	if (anglediff > ANGLE_67h)
	{
		// Wrong way!
		cmd->forwardmove = -MAXPLMOVE;
		cmd->buttons |= BT_BRAKE;
	}
	else
	{
		const fixed_t playerwidth = (player->mo->radius * 2);
		fixed_t realrad = predict->radius*3/4; // Remove a "safe" distance away from the edges of the road
		fixed_t rad = realrad;
		fixed_t dirdist = K_DistanceOfLineFromPoint(
			player->mo->x, player->mo->y,
			player->mo->x + FINECOSINE(moveangle >> ANGLETOFINESHIFT), player->mo->y + FINESINE(moveangle >> ANGLETOFINESHIFT),
			predict->x, predict->y
		);

		if (realrad < playerwidth)
		{
			realrad = playerwidth;
		}

		// Become more precise based on how hard you need to turn
		// This makes predictions into turns a little nicer
		// Facing 90 degrees away from the predicted point gives you 0 radius
		rad = FixedMul(rad,
			FixedDiv(std::max<angle_t>(0, ANGLE_90 - anglediff), ANGLE_90)
		);

		// Become more precise the slower you're moving
		// Also helps with turns
		// Full speed uses full radius
		rad = FixedMul(rad,
			FixedDiv(K_BotSpeedScaled(player, player->speed), K_GetKartSpeed(player, false, false))
		);

		// Cap the radius to reasonable bounds
		if (rad > realrad)
		{
			rad = realrad;
		}
		else if (rad < playerwidth)
		{
			rad = playerwidth;
		}

		// Full speed ahead!
		cmd->buttons |= BT_ACCELERATE;
		cmd->forwardmove = MAXPLMOVE;

		if (dirdist <= rad
		 && player->botvars.driftstate != DRIFTSTATE_STARTING) // steer towards waypoints when starting drift
		{
			// Going the right way, don't turn at all.
			turnamt = 0;
		}

		// 0.5 on Easy, 1.0 on Normal, 1.5 on Hard.
		//INT32 mindriftamt = FixedMul(MINBOTDRIFT * (cv_kartspeed.value + 1), 2 * FRACUNIT);

		// Start or continue a drift.
		if (player->botvars.driftstate == DRIFTSTATE_ACTIVE || player->botvars.driftstate == DRIFTSTATE_ENDING)
		{
			cmd->buttons |= BT_DRIFT;

			fixed_t angofs = K_GetKartSpeedFromStat(5 - (player->kartspeed - 5), false) * -player->botvars.driftturn;

			// adjust for speed
			angofs = FixedMul(angofs, speedfactor - (2-gamespeed)*FRACUNIT/4);

			fixed_t driftpower = angofs - FixedDiv(anglediff2, ANG1);

			// arbitrary divider on the final driftpower
			driftpower /= FixedInt(cv_test2.value);

			// brakedrift if we're steering too hard
			if (abs(driftpower) >= FRACUNIT)
				cmd->buttons |= BT_BRAKE;

			// get the raw turn value and "invert" it (higher weight needs harder steering!)
			INT16 turnvalue = abs(K_GetKartTurnValue(player, KART_FULLTURN * (player->botvars.driftturn < 0 ? 1 : -1)));
			turnvalue = 541 - (turnvalue - 541); // weight 5 = 541

			turnamt = std::clamp(FixedMul(driftpower, turnvalue), -KART_FULLTURN, KART_FULLTURN);
		}
		/*
		else if ((turnamt) && (player->botvars.driftstate == DRIFTSTATE_AUTO) &&
			 (turnpower > FixedPercentage(DRIFTSTARTPCT)))
		{
			// TODO: Figure out a drift prediction system.
		}
		*/
	}

	return turnamt;
}

#undef MINBOTDRIFT

/*--------------------------------------------------
	static INT32 K_HandleBotReverse(const player_t *player, ticcmd_t *cmd, botprediction_t *predict)

		Determines inputs for reversing.

	Input Arguments:-
		player - Player to generate the ticcmd for.
		cmd - The player's ticcmd to modify.
		predict - Pointer to the bot's prediction.

	Return:-
		New value for turn amount.
--------------------------------------------------*/
/*static INT32 K_HandleBotReverse(player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle)
{
	ZoneScoped;

	// Handle steering towards waypoints!
	INT32 turnamt = 0;
	SINT8 turnsign = 0;
	angle_t moveangle, angle;
	INT16 anglediff, momdiff;

	if (predict != nullptr)
	{
		// TODO: Should we reverse through bot controllers?
		return K_HandleBotTrack(player, cmd, predict, destangle);
	}

	if (player->nextwaypoint == nullptr
		|| player->nextwaypoint->mobj == nullptr
		|| P_MobjWasRemoved(player->nextwaypoint->mobj))
	{
		// No data available...
		return 0;
	}

	if ((player->nextwaypoint->prevwaypoints != nullptr)
		&& (player->nextwaypoint->numprevwaypoints > 0U))
	{
		size_t i;
		for (i = 0U; i < player->nextwaypoint->numprevwaypoints; i++)
		{
			if (!K_GetWaypointIsEnabled(player->nextwaypoint->prevwaypoints[i]))
			{
				continue;
			}

			destangle = R_PointToAngle2(
				player->nextwaypoint->prevwaypoints[i]->mobj->x, player->nextwaypoint->prevwaypoints[i]->mobj->y,
				player->nextwaypoint->mobj->x, player->nextwaypoint->mobj->y
			);

			break;
		}
	}

	// Calculate turn direction first.
	moveangle = player->mo->angle;
	angle = (moveangle - destangle);

	if (angle < ANGLE_180)
	{
		turnsign = -1; // Turn right
		anglediff = AngleFixed(angle)>>FRACBITS;
	}
	else
	{
		turnsign = 1; // Turn left
		anglediff = 360-(AngleFixed(angle)>>FRACBITS);
	}

	anglediff = abs(anglediff);
	turnamt = KART_FULLTURN * turnsign;

	// Now calculate momentum
	momdiff = 180;
	if (player->speed > player->mo->scale)
	{
		momdiff = 0;
		moveangle = K_MomentumAngle(player->mo);
		angle = (moveangle - destangle);

		if (angle < ANGLE_180)
		{
			momdiff = AngleFixed(angle)>>FRACBITS;
		}
		else
		{
			momdiff = 360-(AngleFixed(angle)>>FRACBITS);
		}

		momdiff = abs(momdiff);
	}

	if (anglediff > 90 || momdiff < 90)
	{
		// We're not facing the track,
		// or we're going too fast.
		// Let's E-Brake.
		cmd->forwardmove = 0;
		cmd->buttons |= BT_ACCELERATE|BT_BRAKE;
	}
	else
	{
		fixed_t slopeMul = FRACUNIT;

		if (player->mo->standingslope != nullptr)
		{
			const pslope_t *slope = player->mo->standingslope;

			if (!(slope->flags & SL_NOPHYSICS) && abs(slope->zdelta) >= FRACUNIT/21)
			{
				angle_t sangle = player->mo->angle - slope->xydirection;

				if (P_MobjFlip(player->mo) * slope->zdelta < 0)
					sangle ^= ANGLE_180;

				slopeMul = FRACUNIT - FINECOSINE(sangle >> ANGLETOFINESHIFT);
			}
		}

#define STEEP_SLOPE (FRACUNIT*11/10)
		if (slopeMul > STEEP_SLOPE)
		{
			// Slope is too steep to reverse -- EBrake.
			cmd->forwardmove = 0;
			cmd->buttons |= BT_ACCELERATE|BT_BRAKE;
		}
		else
		{
			cmd->forwardmove = -MAXPLMOVE;
			cmd->buttons |= BT_BRAKE; //|BT_LOOKBACK
		}
#undef STEEP_SLOPE

		if (anglediff < 10)
		{
			turnamt = 0;
		}
	}

	return turnamt;
}*/

/*--------------------------------------------------
	static void K_BuildBotTiccmdNormal(const player_t *player, ticcmd_t *cmd)

		Build ticcmd for bots with a style of BOT_STYLE_NORMAL
--------------------------------------------------*/
static void K_BuildBotTiccmdNormal(player_t *player, ticcmd_t *cmd)
{
	precise_t t = 0;

	botprediction_t *predict = nullptr;
	auto predict_finally = srb2::finally([&predict]() { Z_Free(predict); });

	angle_t destangle = 0;
	INT32 turnamt = 0;

	if (!(gametyperules & GTR_BOTS) // No bot behaviors
		|| K_GetNumWaypoints() == 0 // No waypoints
		|| leveltime <= introtime // During intro camera
		|| player->playerstate == PST_DEAD // Dead, respawning.
		|| player->mo->scale <= 1) // Post-finish "death" animation
	{
		// No need to do anything else.
		return;
	}

	if (player->exiting && player->nextwaypoint == K_GetFinishLineWaypoint() && ((mapheaderinfo[gamemap - 1]->levelflags & LF_SECTIONRACE) == LF_SECTIONRACE))
	{
		// Sprint map finish, don't give Sal's children migraines trying to pathfind out
		return;
	}

	// Defanging bots for testing.
	if (!cv_botcontrol.value)
		return;

	// Actual gameplay behaviors below this block!
	const botcontroller_t *botController = K_GetBotController(player->mo);

	if (botController != nullptr && (botController->flags & TMBOT_NOCONTROL) == TMBOT_NOCONTROL)
	{
		// Disable bot controls entirely.
		return;
	}

	if (player->exiting)
	{

		//Bot finish
		// TODO: Make bots spin around like a player would based on random chance
		return;
	}

	if (player->botvars.respawnconfirm >= BOTRESPAWNCONFIRM)
	{
		// We want to respawn. Simply hold brake and stop here!
		cmd->buttons &= ~BT_ACCELERATE|BT_DRIFT|BT_ATTACK;
		if (player->speed > 0)
		{
			cmd->buttons |= (BT_BRAKE);
			cmd->bot.respawnconfirm++;
		}

		if ((player->speed < 10*FRACUNIT))
		{
			cmd->bot.respawnconfirm = TICRATE;
		}
		return;
	}
	else
	{
		cmd->bot.respawnconfirm = 0;
	}
	destangle = player->mo->angle;

	boolean forcedDir = false;
	if (botController != nullptr && (botController->flags & TMBOT_FORCEDIR) == TMBOT_FORCEDIR)
	{
		const fixed_t dist = DEFAULT_WAYPOINT_RADIUS * player->mo->scale;

		// Overwritten prediction
		predict = static_cast<botprediction_t *>(Z_Calloc(sizeof(botprediction_t), PU_STATIC, nullptr));

		predict->x = player->mo->x + FixedMul(dist, FINECOSINE(botController->forceAngle >> ANGLETOFINESHIFT));
		predict->y = player->mo->y + FixedMul(dist, FINESINE(botController->forceAngle >> ANGLETOFINESHIFT));
		predict->radius = (DEFAULT_WAYPOINT_RADIUS / 4) * mapobjectscale;

		forcedDir = true;
	}

	if (forcedDir == true)
	{
		destangle = R_PointToAngle2(player->mo->x, player->mo->y, predict->x, predict->y);
		turnamt = K_HandleBotTrack(player, cmd, predict, destangle);
	}
	else if (leveltime <= starttime)
	{
		UINT8 timing = P_RandomRange(0, 5);
		UINT8 finaltiming = (MAXBOTDIFFICULTY/2)-(player->botvars.difficulty/2)+timing;
		if (player->botvars.difficulty > 4)
		{
			if (leveltime >= starttime-TICRATE-TICRATE/7+finaltiming)
			{
				cmd->buttons |= BT_ACCELERATE;
				cmd->forwardmove = MAXPLMOVE;
			}
		}
	}
	else
	{
		// Handle steering towards waypoints!
		if (predict == nullptr)
		{
			// Create a prediction.
			predict = K_CreateBotPrediction(player);
		}

		if (predict != nullptr)
		{
			K_NudgePredictionTowardsObjects(predict, player);
			destangle = R_PointToAngle2(player->mo->x, player->mo->y, predict->x, predict->y);
			turnamt = K_HandleBotTrack(player, cmd, predict, destangle);
		}
	}

	if (player->exiting == 0)
	{
		// TODO: Allowing projectile items like orbinaut while e-braking would be nice, maybe just pass in the spindash variable?
		t = I_GetPreciseTime();
		K_BotItemUsage(player, cmd, turnamt);
		ps_bots[player - players].item = I_GetPreciseTime() - t;
	}

	if (turnamt != 0)
	{
		if (turnamt > KART_FULLTURN)
		{
			turnamt = KART_FULLTURN;
		}
		else if (turnamt < -KART_FULLTURN)
		{
			turnamt = -KART_FULLTURN;
		}

		if (turnamt > 0)
		{
			// Count up
			if (player->botvars.turnconfirm < BOTTURNCONFIRM)
			{
				cmd->bot.turnconfirm++;
			}
		}
		else if (turnamt < 0)
		{
			// Count down
			if (player->botvars.turnconfirm > -BOTTURNCONFIRM)
			{
				cmd->bot.turnconfirm--;
			}
		}
		else
		{
			// Back to neutral
			if (player->botvars.turnconfirm < 0)
			{
				cmd->bot.turnconfirm++;
			}
			else if (player->botvars.turnconfirm > 0)
			{
				cmd->bot.turnconfirm--;
			}
		}

		if (abs(player->botvars.turnconfirm) >= BOTTURNCONFIRM)
		{
			// You're commiting to your turn, you're allowed!
			cmd->turning = turnamt;
		}
	}

	// Free the prediction we made earlier
	if (predict != nullptr)
	{
		if (cv_kartdebugbot.value != 0 && player - players == displayplayers[0] && !(paused || P_AutoPause()))
		{
			K_DrawPredictionDebug(predict, player);
		}
	}
}

/*--------------------------------------------------
	void K_BuildBotTiccmd(player_t *player, ticcmd_t *cmd)

		See header file for description.
--------------------------------------------------*/
void K_BuildBotTiccmd(
	player_t *player, // annoyingly NOT const because of LUA_HookTiccmd... grumble grumble
	ticcmd_t *cmd)
{
	ZoneScoped;

	// Remove any existing controls
	memset(cmd, 0, sizeof(ticcmd_t));

	if (player->mo == nullptr
		|| player->spectator == true
		|| G_GamestateUsesLevel() == false)
	{
		// Not in the level.
		return;
	}

	// Complete override of all ticcmd functionality.
	// May add more hooks to individual pieces of bot ticcmd,
	// but this should always be here so anyone can roll
	// their own :)
	if (LUA_HookTiccmd(player, cmd, HOOK(BotTiccmd)) == true)
	{
		cmd->flags |= TICCMD_BOT;
		return;
	}

	cmd->flags |= TICCMD_BOT;

	switch (player->botvars.style)
	{
		case BOT_STYLE_STAY:
		{
			// Hey, this one's pretty easy :P
			break;
		}
		default:
		{
			K_BuildBotTiccmdNormal(player, cmd);
			break;
		}
	}
}

static void K_IncrementBotRespawn(player_t *player, UINT32 *respawn, const UINT32 respawnmax)
{
	const fixed_t requireDist = (12*player->mo->scale) / FRACUNIT;
	INT32 progress = player->distancetofinishprev - player->distancetofinish;
	boolean exceptions = (
		(leveltime < starttime)
		|| player->flashing != 0
		|| player->spinouttimer != 0
		|| player->airtime > 3*TICRATE/2
		|| (player->justbumped > 0 && player->justbumped < bumptime-1)
	);

	if (!exceptions && (progress < requireDist))
	{
		if (*respawn < respawnmax)
		{
			// Making no progress, start counting against you.
			*respawn = *respawn + 1;
			if (progress < -requireDist && *respawn < respawnmax)
			{
				// Making NEGATIVE progress? Start counting even harder.
				*respawn = *respawn + 1;
			}
		}
	}
	else if (*respawn > 0)
	{
		// Playing normally.
		*respawn = *respawn - 1;
	}
}

/*--------------------------------------------------
	void K_UpdateBotGameplayVars(player_t *player);

		See header file for description.
--------------------------------------------------*/
void K_UpdateBotGameplayVars(player_t *player)
{
	if (gamestate != GS_LEVEL || !player->mo)
	{
		// Not in the level.
		return;
	}

	player->botvars.rubberband = K_UpdateRubberband(player);

	player->botvars.turnconfirm += player->cmd.bot.turnconfirm;

	// Is a bot not making any progress? Kill it and respawn at next waypoint.
	K_IncrementBotRespawn(player, &player->botvars.respawnconfirm, BOTRESPAWNCONFIRM);

	if ((player->cmd.bot.respawnconfirm >= TICRATE) && (player->botvars.respawnconfirm >= BOTRESPAWNCONFIRM))
	{
		// Now a clean function! Neat, eh?
		K_SetRespawnAtNextWaypoint(player);

		// WHAT ARE YOU DOING??? RACE ALREADY!
		P_DamageMobj(player->mo, NULL, NULL, 1, DMG_INSTAKILL);

		player->botvars.respawnconfirm = 0;
	}
	else if (player->cmd.forwardmove < 0)
	{
		// stop drifting if we're reversing
		K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT);
	}
	else
	{
		// Figure out if we need to drift.
		// Drift-ending waypoints will kill the drift timer,
		// so no need to worry about doing that ourselves.
		K_BotStartDrift(player);

		INT32 limit = FixedInt(cv_test3.value) - gamespeed*5;
		INT32 dtime = ++player->botvars.drifttime;

		// the faster we are going, the sooner we need to drift
		fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false));

		switch (player->botvars.driftstate)
		{
		case DRIFTSTATE_STARTING:
			limit = std::max(0, limit - FixedMul(TICRATE/5, speedfactor));
			if (dtime > limit)
			{
				K_BotSetDriftState(player, DRIFTSTATE_ACTIVE, 0);
			}
			break;

		case DRIFTSTATE_ENDING:
			limit = std::max(0, limit - FixedMul(TICRATE/5, speedfactor));
			if (dtime > limit)
			{
				K_BotSetDriftState(player, DRIFTSTATE_AUTO, 0);
			}
			break;

		default:
			break;
		}
	}

	K_UpdateBotGameplayVarsItemUsage(player);
}
