code:
#include "common.h"

#include "main.h"
#include "FileMgr.h"
#include "Physical.h"
#include "HandlingMgr.h"

cHandlingDataMgr mod_HandlingManager;

const char *HandlingFilename = "HANDLING.CFG";

const char VehicleNames[NUMHANDLINGS][14] = {
	"LANDSTAL",
	"IDAHO",
	"STINGER",
	"LINERUN",
	"PEREN",
	"SENTINEL",
	"PATRIOT",
	"FIRETRUK",
	"TRASH",
	"STRETCH",
	"MANANA",
	"INFERNUS",
	"BLISTA",
	"PONY",
	"MULE",
	"CHEETAH",
	"AMBULAN",
	"FBICAR",
	"MOONBEAM",
	"ESPERANT",
	"TAXI",
	"KURUMA",
	"BOBCAT",
	"MRWHOOP",
	"BFINJECT",
	"POLICE",
	"ENFORCER",
	"SECURICA",
	"BANSHEE",
	"PREDATOR",
	"BUS",
	"RHINO",
	"BARRACKS",
	"TRAIN",
	"HELI",
	"DODO",
	"COACH",
	"CABBIE",
	"STALLION",
	"RUMPO",
	"RCBANDIT",
	"BELLYUP",
	"MRWONGS",
	"MAFIA",
	"YARDIE",
	"YAKUZA",
	"DIABLOS",
	"COLUMB",
	"HOODS",
	"AIRTRAIN",
	"DEADDODO",
	"SPEEDER",
	"REEFER",
	"PANLANT",
	"FLATBED",
	"YANKEE",
	"BORGNINE"
};

cHandlingDataMgr::cHandlingDataMgr(void)
{
	memset(this, 0, sizeof(*this));
}

void
cHandlingDataMgr::Initialise(void)
{
	LoadHandlingData();
	field_0 = 0.1f;
	fWheelFriction = 0.9f;
	field_8 = 1.0f;
	field_C = 0.8f;
	field_10 = 0.98f;
}

void
cHandlingDataMgr::LoadHandlingData(void)
{
	char *start, *end;
	char line[201];	// weird value
	char delim[4];	// not sure
	char *word;
	int field, handlingId;
	int keepGoing;
	tHandlingData *handling;

	CFileMgr::SetDir("DATA");
	CFileMgr::LoadFile(HandlingFilename, work_buff, sizeof(work_buff), "r");
	CFileMgr::SetDir("");

	start = (char*)work_buff;
	end = start+1;
	handling = nil;
	keepGoing = 1;

	while(keepGoing){
		// find end of line
		while(*end != '\n') end++;

		// get line
		strncpy(line, start, end - start);
		line[end - start] = '\0';
		start = end+1;
		end = start+1;

		// yeah, this is kinda crappy
		if(strcmp(line, ";the end") == 0)
			keepGoing = 0;
		else if(line[0] != ';'){
			field = 0;
			strcpy(delim, " \t");
			// FIX: game seems to use a do-while loop here
			for(word = strtok(line, delim); word; word = strtok(nil, delim)){
				switch(field){
				case  0:
					handlingId = FindExactWord(word, (const char*)VehicleNames, 14, NUMHANDLINGS);
					assert(handlingId >= 0 && handlingId < NUMHANDLINGS);
					handling = &HandlingData[handlingId];
					handling->nIdentifier = (tVehicleType)handlingId;
					break;
				case  1: handling->fMass = strtod(word, nil); break;
				case  2: handling->Dimension.x = strtod(word, nil); break;
				case  3: handling->Dimension.y = strtod(word, nil); break;
				case  4: handling->Dimension.z = strtod(word, nil); break;
				case  5: handling->CentreOfMass.x = strtod(word, nil); break;
				case  6: handling->CentreOfMass.y = strtod(word, nil); break;
				case  7: handling->CentreOfMass.z = strtod(word, nil); break;
				case  8: handling->nPercentSubmerged = atoi(word); break;
				case  9: handling->fTractionMultiplier = strtod(word, nil); break;
				case 10: handling->fTractionLoss = strtod(word, nil); break;
				case 11: handling->fTractionBias = strtod(word, nil); break;
				case 12: handling->Transmission.nNumberOfGears = atoi(word); break;
				case 13: handling->Transmission.fMaxVelocity = strtod(word, nil); break;
				case 14: handling->Transmission.fEngineAcceleration = strtod(word, nil) * 0.4; break;
				case 15: handling->Transmission.nDriveType = word[0]; break;
				case 16: handling->Transmission.nEngineType = word[0]; break;
				case 17: handling->fBrakeDeceleration = strtod(word, nil); break;
				case 18: handling->fBrakeBias = strtod(word, nil); break;
				case 19: handling->bABS = !!atoi(word); break;
				case 20: handling->fSteeringLock = strtod(word, nil); break;
				case 21: handling->fSuspensionForceLevel = strtod(word, nil); break;
				case 22: handling->fSuspensionDampingLevel = strtod(word, nil); break;
				case 23: handling->fSeatOffsetDistance = strtod(word, nil); break;
				case 24: handling->fCollisionDamageMultiplier = strtod(word, nil); break;
				case 25: handling->nMonetaryValue = atoi(word); break;
				case 26: handling->fSuspensionUpperLimit = strtod(word, nil); break;
				case 27: handling->fSuspensionLowerLimit = strtod(word, nil); break;
				case 28: handling->fSuspensionBias = strtod(word, nil); break;
				case 29:
					sscanf(word, "%x", &handling->Flags);
					handling->Transmission.Flags = handling->Flags;
					break;
				case 30: handling->FrontLights = atoi(word); break;
				case 31: handling->RearLights = atoi(word); break;
				}
				field++;
			}
			ConvertDataToGameUnits(handling);
		}
	}
}

int
cHandlingDataMgr::FindExactWord(const char *word, const char *words, int wordLen, int numWords)
{
	int i;

	for(i = 0; i < numWords; i++){
		// BUG: the game does something really stupid here, it's fixed here
		if(strncmp(word, words, wordLen) == 0)
			return i;
		words += wordLen;
	}
	return numWords;
}

void
cHandlingDataMgr::ConvertDataToGameUnits(tHandlingData *handling)
{
	// acceleration is in ms^-2, but we need mf^-2 where f is one frame time (50fps)
	float velocity, a, b;

	handling->Transmission.fEngineAcceleration *= 1.0f/(50.0f*50.0f);
	handling->Transmission.fMaxVelocity *= 1000.0f/(60.0f*60.0f * 50.0f);
	handling->fBrakeDeceleration *= 1.0f/(50.0f*50.0f);
	handling->fTurnMass = (sq(handling->Dimension.x) + sq(handling->Dimension.y)) * handling->fMass / 12.0f;
	if(handling->fTurnMass < 10.0f)
		handling->fTurnMass *= 5.0f;
	handling->fInvMass = 1.0f/handling->fMass;
	handling->fBuoyancy = 100.0f/handling->nPercentSubmerged * GRAVITY*handling->fMass;

	// Don't quite understand this. What seems to be going on is that
	// we calculate a drag (air resistance) deceleration for a given velocity and
	// find the intersection between that and the max engine acceleration.
	// at that point the car cannot accelerate any further and we've found the max velocity.
	a = 0.0f;
	b = 100.0f;
	velocity = handling->Transmission.fMaxVelocity;
	while(a < b && velocity > 0.0f){
		velocity -= 0.01f;
		// what's the 1/6?
		a = handling->Transmission.fEngineAcceleration/6.0f;
		// no density or drag coefficient here...
		float a_drag = 0.5f*SQR(velocity) * handling->Dimension.x*handling->Dimension.z / handling->fMass;
		// can't make sense of this... maybe  v - v/(drag + 1)  ? but that doesn't make so much sense either
		b = -velocity * (1.0f/(a_drag + 1.0f) - 1.0f);
	}

	if(handling->nIdentifier == HANDLING_RCBANDIT){
		handling->Transmission.fMaxCruiseVelocity = handling->Transmission.fMaxVelocity;
	}else{
		handling->Transmission.fMaxCruiseVelocity = velocity;
		handling->Transmission.fMaxVelocity = velocity * 1.2f;
	}
	handling->Transmission.fMaxReverseVelocity = -0.2f;

	if(handling->Transmission.nDriveType == '4')
		handling->Transmission.fEngineAcceleration /= 4.0f;
	else
		handling->Transmission.fEngineAcceleration /= 2.0f;

	handling->Transmission.InitGearRatios();
}

int32
cHandlingDataMgr::GetHandlingId(const char *name)
{
	int i;
	for(i = 0; i < NUMHANDLINGS; i++)
		if(strncmp(VehicleNames[i], name, 14) == 0)
			break;
	return i;
}

void
cHandlingDataMgr::ConvertDataToWorldUnits(tHandlingData *handling)
{
	// TODO: mobile code
}

void
cHandlingDataMgr::RangeCheck(tHandlingData *handling)
{
	// TODO: mobile code
}

void
cHandlingDataMgr::ModifyHandlingValue(CVehicle *, const tVehicleType &, const tField &, const bool &)
{
	// TODO: mobile code
}

void
cHandlingDataMgr::DisplayHandlingData(CVehicle *, tHandlingData *, uint8, bool)
{
	// TODO: mobile code
}


code:
#pragma once

#include "Transmission.h"

enum tVehicleType
{
	HANDLING_LANDSTAL,
	HANDLING_IDAHO,
	HANDLING_STINGER,
	HANDLING_LINERUN,
	HANDLING_PEREN,
	HANDLING_SENTINEL,
	HANDLING_PATRIOT,
	HANDLING_FIRETRUK,
	HANDLING_TRASH,
	HANDLING_STRETCH,
	HANDLING_MANANA,
	HANDLING_INFERNUS,
	HANDLING_BLISTA,
	HANDLING_PONY,
	HANDLING_MULE,
	HANDLING_CHEETAH,
	HANDLING_AMBULAN,
	HANDLING_FBICAR,
	HANDLING_MOONBEAM,
	HANDLING_ESPERANT,
	HANDLING_TAXI,
	HANDLING_KURUMA,
	HANDLING_BOBCAT,
	HANDLING_MRWHOOP,
	HANDLING_BFINJECT,
	HANDLING_POLICE,
	HANDLING_ENFORCER,
	HANDLING_SECURICA,
	HANDLING_BANSHEE,
	HANDLING_PREDATOR,
	HANDLING_BUS,
	HANDLING_RHINO,
	HANDLING_BARRACKS,
	HANDLING_TRAIN,
	HANDLING_HELI,
	HANDLING_DODO,
	HANDLING_COACH,
	HANDLING_CABBIE,
	HANDLING_STALLION,
	HANDLING_RUMPO,
	HANDLING_RCBANDIT,
	HANDLING_BELLYUP,
	HANDLING_MRWONGS,
	HANDLING_MAFIA,
	HANDLING_YARDIE,
	HANDLING_YAKUZA,
	HANDLING_DIABLOS,
	HANDLING_COLUMB,
	HANDLING_HOODS,
	HANDLING_AIRTRAIN,
	HANDLING_DEADDODO,
	HANDLING_SPEEDER,
	HANDLING_REEFER,
	HANDLING_PANLANT,
	HANDLING_FLATBED,
	HANDLING_YANKEE,
	HANDLING_BORGNINE,

	NUMHANDLINGS
};

enum tField // most likely a handling field enum, never used so :shrug:
{

};

enum
{
	HANDLING_1G_BOOST = 1,
	HANDLING_2G_BOOST = 2,
	HANDLING_REV_BONNET = 4,
	HANDLING_HANGING_BOOT = 8,
	HANDLING_NO_DOORS = 0x10,
	HANDLING_IS_VAN = 0x20,
	HANDLING_IS_BUS = 0x40,
	HANDLING_IS_LOW = 0x80,
	HANDLING_DBL_EXHAUST = 0x100,
	HANDLING_TAILGATE_BOOT = 0x200,
	HANDLING_NOSWING_BOOT = 0x400,
	HANDLING_NONPLAYER_STABILISER = 0x800,
	HANDLING_NEUTRALHANDLING = 0x1000,
	HANDLING_HAS_NO_ROOF = 0x2000,
	HANDLING_IS_BIG = 0x4000,
	HANDLING_HALOGEN_LIGHTS = 0x8000,
};

struct tHandlingData
{
	tVehicleType nIdentifier;
	float fMass;
	float fInvMass;
	float fTurnMass;
	CVector Dimension;
	CVector CentreOfMass;
	int8 nPercentSubmerged;
	float fBuoyancy;
	float fTractionMultiplier;
	cTransmission Transmission;
	float fBrakeDeceleration;
	float fBrakeBias;
	int8 bABS;
	float fSteeringLock;
	float fTractionLoss;
	float fTractionBias;
	float fUnused;
	float fSuspensionForceLevel;
	float fSuspensionDampingLevel;
	float fSuspensionUpperLimit;
	float fSuspensionLowerLimit;
	float fSuspensionBias;
	float fCollisionDamageMultiplier;
	uint32 Flags;
	float fSeatOffsetDistance;
	int32 nMonetaryValue;
	int8 FrontLights;
	int8 RearLights;
};
VALIDATE_SIZE(tHandlingData, 0xD8);

class CVehicle;

