#include "driver2.h"
#include "cop_ai.h"
#include "civ_ai.h"
#include "mission.h"
#include "system.h"
#include "felony.h"
#include "cars.h"
#include "players.h"
#include "pathfind.h"
#include "gamesnd.h"
#include "cutscene.h"
#include "convert.h"
#include "map.h"
#include "camera.h"
#include "handling.h"
#include "objcoll.h"
#include "wheelforces.h"
#include "pad.h"
#include "pedest.h"
#include "pres.h"

enum AIZone
{
	zoneFrnt = 0,
	zoneBack = 1,
	zoneLeft = 2,
	zoneRght = 3,
};

struct iVectNT
{
	int n;
	int t;
};

COP_DATA gCopData = {
	0,
	0,
	0,
	2048,
	0,
	4096,
	2048,
	3000000,
	{ 0, 0, 0, 0, 0 }
};

int speed1[] = { 203, 210, 217 };
int speed2[] = { 238, 266, 294 };

int numRoadblockCars = 0;
int roadblockCount = 0;
int copsAreInPursuit = 0;
int requestRoadblock = 0;
int roadblockDelay = 250;
int roadblockDelayDiff[3] = { 1500, 1000, 800 };
int requestCopCar = 0;
int cop_respawn_timer = 0;
char first_offence = 0;

ROADBLOCK Roadblock;
VECTOR roadblockLoc;

static int said_picked_up = 0;

char last_cop_phrase = 0;

char CopWorkMem[444];		// PVS table
COP_SIGHT_DATA copSightData;

int player_position_known = 0;
VECTOR lastKnownPosition;
VECTOR CarTail;
CAR_DATA *targetVehicle;

int numActiveCops = 0;
int pathStraight = 0;
int LastHeading = 0;
int OutOfSightCount = 0;

// [D] [T]
void InitCopState(CAR_DATA *cp, char *extraData)
{
	ClearMem((char *)&cp->ai.p, sizeof(COP));

	cp->controlType = CONTROL_TYPE_PURSUER_AI;
	cp->controlFlags = 0;
	cp->ai.p.justPinged = 1;

	if (gCopDifficultyLevel == 0)
		cp->hndType = 2;
	else if (gCopDifficultyLevel == 1) 
		cp->hndType = 3;
	else
		cp->hndType = 4;
}

int ReplayLog_Fnarr_He_Said_Log(int val)
{
	return 0;
}


// [D] [T]
int FindCost(int x, int z, int dvx, int dvz)
{
	static unsigned char sqtbl[] = {
		0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
		0x81, 0x81, 0x81, 0x82, 0x82, 0x83, 0x83, 0x83, 0x84,
		0x84, 0x85, 0x86, 0x86, 0x87, 0x88, 0x88, 0x89, 0x8A,
		0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x90, 0x91,
		0x92, 0x93, 0x94, 0x95, 0x96, 0x98, 0x99, 0x9A, 0x9B,
		0x9C, 0x9D, 0x9E, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA6,
		0xA7, 0xA8, 0xAA, 0xAB, 0xAC, 0xAE, 0xAF, 0xB0, 0xB2,
		0xB3, 0xB5
	};

	int tx, tz, dx, dz;
	int d;

	tx = targetVehicle->hd.where.t[0] + dvx / 256;
	tz = targetVehicle->hd.where.t[2] + dvz / 256;

	dx = ABS(x - tx);
	dz = ABS(z - tz);

	// swap
	if (dz > dx)
	{
		d = dx;

		dx = dz;
		dz = d;
	}

	if (dx == 0)
		return 0;

	return (dx * sqtbl[(dz * 64) / dx]) / 128;
}

extern int distanceReturnedLog[8];
extern int distLogIndex;
extern int lastDistanceFound;

