1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3 
4 #include "Propulsion.h"
5 
6 #include "Game.h"
7 #include "GameSaveError.h"
8 #include "Pi.h"
9 #include "Player.h"
10 #include "PlayerShipController.h"
11 
SaveToJson(Json & jsonObj,Space * space)12 void Propulsion::SaveToJson(Json &jsonObj, Space *space)
13 {
14 	//Json PropulsionObj(Json::objectValue); // Create JSON object to contain propulsion data.
15 	jsonObj["ang_thrusters"] = m_angThrusters;
16 	jsonObj["thrusters"] = m_linThrusters;
17 	jsonObj["thruster_fuel"] = m_thrusterFuel;
18 	jsonObj["reserve_fuel"] = m_reserveFuel;
19 	// !!! These are commented to avoid savegame bumps:
20 	//jsonObj["tank_mass"] = m_fuelTankMass;
21 	//jsonObj["propulsion"] = PropulsionObj;
22 }
23 
LoadFromJson(const Json & jsonObj,Space * space)24 void Propulsion::LoadFromJson(const Json &jsonObj, Space *space)
25 {
26 	try {
27 		SetAngThrusterState(jsonObj["ang_thrusters"]);
28 		SetLinThrusterState(jsonObj["thrusters"]);
29 
30 		m_thrusterFuel = jsonObj["thruster_fuel"];
31 		m_reserveFuel = jsonObj["reserve_fuel"];
32 
33 		// !!! This is commented to avoid savegame bumps:
34 		//m_fuelTankMass = jsonObj["tank_mass"].asInt();
35 	} catch (Json::type_error &) {
36 		throw SavedGameCorruptException();
37 	}
38 }
39 
Propulsion()40 Propulsion::Propulsion()
41 {
42 	m_fuelTankMass = 1;
43 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
44 		m_linThrust[i] = 0.0;
45 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
46 		m_linAccelerationCap[i] = INFINITY;
47 	m_angThrust = 0.0;
48 	m_effectiveExhaustVelocity = 100000.0;
49 	m_thrusterFuel = 0.0; //0.0-1.0, remaining fuel
50 	m_reserveFuel = 0.0;
51 	m_fuelStateChange = false;
52 	m_linThrusters = vector3d(0, 0, 0);
53 	m_angThrusters = vector3d(0, 0, 0);
54 	m_smodel = nullptr;
55 	m_dBody = nullptr;
56 }
57 
Init(DynamicBody * b,SceneGraph::Model * m,const int tank_mass,const double effExVel,const float lin_Thrust[],const float ang_Thrust)58 void Propulsion::Init(DynamicBody *b, SceneGraph::Model *m, const int tank_mass, const double effExVel, const float lin_Thrust[], const float ang_Thrust)
59 {
60 	m_fuelTankMass = tank_mass;
61 	m_effectiveExhaustVelocity = effExVel;
62 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
63 		m_linThrust[i] = lin_Thrust[i];
64 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
65 		m_linAccelerationCap[i] = INFINITY;
66 	m_angThrust = ang_Thrust;
67 	m_smodel = m;
68 	m_dBody = b;
69 }
70 
Init(DynamicBody * b,SceneGraph::Model * m,const int tank_mass,const double effExVel,const float lin_Thrust[],const float ang_Thrust,const float lin_AccelerationCap[])71 void Propulsion::Init(DynamicBody *b, SceneGraph::Model *m, const int tank_mass, const double effExVel, const float lin_Thrust[], const float ang_Thrust, const float lin_AccelerationCap[])
72 {
73 	Init(b, m, tank_mass, effExVel, lin_Thrust, ang_Thrust);
74 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
75 		m_linAccelerationCap[i] = lin_AccelerationCap[i];
76 }
77 
SetThrustPowerMult(double p,const float lin_Thrust[],const float ang_Thrust)78 void Propulsion::SetThrustPowerMult(double p, const float lin_Thrust[], const float ang_Thrust)
79 {
80 	// Init of Propulsion:
81 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
82 		m_linThrust[i] = lin_Thrust[i] * p;
83 	m_angThrust = ang_Thrust * p;
84 }
85 
SetAccelerationCapMult(double p,const float lin_AccelerationCap[])86 void Propulsion::SetAccelerationCapMult(double p, const float lin_AccelerationCap[])
87 {
88 	for (int i = 0; i < Thruster::THRUSTER_MAX; i++)
89 		m_linAccelerationCap[i] = lin_AccelerationCap[i] * p;
90 }
91 
SetAngThrusterState(const vector3d & levels)92 void Propulsion::SetAngThrusterState(const vector3d &levels)
93 {
94 	if (m_thrusterFuel <= 0.f) {
95 		m_angThrusters = vector3d(0.0);
96 	} else {
97 		m_angThrusters.x = Clamp(levels.x, -1.0, 1.0);
98 		m_angThrusters.y = Clamp(levels.y, -1.0, 1.0);
99 		m_angThrusters.z = Clamp(levels.z, -1.0, 1.0);
100 	}
101 }
102 
ClampLinThrusterState(int axis,double level) const103 double Propulsion::ClampLinThrusterState(int axis, double level) const
104 {
105 	level = Clamp(level, -1.0, 1.0);
106 	Thruster thruster;
107 
108 	if (axis == 0) {
109 		thruster = (level > 0) ? THRUSTER_RIGHT : THRUSTER_LEFT;
110 	} else if (axis == 1) {
111 		thruster = (level > 0) ? THRUSTER_UP : THRUSTER_DOWN;
112 	} else {
113 		thruster = (level > 0) ? THRUSTER_REVERSE : THRUSTER_FORWARD;
114 	}
115 
116 	return m_linThrust[thruster] > 0.0 ? level * GetThrust(thruster) / m_linThrust[thruster] : 0.0;
117 }
118 
ClampLinThrusterState(const vector3d & levels) const119 vector3d Propulsion::ClampLinThrusterState(const vector3d &levels) const
120 {
121 	vector3d clamped = levels;
122 	Thruster thruster;
123 
124 	thruster = (clamped.x > 0) ? THRUSTER_RIGHT : THRUSTER_LEFT;
125 	clamped.x = Clamp(clamped.x, -1.0, 1.0);
126 	clamped.x *= GetThrust(thruster) / m_linThrust[thruster];
127 
128 	thruster = (clamped.y > 0) ? THRUSTER_UP : THRUSTER_DOWN;
129 	clamped.y = Clamp(clamped.y, -1.0, 1.0);
130 	clamped.y *= GetThrust(thruster) / m_linThrust[thruster];
131 
132 	thruster = (clamped.z > 0) ? THRUSTER_REVERSE : THRUSTER_FORWARD;
133 	clamped.z = Clamp(clamped.z, -1.0, 1.0);
134 	clamped.z *= GetThrust(thruster) / m_linThrust[thruster];
135 
136 	return clamped;
137 }
138 
SetLinThrusterState(int axis,double level)139 void Propulsion::SetLinThrusterState(int axis, double level)
140 {
141 	if (m_thrusterFuel <= 0.f) level = 0.0;
142 	m_linThrusters[axis] = ClampLinThrusterState(axis, level);
143 }
144 
SetLinThrusterState(const vector3d & levels)145 void Propulsion::SetLinThrusterState(const vector3d &levels)
146 {
147 	if (m_thrusterFuel <= 0.f) {
148 		m_linThrusters = vector3d(0.0);
149 	} else {
150 		m_linThrusters = ClampLinThrusterState(levels);
151 	}
152 }
153 
GetThrust(Thruster thruster) const154 double Propulsion::GetThrust(Thruster thruster) const
155 {
156 	// acceleration = thrust / mass
157 	// thrust = acceleration * mass
158 	const float mass = static_cast<float>(m_dBody->GetMass());
159 	return std::min(
160 		m_linThrust[thruster],
161 		m_linAccelerationCap[thruster] * mass);
162 }
163 
GetThrust(const vector3d & dir) const164 vector3d Propulsion::GetThrust(const vector3d &dir) const
165 {
166 	vector3d maxThrust;
167 
168 	maxThrust.x = (dir.x > 0) ? GetThrust(THRUSTER_RIGHT) : GetThrust(THRUSTER_LEFT);
169 	maxThrust.y = (dir.y > 0) ? GetThrust(THRUSTER_UP) : GetThrust(THRUSTER_DOWN);
170 	maxThrust.z = (dir.z > 0) ? GetThrust(THRUSTER_REVERSE) : GetThrust(THRUSTER_FORWARD);
171 
172 	return maxThrust;
173 }
174 
GetThrustMin() const175 double Propulsion::GetThrustMin() const
176 {
177 	// These are the weakest thrusters in a ship
178 	double val = static_cast<double>(m_linThrust[THRUSTER_UP]);
179 	val = std::min(val, static_cast<double>(m_linThrust[THRUSTER_RIGHT]));
180 	val = std::min(val, static_cast<double>(m_linThrust[THRUSTER_LEFT]));
181 	return val;
182 }
183 
GetThrustUncapped(const vector3d & dir) const184 vector3d Propulsion::GetThrustUncapped(const vector3d &dir) const
185 {
186 	vector3d maxThrust;
187 
188 	maxThrust.x = (dir.x > 0) ? m_linThrust[THRUSTER_RIGHT] : m_linThrust[THRUSTER_LEFT];
189 	maxThrust.y = (dir.y > 0) ? m_linThrust[THRUSTER_UP] : m_linThrust[THRUSTER_DOWN];
190 	maxThrust.z = (dir.z > 0) ? m_linThrust[THRUSTER_REVERSE] : m_linThrust[THRUSTER_FORWARD];
191 
192 	return maxThrust;
193 }
194 
GetFuelUseRate()195 float Propulsion::GetFuelUseRate()
196 {
197 	const float denominator = m_fuelTankMass * m_effectiveExhaustVelocity * 10;
198 	return denominator > 0 ? m_linThrust[THRUSTER_FORWARD] / denominator : 1e9;
199 }
200 
UpdateFuel(const float timeStep)201 void Propulsion::UpdateFuel(const float timeStep)
202 {
203 	const double fuelUseRate = GetFuelUseRate() * 0.01;
204 	double totalThrust = (fabs(m_linThrusters.x) + fabs(m_linThrusters.y) + fabs(m_linThrusters.z));
205 	FuelState lastState = GetFuelState();
206 	m_thrusterFuel -= timeStep * (totalThrust * fuelUseRate);
207 	FuelState currentState = GetFuelState();
208 
209 	if (currentState != lastState)
210 		m_fuelStateChange = true;
211 	else
212 		m_fuelStateChange = false;
213 }
214 
215 // returns speed that can be reached using fuel minus reserve according to the Tsiolkovsky equation
GetSpeedReachedWithFuel() const216 double Propulsion::GetSpeedReachedWithFuel() const
217 {
218 	const double mass = m_dBody->GetMass();
219 	// Why is the fuel mass multiplied by 1000 and the fuel use rate divided by 1000?
220 	// (see Propulsion::UpdateFuel and Propulsion::GetFuelUseRate)
221 	const double fuelmass = 1000 * m_fuelTankMass * (m_thrusterFuel - m_reserveFuel);
222 	if (fuelmass < 0) return 0.0;
223 	return m_effectiveExhaustVelocity * log(mass / (mass - fuelmass));
224 }
225 
Render(Graphics::Renderer * r,const Camera * camera,const vector3d & viewCoords,const matrix4x4d & viewTransform)226 void Propulsion::Render(Graphics::Renderer *r, const Camera *camera, const vector3d &viewCoords, const matrix4x4d &viewTransform)
227 {
228 	/* TODO: allow Propulsion to know SceneGraph::Thruster and
229 	 * to work directly with it (this could lead to movable
230 	 * thruster and so on)... this code is :-/
231 	*/
232 	//angthrust negated, for some reason
233 	if (m_smodel != nullptr) m_smodel->SetThrust(vector3f(GetLinThrusterState()), -vector3f(GetAngThrusterState()));
234 }
235 
AIModelCoordsMatchAngVel(const vector3d & desiredAngVel,double softness)236 void Propulsion::AIModelCoordsMatchAngVel(const vector3d &desiredAngVel, double softness)
237 {
238 	double angAccel = m_angThrust / m_dBody->GetAngularInertia();
239 	const double softTimeStep = Pi::game->GetTimeStep() * softness;
240 
241 	vector3d angVel = desiredAngVel - m_dBody->GetAngVelocity() * m_dBody->GetOrient();
242 	vector3d thrust;
243 	for (int axis = 0; axis < 3; axis++) {
244 		if (angAccel * softTimeStep >= fabs(angVel[axis])) {
245 			thrust[axis] = angVel[axis] / (softTimeStep * angAccel);
246 		} else {
247 			thrust[axis] = (angVel[axis] > 0.0 ? 1.0 : -1.0);
248 		}
249 	}
250 	SetAngThrusterState(thrust);
251 }
252 
AIModelCoordsMatchSpeedRelTo(const vector3d & v,const DynamicBody * other)253 void Propulsion::AIModelCoordsMatchSpeedRelTo(const vector3d &v, const DynamicBody *other)
254 {
255 	vector3d relToVel = other->GetVelocity() * m_dBody->GetOrient() + v;
256 	AIAccelToModelRelativeVelocity(relToVel);
257 }
258 
259 // Try to reach this model-relative velocity.
260 // (0,0,-100) would mean going 100m/s forward.
261 
AIAccelToModelRelativeVelocity(const vector3d & v)262 void Propulsion::AIAccelToModelRelativeVelocity(const vector3d &v)
263 {
264 	vector3d difVel = v - m_dBody->GetVelocity() * m_dBody->GetOrient(); // required change in velocity
265 	vector3d maxThrust = GetThrust(difVel);
266 	vector3d maxFrameAccel = maxThrust * (Pi::game->GetTimeStep() / m_dBody->GetMass());
267 
268 	SetLinThrusterState(0, is_zero_exact(maxFrameAccel.x) ? 0.0 : difVel.x / maxFrameAccel.x);
269 	SetLinThrusterState(1, is_zero_exact(maxFrameAccel.y) ? 0.0 : difVel.y / maxFrameAccel.y);
270 	SetLinThrusterState(2, is_zero_exact(maxFrameAccel.z) ? 0.0 : difVel.z / maxFrameAccel.z); // use clamping
271 }
272 
273 /* NOTE: following code were in Ship-AI.cpp file,
274  * no changes were made, except those needed
275  * to make it compatible with actual Propulsion
276  * class (and yes: it's only a copy-paste,
277  * including comments :) )
278 */
279 
280 // Because of issues when reducing timestep, must do parts of this as if 1x accel
281 // final frame has too high velocity to correct if timestep is reduced
282 // fix is too slow in the terminal stages:
283 //	if (endvel <= vel) { endvel = vel; ivel = dist / Pi::game->GetTimeStep(); }	// last frame discrete correction
284 //	ivel = std::min(ivel, endvel + 0.5*acc/PHYSICS_HZ);	// unknown next timestep discrete overshoot correction
285 
286 // yeah ok, this doesn't work
287 // sometimes endvel is too low to catch moving objects
288 // worked around with half-accel hack in dynamicbody & pi.cpp
289 
calc_ivel(double dist,double vel,double acc)290 double calc_ivel(double dist, double vel, double acc)
291 {
292 	bool inv = false;
293 	if (dist < 0) {
294 		dist = -dist;
295 		vel = -vel;
296 		inv = true;
297 	}
298 	double ivel = 0.9 * sqrt(vel * vel + 2.0 * acc * dist); // fudge hardly necessary
299 
300 	double endvel = ivel - (acc * Pi::game->GetTimeStep());
301 	if (endvel <= 0.0)
302 		ivel = dist / Pi::game->GetTimeStep(); // last frame discrete correction
303 	else
304 		ivel = (ivel + endvel) * 0.5; // discrete overshoot correction
305 	//	else ivel = endvel + 0.5*acc/PHYSICS_HZ;                  // unknown next timestep discrete overshoot correction
306 
307 	return (inv) ? -ivel : ivel;
308 }
309 
310 // version for all-positive values
calc_ivel_pos(double dist,double vel,double acc)311 double calc_ivel_pos(double dist, double vel, double acc)
312 {
313 	double ivel = 0.9 * sqrt(vel * vel + 2.0 * acc * dist); // fudge hardly necessary
314 
315 	double endvel = ivel - (acc * Pi::game->GetTimeStep());
316 	if (endvel <= 0.0)
317 		ivel = dist / Pi::game->GetTimeStep(); // last frame discrete correction
318 	else
319 		ivel = (ivel + endvel) * 0.5; // discrete overshoot correction
320 
321 	return ivel;
322 }
323 
324 // vel is desired velocity in ship's frame
325 // returns true if this can be attained in a single timestep
AIMatchVel(const vector3d & vel)326 bool Propulsion::AIMatchVel(const vector3d &vel)
327 {
328 	vector3d diffvel = (vel - m_dBody->GetVelocity()) * m_dBody->GetOrient();
329 	return AIChangeVelBy(diffvel);
330 }
331 
332 // diffvel is required change in velocity in object space
333 // returns true if this can be done in a single timestep
AIChangeVelBy(const vector3d & diffvel)334 bool Propulsion::AIChangeVelBy(const vector3d &diffvel)
335 {
336 	// counter external forces
337 	vector3d extf = m_dBody->GetExternalForce() * (Pi::game->GetTimeStep() / m_dBody->GetMass());
338 	vector3d diffvel2 = diffvel - extf * m_dBody->GetOrient();
339 
340 	vector3d maxThrust = GetThrust(diffvel2);
341 	vector3d maxFrameAccel = maxThrust * (Pi::game->GetTimeStep() / m_dBody->GetMass());
342 	vector3d thrust(diffvel2.x / maxFrameAccel.x,
343 		diffvel2.y / maxFrameAccel.y,
344 		diffvel2.z / maxFrameAccel.z);
345 	SetLinThrusterState(thrust); // use clamping
346 	if (thrust.x * thrust.x > 1.0 || thrust.y * thrust.y > 1.0 || thrust.z * thrust.z > 1.0) return false;
347 	return true;
348 }
349 
350 // Change object-space velocity in direction of param
AIChangeVelDir(const vector3d & reqdiffvel)351 vector3d Propulsion::AIChangeVelDir(const vector3d &reqdiffvel)
352 {
353 	// get max thrust in desired direction after external force compensation
354 	vector3d maxthrust = GetThrust(reqdiffvel);
355 	maxthrust += m_dBody->GetExternalForce() * m_dBody->GetOrient();
356 	vector3d maxFA = maxthrust * (Pi::game->GetTimeStep() / m_dBody->GetMass());
357 	maxFA.x = fabs(maxFA.x);
358 	maxFA.y = fabs(maxFA.y);
359 	maxFA.z = fabs(maxFA.z);
360 
361 	// crunch diffvel by relative thruster power to get acceleration in right direction
362 	vector3d diffvel = reqdiffvel;
363 	if (fabs(diffvel.x) > maxFA.x) diffvel *= maxFA.x / fabs(diffvel.x);
364 	if (fabs(diffvel.y) > maxFA.y) diffvel *= maxFA.y / fabs(diffvel.y);
365 	if (fabs(diffvel.z) > maxFA.z) diffvel *= maxFA.z / fabs(diffvel.z);
366 
367 	AIChangeVelBy(diffvel);								  // should always return true because it's already capped?
368 	return m_dBody->GetOrient() * (reqdiffvel - diffvel); // should be remaining diffvel to correct
369 }
370 
371 // Input in object space
AIMatchAngVelObjSpace(const vector3d & angvel)372 void Propulsion::AIMatchAngVelObjSpace(const vector3d &angvel)
373 {
374 	double maxAccel = m_angThrust / m_dBody->GetAngularInertia();
375 	double invFrameAccel = 1.0 / (maxAccel * Pi::game->GetTimeStep());
376 
377 	vector3d diff = angvel - m_dBody->GetAngVelocity() * m_dBody->GetOrient(); // find diff between current & desired angvel
378 	SetAngThrusterState(diff * invFrameAccel);
379 }
380 
381 // get updir as close as possible just using roll thrusters
AIFaceUpdir(const vector3d & updir,double av)382 double Propulsion::AIFaceUpdir(const vector3d &updir, double av)
383 {
384 	double maxAccel = m_angThrust / m_dBody->GetAngularInertia(); // should probably be in stats anyway
385 	double frameAccel = maxAccel * Pi::game->GetTimeStep();
386 
387 	vector3d uphead = updir * m_dBody->GetOrient(); // create desired object-space updir
388 	if (uphead.z > 0.99999) return 0;				// bail out if facing updir
389 	uphead.z = 0;
390 	uphead = uphead.Normalized(); // only care about roll axis
391 
392 	double ang = 0.0, dav = 0.0;
393 	if (uphead.y < 0.99999999) {
394 		ang = acos(Clamp(uphead.y, -1.0, 1.0));					 // scalar angle from head to curhead
395 		double iangvel = av + calc_ivel_pos(ang, 0.0, maxAccel); // ideal angvel at current time
396 
397 		dav = uphead.x > 0 ? -iangvel : iangvel;
398 	}
399 	double cav = (m_dBody->GetAngVelocity() * m_dBody->GetOrient()).z; // current obj-rel angvel
400 	double diff = (dav - cav) / frameAccel;							   // find diff between current & desired angvel
401 
402 	SetAngThrusterState(2, diff);
403 	return ang;
404 }
405 
406 // Input: direction in ship's frame, doesn't need to be normalized
407 // Approximate positive angular velocity at match point
408 // Applies thrust directly
409 // old: returns whether it can reach that direction in this frame
410 // returns angle to target
AIFaceDirection(const vector3d & dir,double av)411 double Propulsion::AIFaceDirection(const vector3d &dir, double av)
412 {
413 	double maxAccel = m_angThrust / m_dBody->GetAngularInertia(); // should probably be in stats anyway
414 
415 	vector3d head = (dir * m_dBody->GetOrient()).Normalized(); // create desired object-space heading
416 	vector3d dav(0.0, 0.0, 0.0);							   // desired angular velocity
417 
418 	double ang = 0.0;
419 	if (head.z > -0.99999999) {
420 		ang = acos(Clamp(-head.z, -1.0, 1.0));					 // scalar angle from head to curhead
421 		double iangvel = av + calc_ivel_pos(ang, 0.0, maxAccel); // ideal angvel at current time
422 
423 		// Normalize (head.x, head.y) to give desired angvel direction
424 		if (head.z > 0.999999) head.x = 1.0;
425 		double head2dnorm = 1.0 / sqrt(head.x * head.x + head.y * head.y); // NAN fix shouldn't be necessary if inputs are normalized
426 		dav.x = head.y * head2dnorm * iangvel;
427 		dav.y = -head.x * head2dnorm * iangvel;
428 	}
429 	const vector3d cav = m_dBody->GetAngVelocity() * m_dBody->GetOrient(); // current obj-rel angvel
430 	const double frameAccel = maxAccel * Pi::game->GetTimeStep();
431 	vector3d diff = is_zero_exact(frameAccel) ? vector3d(0.0) : (dav - cav) / frameAccel; // find diff between current & desired angvel
432 
433 	// If the player is pressing a roll key, don't override roll.
434 	// HACK this really shouldn't be here. a better way would be to have a
435 	// field in Ship describing the wanted angvel adjustment from input. the
436 	// baseclass version in Ship would always be 0. the version in Player
437 	// would be constructed from user input. that adjustment could then be
438 	// considered by this method when computing the required change
439 	if (m_dBody->IsType(ObjectType::PLAYER)) {
440 		auto *playerController = static_cast<const Player *>(m_dBody)->GetPlayerController();
441 		if (playerController->InputBindings.roll->IsActive())
442 			diff.z = GetAngThrusterState().z;
443 	}
444 
445 	SetAngThrusterState(diff);
446 	return ang;
447 }
448 
449 // returns direction in ship's frame from this ship to target lead position
AIGetLeadDir(const Body * target,const vector3d & targaccel,double projspeed)450 vector3d Propulsion::AIGetLeadDir(const Body *target, const vector3d &targaccel, double projspeed)
451 {
452 	assert(target);
453 	const vector3d targpos = target->GetPositionRelTo(m_dBody);
454 	const vector3d targvel = target->GetVelocityRelTo(m_dBody);
455 	// todo: should adjust targpos for gunmount offset
456 	vector3d leadpos;
457 	// avoid a divide-by-zero floating point exception (very nearly zero is ok)
458 	if (!is_zero_exact(projspeed)) {
459 		// first attempt
460 		double projtime = targpos.Length() / projspeed;
461 		leadpos = targpos + targvel * projtime + 0.5 * targaccel * projtime * projtime;
462 
463 		// second pass
464 		projtime = leadpos.Length() / projspeed;
465 		leadpos = targpos + targvel * projtime + 0.5 * targaccel * projtime * projtime;
466 	} else {
467 		// default
468 		leadpos = targpos;
469 	}
470 	return leadpos.Normalized();
471 }
472