class cHandlingDataMgr
{
	float field_0;	// unused it seems
public:
	float fWheelFriction;	// wheel related
private:
	float field_8;	//
	float field_C;	// unused it seems
	float field_10;	//
	tHandlingData HandlingData[NUMHANDLINGS];
	uint32 field_302C;	// unused it seems

public:
	cHandlingDataMgr(void);
	void Initialise(void);
	void LoadHandlingData(void);
	int FindExactWord(const char *word, const char *words, int wordLen, int numWords);
	void ConvertDataToWorldUnits(tHandlingData *handling);
	void ConvertDataToGameUnits(tHandlingData *handling);
	void RangeCheck(tHandlingData *handling);
	void ModifyHandlingValue(CVehicle *, const tVehicleType &, const tField &, const bool &);
	void DisplayHandlingData(CVehicle *, tHandlingData *, uint8, bool);
	int32 GetHandlingId(const char *name);
	tHandlingData *GetHandlingData(tVehicleType id) { return &HandlingData[id]; }
	bool HasRearWheelDrive(tVehicleType id) { return HandlingData[id].Transmission.nDriveType != 'F'; }
	bool HasFrontWheelDrive(tVehicleType id) { return HandlingData[id].Transmission.nDriveType != 'R'; }
};
VALIDATE_SIZE(cHandlingDataMgr, 0x3030);
extern cHandlingDataMgr mod_HandlingManager;

license: mit


code:
#include "common.h"
#include "main.h"

#include "General.h"
#include "Darkel.h"
#include "Stats.h"
#include "SurfaceTable.h"
#include "ModelIndices.h"
#include "Streaming.h"
#include "Camera.h"
#include "VisibilityPlugins.h"
#include "ZoneCull.h"
#include "Particle.h"
#include "Shadows.h"
#include "Coronas.h"
#include "Explosion.h"
#include "Timecycle.h"
#include "TempColModels.h"
#include "World.h"
#include "WaterLevel.h"
#include "PlayerPed.h"
#include "Wanted.h"
#include "DMAudio.h"
#include "Object.h"
#include "HandlingMgr.h"
#include "Heli.h"
#ifdef FIX_BUGS
#include "Replay.h"
#endif

enum
{
	HELI_STATUS_HOVER,
	HELI_STATUS_CHASE_PLAYER,
	HELI_STATUS_FLY_AWAY,
	HELI_STATUS_SHOT_DOWN,
	HELI_STATUS_HOVER2,
};

CHeli *CHeli::pHelis[NUM_HELIS];
int16 CHeli::NumRandomHelis;
uint32 CHeli::TestForNewRandomHelisTimer;
int16 CHeli::NumScriptHelis;	// unused
bool CHeli::CatalinaHeliOn;
bool CHeli::CatalinaHasBeenShotDown;
bool CHeli::ScriptHeliOn;

CHeli::CHeli(int32 id, uint8 CreatedBy)
 : CVehicle(CreatedBy)
{
	int i;

	CVehicleModelInfo *mi = (CVehicleModelInfo*)CModelInfo::GetModelInfo(id);
	m_vehType = VEHICLE_TYPE_HELI;
	pHandling = mod_HandlingManager.GetHandlingData((tVehicleType)mi->m_handlingId);
	SetModelIndex(id);
	m_heliStatus = HELI_STATUS_HOVER;
	m_pathState = 0;

	m_fMass = 100000000.0f;
	m_fTurnMass = 100000000.0f;
	m_fAirResistance = 0.9994f;
	m_fElasticity = 0.05f;

	m_nHeliId = 0;
	m_fRotorRotation = 0.0f;
	m_nBulletDamage = 0;
	m_fAngularSpeed = 0.0f;
	m_fRotation = 0.0f;
	m_nSearchLightTimer = CTimer::GetTimeInMilliseconds();
	for(i = 0; i < 6; i++){
		m_aSearchLightHistoryX[i] = 0.0f;
		m_aSearchLightHistoryY[i] = 0.0f;
	}

	for(i = 0; i < 8; i++)
		m_fHeliDustZ[i] = -50.0f;

	m_nPoliceShoutTimer = CTimer::GetTimeInMilliseconds();
	SetStatus(STATUS_HELI);
	m_bTestRight = true;
	m_fTargetOffset = 0.0f;
	m_fSearchLightX = m_fSearchLightY = 0.0f;

	// BUG: not in game but gets initialized to CDCDCDCD in debug
	m_nLastShotTime = 0;
}

void
CHeli::SetModelIndex(uint32 id)
{
	int i;

	CVehicle::SetModelIndex(id);
	for(i = 0; i < NUM_HELI_NODES; i++)
		m_aHeliNodes[i] = nil;
	CClumpModelInfo::FillFrameArray(GetClump(), m_aHeliNodes);
}

static float CatalinaTargetX[7] = { -478.0, -677.0, -907.0, -1095.0, -1152.0, -1161.0, -1161.0 };
static float CatalinaTargetY[7] = { 227.0, 206.0, 210.0, 242.0, 278.0, 341.0, 341.0 };
static float CatalinaTargetZ[7] = { 77.0, 66.0, 60.0, 53.0, 51.0, 46.0, 30.0 };
static float DamPathX[6] = { -1191.0, -1176.0, -1128.0, -1072.0, -1007.0, -971.0 };
static float DamPathY[6] = { 350.0, 388.0, 429.0, 447.0, 449.0, 416.0 };
static float DamPathZ[6] = { 42.0, 37.0, 28.0, 28.0, 31.0, 33.0 };
static float ShortPathX[4] = { -974.0, -1036.0, -1112.0, -1173.0 };
static float ShortPathY[4] = { 340.0, 312.0, 317.0, 294.0 };
static float ShortPathZ[4] = { 41.0, 38.0, 32.0, 39.0 };
static float LongPathX[7] = { -934.0, -905.0, -906.0, -1063.0, -1204.0, -1233.0, -1207.0 };
static float LongPathY[7] = { 371.0, 362.0, 488.0, 548.0, 451.0, 346.0, 308.0 };
static float LongPathZ[7] = { 57.0, 90.0, 105.0, 100.0, 81.0, 79.0, 70.0 };

static int PathPoint;