// [D] [T]
void WibbleDownTheRoad(VECTOR *from, int distance, VECTOR *to)
{
	int val;
	int j;
	int thl[4];
	VECTOR pos;
	VECTOR dir;

	pos.vx = from->vx;
	pos.vy = from->vy;
	pos.vz = from->vz;
	pos.pad = from->pad;

	for (j = 0; j < 4; j++)
	{
		ReplayLog_Fnarr_He_Said_Log(pos.vx);
		ReplayLog_Fnarr_He_Said_Log(pos.vy);
		ReplayLog_Fnarr_He_Said_Log(pos.vz);

		val = getHeadingToPlayer(pos.vx, pos.vy, pos.vz);
		ReplayLog_Fnarr_He_Said_Log(val);

		if (j == 0)
		{
			distanceReturnedLog[distLogIndex & 7] = lastDistanceFound;
			distLogIndex++;
		}

		dir.vy = 0;
		dir.vx = FIXEDH((distance >> 2) * RSIN(val));
		dir.vz = FIXEDH((distance >> 2) * RCOS(val));

		thl[j] = val;

		VECTOR p2 = pos;

		pos.vx += dir.vx;
		pos.vy += dir.vy;
		pos.vz += dir.vz;

#if defined(COLLISION_DEBUG) && !defined(PSX)
		extern int gShowCollisionDebug;
		if (gShowCollisionDebug == 3)
		{
			extern void Debug_AddLine(VECTOR & pointA, VECTOR & pointB, CVECTOR & color);
			extern void Debug_AddLineOfs(VECTOR & pointA, VECTOR & pointB, VECTOR & ofs, CVECTOR & color);

			CVECTOR ggcv = { 0, 250, 0 };
			CVECTOR bbcv = { 0, 0, 250 };
			CVECTOR rrcv = { 250, 0, 0 };

			VECTOR _zero = { 0 };
			VECTOR _up = { 0, 1000, 0 };

			Debug_AddLine(p2, pos, bbcv);
		}
#endif
	}

	to->vx = pos.vx;
	to->vy = pos.vy;
	to->vz = pos.vz;
	to->pad = pos.pad;

	if (((thl[3] - thl[0]) + 200U & 0xfff) < 400) 
	{
		pathStraight = 1;
	}
}

// [D] [T]
void UpdateCopSightData(void)
{
	short* playerFelony;

	if (player[0].playerCarId < 0)
		playerFelony = &pedestrianFelony;
	else
		playerFelony = &car_data[player[0].playerCarId].felonyRating;

	if (*playerFelony > FELONY_PURSUIT_MIN_VALUE)
	{
		copSightData.surroundViewDistance = 5440;
		copSightData.frontViewDistance = 16320;
		copSightData.frontViewAngle = 1024;
		return;
	}

	copSightData.surroundViewDistance = 2720;
	copSightData.frontViewDistance = 7820;
	copSightData.frontViewAngle = 512;
}

// [D] [T]
void InitCopData(COP_DATA* pCopData)
{
	short* pTrigger;
	int trigger;

	pTrigger = pCopData->trigger;
	trigger = 0;

	while (pTrigger < pCopData->trigger + 4)
	{
		*pTrigger = trigger;
		pTrigger++;

		trigger += 0x333;
	}

	*pTrigger = 0x7fff;
	said_picked_up = 0;
}

// [D] [T]
void InitCops(void)
{
	targetVehicle = &car_data[0];
	requestCopCar = 0;

	InitFelonySystem();
	InitPathFinding();

	felonyData.pursuitFelonyScale = 3000;

	gCopDesiredSpeedScale = 4096;
	gCopMaxPowerScale = 4096;
	gPuppyDogCop = 0;

	copsAreInPursuit = 0;

	player_position_known = 2;
	OutOfSightCount = 0;

	UpdateCopSightData();

	gCopData.autoMaxPowerScaleLimit = 0;
	gCopData.autoDesiredSpeedScaleLimit = 0;

	if (GameType == GAME_GETAWAY) 
	{
		gCopRespawnTime = 10000;
	}
	else if (GameType == GAME_SURVIVAL)
	{
		gCopRespawnTime = 0;
		gCopDesiredSpeedScale = 4596;
		gCopMaxPowerScale = 4596;
	
		gCopData.autoMaxPowerScaleLimit = 1024;
		gCopData.autoDesiredSpeedScaleLimit = 1024;
	}
	else
	{
		gCopRespawnTime = 320;

		if (gCurrentMissionNumber < 40)
			gCopRespawnTime = gCurrentMissionNumber * -4 + 320;
	}

	LastHeading = -1;
	pathStraight = 0;
	numActiveCops = 0;

	OutOfSightCount = 0;
	cop_respawn_timer = 0;

	CarTail.vx = 0;
	CarTail.vy = 0;
	CarTail.vz = 0;

	
	lastKnownPosition.vx = INT_MAX;
	lastKnownPosition.vy = INT_MAX;
	lastKnownPosition.vz = INT_MAX;

	InitCopData(&gCopData);

	copsAreInPursuit = 0;
}

VECTOR targetPoint = { 0,0,0 };

