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