void
CHeli::ProcessControl(void)
{
	int i;

	if(gbModelViewer)
		return;

	// Find target
	CVector target(0.0f, 0.0f, 0.0f);
	CVector2D vTargetDist;
	if(m_heliType == HELI_TYPE_CATALINA && m_heliStatus != HELI_STATUS_SHOT_DOWN){
		switch(m_pathState){
		case 0:
		case 1:
		case 2:
		case 3:
		case 4:
		case 5:
			target.x = CatalinaTargetX[m_pathState];
			target.y = CatalinaTargetY[m_pathState];
			target.z = CatalinaTargetZ[m_pathState];
			if((target - GetPosition()).Magnitude() < 9.0f)
				m_pathState++;
			break;
		case 6:
			target.x = CatalinaTargetX[m_pathState];
			target.y = CatalinaTargetY[m_pathState];
			target.z = CatalinaTargetZ[m_pathState];
			if(GetPosition().z > 31.55f)
				break;
			m_pathState = 7;
			GetMatrix().GetPosition().z = 31.55f;
			m_vecMoveSpeed = CVector(0.0f, 0.0f, 0.0f);
			break;
		case 7:
			GetMatrix().GetPosition().z = 31.55f;
			target = GetPosition();
			break;

		// Take off
		case 8:
			target.x = GetPosition().x;
			target.y = GetPosition().y;
			target.z = 74.0f;
			if(GetPosition().z < 40.0f)
				break;
			PathPoint = 2;
			m_pathState = 9;
			break;
		// Circle around dam
		case 9:
			target.x = DamPathX[PathPoint];
			target.y = DamPathY[PathPoint];
			target.z = DamPathZ[PathPoint];
			if((target - GetPosition()).Magnitude() < 9.0f){
				PathPoint++;
				if(PathPoint >= 6){
					m_pathState = 10;
					PathPoint = 0;
				}
			}
			break;
		case 10:
			target.x = ShortPathX[PathPoint];
			target.y = ShortPathY[PathPoint];
			target.z = ShortPathZ[PathPoint];
			if((target - GetPosition()).Magnitude() < 9.0f){
				PathPoint++;
				if(PathPoint >= 3){
					m_pathState = 9;
					PathPoint = 1;
				}
			}
			break;
		// how do we get here?
		case 11:
			target.x = LongPathX[PathPoint];
			target.y = LongPathY[PathPoint];
			target.z = LongPathZ[PathPoint];
			if((target - GetPosition()).Magnitude() < 9.0f){
				PathPoint++;
				if(PathPoint >= 7){
					m_pathState = 9;
					PathPoint = 0;
				}
			}
			break;

		// Fly away
		case 12:
			target.x = GetPosition().x;
			target.y = GetPosition().y;
			target.z = 200.0f;
			break;
		}

		vTargetDist = target - GetPosition();
		m_fTargetZ = target.z;
		if(m_pathState == 6){
			GetMatrix().GetPosition().x = GetMatrix().GetPosition().x*0.99f + target.x*0.01f;
			GetMatrix().GetPosition().y = GetMatrix().GetPosition().y*0.99f + target.y*0.01f;
		}
	}else{
		vTargetDist = FindPlayerCoors() - GetPosition();
		m_fTargetZ = FindPlayerCoors().z;

		// Heli flies away to (0, 0)
		if(m_heliStatus == HELI_STATUS_FLY_AWAY && GetPosition().z > 20.0f){
			vTargetDist.x = 0.0f - GetPosition().x;
			vTargetDist.y = 0.0f - GetPosition().y;
		}

		float groundZ;
		switch(m_heliStatus){
		case HELI_STATUS_HOVER:
			groundZ = CWorld::FindGroundZFor3DCoord(GetPosition().x, GetPosition().y, 1000.0f, nil);
			m_fTargetZ = Max(groundZ, m_fTargetZ) + 8.0f;
			break;
		case HELI_STATUS_SHOT_DOWN:
			groundZ = CWorld::FindGroundZFor3DCoord(GetPosition().x, GetPosition().y, 1000.0f, nil);
			m_fTargetZ = Max(groundZ, m_fTargetZ) + 8.0f + m_fTargetOffset;
			break;
		case HELI_STATUS_HOVER2:
			groundZ = CWorld::FindGroundZFor3DCoord(GetPosition().x, GetPosition().y, 1000.0f, nil);
			m_fTargetZ = Max(groundZ, m_fTargetZ) + 8.0f + m_fTargetOffset;
			break;
		default:
			groundZ = CWorld::FindGroundZFor3DCoord(GetPosition().x, GetPosition().y, 1000.0f, nil);
			m_fTargetZ = Max(groundZ, m_fTargetZ) + 12.0f;
			break;
		}

		// Move up if too low
		if(GetPosition().z - 2.0f < groundZ && m_heliStatus != HELI_STATUS_SHOT_DOWN)
			m_vecMoveSpeed.z += CTimer::GetTimeStep()*0.01f;
		m_vecMoveSpeed.z = Clamp(m_vecMoveSpeed.z, -0.3f, 0.3f);
	}

	float fTargetDist = vTargetDist.Magnitude();

	switch(m_heliStatus){
	case HELI_STATUS_HOVER:
	case HELI_STATUS_HOVER2:{
		float targetHeight;
		if(m_heliType == HELI_TYPE_CATALINA)
			targetHeight = 8.0f;
		else
			targetHeight = 40.0f - m_nHeliId*10.0f;
		if(fTargetDist > targetHeight)
			m_heliStatus = HELI_STATUS_CHASE_PLAYER;
		}
#ifdef FIX_BUGS
		break;
#endif
	case HELI_STATUS_CHASE_PLAYER:{
		float targetHeight;
		if(m_heliType == HELI_TYPE_CATALINA)
			targetHeight = 4.0f;
		else
			targetHeight = 30.0f - m_nHeliId*7.5f;
		if(fTargetDist < 1.0f ||
		   fTargetDist < targetHeight && CWorld::GetIsLineOfSightClear(GetPosition(), FindPlayerCoors(), true, false, false, false, false, false))
			m_heliStatus = HELI_STATUS_HOVER;
		}
	}

	// Find xy speed
	float speed;
	if(fTargetDist > 100.0f)
		speed = 1.0f;
	else if(fTargetDist > 75.0f)
		speed = 0.7f;
	else
		speed = 0.4f;
	if(m_heliStatus == HELI_STATUS_HOVER || m_heliStatus == HELI_STATUS_HOVER2 || m_heliStatus == HELI_STATUS_SHOT_DOWN)
		speed = 0.0f;

	if(fTargetDist != 0.0f)
		vTargetDist /= fTargetDist;
	else
		vTargetDist.x = 1.0f;
	CVector2D targetSpeed = vTargetDist * speed;

	if(m_heliStatus == HELI_STATUS_HOVER2 || m_heliStatus == HELI_STATUS_SHOT_DOWN){
		bool force = !!((CTimer::GetFrameCounter() + m_randomSeed) & 8);
		if(m_bTestRight){
			if(force || CWorld::TestSphereAgainstWorld(GetPosition() + 4.0f*GetRight(), 2.0f, this, true, false, false, false, false, false) == nil){
				if(m_heliStatus == HELI_STATUS_SHOT_DOWN){
					m_fTargetOffset -= CTimer::GetTimeStep()*0.05f;
					targetSpeed.x -= -vTargetDist.x*0.15f;
					targetSpeed.y -= vTargetDist.y*0.15f;
				}else{
					targetSpeed.x -= -vTargetDist.x*0.05f;
					targetSpeed.y -= vTargetDist.y*0.05f;
				}
			}else{
				m_bTestRight = false;
				if(m_heliStatus == HELI_STATUS_HOVER2)
					m_fTargetOffset += 5.0f;
				else
					m_fTargetOffset -= 5.0f;
			}
		}else{
			if(force || CWorld::TestSphereAgainstWorld(GetPosition() - 4.0f*GetRight(), 2.0f, this, true, false, false, false, false, false) == nil){
				if(m_heliStatus == HELI_STATUS_SHOT_DOWN){
					m_fTargetOffset -= CTimer::GetTimeStep()*0.05f;
					targetSpeed.x += -vTargetDist.x*0.15f;
					targetSpeed.y += vTargetDist.y*0.15f;
				}else{
					targetSpeed.x += -vTargetDist.x*0.05f;
					targetSpeed.y += vTargetDist.y*0.05f;
				}
			}else{
				m_bTestRight = true;
				if(m_heliStatus == HELI_STATUS_HOVER2)
					m_fTargetOffset += 5.0f;
				else
					m_fTargetOffset -= 5.0f;
			}
		}

		if(m_fTargetOffset > 30.0f)
			m_fTargetOffset = 30.0f;

		if(m_heliStatus == HELI_STATUS_SHOT_DOWN && force){
			if(CWorld::TestSphereAgainstWorld(GetPosition() + 1.5f*GetForward(), 2.0f, this, true, false, false, false, false, false) ||
			   CWorld::TestSphereAgainstWorld(GetPosition() - 1.5f*GetForward(), 2.0f, this, true, false, false, false, false, false))
				m_nExplosionTimer = CTimer::GetPreviousTimeInMilliseconds();
		}
	}else
		if(m_fTargetOffset >= 2.0f)
			m_fTargetOffset -= 2.0f;

	if(m_heliType == HELI_TYPE_CATALINA)
		if(m_pathState == 9 || m_pathState == 11 || m_pathState == 10){
			float f = Pow(0.997f, CTimer::GetTimeStep());
			m_vecMoveSpeed.x *= f;
			m_vecMoveSpeed.y *= f;
		}

	CVector2D speedDir = targetSpeed - m_vecMoveSpeed;
	float speedDiff = speedDir.Magnitude();
	if(speedDiff != 0.0f)
		speedDir /= speedDiff;
	else
		speedDir.x = 1.0f;
	float speedInc = CTimer::GetTimeStep()*0.002f;
	if(speedDiff < speedInc){
		m_vecMoveSpeed.x = targetSpeed.x;
		m_vecMoveSpeed.y = targetSpeed.y;
	}else{
		m_vecMoveSpeed.x += speedDir.x*speedInc;
		m_vecMoveSpeed.y += speedDir.y*speedInc;
	}
	GetMatrix().GetPosition().x += m_vecMoveSpeed.x*CTimer::GetTimeStep();
	GetMatrix().GetPosition().y += m_vecMoveSpeed.y*CTimer::GetTimeStep();

	// Find z target
	if(m_heliStatus == HELI_STATUS_FLY_AWAY)
		m_fTargetZ = 1000.0f;
	if((CTimer::GetTimeInMilliseconds() + 800*m_nHeliId) & 0x800)
		m_fTargetZ += 2.0f;
	m_fTargetZ += m_nHeliId*5.0f;

	// Find z speed
	float targetSpeedZ = (m_fTargetZ - GetPosition().z)*0.01f;
	float speedDiffZ = targetSpeedZ - m_vecMoveSpeed.z;
	float speedIncZ = CTimer::GetTimeStep()*0.001f;
	if(m_heliStatus == HELI_STATUS_FLY_AWAY)
		speedIncZ *= 1.5f;
	if(Abs(speedDiffZ) < speedIncZ)
		m_vecMoveSpeed.z = targetSpeedZ;
	else if(speedDiffZ < 0.0f)
		m_vecMoveSpeed.z -= speedIncZ;
	else
		m_vecMoveSpeed.z += speedIncZ*1.5f;
	GetMatrix().GetPosition().z += m_vecMoveSpeed.z*CTimer::GetTimeStep();

	// Find angular speed
	float targetAngularSpeed;
	m_fAngularSpeed *= Pow(0.995f, CTimer::GetTimeStep());
	if(fTargetDist < 8.0f)
		targetAngularSpeed = 0.0f;
	else{
		float rotationDiff = CGeneral::GetATanOfXY(vTargetDist.x, vTargetDist.y) - m_fRotation;
		while(rotationDiff < -3.14f) rotationDiff += 6.28f;
		while(rotationDiff > 3.14f) rotationDiff -= 6.28f;
		if(Abs(rotationDiff) > 0.4f){
			if(rotationDiff < 0.0f)
				targetAngularSpeed = -0.2f;
			else
				targetAngularSpeed = 0.2f;
		}else
			targetAngularSpeed = 0.0f;
	}
	float angularSpeedDiff = targetAngularSpeed - m_fAngularSpeed;
	float angularSpeedInc = CTimer::GetTimeStep()*0.0001f;
	if(Abs(angularSpeedDiff) < angularSpeedInc)
		m_fAngularSpeed = targetAngularSpeed;
	else if(angularSpeedDiff < 0.0f)
		m_fAngularSpeed -= angularSpeedInc;
	else
		m_fAngularSpeed += angularSpeedInc;
	m_fRotation += m_fAngularSpeed * CTimer::GetTimeStep();

	// Set matrix
	CVector up(3.0f*m_vecMoveSpeed.x, 3.0f*m_vecMoveSpeed.y, 1.0f);
	up.Normalise();
	CVector fwd(-Cos(m_fRotation), -Sin(m_fRotation), 0.0f);	// not really forward
	CVector right = CrossProduct(up, fwd);
	fwd = CrossProduct(up, right);
	GetRight() = right;
	GetForward() = fwd;
	GetUp() = up;

	// Search light and shooting
	if(m_heliStatus == HELI_STATUS_FLY_AWAY || m_heliType == HELI_TYPE_CATALINA || CCullZones::PlayerNoRain())
		m_fSearchLightIntensity = 0.0f;
	else {
		// Update search light history once every 1000ms
		int timeDiff = CTimer::GetTimeInMilliseconds() - m_nSearchLightTimer;
		while (timeDiff > 1000) {
			for (i = 5; i > 0; i--) {
				m_aSearchLightHistoryX[i] = m_aSearchLightHistoryX[i - 1];
				m_aSearchLightHistoryY[i] = m_aSearchLightHistoryY[i - 1];
			}
			m_aSearchLightHistoryX[0] = FindPlayerCoors().x + FindPlayerSpeed().x * 50.0f * (m_nHeliId + 2);
			m_aSearchLightHistoryY[0] = FindPlayerCoors().y + FindPlayerSpeed().y * 50.0f * (m_nHeliId + 2);

			timeDiff -= 1000;
			m_nSearchLightTimer += 1000;
		}
		assert(timeDiff <= 1000);
		float f1 = timeDiff / 1000.0f;
		float f2 = 1.0f - f1;
		m_fSearchLightX = m_aSearchLightHistoryX[m_nHeliId + 2] * f2 + m_aSearchLightHistoryX[m_nHeliId + 2 - 1] * f1;
		m_fSearchLightY = m_aSearchLightHistoryY[m_nHeliId + 2] * f2 + m_aSearchLightHistoryY[m_nHeliId + 2 - 1] * f1;

		float searchLightDist = (CVector2D(m_fSearchLightX, m_fSearchLightY) - GetPosition()).Magnitude();
		if (searchLightDist > 60.0f)
			m_fSearchLightIntensity = 0.0f;
		else if (searchLightDist < 40.0f)
			m_fSearchLightIntensity = 1.0f;
		else
			m_fSearchLightIntensity = 1.0f - (40.0f - searchLightDist) / (60.0f-40.0f);

		if (m_fSearchLightIntensity < 0.9f || sq(FindPlayerCoors().x - m_fSearchLightX) + sq(FindPlayerCoors().y - m_fSearchLightY) > sq(7.0f))
			m_nShootTimer = CTimer::GetTimeInMilliseconds();
		else if (CTimer::GetTimeInMilliseconds() > m_nPoliceShoutTimer) {
			DMAudio.PlayOneShot(m_audioEntityId, SOUND_PED_HELI_PLAYER_FOUND, 0.0f);
			m_nPoliceShoutTimer = CTimer::GetTimeInMilliseconds() + 4500 + (CGeneral::GetRandomNumber() & 0xFFF);
		}
#ifdef FIX_BUGS
		if (!CReplay::IsPlayingBack())
#endif
		{
			// Shoot
			int shootTimeout;
			if (m_heliType == HELI_TYPE_RANDOM) {
				switch (FindPlayerPed()->m_pWanted->GetWantedLevel()) {
				case 0:
				case 1:
				case 2: shootTimeout = 999999; break;
				case 3: shootTimeout = 10000; break;
				case 4: shootTimeout = 5000; break;
				case 5: shootTimeout = 3500; break;
				case 6: shootTimeout = 2000; break;
				}
				if (CCullZones::NoPolice())
					shootTimeout /= 2;
			}
			else
				shootTimeout = 1500;

			if (FindPlayerPed()->m_pWanted->IsIgnored())
				m_nShootTimer = CTimer::GetTimeInMilliseconds();
			else {
				// Check if line of sight is clear
				if (CTimer::GetTimeInMilliseconds() > m_nShootTimer + shootTimeout &&
					CTimer::GetPreviousTimeInMilliseconds() <= m_nShootTimer + shootTimeout) {
					if (CWorld::GetIsLineOfSightClear(GetPosition(), FindPlayerCoors(), true, false, false, false, false, false)) {
						if (m_heliStatus == HELI_STATUS_HOVER2)
							m_heliStatus = HELI_STATUS_HOVER;
					}
					else {
						m_nShootTimer = CTimer::GetTimeInMilliseconds();
						if (m_heliStatus == HELI_STATUS_HOVER)
							m_heliStatus = HELI_STATUS_HOVER2;
					}
				}

				// Shoot!
				if (CTimer::GetTimeInMilliseconds() > m_nShootTimer + shootTimeout &&
					CTimer::GetTimeInMilliseconds() > m_nLastShotTime + 200) {
					CVector shotTarget = FindPlayerCoors();
					// some inaccuracy
					shotTarget.x += ((CGeneral::GetRandomNumber() & 0xFF) - 128) / 50.0f;
					shotTarget.y += ((CGeneral::GetRandomNumber() & 0xFF) - 128) / 50.0f;
					CVector direction = FindPlayerCoors() - GetPosition();
					direction.Normalise();
					shotTarget += 3.0f * direction;
					CVector shotSource = GetPosition();
					shotSource += 3.0f * direction;
					FireOneInstantHitRound(&shotSource, &shotTarget, 20);
					DMAudio.PlayOneShot(m_audioEntityId, SOUND_WEAPON_SHOT_FIRED, 0.0f);
					m_nLastShotTime = CTimer::GetTimeInMilliseconds();
				}
			}
		}
	}

	// Drop Catalina's bombs
	if(m_heliType == HELI_TYPE_CATALINA && m_pathState > 8 && (CTimer::GetTimeInMilliseconds()>>9) != (CTimer::GetPreviousTimeInMilliseconds()>>9)){
		CVector bombPos = GetPosition() - 60.0f*m_vecMoveSpeed;
		if(sq(FindPlayerCoors().x-bombPos.x) + sq(FindPlayerCoors().y-bombPos.y) < sq(35.0f)){
			bool found;
			float groundZ = CWorld::FindGroundZFor3DCoord(bombPos.x, bombPos.y, bombPos.z, &found);
			float waterZ;
			if(!CWaterLevel::GetWaterLevelNoWaves(bombPos.x, bombPos.y, bombPos.z, &waterZ))
				waterZ = 0.0f;
			if(groundZ > waterZ){
				bombPos.z = groundZ + 2.0f;
				CExplosion::AddExplosion(nil, this, EXPLOSION_HELI_BOMB, bombPos, 0);
			}else{
				bombPos.z = waterZ;
				CVector dir;
				for(i = 0; i < 16; i++){
					dir.x = ((CGeneral::GetRandomNumber()&0xFF)-127)*0.001f;
					dir.y = ((CGeneral::GetRandomNumber()&0xFF)-127)*0.001f;
					dir.z = 0.5f;
					CParticle::AddParticle(PARTICLE_BOAT_SPLASH, bombPos, dir, nil, 0.2f);
				}
			}
		}
	}

	RemoveAndAdd();
	bIsInSafePosition = true;
	GetMatrix().UpdateRW();
	UpdateRwFrame();
}

void
CHeli::PreRender(void)
{
	float angle;
	uint8 i;
	CColPoint point;
	CEntity *entity;
	uint8 r, g, b;
	float testLowZ = FindPlayerCoors().z - 10.0f;
	float radius = (GetPosition().z - FindPlayerCoors().z - 10.0f - 1.0f) * 0.3f + 10.0f;
	int frm = CTimer::GetFrameCounter() & 7;

	i = 0;
	for(angle = 0.0f; angle < TWOPI; angle += TWOPI/32){
		CVector pos(radius*Cos(angle), radius*Sin(angle), 0.0f);
		CVector dir = CVector(pos.x, pos.y, 1.0f)*0.01f;
		pos += GetPosition();

		if(CWorld::ProcessVerticalLine(pos, testLowZ, point, entity, true, false, false, false, true, false, nil))
			m_fHeliDustZ[frm] = point.point.z;
		else
			m_fHeliDustZ[frm] = -101.0f;

		switch(point.surfaceB){
		default:
		case SURFACE_TARMAC:
			r = 10;
			g = 10;
			b = 10;
			break;
		case SURFACE_GRASS:
			r = 10;
			g = 6;
			b = 3;
			break;
		case SURFACE_GRAVEL:
			r = 10;
			g = 8;
			b = 7;
			break;
		case SURFACE_MUD_DRY:
			r = 10;
			g = 6;
			b = 3;
			break;
		}
		RwRGBA col = { r, g, b, 32 };
#ifdef FIX_BUGS
		pos.z = m_fHeliDustZ[frm];
#else
		// What the hell is the point of this?
		pos.z = m_fHeliDustZ[(i - (i&3))/4];	// advance every 4 iterations, why not just /4?
#endif
		if(pos.z > -200.0f && GetPosition().z - pos.z < 20.0f)
			CParticle::AddParticle(PARTICLE_HELI_DUST, pos, dir, nil, 0.0f, col);
		i++;
	}
}