// [D] [T]
void CopControl1(CAR_DATA *cp)
{
	int targetFound;
	int steeringFac;
	int dist;
	short* playerFelony;
	int steerDif;
	int dz;
	int plcrspd;
	int dx;
	int dvx, dvz, idvx, idvz;
	int desiredSteerAngle;
	int x;
	int z;
	int currentSpeed;
	int slidevel;
	int maxPower;
	int speedDif;
	int doBatter;
	VECTOR pos;
	iVectNT path[2];
	AIZone targetZone;

	if (player[0].playerCarId < 0)
		playerFelony = &pedestrianFelony;
	else
		playerFelony = &car_data[player[0].playerCarId].felonyRating;

	desiredSteerAngle = 0;

	cp->wheelspin = 0;
	currentSpeed = FIXEDH(cp->hd.wheel_speed);

	if (CopsAllowed == 0)
	{
		cp->thrust = 0;
		cp->handbrake = 1;

		return;
	}

	// [A] new cop batter logic
	// survival setting really
	if(gBatterPlayer)
		doBatter = 1;
	else
		doBatter = 0;

	// [A] periodically beat player in ass
	if (!doBatter && *playerFelony > gCopData.autoBatterPlayerTrigger)
	{
		int batterTrigger;

		if(gCopDifficultyLevel == 0)
			batterTrigger = 80;
		else if (gCopDifficultyLevel == 1)
			batterTrigger = 50;
		else if (gCopDifficultyLevel == 2)
			batterTrigger = 0;

		if (cp->ai.p.batterTimer > batterTrigger)
			doBatter = 1;
		
		cp->ai.p.batterTimer++;
		cp->ai.p.batterTimer &= 127;
	}



	if (cp->ai.p.dying != 0 || 
		cp->totalDamage > 27000 && gCopData.immortal == 0)
	{
		cp->thrust = 0;

		if (cp->ai.p.dying > 85)
			return;

		if (cp->ai.p.dying == 0)
			numActiveCops--;

		cp->ai.p.dying++;
		return;
	}

	if (gStopCops.radius != 0)
	{
		dx = ABS(cp->hd.where.t[0] - gStopCops.pos.vx);
		dz = ABS(cp->hd.where.t[2] - gStopCops.pos.vz);

		if (dx + dz < gStopCops.radius * 3 / 2 && 
			SquareRoot0(dx * dx + dz * dz) < gStopCops.radius)
		{
			cp->thrust = 0;
			cp->handbrake = 1;
			return;
		}
	}

	if ((CameraCnt & 7U) == 0)
	{
		pos.vx = cp->hd.where.t[0] + FIXEDH(cp->hd.where.m[0][2] * 400 - cp->hd.where.m[0][0] * 150);
		pos.vy = cp->hd.where.t[1] + FIXEDH(cp->hd.where.m[1][2] * 400 - cp->hd.where.m[1][0] * 150);
		pos.vz = cp->hd.where.t[2] + FIXEDH(cp->hd.where.m[2][2] * 400 - cp->hd.where.m[2][0] * 150);

		cp->ai.p.frontLClear = CellAtPositionEmpty(&pos, 80);

#if defined(COLLISION_DEBUG) && !defined(PSX)
		extern int gShowCollisionDebug;
		if (gShowCollisionDebug == 3)
		{
			extern void Debug_AddLine(VECTOR & pointA, VECTOR & pointB, CVECTOR & color);
			extern void Debug_AddLineOfs(VECTOR & pointA, VECTOR & pointB, VECTOR & ofs, CVECTOR & color);

			CVECTOR ggcv = { 0, 250, 0 };
			CVECTOR bbcv = { 0, 0, 250 };
			CVECTOR rrcv = { 250, 0, 0 };

			VECTOR _zero = { 0 };
			VECTOR _up = { 0, 1000, 0 };

			Debug_AddLineOfs(_zero, _up, pos, cp->ai.p.frontLClear ? ggcv : rrcv);
		}
#endif

		pos.vx = cp->hd.where.t[0] + FIXEDH(cp->hd.where.m[0][2] * 400 + cp->hd.where.m[0][0] * 150);
		pos.vy = cp->hd.where.t[1] + FIXEDH(cp->hd.where.m[1][2] * 400 + cp->hd.where.m[1][0] * 150);
		pos.vz = cp->hd.where.t[2] + FIXEDH(cp->hd.where.m[2][2] * 400 + cp->hd.where.m[2][0] * 150);

		cp->ai.p.frontRClear = CellAtPositionEmpty(&pos, 80);

#if defined(COLLISION_DEBUG) && !defined(PSX)
		extern int gShowCollisionDebug;
		if (gShowCollisionDebug == 3)
		{
			extern void Debug_AddLine(VECTOR & pointA, VECTOR & pointB, CVECTOR & color);
			extern void Debug_AddLineOfs(VECTOR & pointA, VECTOR & pointB, VECTOR & ofs, CVECTOR & color);

			CVECTOR ggcv = { 0, 250, 0 };
			CVECTOR bbcv = { 0, 0, 250 };
			CVECTOR rrcv = { 250, 0, 0 };

			VECTOR _zero = { 0 };
			VECTOR _up = { 0, 1000, 0 };

			Debug_AddLineOfs(_zero, _up, pos, cp->ai.p.frontRClear ? ggcv : rrcv);
		}
#endif
	}

	x = cp->hd.where.t[0];
	z = cp->hd.where.t[2];

	pathStraight = 0;

	if (cp->hd.speed < 6 && cp->ai.p.desiredSpeed != 0)
		cp->ai.p.stuckTimer++;
	else
		cp->ai.p.stuckTimer = 0;

	if (cp->ai.p.stuckTimer > 10 && cp->ai.p.recoveryTimer == 0)
	{
		cp->ai.p.recoveryTimer = 22;
		cp->ai.p.lastRecoverStrategy = cp->ai.p.lastRecoverStrategy & 7;

		if (cp->ai.p.desiredSpeed < 0)
			cp->ai.p.desiredSpeed = 200;
		else
			cp->ai.p.desiredSpeed = -200;

		desiredSteerAngle = ((cp->ai.p.lastRecoverStrategy * 5 & 6) - 3) * 0x80;
		cp->ai.p.stuckTimer = 0;
	}

	dvx = targetVehicle->st.n.linearVelocity[0] - cp->st.n.linearVelocity[0];
	dvz = targetVehicle->st.n.linearVelocity[2] - cp->st.n.linearVelocity[2];
	
	dist = FindCost(x, z, dvx, dvz);

	cp->ai.p.close_pursuit = (dist < 2800);
	
	targetFound = 0;

	if (cp->ai.p.recoveryTimer < 1)
	{
		if (dist < 8400)
		{
			int dd;
			
			dx = x - targetVehicle->hd.where.t[0];
			dz = targetVehicle->hd.where.t[2] - z;
			
			idvx = FIXEDH(dvx);
			idvz = FIXEDH(dvz);
			
			dd = (dx * idvx - dz * idvz) / (idvx * idvx + idvz * idvz + 1);

			if (doBatter == 0)
				dd = dd * 3 / 4;

			if (dd < 0)
				dd = 0;
			else if (dd > 60) 
				dd = 60;

			dvz = targetVehicle->hd.where.t[0] + FIXEDH(targetVehicle->st.n.linearVelocity[0] * dd);
			dvx = targetVehicle->hd.where.t[2] + FIXEDH(targetVehicle->st.n.linearVelocity[2] * dd);

			if (doBatter == 0)
			{
				CarTail.vx = dvz - (targetVehicle->hd.where.m[0][2] * 5 >> 7);
				CarTail.vz = dvx - (targetVehicle->hd.where.m[2][2] * 5 >> 7);
			}
			else 
			{
				CarTail.vx = dvz + (targetVehicle->hd.where.m[0][0] * 5 >> 8);
				CarTail.vz = dvx + (targetVehicle->hd.where.m[2][0] * 5 >> 8);
			}
		}
		else 
		{
			CarTail.vx = targetVehicle->hd.where.t[0];
			CarTail.vz = targetVehicle->hd.where.t[2];
		}

		if (cp->ai.p.close_pursuit == 0) 
		{
			WibbleDownTheRoad((VECTOR*)cp->hd.where.t, cp->hd.speed * 10 + 680, &targetPoint);
		}
		else 
		{
			targetPoint.vx = CarTail.vx;
			targetPoint.vz = CarTail.vz;

#if 0
			{
				extern void Debug_AddLine(VECTOR & pointA, VECTOR & pointB, CVECTOR & color);
				extern void Debug_AddLineOfs(VECTOR & pointA, VECTOR & pointB, VECTOR & ofs, CVECTOR & color);

				CVECTOR ggcv = { 0, 250, 0 };
				CVECTOR bbcv = { 0, 0, 250 };
				CVECTOR rrcv = { 250, 0, 0 };

				VECTOR _zero = { 0 };
				VECTOR _up = { 0, 1000, 0 };

				Debug_AddLineOfs(_zero, _up, targetPoint, rrcv);
			}
#endif
		}

		targetFound = 1;

		cp->ai.p.targetHistory[1].vx = cp->ai.p.targetHistory[0].vx;
		cp->ai.p.targetHistory[1].vz = cp->ai.p.targetHistory[0].vz;
		cp->ai.p.targetHistory[0].vx = targetPoint.vx;
		cp->ai.p.targetHistory[0].vz = targetPoint.vz;
	}
	else 
	{
		cp->ai.p.recoveryTimer--;
	}

	//if (gTimeInWater == 0) 
	//	targetFound = 0;

	if (targetFound) 
	{
		slidevel = FIXEDH(cp->hd.where.m[0][0] * cp->st.n.linearVelocity[0] + cp->hd.where.m[2][0] * cp->st.n.linearVelocity[2]);

		path[1].t = dvx = FIXEDH((x - targetPoint.vx) * cp->hd.where.m[2][0] + (targetPoint.vz - z) * cp->hd.where.m[0][0]);
		path[1].n = steeringFac = FIXEDH((targetPoint.vx - x) * cp->hd.where.m[0][0] + (targetPoint.vz - z) * cp->hd.where.m[2][0]) - slidevel / 140;

		if (path[1].n < 1) 
		{
			if (path[1].n * -2 >= path[1].t || cp->ai.p.frontLClear == 0)
			{
				targetZone = zoneLeft;
				targetFound = -path[1].n < (path[1].t << 2);

				if (targetFound && cp->ai.p.close_pursuit != 0) 
					targetZone = zoneBack;
			}
			else
				targetZone = zoneFrnt;
		}
		else 
		{
			if (path[1].n * 2 >= path[1].t || cp->ai.p.frontRClear == 0) 
			{
				targetZone = zoneRght;
				targetFound = path[1].n < (path[1].t << 2);
				
				if (targetFound && cp->ai.p.close_pursuit != 0) 
					targetZone = zoneBack;
			}
			else
				targetZone = zoneFrnt;
		}

		steerDif = path[1].n + slidevel / 140;

		if (targetZone == zoneFrnt)
		{			
			if (pathStraight == 0)
				cp->ai.p.desiredSpeed = speed1[gCopDifficultyLevel];
			else
				cp->ai.p.desiredSpeed = speed2[gCopDifficultyLevel];

			if (gCopData.cutOffDistance < cp->ai.p.DistanceToPlayer)
				maxPower = gCopData.cutOffPowerScale;
			else
				maxPower = gCopDesiredSpeedScale;

			cp->ai.p.desiredSpeed = FIXEDH(cp->ai.p.desiredSpeed * (maxPower + FIXEDH(*playerFelony * gCopData.autoDesiredSpeedScaleLimit)));

			if ((gPuppyDogCop || player[0].playerType == 2) && cp->ai.p.close_pursuit)
			{
				plcrspd = targetVehicle->hd.speed + 10;
				
				if (dist > 4000)
				{
					speedDif = (cp->ai.p.desiredSpeed - plcrspd) * (dist - 4000);
					cp->ai.p.desiredSpeed = plcrspd + speedDif / 4000;
				}
				else
				{
					cp->ai.p.desiredSpeed = plcrspd;
				}
			}

			desiredSteerAngle = (steerDif * 512) / path[1].t;
		}
		else if (targetZone == zoneBack)
		{
			cp->ai.p.desiredSpeed = -70;
			
			if ((steerDif ^ currentSpeed) < 0) 
				desiredSteerAngle = -284;
			else
				desiredSteerAngle = 284;
		}
		else
		{
			cp->ai.p.desiredSpeed = 110;

			if (cp->ai.p.frontLClear == 0 && cp->ai.p.frontRClear == 0)
			{
				cp->ai.p.desiredSpeed = -120;
			}
			else if ((targetZone == zoneLeft && cp->st.n.angularVelocity[1] > -130) || 
					 (targetZone == zoneRght && cp->st.n.angularVelocity[1] < 130))
			{
				cp->wheelspin = 1;
			}

			if ((steerDif ^ currentSpeed) < 0)
				desiredSteerAngle = -512;
			else
				desiredSteerAngle = 512;
		}
	}
	else if (cp->ai.p.recoveryTimer < 1) 
	{
		cp->ai.p.desiredSpeed = 0;
		desiredSteerAngle = 0;
	}

	// calculate the desired speed
	if (dist < 4096 && cp->ai.p.desiredSpeed > 0 && doBatter == 0)
	{
		plcrspd = targetVehicle->hd.speed - 20;

		if (plcrspd < 20)
			plcrspd = 20;

		cp->ai.p.desiredSpeed = FIXEDH(dist * cp->ai.p.desiredSpeed + (4096 - dist) * plcrspd);
	}

	// calculate acceleration
	maxPower = steeringFac = (gCopDifficultyLevel + 8) * 1024;
	currentSpeed = cp->ai.p.desiredSpeed - currentSpeed;

	if (pathStraight != 0)
		maxPower += (gCopDifficultyLevel + 4) * 1024;

	if (player[0].playerCarId < 0)
		playerFelony = &pedestrianFelony;
	else
		playerFelony = &car_data[player[0].playerCarId].felonyRating;

	maxPower = FIXEDH(maxPower * (gCopMaxPowerScale + FIXEDH(*playerFelony * gCopData.autoMaxPowerScaleLimit)));

	if (currentSpeed < -50)
		cp->thrust = -maxPower;
	else if (currentSpeed > 50)
		cp->thrust = maxPower;
	else
		cp->thrust = (currentSpeed * maxPower) / 50;

	if (cp->hd.speed < 48) 
		cp->thrust = (cp->thrust * (cp->hd.speed + 80) >> 7);

	if (handlingType[cp->hndType].fourWheelDrive == 1)
		cp->thrust /= 2;

	cp->thrust = (cp->thrust * 7) / 10;

	// calculate steering and direction
	steeringFac = DIFF_ANGLES(0, desiredSteerAngle) - cp->wheel_angle; //((desiredSteerAngle + 2048U & 0xfff) - 2048) - cp->wheel_angle;

	if (steeringFac < -200)
		steeringFac = -200;
	else if (steeringFac > 200)
		steeringFac = 200;

	cp->wheel_angle += steeringFac;

	if (cp->ai.p.justPinged == 1)
	{
		cp->hd.direction = getHeadingToPlayer(cp->hd.where.t[0], cp->hd.where.t[1], cp->hd.where.t[2]);
		TempBuildHandlingMatrix((CAR_DATA*)cp, 0);

		cp->ai.p.justPinged = 0;
	}
}