void
CHeli::Render(void)
{
	CMatrix mat;
	CVector pos;

	mat.Attach(RwFrameGetMatrix(m_aHeliNodes[HELI_TOPROTOR]));
	pos = mat.GetPosition();
	mat.SetRotateZ(m_fRotorRotation);
	mat.Translate(pos);
	mat.UpdateRW();

	m_fRotorRotation += 3.14f/6.5f;
	if(m_fRotorRotation > 6.28f)
		m_fRotorRotation -= 6.28f;

	mat.Attach(RwFrameGetMatrix(m_aHeliNodes[HELI_BACKROTOR]));
	pos = mat.GetPosition();
	mat.SetRotateX(m_fRotorRotation);
	mat.Translate(pos);
	mat.UpdateRW();

	CEntity::Render();
}

void
CHeli::PreRenderAlways(void)
{
	CVector shadowPos(m_fSearchLightX, m_fSearchLightY, GetPosition().z);
	if(m_fSearchLightIntensity > 0.0f){
		CShadows::StoreShadowToBeRendered(SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &shadowPos,
			6.0f, 0.0f, 0.0f, -6.0f,
			80*m_fSearchLightIntensity, 80*m_fSearchLightIntensity, 80*m_fSearchLightIntensity, 80*m_fSearchLightIntensity,
			50.0f, true, 1.0f);

		CVector front = GetMatrix() * CVector(0.0f, 7.0f, 0.0f);
		CVector toPlayer = FindPlayerCoors() - front;
		toPlayer.Normalise();
		float intensity = m_fSearchLightIntensity*sq(CTimeCycle::GetSpriteBrightness());
		if(DotProduct(toPlayer, TheCamera.GetForward()) < -0.8f)
			CCoronas::RegisterCorona((uintptr)this, 255*intensity, 255*intensity, 255*intensity, 255,
				front, 10.0f, 60.0f, CCoronas::TYPE_STAR,
				CCoronas::FLARE_HEADLIGHTS, CCoronas::REFLECTION_OFF, CCoronas::LOSCHECK_OFF, CCoronas::STREAK_OFF, 0.0f);
		else
			CCoronas::RegisterCorona((uintptr)this, 200*intensity, 200*intensity, 200*intensity, 255,
				front, 8.0f, 60.0f, CCoronas::TYPE_STAR,
				CCoronas::FLARE_HEADLIGHTS, CCoronas::REFLECTION_OFF, CCoronas::LOSCHECK_OFF, CCoronas::STREAK_OFF, 0.0f);
	}

	CVector back = GetMatrix() * CVector(0.0f, -9.0f, 0.0f);
	if(CTimer::GetTimeInMilliseconds() & 0x100)
		CCoronas::RegisterCorona((uintptr)this + 2, 255, 0, 0, 255,
			back, 1.0f, 60.0f, CCoronas::TYPE_STAR,
			CCoronas::FLARE_NONE, CCoronas::REFLECTION_OFF, CCoronas::LOSCHECK_OFF, CCoronas::STREAK_OFF, 0.0f);
	else
		CCoronas::RegisterCorona((uintptr)this + 2, 0, 0, 0, 255,
			back, 1.0f, 60.0f, CCoronas::TYPE_STAR,
			CCoronas::FLARE_NONE, CCoronas::REFLECTION_OFF, CCoronas::LOSCHECK_OFF, CCoronas::STREAK_OFF, 0.0f);
}

RwObject*
GetHeliAtomicObjectCB(RwObject *object, void *data)
{
	RpAtomic *atomic = (RpAtomic*)object;
	assert(RwObjectGetType(object) == rpATOMIC);
	if(RpAtomicGetFlags(atomic) & rpATOMICRENDER)
		*(RpAtomic**)data = atomic;
	return object;
}

CObject*
CHeli::SpawnFlyingComponent(int32 component)
{
	RpAtomic *atomic;
	RwFrame *frame;
	RwMatrix *matrix;
	CObject *obj;

	if(m_aHeliNodes[component] == nil)
		return nil;

	atomic = nil;
	RwFrameForAllObjects(m_aHeliNodes[component], GetHeliAtomicObjectCB, &atomic);
	if(atomic == nil)
		return nil;

	obj = new CObject;
	if(obj == nil)
		return nil;

	obj->SetModelIndexNoCreate(MI_CAR_WHEEL);
	// object needs base model
	obj->RefModelInfo(GetModelIndex());

	// create new atomic
	matrix = RwFrameGetLTM(m_aHeliNodes[component]);
	frame = RwFrameCreate();
	atomic = RpAtomicClone(atomic);
	*RwFrameGetMatrix(frame) = *matrix;
	RpAtomicSetFrame(atomic, frame);
	CVisibilityPlugins::SetAtomicRenderCallback(atomic, nil);
	obj->AttachToRwObject((RwObject*)atomic);

	// init object
	obj->m_fMass = 10.0f;
	obj->m_fTurnMass = 25.0f;
	obj->m_fAirResistance = 0.99f;
	obj->m_fElasticity = 0.1f;
	obj->m_fBuoyancy = obj->m_fMass*GRAVITY/0.75f;
	obj->ObjectCreatedBy = TEMP_OBJECT;
	obj->SetIsStatic(false);
	obj->bIsPickup = false;

	// life time
	CObject::nNoTempObjects++;
	if(component == HELI_TOPROTOR)
		obj->m_nEndOfLifeTime = CTimer::GetTimeInMilliseconds() + 1000;
	else
		obj->m_nEndOfLifeTime = CTimer::GetTimeInMilliseconds() + 3000;

	obj->m_vecMoveSpeed = m_vecMoveSpeed;
	if(obj->m_vecMoveSpeed.z > 0.0f)
		obj->m_vecMoveSpeed.z = 0.3f;
	else
		obj->m_vecMoveSpeed.z = 0.0f;

	obj->m_vecTurnSpeed = m_vecTurnSpeed*2.0f;

	if(component == HELI_BACKROTOR)
		obj->m_vecTurnSpeed.x = 0.5f;
	else if(component == HELI_TOPROTOR || component == HELI_TOPKNOT)
		obj->m_vecTurnSpeed.z = 0.5f;
	else
		obj->m_vecTurnSpeed.y = 0.5f;

	obj->bRenderScorched = true;

	CWorld::Add(obj);

	atomic = nil;
	RwFrameForAllObjects(m_aHeliNodes[component], GetHeliAtomicObjectCB, &atomic);
	if(atomic)
		RpAtomicSetFlags(atomic, 0);

	return obj;
}

void
CHeli::InitHelis(void)
{
	int i;

	NumRandomHelis = 0;
	TestForNewRandomHelisTimer = 0;
	NumScriptHelis = 0;
	CatalinaHeliOn = false;
	ScriptHeliOn = false;
	for(i = 0; i < NUM_HELIS; i++)
		pHelis[i] = nil;

#if GTA_VERSION >= GTA3_PS2_160
	((CVehicleModelInfo*)CModelInfo::GetModelInfo(MI_ESCAPE))->SetColModel(&CTempColModels::ms_colModelPed1);
	((CVehicleModelInfo*)CModelInfo::GetModelInfo(MI_CHOPPER))->SetColModel(&CTempColModels::ms_colModelPed1);
#endif
}

CHeli*
CHeli::GenerateHeli(bool catalina)
{
	CHeli *heli;
	CVector heliPos;
	int i;

#if GTA_VERSION < GTA3_PS2_160
	if(catalina)
		((CVehicleModelInfo*)CModelInfo::GetModelInfo(MI_ESCAPE))->SetColModel(&CTempColModels::ms_colModelPed1);
	else
		((CVehicleModelInfo*)CModelInfo::GetModelInfo(MI_CHOPPER))->SetColModel(&CTempColModels::ms_colModelPed1);
#endif

	if(catalina)
		heli = new CHeli(MI_ESCAPE, PERMANENT_VEHICLE);
	else
		heli = new CHeli(MI_CHOPPER, PERMANENT_VEHICLE);

	if(catalina)
		heliPos = CVector(-224.0f, 201.0f, 83.0f);
	else{
		heliPos = FindPlayerCoors();
		float angle = (float)(CGeneral::GetRandomNumber() & 0xFF)/0x100 * 6.28f;
		heliPos.x += 250.0f*Sin(angle);
		heliPos.y += 250.0f*Cos(angle);
		if(heliPos.x < -2000.0f || heliPos.x > 2000.0f || heliPos.y < -2000.0f || heliPos.y > 2000.0f){
			heliPos = FindPlayerCoors();
			heliPos.x -= 250.0f*Sin(angle);
			heliPos.y -= 250.0f*Cos(angle);
		}
		heliPos.z += 50.0f;
	}
	heli->GetMatrix().SetTranslate(heliPos);
	if(catalina)
		heli->GetMatrix().SetRotateZOnly(DEGTORAD(270.0f));	// game actually uses 3.14 here

	heli->SetStatus(STATUS_ABANDONED);
	heli->bIsLocked = true;

	int id = -1;
	bool found = false;
	while(!found){
		id++;
		found = true;
		for(i = 0; i < 4; i++)
			if(pHelis[i] && pHelis[i]->m_nHeliId == id)
				found = false;
	}
	heli->m_nHeliId = id;

	CWorld::Add(heli);

	return heli;
}

void
CHeli::UpdateHelis(void)
{
	int i, j;

	// Spawn new police helis
	int numHelisRequired =
#ifdef FIX_BUGS
		CReplay::IsPlayingBack() ? 0 :
#endif
		FindPlayerPed()->m_pWanted->NumOfHelisRequired();
	if(CStreaming::HasModelLoaded(MI_CHOPPER) && CTimer::GetTimeInMilliseconds() > TestForNewRandomHelisTimer){
		// Spawn a police heli
		TestForNewRandomHelisTimer = CTimer::GetTimeInMilliseconds() + 15000;
		if(NumRandomHelis < numHelisRequired){
			NumRandomHelis++;
			CHeli *heli = GenerateHeli(false);
			heli->m_heliType = HELI_TYPE_RANDOM;
			if(pHelis[HELI_RANDOM0] == nil)
				pHelis[HELI_RANDOM0] = heli;
			else if(pHelis[HELI_RANDOM1] == nil)
				pHelis[HELI_RANDOM1] = heli;
			else
				assert(0 && "too many helis");
		}
	}

	// Handle script heli
	if(ScriptHeliOn){
		if(CStreaming::HasModelLoaded(MI_CHOPPER) && pHelis[HELI_SCRIPT] == nil){
			pHelis[HELI_SCRIPT] = GenerateHeli(false);
			pHelis[HELI_SCRIPT]->m_heliType = HELI_TYPE_SCRIPT;
		}else
			CStreaming::RequestModel(MI_CHOPPER, 0);
	}else{
		if(pHelis[HELI_SCRIPT])
			pHelis[HELI_SCRIPT]->m_heliStatus = HELI_STATUS_FLY_AWAY;
	}

	// Handle Catalina's heli
	if(CatalinaHeliOn){
		if(CStreaming::HasModelLoaded(MI_ESCAPE) && pHelis[HELI_CATALINA] == nil){
			pHelis[HELI_CATALINA] = GenerateHeli(true);
			pHelis[HELI_CATALINA]->m_heliType = HELI_TYPE_CATALINA;
		}else
			CStreaming::RequestModel(MI_ESCAPE, STREAMFLAGS_DONT_REMOVE);
	}else{
		if(pHelis[HELI_CATALINA])
			pHelis[HELI_CATALINA]->m_heliStatus = HELI_STATUS_FLY_AWAY;
	}

	// Delete helis that we no longer need
	for(i = 0; i < NUM_HELIS; i++)
		if(pHelis[i] && pHelis[i]->m_heliStatus == HELI_STATUS_FLY_AWAY && pHelis[i]->GetPosition().z > 150.0f){
			CWorld::Remove(pHelis[i]);
			delete pHelis[i];
			pHelis[i] = nil;
			if(i != HELI_SCRIPT && i != HELI_CATALINA)
				NumRandomHelis--;
		}

	// Handle explosions
	for(i = 0; i < NUM_HELIS; i++){
		if(pHelis[i] && pHelis[i]->m_heliStatus == HELI_STATUS_SHOT_DOWN && CTimer::GetTimeInMilliseconds() > pHelis[i]->m_nExplosionTimer){
			// Second part of explosion
			static int nFrameGen;
			CRGBA colors[8];

			TheCamera.CamShake(0.7f, pHelis[i]->GetPosition().x, pHelis[i]->GetPosition().y, pHelis[i]->GetPosition().z);

			colors[0] = CRGBA(0, 0, 0, 255);
			colors[1] = CRGBA(224, 230, 238, 255);
			colors[2] = CRGBA(0, 0, 0, 255);
			colors[3] = CRGBA(0, 0, 0, 255);
			colors[4] = CRGBA(66, 162, 252, 255);
			colors[5] = CRGBA(0, 0, 0, 255);
			colors[6] = CRGBA(0, 0, 0, 255);
			colors[7] = CRGBA(0, 0, 0, 255);

			CVector pos = pHelis[i]->GetPosition();
			CVector dir;
			for(j = 0; j < 40; j++){
				dir.x = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
				dir.y = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
				dir.z = CGeneral::GetRandomNumberInRange(0.0f, 2.0f);
				int rotSpeed = CGeneral::GetRandomNumberInRange(10, 30);
				if(CGeneral::GetRandomNumber() & 1)
					rotSpeed = -rotSpeed;
				int f = ++nFrameGen & 3;
				CParticle::AddParticle(PARTICLE_HELI_DEBRIS, pos, dir,
					nil, CGeneral::GetRandomNumberInRange(0.1f, 1.0f),
					colors[nFrameGen], rotSpeed, 0, f, 0);
			}

			CExplosion::AddExplosion(nil, nil, EXPLOSION_HELI, pos, 0);

			pHelis[i]->SpawnFlyingComponent(HELI_SKID_LEFT);
			pHelis[i]->SpawnFlyingComponent(HELI_SKID_RIGHT);
			pHelis[i]->SpawnFlyingComponent(HELI_TOPROTOR);

			CDarkel::RegisterCarBlownUpByPlayer(pHelis[i]);
			CWorld::Remove(pHelis[i]);
			delete pHelis[i];
			pHelis[i] = nil;
			if(i != HELI_SCRIPT && i != HELI_CATALINA)
				NumRandomHelis--;
			if(i == HELI_CATALINA)
				CatalinaHasBeenShotDown = true;

			CStats::HelisDestroyed++;
			CStats::PeopleKilledByPlayer += 2;
			CStats::PedsKilledOfThisType[PEDTYPE_COP] += 2;
			CWorld::Players[CWorld::PlayerInFocus].m_nMoney += 250;
			pos = CWorld::Players[CWorld::PlayerInFocus].m_pPed->GetPosition();
			CWorld::Players[CWorld::PlayerInFocus].m_pPed->m_pWanted->RegisterCrime_Immediately(CRIME_SHOOT_HELI,
				pos, i + 19843, false);

			TestForNewRandomHelisTimer = CTimer::GetTimeInMilliseconds() + 50000;
		}else if(pHelis[i] && pHelis[i]->m_heliStatus == HELI_STATUS_SHOT_DOWN && CTimer::GetTimeInMilliseconds()+7000 > pHelis[i]->m_nExplosionTimer){
			// First part of explosion
			if(CTimer::GetPreviousTimeInMilliseconds()+7000 < pHelis[i]->m_nExplosionTimer){
				pHelis[i]->SpawnFlyingComponent(HELI_BACKROTOR);
				pHelis[i]->SpawnFlyingComponent(HELI_TAIL);
				pHelis[i]->m_fAngularSpeed *= -2.5f;
				pHelis[i]->bRenderScorched = true;

				TheCamera.CamShake(0.4f, pHelis[i]->GetPosition().x, pHelis[i]->GetPosition().y, pHelis[i]->GetPosition().z);

				CVector pos = pHelis[i]->GetPosition() - 2.5f*pHelis[i]->GetForward();
				CExplosion::AddExplosion(nil, nil, EXPLOSION_HELI, pos, 0);
			}else
				pHelis[i]->m_fAngularSpeed *= 1.03f;
		}
	}

	// Find police helis to remove
	for(i = 0; i < 2; i++)
		if(pHelis[i] && pHelis[i]->m_heliStatus != HELI_STATUS_FLY_AWAY){
			if(numHelisRequired > 0)
				numHelisRequired--;
			else
				pHelis[i]->m_heliStatus = HELI_STATUS_FLY_AWAY;
		}

	// Remove all helis if in a tunnel or under water
	if(FindPlayerCoors().z < - 2.0f)
		for(i = 0; i < NUM_HELIS; i++)
			if(pHelis[i] && pHelis[i]->m_heliStatus != HELI_STATUS_SHOT_DOWN)
				pHelis[i]->m_heliStatus = HELI_STATUS_FLY_AWAY;
}

void
CHeli::SpecialHeliPreRender(void)
{
	int i;
	for(i = 0; i < NUM_HELIS; i++)
		if(pHelis[i])
			pHelis[i]->PreRenderAlways();
}

bool
CHeli::TestRocketCollision(CVector *rocketPos)
{
	int i;
	bool hit = false;

	for(i = 0; i < NUM_HELIS; i++){
		if(pHelis[i] && !pHelis[i]->bExplosionProof && (*rocketPos - pHelis[i]->GetPosition()).MagnitudeSqr() < sq(8.0f)){
			pHelis[i]->m_fAngularSpeed = CGeneral::GetRandomTrueFalse() ? 0.05f : -0.05f;
			pHelis[i]->m_heliStatus = HELI_STATUS_SHOT_DOWN;
			pHelis[i]->m_nExplosionTimer = CTimer::GetTimeInMilliseconds() + 10000;
			hit = true;
		}
	}
	return hit;
}

bool
CHeli::TestBulletCollision(CVector *line0, CVector *line1, CVector *bulletPos, int32 damage)
{
	int i;
	bool hit = false;

	for(i = 0; i < NUM_HELIS; i++)
		if(pHelis[i] && !pHelis[i]->bBulletProof && CCollision::DistToLine(line0, line1, &pHelis[i]->GetPosition()) < 5.0f){
			// Find bullet position
			float distToHeli = (pHelis[i]->GetPosition() - *line0).Magnitude();
			CVector line = (*line1 - *line0);
			float lineLength = line.Magnitude();
			*bulletPos = *line0 + line*Max(1.0f, distToHeli-5.0f)/lineLength;

			pHelis[i]->m_nBulletDamage += damage;

			if(pHelis[i]->m_heliType == HELI_CATALINA && pHelis[i]->m_nBulletDamage > 400 ||
			   pHelis[i]->m_heliType != HELI_CATALINA && pHelis[i]->m_nBulletDamage > 700){
				pHelis[i]->m_fAngularSpeed = CGeneral::GetRandomTrueFalse() ? 0.05f : -0.05f;
				pHelis[i]->m_heliStatus = HELI_STATUS_SHOT_DOWN;
				pHelis[i]->m_nExplosionTimer = CTimer::GetTimeInMilliseconds() + 10000;
			}

			hit = true;
		}
	return hit;
}

void CHeli::StartCatalinaFlyBy(void)
{
	CatalinaHeliOn = true;
	CatalinaHasBeenShotDown = false;
}

void
CHeli::RemoveCatalinaHeli(void)
{
	CatalinaHeliOn = false;
	if(pHelis[HELI_CATALINA]){
		CWorld::Remove(pHelis[HELI_CATALINA]);
		delete pHelis[HELI_CATALINA];
		pHelis[HELI_CATALINA] = nil;
	}
}

CHeli *CHeli::FindPointerToCatalinasHeli(void) { return pHelis[HELI_CATALINA]; }
void CHeli::CatalinaTakeOff(void) { pHelis[HELI_CATALINA]->m_pathState = 8; }
void CHeli::MakeCatalinaHeliFlyAway(void) { pHelis[HELI_CATALINA]->m_pathState = 12; }
bool CHeli::HasCatalinaBeenShotDown(void) { return CatalinaHasBeenShotDown; }

void CHeli::ActivateHeli(bool activate) { ScriptHeliOn = activate; }


code:
#pragma once

#include "Vehicle.h"

class CObject;

enum eHeliNodes
{
	HELI_CHASSIS = 1,
	HELI_TOPROTOR,
	HELI_BACKROTOR,
	HELI_TAIL,
	HELI_TOPKNOT,
	HELI_SKID_LEFT,
	HELI_SKID_RIGHT,
	NUM_HELI_NODES
};

enum
{
	HELI_RANDOM0,
	HELI_RANDOM1,
	HELI_SCRIPT,
	HELI_CATALINA,
	NUM_HELIS
};

enum
{
	HELI_TYPE_RANDOM,
	HELI_TYPE_SCRIPT,
	HELI_TYPE_CATALINA,
};

class CHeli : public CVehicle
{
public:
	// 0x288
	RwFrame *m_aHeliNodes[NUM_HELI_NODES];
	int8 m_heliStatus;
	float m_fSearchLightX;
	float m_fSearchLightY;
	uint32 m_nExplosionTimer;
	float m_fRotation;
	float m_fAngularSpeed;
	float m_fTargetZ;
	float m_fSearchLightIntensity;
	int8 m_nHeliId;
	int8 m_heliType;
	int8 m_pathState;
	float m_aSearchLightHistoryX[6];
	float m_aSearchLightHistoryY[6];
	uint32 m_nSearchLightTimer;
	uint32 m_nShootTimer;
	uint32 m_nLastShotTime;
	uint32 m_nBulletDamage;
	float m_fRotorRotation;
	float m_fHeliDustZ[8];
	uint32 m_nPoliceShoutTimer;
	float m_fTargetOffset;
	bool m_bTestRight;

	static CHeli *pHelis[NUM_HELIS];
	static int16 NumRandomHelis;
	static uint32 TestForNewRandomHelisTimer;
	static int16 NumScriptHelis;	// unused
	static bool CatalinaHeliOn;
	static bool CatalinaHasBeenShotDown;
	static bool ScriptHeliOn;

	CHeli(int32 id, uint8 CreatedBy);

	// from CEntity
	void SetModelIndex(uint32 id);
	void ProcessControl(void);
	void PreRender(void);
	void Render(void);

	void PreRenderAlways(void);
	CObject *SpawnFlyingComponent(int32 component);

	static void InitHelis(void);
	static CHeli *GenerateHeli(bool catalina); // out of class in III PC and later because of SecuROM
	static void UpdateHelis(void);
	static void SpecialHeliPreRender(void);
	static bool TestRocketCollision(CVector *coors);
	static bool TestBulletCollision(CVector *line0, CVector *line1, CVector *bulletPos, int32 damage);

	static void StartCatalinaFlyBy(void); // out of class in III PC and later because of SecuROM
	static void RemoveCatalinaHeli(void);
	static CHeli *FindPointerToCatalinasHeli(void);
	static void CatalinaTakeOff(void);
	static void MakeCatalinaHeliFlyAway(void);
	static bool HasCatalinaBeenShotDown(void);

	static void ActivateHeli(bool activate);
};

VALIDATE_SIZE(CHeli, 0x33C);


code:
#include "common.h"
#include "main.h"

#include "General.h"
#include "ModelIndices.h"
#include "FileMgr.h"
#include "Streaming.h"
#include "Replay.h"
#include "Camera.h"
#include "DMAudio.h"
#include "Wanted.h"
#include "Coronas.h"
#include "Particle.h"
#include "Explosion.h"
#include "World.h"
#include "HandlingMgr.h"
#include "Plane.h"
#include "MemoryHeap.h"

CPlaneNode *pPathNodes;
CPlaneNode *pPath2Nodes;
CPlaneNode *pPath3Nodes;
CPlaneNode *pPath4Nodes;
int32 NumPathNodes;
int32 NumPath2Nodes;
int32 NumPath3Nodes;
int32 NumPath4Nodes;
float TotalLengthOfFlightPath;
float TotalLengthOfFlightPath2;
float TotalLengthOfFlightPath3;
float TotalLengthOfFlightPath4;
float TotalDurationOfFlightPath;
float TotalDurationOfFlightPath2;
float TotalDurationOfFlightPath3;
float TotalDurationOfFlightPath4;
float LandingPoint;
float TakeOffPoint;
CPlaneInterpolationLine aPlaneLineBits[6];

float PlanePathPosition[3];
float OldPlanePathPosition[3];
float PlanePathSpeed[3];
float PlanePath2Position[3];
float PlanePath3Position;
float PlanePath4Position;
float PlanePath2Speed[3];
float PlanePath3Speed;
float PlanePath4Speed;

enum
{
	CESNA_STATUS_NONE,	// doesn't even exist
	CESNA_STATUS_FLYING,
	CESNA_STATUS_DESTROYED,
	CESNA_STATUS_LANDED,
};

int32 CesnaMissionStatus;
int32 CesnaMissionStartTime;
CPlane *pDrugRunCesna;
int32 DropOffCesnaMissionStatus;
int32 DropOffCesnaMissionStartTime;
CPlane *pDropOffCesna;

CPlane::CPlane(int32 id, uint8 CreatedBy)
 : CVehicle(CreatedBy)
{
	CVehicleModelInfo *mi = (CVehicleModelInfo*)CModelInfo::GetModelInfo(id);
	m_vehType = VEHICLE_TYPE_PLANE;
	pHandling = mod_HandlingManager.GetHandlingData((tVehicleType)mi->m_handlingId);
	SetModelIndex(id);

	m_fMass = 100000000.0f;
	m_fTurnMass = 100000000.0f;
	m_fAirResistance = 0.9994f;
	m_fElasticity = 0.05f;

	bUsesCollision = false;
	m_bHasBeenHit = false;
	m_bIsDrugRunCesna = false;
	m_bIsDropOffCesna = false;

	SetStatus(STATUS_PLANE);
	bIsBIGBuilding = true;
	m_level = LEVEL_GENERIC;

#ifdef FIX_BUGS
	m_isFarAway = false;
#endif
}

CPlane::~CPlane()
{
	DeleteRwObject();
}

void
CPlane::SetModelIndex(uint32 id)
{
	CVehicle::SetModelIndex(id);
}

void
CPlane::DeleteRwObject(void)
{
	if(m_rwObject && RwObjectGetType(m_rwObject) == rpATOMIC){
		GetMatrix().Detach();
		if(RwObjectGetType(m_rwObject) == rpATOMIC){	// useless check
			RwFrame *f = RpAtomicGetFrame((RpAtomic*)m_rwObject);
			RpAtomicDestroy((RpAtomic*)m_rwObject);
			RwFrameDestroy(f);
		}
		m_rwObject = nil;
	}
	CEntity::DeleteRwObject();
}