// [D] [T]
void ControlCopDetection(void)
{
	bool spotted;
	u_int distanceToPlayer;
	u_int minDistanceToPlayer;
	int heading;
	int dx, dz;
	CAR_DATA *cp;
	VECTOR vec;
	int ccx, ccz;

	vec.vx = player[0].pos[0];
	vec.vy = player[0].pos[1];
	vec.vz = player[0].pos[2];

	GetVisSetAtPosition(&vec, CopWorkMem, &ccx, &ccz);

	UpdateCopSightData();

	if (player_position_known < 1) 
		player_position_known = 1;
	else
		player_position_known = 2;

	CopsCanSeePlayer = (GameType == GAME_SURVIVAL);

	minDistanceToPlayer = UINT_MAX;

	// check roadblock visibility
	if (numRoadblockCars != 0)
	{
		dx = ABS(roadblockLoc.vx - vec.vx) >> 8;
		dz = ABS(roadblockLoc.vz - vec.vz) >> 8;

		distanceToPlayer = dx * dx + dz * dz;
		
		if (distanceToPlayer < 1640 &&
			newPositionVisible(&roadblockLoc, CopWorkMem, ccx, ccz) != 0)
		{
			CopsCanSeePlayer = 1;

			if (distanceToPlayer << 8 < minDistanceToPlayer)
				minDistanceToPlayer = distanceToPlayer << 6;
		}
	}

	if (!CopsCanSeePlayer && !((gCurrentMissionNumber == 24 || gCurrentMissionNumber == 30) && CameraCnt-frameStart < 100))
	{
		cp = &car_data[MAX_CARS-1];

		while (car_data <= cp)
		{
			if (cp->controlType == CONTROL_TYPE_PURSUER_AI && cp->ai.p.dying == 0 || 
				(cp->controlFlags & CONTROL_FLAG_COP))
			{
				dx = ABS(cp->hd.where.t[0] - vec.vx) >> 8;
				dz = ABS(cp->hd.where.t[2] - vec.vz) >> 8;

				distanceToPlayer = SquareRoot0(dx * dx + dz * dz) << 8;

				if (distanceToPlayer < minDistanceToPlayer)
					minDistanceToPlayer = distanceToPlayer;

				if (cp->controlType == CONTROL_TYPE_PURSUER_AI)
				{
					cp->ai.p.DistanceToPlayer = distanceToPlayer;

					if(cp->ai.p.close_pursuit)
					{
						CopsCanSeePlayer = 1;
						break;
					}
				}

				if (newPositionVisible(&vec, CopWorkMem, ccx, ccz) )
				{
					spotted = false;
					
					if (distanceToPlayer < copSightData.surroundViewDistance) 
					{
						spotted = true;
					}
					else if (distanceToPlayer < copSightData.frontViewDistance)
					{
						int theta;

						dz = vec.vx - cp->hd.where.t[0];
						dx = vec.vz - cp->hd.where.t[2];

						theta = ABS(ratan2(dz, dx) - cp->hd.direction);
						
						if (theta < copSightData.frontViewAngle || 
							(theta + 512 & 0xfff) < copSightData.frontViewAngle + 512)
						{
							spotted = true;
						}
					}

					// [A] also check player elevation from cops (block cops vision from bridges, tunnels etc)
					if (spotted && ABS(cp->hd.where.t[1] - vec.vy) < 1000) 
					{
						CopsCanSeePlayer = 1;
						break;
					}
				}
			}
			cp--;
		}
	}

	// [A] if Tanner is outside car, cops can arrest him if they are too close
	if(player[0].playerType == 2 && minDistanceToPlayer < 2048 && !player[0].dying && pedestrianFelony > FELONY_PURSUIT_MIN_VALUE)
	{
		player[0].dying = 1;
		
		SetMissionMessage(G_LTXT(GTXT_YouveBeenCaught),3,2);
		SetMissionFailed(FAILED_MESSAGESET);
	}

	if (numActiveCops == 0 && OutOfSightCount < 256 && CameraCnt > 8) 
	{
		OutOfSightCount = 256;
	}

	// if cops can't see player - get out of pursued state
	if (!CopsCanSeePlayer)
	{
		if (OutOfSightCount <= 255) 
		{
			OutOfSightCount++;
		}
		else if (OutOfSightCount == 256) 
		{
			player_position_known = -1;
			OutOfSightCount = 257;

			if (first_offence == 0)
			{
				CopSay(12, 0);
				FunkUpDaBGMTunez(0);
			}
		}
		else 
		{
			player_position_known = 0;
		}
	}
	else 
	{
		OutOfSightCount = 0;
	}

	if (player_position_known < 1)
	{
		cp = &car_data[MAX_CARS-1];

		do {
			if (cp->controlType == CONTROL_TYPE_PURSUER_AI)
			{
				vec.vx = cp->hd.where.t[0];
				vec.vz = cp->hd.where.t[2];

				// make cop lose target if target is hidden
				if (newPositionVisible(&vec, CopWorkMem, ccx, ccz) == 0 && cp->ai.p.hiddenTimer++ > 50)
				{
					cp->controlType = CONTROL_TYPE_NONE;
					cp->ai.p.hiddenTimer = 0;
					cp->thrust = 0;
					numCopCars--;
				}
			}

			cp--;
		} while (car_data <= cp);
	}

	if (player_position_known == 1 && first_offence == 0) 
	{
		heading = GetCarDirectionOfTravel(&car_data[player[0].playerCarId]);

		CopSay(Random2(2) % 2 + 10, heading);

		said_picked_up = player_position_known;

		LastHeading = heading;
	}

	if (!CopsCanSeePlayer)
	{
		said_picked_up = 0;
	}
	else if (!first_offence && !said_picked_up) 
	{
		int rnd;
		rnd = Random2(2);

		heading = GetCarDirectionOfTravel(&car_data[player[0].playerCarId]);

		if ((rnd == (rnd / 5) * 5) && (dx != LastHeading))
			CopSay(rnd % 2 + 10, heading);

		said_picked_up = 1;
		LastHeading = heading;
	}
}