// There's a LOT of copy and paste in here. Maybe this could be refactored somehow
void
CPlane::ProcessControl(void)
{
	int i;
	CVector pos;

	// Explosion
	if(m_bHasBeenHit){
		// BUG: since this is all based on frames, you can skip the explosion processing when you go into the menu
		if(GetModelIndex() == MI_AIRTRAIN){
			int frm = CTimer::GetFrameCounter() - m_nFrameWhenHit;
			if(frm == 20){
				static int nFrameGen;
				CRGBA colors[8];

				CExplosion::AddExplosion(nil, FindPlayerPed(), EXPLOSION_HELI, GetMatrix() * CVector(0.0f, 0.0f, 0.0f), 0);

				colors[0] = CRGBA(0, 0, 0, 255);
				colors[1] = CRGBA(224, 230, 238, 255);
				colors[2] = CRGBA(224, 230, 238, 255);
				colors[3] = CRGBA(0, 0, 0, 255);
				colors[4] = CRGBA(224, 230, 238, 255);
				colors[5] = CRGBA(0, 0, 0, 255);
				colors[6] = CRGBA(0, 0, 0, 255);
				colors[7] = CRGBA(224, 230, 238, 255);

				CVector dir;
				for(i = 0; i < 40; i++){
					dir.x = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
					dir.y = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
					dir.z = CGeneral::GetRandomNumberInRange(0.0f, 2.0f);
					int rotSpeed = CGeneral::GetRandomNumberInRange(10, 30);
					if(CGeneral::GetRandomNumber() & 1)
						rotSpeed = -rotSpeed;
					int f = ++nFrameGen & 3;
					CParticle::AddParticle(PARTICLE_HELI_DEBRIS, GetMatrix() * CVector(0.0f, 0.0f, 0.0f), dir,
						nil, CGeneral::GetRandomNumberInRange(0.1f, 1.0f),
						colors[nFrameGen&7], rotSpeed, 0, f, 0);
				}
			}
			if(frm >= 40 && frm <= 80 && frm & 1){
				if(frm & 1){
					pos.x = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.2f;
					pos.z = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.2f;
					pos.y = frm - 40;
					pos = GetMatrix() * pos;
				}else{
					pos.x = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.2f;
					pos.z = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.2f;
					pos.y = 40 - frm;
					pos = GetMatrix() * pos;
				}
				CExplosion::AddExplosion(nil, FindPlayerPed(), EXPLOSION_HELI, pos, 0);
			}
			if(frm == 60)
				bRenderScorched = true;
			if(frm == 82){
				TheCamera.SetFadeColour(255, 255, 255);
				TheCamera.Fade(0.0f, FADE_OUT);
				TheCamera.ProcessFade();
				TheCamera.Fade(1.0f, FADE_IN);
				FlagToDestroyWhenNextProcessed();
			}
		}else{
			int frm = CTimer::GetFrameCounter() - m_nFrameWhenHit;
			if(frm == 20){
				static int nFrameGen;
				CRGBA colors[8];

				CExplosion::AddExplosion(nil, FindPlayerPed(), EXPLOSION_HELI, GetMatrix() * CVector(0.0f, 0.0f, 0.0f), 0);

				colors[0] = CRGBA(0, 0, 0, 255);
				colors[1] = CRGBA(224, 230, 238, 255);
				colors[2] = CRGBA(224, 230, 238, 255);
				colors[3] = CRGBA(0, 0, 0, 255);
				colors[4] = CRGBA(252, 66, 66, 255);
				colors[5] = CRGBA(0, 0, 0, 255);
				colors[6] = CRGBA(0, 0, 0, 255);
				colors[7] = CRGBA(252, 66, 66, 255);

				CVector dir;
				for(i = 0; i < 40; i++){
					dir.x = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
					dir.y = CGeneral::GetRandomNumberInRange(-2.0f, 2.0f);
					dir.z = CGeneral::GetRandomNumberInRange(0.0f, 2.0f);
					int rotSpeed = CGeneral::GetRandomNumberInRange(30.0f, 20.0f);
					if(CGeneral::GetRandomNumber() & 1)
						rotSpeed = -rotSpeed;
					int f = ++nFrameGen & 3;
					CParticle::AddParticle(PARTICLE_HELI_DEBRIS, GetMatrix() * CVector(0.0f, 0.0f, 0.0f), dir,
						nil, CGeneral::GetRandomNumberInRange(0.1f, 1.0f),
						colors[nFrameGen&7], rotSpeed, 0, f, 0);
				}
			}
			if(frm >= 40 && frm <= 60 && frm & 1){
				if(frm & 1){
					pos.x = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.1f;
					pos.z = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.1f;
					pos.y = (frm - 40)*0.3f;
					pos = GetMatrix() * pos;
				}else{
					pos.x = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.1f;
					pos.z = ((CGeneral::GetRandomNumber() & 0x3F) - 32) * 0.1f;
					pos.y = (40 - frm)*0.3f;
					pos = GetMatrix() * pos;
				}
				CExplosion::AddExplosion(nil, FindPlayerPed(), EXPLOSION_HELI, pos, 0);
			}
			if(frm == 30)
				bRenderScorched = true;
			if(frm == 62){
				TheCamera.SetFadeColour(200, 200, 200);
				TheCamera.Fade(0.0f, FADE_OUT);
				TheCamera.ProcessFade();
				TheCamera.Fade(1.0f, FADE_IN);
				if(m_bIsDrugRunCesna){
					CesnaMissionStatus = CESNA_STATUS_DESTROYED;
					pDrugRunCesna = nil;
				}
				if(m_bIsDropOffCesna){
					DropOffCesnaMissionStatus = CESNA_STATUS_DESTROYED;
					pDropOffCesna = nil;
				}
				FlagToDestroyWhenNextProcessed();
			}
		}
	}

	// Update plane position and speed
	if(GetModelIndex() == MI_AIRTRAIN || !m_isFarAway || ((CTimer::GetFrameCounter() + m_randomSeed) & 7) == 0){
		if(GetModelIndex() == MI_AIRTRAIN){
			float pathPositionRear = PlanePathPosition[m_nPlaneId] - 30.0f;
			if(pathPositionRear < 0.0f)
				pathPositionRear += TotalLengthOfFlightPath;
			float pathPosition = pathPositionRear + 30.0f;

			float pitch = 0.0f;
			float distSinceTakeOff = pathPosition - TakeOffPoint;
			if(distSinceTakeOff <= 0.0f && distSinceTakeOff > -70.0f){
				// shortly before take off
				pitch = 1.0f - distSinceTakeOff/-70.0f;
			}else if(distSinceTakeOff >= 0.0f && distSinceTakeOff < 100.0f){
				// shortly after take off
				pitch = 1.0f - distSinceTakeOff/100.0f;
			}

			float distSinceLanding = pathPosition - LandingPoint;
			if(distSinceLanding <= 0.0f && distSinceLanding > -200.0f){
				// shortly before landing
				pitch = 1.0f - distSinceLanding/-200.0f;
			}else if(distSinceLanding >= 0.0f && distSinceLanding < 70.0f){
				// shortly after landing
				pitch = 1.0f - distSinceLanding/70.0f;
			}

			// Advance current node to appropriate position
			float pos1, pos2;
			int nextTrackNode = m_nCurPathNode + 1;
			pos1 = pPathNodes[m_nCurPathNode].t;
			if(nextTrackNode < NumPathNodes)
				pos2 = pPathNodes[nextTrackNode].t;
			else{
				nextTrackNode = 0;
				pos2 = TotalLengthOfFlightPath;
			}
			while(pathPositionRear < pos1 || pathPositionRear > pos2){
				m_nCurPathNode = (m_nCurPathNode+1) % NumPathNodes;
				nextTrackNode = m_nCurPathNode + 1;
				pos1 = pPathNodes[m_nCurPathNode].t;
				if(nextTrackNode < NumPathNodes)
					pos2 = pPathNodes[nextTrackNode].t;
				else{
					nextTrackNode = 0;
					pos2 = TotalLengthOfFlightPath;
				}
			}
			bool bothOnGround = pPathNodes[m_nCurPathNode].bOnGround && pPathNodes[nextTrackNode].bOnGround;
			if(PlanePathPosition[m_nPlaneId] >= LandingPoint && OldPlanePathPosition[m_nPlaneId] < LandingPoint)
				DMAudio.PlayOneShot(m_audioEntityId, SOUND_PLANE_ON_GROUND, 0.0f);
			float dist = pPathNodes[nextTrackNode].t - pPathNodes[m_nCurPathNode].t;
			if(dist < 0.0f)
				dist += TotalLengthOfFlightPath;
			float f = (pathPositionRear - pPathNodes[m_nCurPathNode].t)/dist;
			CVector posRear = (1.0f - f)*pPathNodes[m_nCurPathNode].p + f*pPathNodes[nextTrackNode].p;

			// Same for the front
			float pathPositionFront = pathPositionRear + 60.0f;
			if(pathPositionFront > TotalLengthOfFlightPath)
				pathPositionFront -= TotalLengthOfFlightPath;
			int curPathNodeFront = m_nCurPathNode;
			int nextPathNodeFront = curPathNodeFront + 1;
			pos1 = pPathNodes[curPathNodeFront].t;
			if(nextPathNodeFront < NumPathNodes)
				pos2 = pPathNodes[nextPathNodeFront].t;
			else{
				nextPathNodeFront = 0;
				pos2 = TotalLengthOfFlightPath;
			}
			while(pathPositionFront < pos1 || pathPositionFront > pos2){
				curPathNodeFront = (curPathNodeFront+1) % NumPathNodes;
				nextPathNodeFront = curPathNodeFront + 1;
				pos1 = pPathNodes[curPathNodeFront].t;
				if(nextPathNodeFront < NumPathNodes)
					pos2 = pPathNodes[nextPathNodeFront].t;
				else{
					nextPathNodeFront = 0;
					pos2 = TotalLengthOfFlightPath;
				}
			}
			dist = pPathNodes[nextPathNodeFront].t - pPathNodes[curPathNodeFront].t;
			if(dist < 0.0f)
				dist += TotalLengthOfFlightPath;
			f = (pathPositionFront - pPathNodes[curPathNodeFront].t)/dist;
			CVector posFront = (1.0f - f)*pPathNodes[curPathNodeFront].p + f*pPathNodes[nextPathNodeFront].p;

			// And for another point 60 units in front of the plane, used to calculate roll
			float pathPositionFront2 = pathPositionFront + 60.0f;
			if(pathPositionFront2 > TotalLengthOfFlightPath)
				pathPositionFront2 -= TotalLengthOfFlightPath;
			int curPathNodeFront2 = m_nCurPathNode;
			int nextPathNodeFront2 = curPathNodeFront2 + 1;
			pos1 = pPathNodes[curPathNodeFront2].t;
			if(nextPathNodeFront2 < NumPathNodes)
				pos2 = pPathNodes[nextPathNodeFront2].t;
			else{
				nextPathNodeFront2 = 0;
				pos2 = TotalLengthOfFlightPath;
			}
			while(pathPositionFront2 < pos1 || pathPositionFront2 > pos2){
				curPathNodeFront2 = (curPathNodeFront2+1) % NumPathNodes;
				nextPathNodeFront2 = curPathNodeFront2 + 1;
				pos1 = pPathNodes[curPathNodeFront2].t;
				if(nextPathNodeFront2 < NumPathNodes)
					pos2 = pPathNodes[nextPathNodeFront2].t;
				else{
					nextPathNodeFront2 = 0;
					pos2 = TotalLengthOfFlightPath;
				}
			}
			dist = pPathNodes[nextPathNodeFront2].t - pPathNodes[curPathNodeFront2].t;
			if(dist < 0.0f)
				dist += TotalLengthOfFlightPath;
			f = (pathPositionFront2 - pPathNodes[curPathNodeFront2].t)/dist;
			CVector posFront2 = (1.0f - f)*pPathNodes[curPathNodeFront2].p + f*pPathNodes[nextPathNodeFront2].p;

			// Now set matrix
			GetMatrix().SetTranslateOnly((posRear + posFront) / 2.0f);
			GetMatrix().GetPosition().z += 4.3f;
			CVector fwd = posFront - posRear;
			fwd.Normalise();
			if(pitch != 0.0f){
				fwd.z += 0.4f*pitch;
				fwd.Normalise();
			}
			CVector fwd2 = posFront2 - posRear;
			fwd2.Normalise();
			CVector roll = CrossProduct(fwd, fwd2);
			CVector right = CrossProduct(fwd, CVector(0.0f, 0.0f, 1.0f));
			if(!bothOnGround)
				right.z += 3.0f*roll.z;
			right.Normalise();
			CVector up = CrossProduct(right, fwd);
			GetMatrix().GetRight() = right;
			GetMatrix().GetUp() = up;
			GetMatrix().GetForward() = fwd;

			// Set speed
			m_vecMoveSpeed = fwd*PlanePathSpeed[m_nPlaneId]/60.0f;
			m_fSpeed = PlanePathSpeed[m_nPlaneId]/60.0f;
			m_vecTurnSpeed = CVector(0.0f, 0.0f, 0.0f);

			m_isFarAway = !((posFront - TheCamera.GetPosition()).MagnitudeSqr2D() < sq(300.0f));
		}else{
			float planePathPosition;
			float totalLengthOfFlightPath;
			CPlaneNode *pathNodes;
			float planePathSpeed;
			int numPathNodes;

			if(m_bIsDrugRunCesna){
				planePathPosition = PlanePath3Position;
				totalLengthOfFlightPath = TotalLengthOfFlightPath3;
				pathNodes = pPath3Nodes;
				planePathSpeed = PlanePath3Speed;
				numPathNodes = NumPath3Nodes;
				if(CesnaMissionStatus == CESNA_STATUS_LANDED){
					pDrugRunCesna = nil;
					FlagToDestroyWhenNextProcessed();
				}
			}else if(m_bIsDropOffCesna){
				planePathPosition = PlanePath4Position;
				totalLengthOfFlightPath = TotalLengthOfFlightPath4;
				pathNodes = pPath4Nodes;
				planePathSpeed = PlanePath4Speed;
				numPathNodes = NumPath4Nodes;
				if(DropOffCesnaMissionStatus == CESNA_STATUS_LANDED){
					pDropOffCesna = nil;
					FlagToDestroyWhenNextProcessed();
				}
			}else{
				planePathPosition = PlanePath2Position[m_nPlaneId];
				totalLengthOfFlightPath = TotalLengthOfFlightPath2;
				pathNodes = pPath2Nodes;
				planePathSpeed = PlanePath2Speed[m_nPlaneId];
				numPathNodes = NumPath2Nodes;
			}

			// Advance current node to appropriate position
			float pathPositionRear = planePathPosition - 10.0f;
			if(pathPositionRear < 0.0f)
				pathPositionRear += totalLengthOfFlightPath;
			float pos1, pos2;
			int nextTrackNode = m_nCurPathNode + 1;
			pos1 = pathNodes[m_nCurPathNode].t;
			if(nextTrackNode < numPathNodes)
				pos2 = pathNodes[nextTrackNode].t;
			else{
				nextTrackNode = 0;
				pos2 = totalLengthOfFlightPath;
			}
			while(pathPositionRear < pos1 || pathPositionRear > pos2){
				m_nCurPathNode = (m_nCurPathNode+1) % numPathNodes;
				nextTrackNode = m_nCurPathNode + 1;
				pos1 = pathNodes[m_nCurPathNode].t;
				if(nextTrackNode < numPathNodes)
					pos2 = pathNodes[nextTrackNode].t;
				else{
					nextTrackNode = 0;
					pos2 = totalLengthOfFlightPath;
				}
			}
			float dist = pathNodes[nextTrackNode].t - pathNodes[m_nCurPathNode].t;
			if(dist < 0.0f)
				dist += totalLengthOfFlightPath;
			float f = (pathPositionRear - pathNodes[m_nCurPathNode].t)/dist;
			CVector posRear = (1.0f - f)*pathNodes[m_nCurPathNode].p + f*pathNodes[nextTrackNode].p;

			// Same for the front
			float pathPositionFront = pathPositionRear + 20.0f;
			if(pathPositionFront > totalLengthOfFlightPath)
				pathPositionFront -= totalLengthOfFlightPath;
			int curPathNodeFront = m_nCurPathNode;
			int nextPathNodeFront = curPathNodeFront + 1;
			pos1 = pathNodes[curPathNodeFront].t;
			if(nextPathNodeFront < numPathNodes)
				pos2 = pathNodes[nextPathNodeFront].t;
			else{
				nextPathNodeFront = 0;
				pos2 = totalLengthOfFlightPath;
			}
			while(pathPositionFront < pos1 || pathPositionFront > pos2){
				curPathNodeFront = (curPathNodeFront+1) % numPathNodes;
				nextPathNodeFront = curPathNodeFront + 1;
				pos1 = pathNodes[curPathNodeFront].t;
				if(nextPathNodeFront < numPathNodes)
					pos2 = pathNodes[nextPathNodeFront].t;
				else{
					nextPathNodeFront = 0;
					pos2 = totalLengthOfFlightPath;
				}
			}
			dist = pathNodes[nextPathNodeFront].t - pathNodes[curPathNodeFront].t;
			if(dist < 0.0f)
				dist += totalLengthOfFlightPath;
			f = (pathPositionFront - pathNodes[curPathNodeFront].t)/dist;
			CVector posFront = (1.0f - f)*pathNodes[curPathNodeFront].p + f*pathNodes[nextPathNodeFront].p;

			// And for another point 30 units in front of the plane, used to calculate roll
			float pathPositionFront2 = pathPositionFront + 30.0f;
			if(pathPositionFront2 > totalLengthOfFlightPath)
				pathPositionFront2 -= totalLengthOfFlightPath;
			int curPathNodeFront2 = m_nCurPathNode;
			int nextPathNodeFront2 = curPathNodeFront2 + 1;
			pos1 = pathNodes[curPathNodeFront2].t;
			if(nextPathNodeFront2 < numPathNodes)
				pos2 = pathNodes[nextPathNodeFront2].t;
			else{
				nextPathNodeFront2 = 0;
				pos2 = totalLengthOfFlightPath;
			}
			while(pathPositionFront2 < pos1 || pathPositionFront2 > pos2){
				curPathNodeFront2 = (curPathNodeFront2+1) % numPathNodes;
				nextPathNodeFront2 = curPathNodeFront2 + 1;
				pos1 = pathNodes[curPathNodeFront2].t;
				if(nextPathNodeFront2 < numPathNodes)
					pos2 = pathNodes[nextPathNodeFront2].t;
				else{
					nextPathNodeFront2 = 0;
					pos2 = totalLengthOfFlightPath;
				}
			}
			dist = pathNodes[nextPathNodeFront2].t - pathNodes[curPathNodeFront2].t;
			if(dist < 0.0f)
				dist += totalLengthOfFlightPath;
			f = (pathPositionFront2 - pathNodes[curPathNodeFront2].t)/dist;
			CVector posFront2 = (1.0f - f)*pathNodes[curPathNodeFront2].p + f*pathNodes[nextPathNodeFront2].p;

			// Now set matrix
			GetMatrix().SetTranslateOnly((posRear + posFront) / 2.0f);
			GetMatrix().GetPosition().z += 1.0f;
			CVector fwd = posFront - posRear;
			fwd.Normalise();
			CVector fwd2 = posFront2 - posRear;
			fwd2.Normalise();
			CVector roll = CrossProduct(fwd, fwd2);
			CVector right = CrossProduct(fwd, CVector(0.0f, 0.0f, 1.0f));
			right.z += 3.0f*roll.z;
			right.Normalise();
			CVector up = CrossProduct(right, fwd);
			GetMatrix().GetRight() = right;
			GetMatrix().GetUp() = up;
			GetMatrix().GetForward() = fwd;

			// Set speed
			m_vecMoveSpeed = fwd*planePathSpeed/60.0f;
			m_fSpeed = planePathSpeed/60.0f;
			m_vecTurnSpeed = CVector(0.0f, 0.0f, 0.0f);

			m_isFarAway = !((posFront - TheCamera.GetPosition()).MagnitudeSqr2D() < sq(300.0f));
		}
	}

	bIsInSafePosition = true;
	GetMatrix().UpdateRW();
	UpdateRwFrame();

	// Handle streaming and such
	CVehicleModelInfo *mi = (CVehicleModelInfo*)CModelInfo::GetModelInfo(GetModelIndex());
	if(m_isFarAway){
		// Switch to LOD model
		if(m_rwObject && RwObjectGetType(m_rwObject) == rpCLUMP){
			DeleteRwObject();
			if(mi->m_planeLodId != -1){
				PUSH_MEMID(MEMID_WORLD);
				m_rwObject = CModelInfo::GetModelInfo(mi->m_planeLodId)->CreateInstance();
				POP_MEMID();
				if(m_rwObject)
					GetMatrix().AttachRW(RwFrameGetMatrix(RpAtomicGetFrame((RpAtomic*)m_rwObject)));
			}
		}
	}else if(CStreaming::HasModelLoaded(GetModelIndex())){
		if(m_rwObject && RwObjectGetType(m_rwObject) == rpATOMIC){
			// Get rid of LOD model
			GetMatrix().Detach();
			if(m_rwObject){	// useless check
				if(RwObjectGetType(m_rwObject) == rpATOMIC){	// useless check
					RwFrame *f = RpAtomicGetFrame((RpAtomic*)m_rwObject);
					RpAtomicDestroy((RpAtomic*)m_rwObject);
					RwFrameDestroy(f);
				}
				m_rwObject = nil;
			}
		}
		// Set high detail model
		if(m_rwObject == nil){
			int id = GetModelIndex();
			m_modelIndex = -1;
			SetModelIndex(id);
		}
	}else{
		CStreaming::RequestModel(GetModelIndex(), STREAMFLAGS_DEPENDENCY);
	}
}

void
CPlane::PreRender(void)
{
	CVehicleModelInfo *mi = (CVehicleModelInfo*)CModelInfo::GetModelInfo(GetModelIndex());

	CVector lookVector = GetPosition() - TheCamera.GetPosition();
	float camDist = lookVector.Magnitude();
	if(camDist != 0.0f)
		lookVector *= 1.0f/camDist;
	else
		lookVector = CVector(1.0f, 0.0f, 0.0f);
	float behindness = DotProduct(lookVector, GetForward());

	// Wing lights
	if(behindness < 0.0f){
		// in front of plane
		CVector lightPos = mi->m_positions[PLANE_POS_LIGHT_RIGHT];
		CVector lightR = GetMatrix() * lightPos;
		CVector lightL = lightR;
		lightL -= GetRight()*2.0f*lightPos.x;

		float intensity = -0.6f*behindness + 0.4f;
		float size = 1.0f - behindness;

		if(behindness < -0.9f && camDist < 50.0f){
			// directly in front
			CCoronas::RegisterCorona((uintptr)this + 10, 255*intensity, 255*intensity, 255*intensity, 255,
				lightL, size, 240.0f,
				CCoronas::TYPE_NORMAL, CCoronas::FLARE_HEADLIGHTS, CCoronas::REFLECTION_ON,
				CCoronas::LOSCHECK_OFF, CCoronas::STREAK_ON, 0.0f);
			CCoronas::RegisterCorona((uintptr)this + 11, 255*intensity, 255*intensity, 255*intensity, 255,
				lightR, size, 240.0f,
				CCoronas::TYPE_NORMAL, CCoronas::FLARE_HEADLIGHTS, CCoronas::REFLECTION_ON,
				CCoronas::LOSCHECK_OFF, CCoronas::STREAK_ON, 0.0f);
		}else{
			CCoronas::RegisterCorona((uintptr)this + 10, 255*intensity, 255*intensity, 255*intensity, 255,
				lightL, size, 240.0f,
				CCoronas::TYPE_NORMAL, CCoronas::FLARE_NONE, CCoronas::REFLECTION_ON,
				CCoronas::LOSCHECK_OFF, CCoronas::STREAK_ON, 0.0f);
			CCoronas::RegisterCorona((uintptr)this + 11, 255*intensity, 255*intensity, 255*intensity, 255,
				lightR, size, 240.0f,
				CCoronas::TYPE_NORMAL, CCoronas::FLARE_NONE, CCoronas::REFLECTION_ON,
				CCoronas::LOSCHECK_OFF, CCoronas::STREAK_ON, 0.0f);
		}
	}

	// Tail light
	if(CTimer::GetTimeInMilliseconds() & 0x200){
		CVector pos = GetMatrix() * mi->m_positions[PLANE_POS_LIGHT_TAIL];

		CCoronas::RegisterCorona((uintptr)this + 12, 255, 0, 0, 255,
			pos, 1.0f, 120.0f,
			CCoronas::TYPE_NORMAL, CCoronas::FLARE_NONE, CCoronas::REFLECTION_ON,
			CCoronas::LOSCHECK_OFF, CCoronas::STREAK_ON, 0.0f);
	}
}

void
CPlane::Render(void)
{
	CEntity::Render();
}

#define CRUISE_SPEED (50.0f)
#define TAXI_SPEED (5.0f)