// [D] [T]
void PassiveCopTasks(CAR_DATA *cp)
{
	short *playerFelony;

	if (player[0].playerCarId < 0)
		playerFelony = &pedestrianFelony;
	else 
		playerFelony = &car_data[player[0].playerCarId].felonyRating;

	if (*playerFelony <= FELONY_PURSUIT_MIN_VALUE)
		return;

	// [A] make an ambush on player in Destroy the yard
	// but don't in Car bomb getaway
	if (player_position_known < 1 || !CopsCanSeePlayer && gCurrentMissionNumber == 24)
		return;

	InitCopState(cp, NULL);

	cp->ai.p.justPinged = 0;

	numCivCars--;
	numActiveCops++;
}

// [D] [T]
void ControlNumberOfCops(void)
{
	short *pTrigger;
	short *playerFelony;
	int tempDist;
	CAR_DATA *lcp;
	int respawnTime;
	int num_closer;
	int cutOffDistance;
	int numWantedCops;

	pTrigger = gCopData.trigger;
	numWantedCops = 0;

	while( true ) 
	{
		if (player[0].playerCarId < 0)
			playerFelony = &pedestrianFelony;
		else
			playerFelony = &car_data[player[0].playerCarId].felonyRating;

		if (*playerFelony < *pTrigger) 
			break;

		pTrigger++;
		numWantedCops++;
	}

	if (numWantedCops < gMinimumCops)
		numWantedCops = gMinimumCops;

	if (numWantedCops > maxCopCars)
		numWantedCops = maxCopCars;
	
	if (numCopCars < numWantedCops && gDontPingInCops == 0)
	{
		respawnTime = gCopRespawnTime;
		
		if (Roadblock.status == 2)
			respawnTime = Roadblock.copRespawnTime;

		if (respawnTime * FIXED(4096 - FIXED(*playerFelony * gCopData.autoRespawnScaleLimit)) < cop_respawn_timer + 1)
		{
			requestCopCar = 1;
		}

		cop_respawn_timer++;
		return;
	}

	if (numCopCars <= numWantedCops) 
	{
		gCopData.cutOffDistance = INT_MAX;
		cop_respawn_timer = 0;
		return;
	}
	
	gCopData.cutOffDistance = INT_MAX;

	do {
		cutOffDistance = 0;
		lcp = car_data;
		num_closer = 0;

		do {
			if (lcp->controlType == CONTROL_TYPE_PURSUER_AI) 
			{
				tempDist = lcp->ai.p.DistanceToPlayer;
				
				if (tempDist < gCopData.cutOffDistance)
				{
					if(cutOffDistance < tempDist)
						cutOffDistance = tempDist;
					
					num_closer++;
				}
			}
			lcp++;
		} while (lcp < &car_data[MAX_CARS]);

		gCopData.cutOffDistance = cutOffDistance;
	} while (numWantedCops < num_closer);

	cop_respawn_timer = 0;
}

// [D] [T]
void ControlCops(void)
{
	short* playerFelony;
	int heading;
	int phrase;
	int copsWereInPursuit;

#if 0 // [A] lineClear debugging code - pls remove after fixing bugs
	extern int gShowCollisionDebug;
	if (gShowCollisionDebug == 3)
	{
		extern void Debug_AddLine(VECTOR & pointA, VECTOR & pointB, CVECTOR & color);
		extern void Debug_AddLineOfs(VECTOR & pointA, VECTOR & pointB, VECTOR & ofs, CVECTOR & color);

		CVECTOR ggcv = { 0, 250, 0 };
		CVECTOR bbcv = { 0, 0, 250 };
		CVECTOR rrcv = { 250, 0, 0 };

		VECTOR _zero = { 0 };
		VECTOR _up = { 0, 1000, 0 };

		VECTOR v1 = *(VECTOR*)targetVehicle->hd.where.t;
		VECTOR v2 = v1;
		v2.vx -= FIXEDH(targetVehicle->hd.where.m[2][0] * 1024);
		v2.vz += FIXEDH(targetVehicle->hd.where.m[2][2] * 1024);

		int result = lineClear(&v1, &v2);

		Debug_AddLine(v1, v2, result ? ggcv : rrcv);
	}
#endif

	if (player[0].playerCarId < 0)
		playerFelony = &pedestrianFelony;
	else
		playerFelony = &car_data[player[0].playerCarId].felonyRating;

	gCopData.autoBatterPlayerTrigger = 2048;

	if (CopsAllowed == 0 || gInGameCutsceneActive != 0)
	{
		player_position_known = 0;
		CopsCanSeePlayer = 0;
		requestCopCar = 0;
		copsWereInPursuit = copsAreInPursuit;
	}
	else
	{
		if (player[0].playerCarId > -1)
			targetVehicle = &car_data[player[0].playerCarId];
		else
			targetVehicle = &car_data[TANNER_COLLIDER_CARID];	// [A] fix bug of chasing car

		if (player_position_known > 0)
		{
			lastKnownPosition.vx = player[0].pos[0];
			lastKnownPosition.vy = player[0].pos[1];
			lastKnownPosition.vz = player[0].pos[2];
			lastKnownPosition.pad = player[0].pos[3];
		}

		// update pathfinding
		UpdateCopMap();

		heading = LastHeading;

		// play the phrases about direction
		if (first_offence == 0 && CopsCanSeePlayer && numActiveCops != 0)
		{
			if (*playerFelony > FELONY_PURSUIT_MIN_VALUE && 
				TimeSinceLastSpeech > 720 && 
				targetVehicle->hd.speed > 20)
			{
				int rnd;
				heading = GetCarDirectionOfTravel(targetVehicle);

				if (heading != LastHeading)
				{
					rnd = Random2(1);

					if ((MaxPlayerDamage[0] * 3) / 4 < car_data[player[0].playerCarId].totalDamage)
						phrase = rnd % 4;
					else
						phrase = rnd % 3;

					//phrase = rnd + phrase;

					if (first_offence == 0 && last_cop_phrase != phrase && TimeSinceLastSpeech > 720)
					{
						if (phrase < 3)
						{
							CopSay(phrase + 15, heading);
							last_cop_phrase = phrase;
						}
						else
						{
							CopSay(6, 0);
							last_cop_phrase = phrase;
						}
					}
				}
			}
		}

		LastHeading = heading;

		ControlCopDetection();

		AdjustFelony(&felonyData);

		ControlNumberOfCops();

		copsWereInPursuit = 0;

		if (player_position_known > 0)
		{
			if (*playerFelony > FELONY_PURSUIT_MIN_VALUE)
				copsWereInPursuit = 1;
		}

		// start pursuit
		if (copsWereInPursuit != 0)
		{
			if (copsAreInPursuit == 0)
				Pads[0].alarmShakeCounter = 12;

			copsAreInPursuit = 1;

			FunkUpDaBGMTunez(1);

			if (first_offence != 0)
			{
				CopSay(9, 0);

				first_offence = 0;
				said_picked_up = 1;
			}
		}
	}

	copsAreInPursuit = copsWereInPursuit;
}

// [D] [T]
void CopControl(CAR_DATA* cp)
{
	CopControl1(cp);

	CheckPingOut(cp);

	if (cp->ai.p.dying == 0 && cp->controlType == CONTROL_TYPE_NONE)
	{
		numActiveCops--;
		numCopCars--;
	}
}