void
CPlane::InitPlanes(void)
{
	int i;

	CesnaMissionStatus = CESNA_STATUS_NONE;

	// Jumbo
	if(pPathNodes == nil){
		pPathNodes = LoadPath("data\\paths\\flight.dat", NumPathNodes, TotalLengthOfFlightPath, true);

		// Figure out which nodes are on ground
		CColPoint colpoint;
		CEntity *entity;
		for(i = 0; i < NumPathNodes; i++){
			if(CWorld::ProcessVerticalLine(pPathNodes[i].p, 1000.0f, colpoint, entity, true, false, false, false, true, false, nil)){
				pPathNodes[i].p.z = colpoint.point.z;
				pPathNodes[i].bOnGround = true;
			}else
				pPathNodes[i].bOnGround = false;
		}

		// Find lading and takeoff points
		LandingPoint = -1.0f;
		TakeOffPoint = -1.0f;
		bool lastOnGround = pPathNodes[NumPathNodes-1].bOnGround;
		for(i = 0; i < NumPathNodes; i++){
			if(pPathNodes[i].bOnGround && !lastOnGround)
				LandingPoint = pPathNodes[i].t;
			else if(!pPathNodes[i].bOnGround && lastOnGround)
				TakeOffPoint = pPathNodes[i].t;
			lastOnGround = pPathNodes[i].bOnGround;
		}

		// Animation
		float time = 0.0f;
		float position = 0.0f;
		// Start on ground with slow speed
		aPlaneLineBits[0].type = 1;
		aPlaneLineBits[0].time = time;
		aPlaneLineBits[0].position = position;
		aPlaneLineBits[0].speed = TAXI_SPEED;
		aPlaneLineBits[0].acceleration = 0.0f;
		float dist = (TakeOffPoint-600.0f) - position;
		time += dist/TAXI_SPEED;
		position += dist;

		// Accelerate to take off
		aPlaneLineBits[1].type = 2;
		aPlaneLineBits[1].time = time;
		aPlaneLineBits[1].position = position;
		aPlaneLineBits[1].speed = TAXI_SPEED;
		aPlaneLineBits[1].acceleration = 618.75f/600.0f;
		time += 600.0f/((CRUISE_SPEED+TAXI_SPEED)/2.0f);
		position += 600.0f;

		// Fly at cruise speed
		aPlaneLineBits[2].type = 1;
		aPlaneLineBits[2].time = time;
		aPlaneLineBits[2].position = position;
		aPlaneLineBits[2].speed = CRUISE_SPEED;
		aPlaneLineBits[2].acceleration = 0.0f;
		dist = LandingPoint - TakeOffPoint;
		time += dist/CRUISE_SPEED;
		position += dist;

		// Brake after landing
		aPlaneLineBits[3].type = 2;
		aPlaneLineBits[3].time = time;
		aPlaneLineBits[3].position = position;
		aPlaneLineBits[3].speed = CRUISE_SPEED;
		aPlaneLineBits[3].acceleration = -618.75f/600.0f;
		time += 600.0f/((CRUISE_SPEED+TAXI_SPEED)/2.0f);
		position += 600.0f;

		// Taxi
		aPlaneLineBits[4].type = 1;
		aPlaneLineBits[4].time = time;
		aPlaneLineBits[4].position = position;
		aPlaneLineBits[4].speed = TAXI_SPEED;
		aPlaneLineBits[4].acceleration = 0.0f;
		time += (TotalLengthOfFlightPath - position)/TAXI_SPEED;

		// end
		aPlaneLineBits[5].time = time;
		TotalDurationOfFlightPath = time;
	}

	// Dodo
	if(pPath2Nodes == nil){
		pPath2Nodes = LoadPath("data\\paths\\flight2.dat", NumPath2Nodes, TotalLengthOfFlightPath2, true);
		TotalDurationOfFlightPath2 = TotalLengthOfFlightPath2/CRUISE_SPEED;
	}

	// Mission Cesna
	if(pPath3Nodes == nil){
		pPath3Nodes = LoadPath("data\\paths\\flight3.dat", NumPath3Nodes, TotalLengthOfFlightPath3, false);
		TotalDurationOfFlightPath3 = TotalLengthOfFlightPath3/CRUISE_SPEED;
	}

	// Mission Cesna
	if(pPath4Nodes == nil){
		pPath4Nodes = LoadPath("data\\paths\\flight4.dat", NumPath4Nodes, TotalLengthOfFlightPath4, false);
		TotalDurationOfFlightPath4 = TotalLengthOfFlightPath4/CRUISE_SPEED;
	}

	CStreaming::LoadAllRequestedModels(false);
	CStreaming::RequestModel(MI_AIRTRAIN, 0);
	CStreaming::LoadAllRequestedModels(false);

	for(i = 0; i < 3; i++){
		CPlane *plane = new CPlane(MI_AIRTRAIN, PERMANENT_VEHICLE);
		plane->GetMatrix().SetTranslate(0.0f, 0.0f, 0.0f);
		plane->SetStatus(STATUS_ABANDONED);
		plane->bIsLocked = true;
		plane->m_nPlaneId = i;
		plane->m_nCurPathNode = 0;
		CWorld::Add(plane);
	}

	CStreaming::RequestModel(MI_DEADDODO, 0);
	CStreaming::LoadAllRequestedModels(false);

	for(i = 0; i < 3; i++){
		CPlane *plane = new CPlane(MI_DEADDODO, PERMANENT_VEHICLE);
		plane->GetMatrix().SetTranslate(0.0f, 0.0f, 0.0f);
		plane->SetStatus(STATUS_ABANDONED);
		plane->bIsLocked = true;
		plane->m_nPlaneId = i;
		plane->m_nCurPathNode = 0;
		CWorld::Add(plane);
	}
}

void
CPlane::Shutdown(void)
{
	delete[] pPathNodes;
	delete[] pPath2Nodes;
	delete[] pPath3Nodes;
	delete[] pPath4Nodes;
	pPathNodes = nil;
	pPath2Nodes = nil;
	pPath3Nodes = nil;
	pPath4Nodes = nil;
}

CPlaneNode*
CPlane::LoadPath(char const *filename, int32 &numNodes, float &totalLength, bool loop)
{
	int bp, lp;
	int i;

	CFileMgr::LoadFile(filename, work_buff, sizeof(work_buff), "r");
	*gString = '\0';
	for(bp = 0, lp = 0; work_buff[bp] != '\n'; bp++, lp++)
		gString[lp] = work_buff[bp];
	bp++;
	gString[lp] = '\0';
	sscanf(gString, "%d", &numNodes);
	CPlaneNode *nodes = new CPlaneNode[numNodes];

	for(i = 0; i < numNodes; i++){
		*gString = '\0';
		for(lp = 0; work_buff[bp] != '\n'; bp++, lp++)
			gString[lp] = work_buff[bp];
		bp++;
		// BUG: game doesn't terminate string
		gString[lp] = '\0';
		sscanf(gString, "%f %f %f", &nodes[i].p.x, &nodes[i].p.y, &nodes[i].p.z);
	}

	// Calculate length of segments and path
	totalLength = 0.0f;
	for(i = 0; i < numNodes; i++){
		nodes[i].t = totalLength;
		float l = (nodes[(i+1) % numNodes].p - nodes[i].p).Magnitude2D();
		if(!loop && i == numNodes-1)
			l = 0.0f;
		totalLength += l;
	}

	return nodes;
}

void
CPlane::UpdatePlanes(void)
{
	int i, j;
	uint32 time;
	float t, deltaT;

	if(CReplay::IsPlayingBack())
		return;

	// Jumbo jets
	time = CTimer::GetTimeInMilliseconds();
	for(i = 0; i < 3; i++){
		t = TotalDurationOfFlightPath * (float)(time & 0x7FFFF)/0x80000;
		// find current frame
		for(j = 0; t > aPlaneLineBits[j+1].time; j++);

		OldPlanePathPosition[i] = PlanePathPosition[i];
		deltaT = t - aPlaneLineBits[j].time;
		switch(aPlaneLineBits[j].type){
		case 0:	// standing still
			PlanePathPosition[i] = aPlaneLineBits[j].position;
			PlanePathSpeed[i] = 0.0f;
			break;
		case 1:	// moving with constant speed
			PlanePathPosition[i] = aPlaneLineBits[j].position + aPlaneLineBits[j].speed*deltaT;
			PlanePathSpeed[i] = (TotalDurationOfFlightPath*1000.0f/0x80000) * aPlaneLineBits[j].speed;
			break;
		case 2:	// accelerating/braking
			PlanePathPosition[i] = aPlaneLineBits[j].position + (aPlaneLineBits[j].speed + aPlaneLineBits[j].acceleration*deltaT)*deltaT;
			PlanePathSpeed[i] = (TotalDurationOfFlightPath*1000.0f/0x80000)*aPlaneLineBits[j].speed + 2.0f*aPlaneLineBits[j].acceleration*deltaT;
			break;
		}

		// time offset for each plane
		time += 0x80000/3;
	}

	time = CTimer::GetTimeInMilliseconds();

	t = TotalDurationOfFlightPath2/0x80000;
	PlanePath2Position[0] = CRUISE_SPEED * (time & 0x7FFFF)*t;
	PlanePath2Position[1] = CRUISE_SPEED * ((time + 0x80000/3) & 0x7FFFF)*t;
	PlanePath2Position[2] = CRUISE_SPEED * ((time + 0x80000/3*2) & 0x7FFFF)*t;
	PlanePath2Speed[0] = CRUISE_SPEED*t;
	PlanePath2Speed[1] = CRUISE_SPEED*t;
	PlanePath2Speed[2] = CRUISE_SPEED*t;

	if(CesnaMissionStatus == CESNA_STATUS_FLYING){
		PlanePath3Speed = CRUISE_SPEED*TotalDurationOfFlightPath3/0x20000;
		PlanePath3Position = PlanePath3Speed * ((time - CesnaMissionStartTime) & 0x1FFFF);
		if(time - CesnaMissionStartTime >= 128072)
			CesnaMissionStatus = CESNA_STATUS_LANDED;
	}

	if(DropOffCesnaMissionStatus == CESNA_STATUS_FLYING){
		PlanePath4Speed = CRUISE_SPEED*TotalDurationOfFlightPath4/0x80000;
		PlanePath4Position = PlanePath4Speed * ((time - DropOffCesnaMissionStartTime) & 0x7FFFF);
		if(time - DropOffCesnaMissionStartTime >= 521288)
			DropOffCesnaMissionStatus = CESNA_STATUS_LANDED;
	}
}

bool
CPlane::TestRocketCollision(CVector *rocketPos)
{
	int i;

	i = CPools::GetVehiclePool()->GetSize();
	while(--i >= 0){
		CPlane *plane = (CPlane*)CPools::GetVehiclePool()->GetSlot(i);
		if(plane &&
#ifdef EXPLODING_AIRTRAIN
		   (plane->GetModelIndex() == MI_AIRTRAIN || plane->GetModelIndex() == MI_DEADDODO) &&
#else
		   plane->GetModelIndex() != MI_AIRTRAIN && plane->GetModelIndex() == MI_DEADDODO &&	// strange check
#endif
		   !plane->m_bHasBeenHit && (*rocketPos - plane->GetPosition()).Magnitude() < 25.0f){
			plane->m_nFrameWhenHit = CTimer::GetFrameCounter();
			plane->m_bHasBeenHit = true;
			CWorld::Players[CWorld::PlayerInFocus].m_pPed->m_pWanted->RegisterCrime_Immediately(CRIME_DESTROYED_CESSNA,
				plane->GetPosition(), i+1983, false);
			return true;
		}
	}
	return false;
}

// BUG: not in CPlane in the game
void
CPlane::CreateIncomingCesna(void)
{
	if(CesnaMissionStatus == CESNA_STATUS_FLYING){
		CWorld::Remove(pDrugRunCesna);
		delete pDrugRunCesna;
		pDrugRunCesna = nil;
	}
	pDrugRunCesna = new CPlane(MI_DEADDODO, PERMANENT_VEHICLE);
	pDrugRunCesna->GetMatrix().SetTranslate(0.0f, 0.0f, 0.0f);
	pDrugRunCesna->SetStatus(STATUS_ABANDONED);
	pDrugRunCesna->bIsLocked = true;
	pDrugRunCesna->m_nPlaneId = 0;
	pDrugRunCesna->m_nCurPathNode = 0;
	pDrugRunCesna->m_bIsDrugRunCesna = true;
	CWorld::Add(pDrugRunCesna);

        CesnaMissionStatus = CESNA_STATUS_FLYING;
        CesnaMissionStartTime = CTimer::GetTimeInMilliseconds();
        printf("CPlane::CreateIncomingCesna(void)\n");
}

void
CPlane::CreateDropOffCesna(void)
{
	if(DropOffCesnaMissionStatus == CESNA_STATUS_FLYING){
		CWorld::Remove(pDropOffCesna);
		delete pDropOffCesna;
		pDropOffCesna = nil;
	}
	pDropOffCesna = new CPlane(MI_DEADDODO, PERMANENT_VEHICLE);
	pDropOffCesna->GetMatrix().SetTranslate(0.0f, 0.0f, 0.0f);
	pDropOffCesna->SetStatus(STATUS_ABANDONED);
	pDropOffCesna->bIsLocked = true;
	pDropOffCesna->m_nPlaneId = 0;
	pDropOffCesna->m_nCurPathNode = 0;
	pDropOffCesna->m_bIsDropOffCesna = true;
	CWorld::Add(pDropOffCesna);

        DropOffCesnaMissionStatus = CESNA_STATUS_FLYING;
        DropOffCesnaMissionStartTime = CTimer::GetTimeInMilliseconds();
        printf("CPlane::CreateDropOffCesna(void)\n");
}

const CVector CPlane::FindDrugPlaneCoordinates(void) { return pDrugRunCesna->GetPosition(); }
const CVector CPlane::FindDropOffCesnaCoordinates(void) { return pDropOffCesna->GetPosition(); }
bool CPlane::HasCesnaLanded(void) { return CesnaMissionStatus == CESNA_STATUS_LANDED; }
bool CPlane::HasCesnaBeenDestroyed(void) { return CesnaMissionStatus == CESNA_STATUS_DESTROYED; }
bool CPlane::HasDropOffCesnaBeenShotDown(void) { return DropOffCesnaMissionStatus == CESNA_STATUS_DESTROYED; }


code:
#pragma once

#include "Vehicle.h"

enum ePlaneNodes
{
	PLANE_WHEEL_FRONT = 2,
	PLANE_WHEEL_READ,
	NUM_PLANE_NODES
};

struct CPlaneNode
{
	CVector p;	// position
	float t;	// xy-distance from start on path
	bool bOnGround;	// i.e. not flying
};

struct CPlaneInterpolationLine
{
	uint8 type;
	float time;	// when does this keyframe start
	// initial values at start of frame
	float position;
	float speed;
	float acceleration;
};

class CPlane : public CVehicle
{
public:
	// 0x288
	int16 m_nPlaneId;
	int16 m_isFarAway;
	int16 m_nCurPathNode;
	float m_fSpeed;
	uint32 m_nFrameWhenHit;
	bool m_bHasBeenHit;
	bool m_bIsDrugRunCesna;
	bool m_bIsDropOffCesna;

	CPlane(int32 id, uint8 CreatedBy);
	~CPlane(void);

	// from CEntity
	void SetModelIndex(uint32 id);
	void DeleteRwObject(void);
	void ProcessControl(void);
	void PreRender(void);
	void Render(void);
	void FlagToDestroyWhenNextProcessed() { bRemoveFromWorld = true; }

	static void InitPlanes(void);
	static void Shutdown(void);
	static CPlaneNode *LoadPath(char const *filename, int32 &numNodes, float &totalLength, bool loop);
	static void UpdatePlanes(void);
	static bool TestRocketCollision(CVector *rocketPos);
	static void CreateIncomingCesna(void);
	static void CreateDropOffCesna(void);
	static const CVector FindDrugPlaneCoordinates(void);
	static const CVector FindDropOffCesnaCoordinates(void);
	static bool HasCesnaLanded(void);
	static bool HasCesnaBeenDestroyed(void);
	static bool HasDropOffCesnaBeenShotDown(void);
};

VALIDATE_SIZE(CPlane, 0x29C);

extern float LandingPoint;
extern float TakeOffPoint;
extern float PlanePathPosition[3];