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 "Space.h"
5
6 #include "Body.h"
7 #include "CityOnPlanet.h"
8 #include "Frame.h"
9 #include "Game.h"
10 #include "GameSaveError.h"
11 #include "HyperspaceCloud.h"
12 #include "Lang.h"
13 #include "MathUtil.h"
14 #include "Pi.h"
15 #include "Planet.h"
16 #include "Player.h"
17 #include "SpaceStation.h"
18 #include "Star.h"
19 #include "SystemView.h"
20 #include "collider/CollisionContact.h"
21 #include "collider/CollisionSpace.h"
22 #include "galaxy/Galaxy.h"
23 #include "graphics/Graphics.h"
24 #include "lua/LuaEvent.h"
25 #include "lua/LuaTimer.h"
26 #include <algorithm>
27 #include <functional>
28
29 //#define DEBUG_CACHE
30
RelocateStarportIfNecessary(SystemBody * sbody,Planet * planet,vector3d & pos,matrix3x3d & rot,const std::vector<vector3d> & prevPositions)31 static void RelocateStarportIfNecessary(SystemBody *sbody, Planet *planet, vector3d &pos, matrix3x3d &rot, const std::vector<vector3d> &prevPositions)
32 {
33 const double radius = planet->GetSystemBody()->GetRadius();
34
35 // suggested position
36 rot = sbody->GetOrbit().GetPlane();
37 pos = rot * vector3d(0, 1, 0);
38
39 // Check if height varies too much around the starport center
40 // by sampling 6 points around it. try upto 100 new positions randomly until a match is found
41 // this is not guaranteed to find a match but greatly increases the chancessteroids which are not too steep.
42
43 bool variationWithinLimits = true;
44 double bestVariation = 1e10; // any high value
45 matrix3x3d rotNotUnderwaterWithLeastVariation = rot;
46 vector3d posNotUnderwaterWithLeastVariation = pos;
47 const double heightVariationCheckThreshold = 0.008; // max variation to radius radius ratio to check for local slope, ganymede is around 0.01
48 const double terrainHeightVariation = planet->GetMaxFeatureRadius(); //in radii
49
50 //Output("%s: terrain height variation %f\n", sbody->name.c_str(), terrainHeightVariation);
51
52 // 6 points are sampled around the starport center by adding/subtracting delta to to coords
53 // points must stay within max height variation to be accepted
54 // 1. delta should be chosen such that it a distance from the starport center that encloses landing pads for the largest starport
55 // 2. maxSlope should be set so maxHeightVariation is less than the height of the landing pads
56 const double delta = 20.0 / radius; // in radii
57 const double maxSlope = 0.2; // 0.0 to 1.0
58 const double maxHeightVariation = maxSlope * delta * radius; // in m
59
60 matrix3x3d rot_ = rot;
61 vector3d pos_ = pos;
62
63 const bool manualRelocationIsEasy = !(planet->GetSystemBody()->GetType() == SystemBody::TYPE_PLANET_ASTEROID || terrainHeightVariation > heightVariationCheckThreshold);
64
65 // warn and leave it up to the user to relocate custom starports when it's easy to relocate manually, i.e. not on asteroids and other planets which are likely to have high variation in a lot of places
66 const bool isRelocatableIfBuried = !(sbody->IsCustomBody() && manualRelocationIsEasy);
67
68 bool isInitiallyUnderwater = false;
69 bool initialVariationTooHigh = false;
70
71 Random r(sbody->GetSeed());
72
73 for (int tries = 0; tries < 200; tries++) {
74 variationWithinLimits = true;
75
76 const double height = planet->GetTerrainHeight(pos_) - radius; // in m
77
78 // check height at 6 points around the starport center stays within variation tolerances
79 // GetHeight gives a varying height field in 3 dimensions.
80 // Given it's smoothly varying it's fine to sample it in arbitary directions to get an idea of how sharply it varies
81 double v[6];
82 v[0] = fabs(planet->GetTerrainHeight(vector3d(pos_.x + delta, pos_.y, pos_.z)) - radius - height);
83 v[1] = fabs(planet->GetTerrainHeight(vector3d(pos_.x - delta, pos_.y, pos_.z)) - radius - height);
84 v[2] = fabs(planet->GetTerrainHeight(vector3d(pos_.x, pos_.y, pos_.z + delta)) - radius - height);
85 v[3] = fabs(planet->GetTerrainHeight(vector3d(pos_.x, pos_.y, pos_.z - delta)) - radius - height);
86 v[4] = fabs(planet->GetTerrainHeight(vector3d(pos_.x, pos_.y + delta, pos_.z)) - radius - height);
87 v[5] = fabs(planet->GetTerrainHeight(vector3d(pos_.x, pos_.y - delta, pos_.z)) - radius - height);
88
89 // break if variation for all points is within limits
90 double variationMax = 0.0;
91 for (int i = 0; i < 6; i++) {
92 variationWithinLimits = variationWithinLimits && (v[i] < maxHeightVariation);
93 variationMax = (v[i] > variationMax) ? v[i] : variationMax;
94 }
95
96 // check if underwater
97 const bool starportUnderwater = (height <= 0.0);
98
99 //Output("%s: try no: %i, Match found: %i, best variation in previous results %f, variationMax this try: %f, maxHeightVariation: %f, Starport is underwater: %i\n",
100 // sbody->name.c_str(), tries, (variationWithinLimits && !starportUnderwater), bestVariation, variationMax, maxHeightVariation, starportUnderwater);
101
102 bool tooCloseToOther = false;
103 for (vector3d oldPos : prevPositions) {
104 // is the distance between points less than the delta distance?
105 if ((pos_ - oldPos).LengthSqr() < (delta * delta)) {
106 tooCloseToOther = true; // then we're too close so try again
107 break;
108 }
109 }
110
111 if (tries == 0) {
112 isInitiallyUnderwater = starportUnderwater;
113 initialVariationTooHigh = !variationWithinLimits;
114 }
115
116 if (!starportUnderwater && variationMax < bestVariation) {
117 bestVariation = variationMax;
118 posNotUnderwaterWithLeastVariation = pos_;
119 rotNotUnderwaterWithLeastVariation = rot_;
120 }
121
122 if (variationWithinLimits && !starportUnderwater && !tooCloseToOther)
123 break;
124
125 // try new random position
126 const double r3 = r.Double();
127 const double r2 = r.Double(); // function parameter evaluation order is implementation-dependent
128 const double r1 = r.Double(); // can't put two rands in the same expression
129 rot_ = matrix3x3d::RotateZ(2.0 * M_PI * r1) * matrix3x3d::RotateY(2.0 * M_PI * r2) * matrix3x3d::RotateX(2.0 * M_PI * r3);
130 pos_ = rot_ * vector3d(0, 1, 0);
131 }
132
133 if (isInitiallyUnderwater || (isRelocatableIfBuried && initialVariationTooHigh)) {
134 pos = posNotUnderwaterWithLeastVariation;
135 rot = rotNotUnderwaterWithLeastVariation;
136 }
137
138 if (sbody->IsCustomBody()) {
139 const SystemPath &p = sbody->GetPath();
140 if (initialVariationTooHigh) {
141 if (isRelocatableIfBuried) {
142 Output("Warning: Lua custom Systems definition: Surface starport has been automatically relocated. This is in order to place it on flatter ground to reduce the chance of landing pads being buried. This is not an error as such and you may attempt to move the starport to another location by changing latitude and longitude fields.\n Surface starport name: %s, Body name: %s, In sector: x = %i, y = %i, z = %i.\n",
143 sbody->GetName().c_str(), sbody->GetParent()->GetName().c_str(), p.sectorX, p.sectorY, p.sectorZ);
144 } else {
145 Output("Warning: Lua custom Systems definition: Surface starport may have landing pads buried. The surface starport has not been automatically relocated as the planet appears smooth enough to manually relocate easily. This is not an error as such and you may attempt to move the starport to another location by changing latitude and longitude fields.\n Surface starport name: %s, Body name: %s, In sector: x = %i, y = %i, z = %i.\n",
146 sbody->GetName().c_str(), sbody->GetParent()->GetName().c_str(), p.sectorX, p.sectorY, p.sectorZ);
147 }
148 }
149 if (isInitiallyUnderwater) {
150 Output("Error: Lua custom Systems definition: Surface starport is underwater (height not greater than 0.0) and has been automatically relocated. Please move the starport to another location by changing latitude and longitude fields.\n Surface starport name: %s, Body name: %s, In sector: x = %i, y = %i, z = %i.\n",
151 sbody->GetName().c_str(), sbody->GetParent()->GetName().c_str(), p.sectorX, p.sectorY, p.sectorZ);
152 }
153 }
154 }
155
Prepare()156 void Space::BodyNearFinder::Prepare()
157 {
158 PROFILE_SCOPED()
159 m_bodyDist.clear();
160
161 for (Body *b : m_space->GetBodies())
162 m_bodyDist.emplace_back(b, b->GetPositionRelTo(m_space->GetRootFrame()).Length());
163
164 std::sort(m_bodyDist.begin(), m_bodyDist.end());
165 }
166
GetBodiesMaybeNear(const Body * b,double dist)167 Space::BodyNearList Space::BodyNearFinder::GetBodiesMaybeNear(const Body *b, double dist)
168 {
169 return GetBodiesMaybeNear(b->GetPositionRelTo(m_space->GetRootFrame()), dist);
170 }
171
GetBodiesMaybeNear(const vector3d & pos,double dist)172 Space::BodyNearList Space::BodyNearFinder::GetBodiesMaybeNear(const vector3d &pos, double dist)
173 {
174 if (m_bodyDist.empty()) {
175 m_nearBodies.clear();
176 return std::move(m_nearBodies);
177 }
178
179 const double len = pos.Length();
180
181 std::vector<BodyDist>::const_iterator min = std::lower_bound(m_bodyDist.begin(), m_bodyDist.end(), len - dist);
182 std::vector<BodyDist>::const_iterator max = std::upper_bound(min, m_bodyDist.cend(), len + dist);
183
184 m_nearBodies.clear();
185 m_nearBodies.reserve(max - min);
186
187 std::for_each(min, max, [&](BodyDist const &bd) { m_nearBodies.push_back(bd.body); });
188
189 return std::move(m_nearBodies);
190 }
191
Space(Game * game,RefCountedPtr<Galaxy> galaxy,Space * oldSpace)192 Space::Space(Game *game, RefCountedPtr<Galaxy> galaxy, Space *oldSpace) :
193 m_starSystemCache(oldSpace ? oldSpace->m_starSystemCache : galaxy->NewStarSystemSlaveCache()),
194 m_game(game),
195 m_bodyIndexValid(false),
196 m_sbodyIndexValid(false),
197 m_bodyNearFinder(this)
198 #ifndef NDEBUG
199 ,
200 m_processingFinalizationQueue(false)
201 #endif
202 {
203 m_background.reset(new Background::Container(Pi::renderer, Pi::rng, this, m_game->GetGalaxy()));
204
205 m_rootFrameId = Frame::CreateFrame(FrameId::Invalid, Lang::SYSTEM, Frame::FLAG_DEFAULT, FLT_MAX);
206
207 GenSectorCache(galaxy, &game->GetHyperspaceDest());
208 }
209
Space(Game * game,RefCountedPtr<Galaxy> galaxy,const SystemPath & path,Space * oldSpace)210 Space::Space(Game *game, RefCountedPtr<Galaxy> galaxy, const SystemPath &path, Space *oldSpace) :
211 m_starSystemCache(oldSpace ? oldSpace->m_starSystemCache : galaxy->NewStarSystemSlaveCache()),
212 m_starSystem(galaxy->GetStarSystem(path)),
213 m_game(game),
214 m_bodyIndexValid(false),
215 m_sbodyIndexValid(false),
216 m_bodyNearFinder(this)
217 #ifndef NDEBUG
218 ,
219 m_processingFinalizationQueue(false)
220 #endif
221 {
222 PROFILE_SCOPED()
223 Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED };
224 Random rand(_init, 5);
225 m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy()));
226
227 CityOnPlanet::SetCityModelPatterns(m_starSystem->GetPath());
228
229 m_rootFrameId = Frame::CreateFrame(FrameId::Invalid, Lang::SYSTEM, Frame::FLAG_DEFAULT, FLT_MAX);
230
231 std::vector<vector3d> positionAccumulator;
232 GenBody(m_game->GetTime(), m_starSystem->GetRootBody().Get(), m_rootFrameId, positionAccumulator);
233 Frame::UpdateOrbitRails(m_game->GetTime(), m_game->GetTimeStep());
234
235 GenSectorCache(galaxy, &path);
236 }
237
Space(Game * game,RefCountedPtr<Galaxy> galaxy,const Json & jsonObj,double at_time)238 Space::Space(Game *game, RefCountedPtr<Galaxy> galaxy, const Json &jsonObj, double at_time) :
239 m_starSystemCache(galaxy->NewStarSystemSlaveCache()),
240 m_game(game),
241 m_bodyIndexValid(false),
242 m_sbodyIndexValid(false),
243 m_bodyNearFinder(this)
244 #ifndef NDEBUG
245 ,
246 m_processingFinalizationQueue(false)
247 #endif
248 {
249 PROFILE_SCOPED()
250 Json spaceObj = jsonObj["space"];
251
252 m_starSystem = StarSystem::FromJson(galaxy, spaceObj);
253
254 const SystemPath &path = m_starSystem->GetPath();
255 Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED };
256 Random rand(_init, 5);
257 m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy()));
258
259 RebuildSystemBodyIndex();
260
261 CityOnPlanet::SetCityModelPatterns(m_starSystem->GetPath());
262
263 if (!spaceObj.count("frame")) throw SavedGameCorruptException();
264 m_rootFrameId = Frame::FromJson(spaceObj["frame"], this, FrameId::Invalid, at_time);
265
266 try {
267 Json bodyArray = spaceObj["bodies"].get<Json::array_t>();
268 for (Uint32 i = 0; i < bodyArray.size(); i++)
269 m_bodies.push_back(Body::FromJson(bodyArray[i], this));
270 } catch (Json::type_error &) {
271 throw SavedGameCorruptException();
272 }
273
274 RebuildBodyIndex();
275
276 Frame::PostUnserializeFixup(m_rootFrameId, this);
277 for (Body *b : m_bodies)
278 b->PostLoadFixup(this);
279
280 // some spaceports could be moved, now their physical bodies were loaded from
281 // json, with offsets, now we should move the system bodies also so that
282 // there is no mismatch
283 for (auto sbody : m_starSystem->GetBodies()) {
284 if (sbody->GetSuperType() == SystemBody::SUPERTYPE_ROCKY_PLANET) {
285 // needs a clean posAccum for each planet
286 std::vector<vector3d> posAccum;
287 SystemPath planet_path = m_starSystem->GetPathOf(sbody.Get());
288 Planet *planet = static_cast<Planet *>(FindBodyForPath(&planet_path));
289 for (auto kid : sbody->GetChildren()) {
290 if (kid->GetType() == SystemBody::TYPE_STARPORT_SURFACE) {
291 // out arguments
292 matrix3x3d rot;
293 vector3d pos;
294 RelocateStarportIfNecessary(kid, planet, pos, rot, posAccum);
295 // the surface starport's location is stored in its "orbit", as orientation matrix
296 kid->SetOrbitPlane(rot);
297 // accumulate for testing against
298 posAccum.push_back(pos);
299 }
300 }
301 }
302 }
303
304 GenSectorCache(galaxy, &path);
305
306 //DebugDumpFrames();
307 }
308
~Space()309 Space::~Space()
310 {
311 UpdateBodies(); // make sure anything waiting to be removed gets removed before we go and kill everything else
312 for (Body *body : m_bodies)
313 KillBody(body);
314 UpdateBodies();
315 Frame::DeleteFrames();
316 }
317
RefreshBackground()318 void Space::RefreshBackground()
319 {
320 PROFILE_SCOPED()
321 const SystemPath &path = m_starSystem->GetPath();
322 Uint32 _init[5] = { path.systemIndex, Uint32(path.sectorX), Uint32(path.sectorY), Uint32(path.sectorZ), UNIVERSE_SEED };
323 Random rand(_init, 5);
324 m_background.reset(new Background::Container(Pi::renderer, rand, this, m_game->GetGalaxy()));
325 }
326
ToJson(Json & jsonObj)327 void Space::ToJson(Json &jsonObj)
328 {
329 PROFILE_SCOPED()
330 RebuildBodyIndex();
331 RebuildSystemBodyIndex();
332
333 Json spaceObj({}); // Create JSON object to contain space data (all the bodies and things).
334
335 StarSystem::ToJson(spaceObj, m_starSystem.Get());
336
337 Json frameObj({});
338 Frame::ToJson(frameObj, m_rootFrameId, this);
339 spaceObj["frame"] = frameObj;
340
341 Json bodyArray = Json::array(); // Create JSON array to contain body data.
342 for (Body *b : m_bodies) {
343 Json bodyArrayEl({}); // Create JSON object to contain body.
344 b->ToJson(bodyArrayEl, this);
345 bodyArray.push_back(bodyArrayEl); // Append body object to array.
346 }
347 spaceObj["bodies"] = bodyArray; // Add body array to space object.
348
349 jsonObj["space"] = spaceObj; // Add space object to supplied object.
350 }
351
GetBodyByIndex(Uint32 idx) const352 Body *Space::GetBodyByIndex(Uint32 idx) const
353 {
354 assert(m_bodyIndexValid);
355 assert(m_bodyIndex.size() > idx);
356 if (idx == SDL_MAX_UINT32 || m_bodyIndex.size() <= idx) {
357 Output("GetBodyByIndex passed bad index %u", idx);
358 return nullptr;
359 }
360 return m_bodyIndex[idx];
361 }
362
GetSystemBodyByIndex(Uint32 idx) const363 SystemBody *Space::GetSystemBodyByIndex(Uint32 idx) const
364 {
365 assert(m_sbodyIndexValid);
366 assert(m_sbodyIndex.size() > idx);
367 return m_sbodyIndex[idx];
368 }
369
GetIndexForBody(const Body * body) const370 Uint32 Space::GetIndexForBody(const Body *body) const
371 {
372 assert(m_bodyIndexValid);
373 for (Uint32 i = 0; i < m_bodyIndex.size(); i++)
374 if (m_bodyIndex[i] == body) return i;
375 assert(false);
376 Output("GetIndexForBody passed unknown body");
377 return SDL_MAX_UINT32;
378 }
379
GetIndexForSystemBody(const SystemBody * sbody) const380 Uint32 Space::GetIndexForSystemBody(const SystemBody *sbody) const
381 {
382 assert(m_sbodyIndexValid);
383 for (Uint32 i = 0; i < m_sbodyIndex.size(); i++)
384 if (m_sbodyIndex[i] == sbody) return i;
385 assert(0);
386 return SDL_MAX_UINT32;
387 }
388
AddSystemBodyToIndex(SystemBody * sbody)389 void Space::AddSystemBodyToIndex(SystemBody *sbody)
390 {
391 assert(sbody);
392 m_sbodyIndex.push_back(sbody);
393 for (Uint32 i = 0; i < sbody->GetNumChildren(); i++)
394 AddSystemBodyToIndex(sbody->GetChildren()[i]);
395 }
396
RebuildBodyIndex()397 void Space::RebuildBodyIndex()
398 {
399 m_bodyIndex.clear();
400 m_bodyIndex.push_back(nullptr);
401
402 for (Body *b : m_bodies) {
403 m_bodyIndex.push_back(b);
404 // also index ships inside clouds
405 // XXX we should not have to know about this. move indexing grunt work
406 // down into the bodies?
407 if (b->IsType(ObjectType::HYPERSPACECLOUD)) {
408 Ship *s = static_cast<HyperspaceCloud *>(b)->GetShip();
409 if (s) m_bodyIndex.push_back(s);
410 }
411 }
412
413 Pi::SetAmountBackgroundStars(Pi::GetAmountBackgroundStars());
414
415 m_bodyIndexValid = true;
416 }
417
RebuildSystemBodyIndex()418 void Space::RebuildSystemBodyIndex()
419 {
420 m_sbodyIndex.clear();
421 m_sbodyIndex.push_back(nullptr);
422
423 if (m_starSystem)
424 AddSystemBodyToIndex(m_starSystem->GetRootBody().Get());
425
426 m_sbodyIndexValid = true;
427 }
428
AddBody(Body * b)429 void Space::AddBody(Body *b)
430 {
431 m_bodies.push_back(b);
432 }
433
RemoveBody(Body * b)434 void Space::RemoveBody(Body *b)
435 {
436 #ifndef NDEBUG
437 assert(!m_processingFinalizationQueue);
438 #endif
439 m_assignedBodies.emplace_back(b, BodyAssignation::REMOVE);
440 }
441
KillBody(Body * b)442 void Space::KillBody(Body *b)
443 {
444 #ifndef NDEBUG
445 assert(!m_processingFinalizationQueue);
446 #endif
447 if (!b->IsDead()) {
448 b->MarkDead();
449
450 // player needs to stay alive so things like the death animation
451 // (which uses a camera positioned relative to the player) can
452 // continue to work. it will be cleaned up with the space is torn down
453 // XXX this seems like the wrong way to do it. since its still "alive"
454 // it still collides, moves, etc. better to just snapshot its position
455 // elsewhere
456 if (b != Pi::player)
457 m_assignedBodies.emplace_back(b, BodyAssignation::KILL);
458 }
459 }
460
GetHyperspaceExitParams(const SystemPath & source,const SystemPath & dest,vector3d & pos,vector3d & vel) const461 void Space::GetHyperspaceExitParams(const SystemPath &source, const SystemPath &dest,
462 vector3d &pos, vector3d &vel) const
463 {
464 assert(m_starSystem);
465 assert(source.IsSystemPath());
466
467 assert(dest.IsSameSystem(m_starSystem->GetPath()));
468
469 RefCountedPtr<const Sector> source_sec = m_sectorCache->GetCached(source);
470 RefCountedPtr<const Sector> dest_sec = m_sectorCache->GetCached(dest);
471
472 Sector::System source_sys = source_sec->m_systems[source.systemIndex];
473 Sector::System dest_sys = dest_sec->m_systems[dest.systemIndex];
474
475 const vector3d sourcePos = vector3d(source_sys.GetFullPosition());
476 const vector3d destPos = vector3d(dest_sys.GetFullPosition());
477
478 Body *primary = 0;
479 if (dest.IsBodyPath()) {
480 assert(dest.bodyIndex < m_starSystem->GetNumBodies());
481 primary = FindBodyForPath(&dest);
482 while (primary && primary->GetSystemBody()->GetSuperType() != SystemBody::SUPERTYPE_STAR) {
483 SystemBody *parent = primary->GetSystemBody()->GetParent();
484 primary = parent ? FindBodyForPath(&parent->GetPath()) : 0;
485 }
486 }
487 if (!primary) {
488 // find the first non-gravpoint. should be the primary star
489 for (Body *b : GetBodies())
490 if (b->GetSystemBody()->GetType() != SystemBody::TYPE_GRAVPOINT) {
491 primary = b;
492 break;
493 }
494 }
495 assert(primary);
496
497 // calculate distance to primary body relative to body's mass and radius
498 const double max_orbit_vel = 100e3;
499 double dist = G * primary->GetSystemBody()->GetMass() /
500 (max_orbit_vel * max_orbit_vel);
501
502 // ensure an absolute minimum and an absolute maximum distance
503 // the minimum distance from the center of the star should not be less than the radius of the star
504 dist = Clamp(dist, primary->GetSystemBody()->GetRadius() * 1.1, std::max(primary->GetSystemBody()->GetRadius() * 1.1, 100 * AU));
505
506 // point velocity vector along the line from source to dest,
507 // make exit position perpendicular to it,
508 // add random component to exit position,
509 // set velocity for (almost) circular orbit
510 vel = (destPos - sourcePos).Normalized();
511 {
512 vector3d a{ MathUtil::OrthogonalDirection(vel) };
513 vector3d b{ vel.Cross(a) };
514 vector3d p{ MathUtil::RandomPointOnCircle(1.) };
515 pos = p.x * a + p.y * b;
516 }
517 pos *= dist * Pi::rng.Double(0.95, 1.2);
518 vel *= sqrt(G * primary->GetSystemBody()->GetMass() / dist);
519
520 assert(pos.Length() > primary->GetSystemBody()->GetRadius());
521 pos += primary->GetPositionRelTo(GetRootFrame());
522 }
523
FindNearestTo(const Body * b,ObjectType t) const524 Body *Space::FindNearestTo(const Body *b, ObjectType t) const
525 {
526 Body *nearest = 0;
527 double dist = FLT_MAX;
528 for (Body *const body : m_bodies) {
529 if (body->IsDead()) continue;
530 if (body->IsType(t)) {
531 double d = body->GetPositionRelTo(b).Length();
532 if (d < dist) {
533 dist = d;
534 nearest = body;
535 }
536 }
537 }
538 return nearest;
539 }
540
FindBodyForPath(const SystemPath * path) const541 Body *Space::FindBodyForPath(const SystemPath *path) const
542 {
543 // it is a bit dumb that currentSystem is not part of Space...
544 SystemBody *body = m_starSystem->GetBodyByPath(path);
545
546 if (!body) return 0;
547
548 for (Body *b : m_bodies) {
549 if (b->GetSystemBody() == body) return b;
550 }
551 return 0;
552 }
553
find_frame_with_sbody(FrameId fId,const SystemBody * b)554 static FrameId find_frame_with_sbody(FrameId fId, const SystemBody *b)
555 {
556 Frame *f = Frame::GetFrame(fId);
557 if (f->GetSystemBody() == b)
558 return fId;
559 else {
560 for (FrameId kid : f->GetChildren()) {
561 FrameId found = find_frame_with_sbody(kid, b);
562 if (found.valid())
563 return found;
564 }
565 }
566 return FrameId::Invalid;
567 }
568
GetFrameWithSystemBody(const SystemBody * b) const569 FrameId Space::GetFrameWithSystemBody(const SystemBody *b) const
570 {
571 return find_frame_with_sbody(m_rootFrameId, b);
572 }
573
574 // used to define a cube centred on your current location
575 static const int sectorRadius = 5;
576
577 // sort using a custom function object
578 class SectorDistanceSort {
579 public:
operator ()(const SystemPath & a,const SystemPath & b)580 bool operator()(const SystemPath &a, const SystemPath &b)
581 {
582 const float dist_a = vector3f(here.sectorX - a.sectorX, here.sectorY - a.sectorY, here.sectorZ - a.sectorZ).LengthSqr();
583 const float dist_b = vector3f(here.sectorX - b.sectorX, here.sectorY - b.sectorY, here.sectorZ - b.sectorZ).LengthSqr();
584 return dist_a < dist_b;
585 }
SectorDistanceSort(const SystemPath * centre)586 SectorDistanceSort(const SystemPath *centre) :
587 here(centre)
588 {}
589
590 private:
SectorDistanceSort()591 SectorDistanceSort() {}
592 SystemPath here;
593 };
594
GenSectorCache(RefCountedPtr<Galaxy> galaxy,const SystemPath * here)595 void Space::GenSectorCache(RefCountedPtr<Galaxy> galaxy, const SystemPath *here)
596 {
597 PROFILE_SCOPED()
598
599 // current location
600 if (!here) {
601 if (!m_starSystem.Valid())
602 return;
603 here = &m_starSystem->GetPath();
604 }
605 const int here_x = here->sectorX;
606 const int here_y = here->sectorY;
607 const int here_z = here->sectorZ;
608
609 SectorCache::PathVector paths;
610 // build all of the possible paths we'll need to build sectors for
611 for (int x = here_x - sectorRadius; x <= here_x + sectorRadius; x++) {
612 for (int y = here_y - sectorRadius; y <= here_y + sectorRadius; y++) {
613 for (int z = here_z - sectorRadius; z <= here_z + sectorRadius; z++) {
614 SystemPath path(x, y, z);
615 paths.push_back(path);
616 }
617 }
618 }
619 // sort them so that those closest to the "here" path are processed first
620 SectorDistanceSort SDS(here);
621 std::sort(paths.begin(), paths.end(), SDS);
622 m_sectorCache = galaxy->NewSectorSlaveCache();
623 const SystemPath ¢er(*here);
624 m_sectorCache->FillCache(paths, [this, center]() { UpdateStarSystemCache(¢er); });
625 }
626
WithinBox(const SystemPath & here,const int Xmin,const int Xmax,const int Ymin,const int Ymax,const int Zmin,const int Zmax)627 static bool WithinBox(const SystemPath &here, const int Xmin, const int Xmax, const int Ymin, const int Ymax, const int Zmin, const int Zmax)
628 {
629 PROFILE_SCOPED()
630 if (here.sectorX >= Xmin && here.sectorX <= Xmax) {
631 if (here.sectorY >= Ymin && here.sectorY <= Ymax) {
632 if (here.sectorZ >= Zmin && here.sectorZ <= Zmax) {
633 return true;
634 }
635 }
636 }
637 return false;
638 }
639
UpdateStarSystemCache(const SystemPath * here)640 void Space::UpdateStarSystemCache(const SystemPath *here)
641 {
642 PROFILE_SCOPED()
643
644 // current location
645 if (!here) {
646 if (!m_starSystem.Valid())
647 return;
648 here = &m_starSystem->GetPath();
649 }
650 const int here_x = here->sectorX;
651 const int here_y = here->sectorY;
652 const int here_z = here->sectorZ;
653
654 // we're going to use these to determine if our StarSystems are within a range that we'll keep for later use
655 static const int survivorRadius = sectorRadius * 3;
656
657 // min/max box limits
658 const int xmin = here->sectorX - survivorRadius;
659 const int xmax = here->sectorX + survivorRadius;
660 const int ymin = here->sectorY - survivorRadius;
661 const int ymax = here->sectorY + survivorRadius;
662 const int zmin = here->sectorZ - survivorRadius;
663 const int zmax = here->sectorZ + survivorRadius;
664
665 #ifdef DEBUG_CACHE
666 unsigned removed = 0;
667 #endif
668 StarSystemCache::CacheMap::const_iterator i = m_starSystemCache->Begin();
669 while (i != m_starSystemCache->End()) {
670 if (!WithinBox(i->second->GetPath(), xmin, xmax, ymin, ymax, zmin, zmax)) {
671 m_starSystemCache->Erase(i++);
672 #ifdef DEBUG_CACHE
673 ++removed;
674 #endif
675 } else
676 ++i;
677 }
678 #ifdef DEBUG_CACHE
679 Output("%s: Erased %u entries.\n", StarSystemCache::CACHE_NAME.c_str(), removed);
680 #endif
681
682 SectorCache::PathVector paths;
683 // build all of the possible paths we'll need to build star systems for
684 for (int x = here_x - sectorRadius; x <= here_x + sectorRadius; x++) {
685 for (int y = here_y - sectorRadius; y <= here_y + sectorRadius; y++) {
686 for (int z = here_z - sectorRadius; z <= here_z + sectorRadius; z++) {
687 SystemPath path(x, y, z);
688 RefCountedPtr<Sector> sec(m_sectorCache->GetIfCached(path));
689 assert(sec);
690 for (const Sector::System &ss : sec->m_systems)
691 paths.push_back(SystemPath(ss.sx, ss.sy, ss.sz, ss.idx));
692 }
693 }
694 }
695 m_starSystemCache->FillCache(paths);
696 }
697
MakeFramesFor(const double at_time,SystemBody * sbody,Body * b,FrameId fId,std::vector<vector3d> & prevPositions)698 static FrameId MakeFramesFor(const double at_time, SystemBody *sbody, Body *b, FrameId fId, std::vector<vector3d> &prevPositions)
699 {
700 PROFILE_SCOPED()
701 if (!sbody->GetParent()) {
702 if (b) b->SetFrame(fId);
703 Frame *f = Frame::GetFrame(fId);
704 f->SetBodies(sbody, b);
705 return fId;
706 }
707
708 if (sbody->GetType() == SystemBody::TYPE_GRAVPOINT) {
709 FrameId orbFrameId = Frame::CreateFrame(fId,
710 sbody->GetName().c_str(),
711 Frame::FLAG_DEFAULT,
712 sbody->GetMaxChildOrbitalDistance() * 1.1);
713 Frame *orbFrame = Frame::GetFrame(orbFrameId);
714 orbFrame->SetBodies(sbody, b);
715 return orbFrameId;
716 }
717
718 SystemBody::BodySuperType supertype = sbody->GetSuperType();
719
720 if ((supertype == SystemBody::SUPERTYPE_GAS_GIANT) ||
721 (supertype == SystemBody::SUPERTYPE_ROCKY_PLANET)) {
722 // for planets we want an non-rotating frame for a few radii
723 // and a rotating frame with no radius to contain attached objects
724 double frameRadius = std::max(4.0 * sbody->GetRadius(), sbody->GetMaxChildOrbitalDistance() * 1.05);
725 FrameId orbFrameId = Frame::CreateFrame(fId,
726 sbody->GetName().c_str(),
727 Frame::FLAG_HAS_ROT,
728 frameRadius);
729 Frame *orbFrame = Frame::GetFrame(orbFrameId);
730 orbFrame->SetBodies(sbody, b);
731 //Output("\t\t\t%s has frame size %.0fkm, body radius %.0fkm\n", sbody->name.c_str(),
732 // (frameRadius ? frameRadius : 10*sbody->GetRadius())*0.001f,
733 // sbody->GetRadius()*0.001f);
734
735 assert(sbody->IsRotating() != 0);
736 // rotating frame has atmosphere radius or feature height, whichever is larger
737 FrameId rotFrameId = Frame::CreateFrame(orbFrameId,
738 sbody->GetName().c_str(),
739 Frame::FLAG_ROTATING,
740 b->GetPhysRadius());
741 Frame *rotFrame = Frame::GetFrame(rotFrameId);
742 rotFrame->SetBodies(sbody, b);
743
744 matrix3x3d rotMatrix = matrix3x3d::RotateX(sbody->GetAxialTilt());
745 double angSpeed = 2.0 * M_PI / sbody->GetRotationPeriod();
746 rotFrame->SetAngSpeed(angSpeed);
747
748 if (sbody->HasRotationPhase())
749 rotMatrix = rotMatrix * matrix3x3d::RotateY(sbody->GetRotationPhaseAtStart());
750 rotFrame->SetInitialOrient(rotMatrix, at_time);
751
752 b->SetFrame(rotFrameId);
753 return orbFrameId;
754 } else if (supertype == SystemBody::SUPERTYPE_STAR) {
755 // stars want a single small non-rotating frame
756 // bigger than it's furtherest orbiting body.
757 // if there are no orbiting bodies use a frame of several radii.
758 FrameId orbFrameId = Frame::CreateFrame(fId, sbody->GetName().c_str());
759 Frame *orbFrame = Frame::GetFrame(orbFrameId);
760 orbFrame->SetBodies(sbody, b);
761 const double bodyRadius = sbody->GetEquatorialRadius();
762 double frameRadius = std::max(10.0 * bodyRadius, sbody->GetMaxChildOrbitalDistance() * 1.1);
763 // Respect the frame of other stars in the multi-star system. We still make sure that the frame ends outside
764 // the body. For a minimum separation of 1.236 radii, nothing will overlap (see StarSystem::StarSystem()).
765 if (sbody->GetParent() && frameRadius > AU * 0.11 * sbody->GetOrbMin())
766 frameRadius = std::max(1.1 * bodyRadius, AU * 0.11 * sbody->GetOrbMin());
767 orbFrame->SetRadius(frameRadius);
768 b->SetFrame(orbFrameId);
769 return orbFrameId;
770 } else if (sbody->GetType() == SystemBody::TYPE_STARPORT_ORBITAL) {
771 // space stations want non-rotating frame to some distance
772 FrameId orbFrameId = Frame::CreateFrame(fId,
773 sbody->GetName().c_str(),
774 Frame::FLAG_DEFAULT,
775 20000.0);
776 Frame *orbFrame = Frame::GetFrame(orbFrameId);
777 orbFrame->SetBodies(sbody, b);
778 b->SetFrame(orbFrameId);
779 return orbFrameId;
780 } else if (sbody->GetType() == SystemBody::TYPE_STARPORT_SURFACE) {
781 // just put body into rotating frame of planet, not in its own frame
782 // (because collisions only happen between objects in same frame,
783 // and we want collisions on starport and on planet itself)
784 FrameId rotFrameId = Frame::GetFrame(fId)->GetRotFrame();
785 b->SetFrame(rotFrameId);
786
787 Frame *rotFrame = Frame::GetFrame(rotFrameId);
788 assert(rotFrame->IsRotFrame());
789 assert(rotFrame->GetBody()->IsType(ObjectType::PLANET));
790 matrix3x3d rot;
791 vector3d pos;
792 Planet *planet = static_cast<Planet *>(rotFrame->GetBody());
793 RelocateStarportIfNecessary(sbody, planet, pos, rot, prevPositions);
794 sbody->SetOrbitPlane(rot);
795 b->SetPosition(pos * planet->GetTerrainHeight(pos));
796 b->SetOrient(rot);
797 // accumulate for testing against
798 prevPositions.push_back(pos);
799 return rotFrameId;
800 } else {
801 assert(0);
802 }
803 return 0;
804 }
805
GenBody(const double at_time,SystemBody * sbody,FrameId fId,std::vector<vector3d> & posAccum)806 void Space::GenBody(const double at_time, SystemBody *sbody, FrameId fId, std::vector<vector3d> &posAccum)
807 {
808 PROFILE_START()
809 Body *b = nullptr;
810
811 if (sbody->GetType() != SystemBody::TYPE_GRAVPOINT) {
812 if (sbody->GetSuperType() == SystemBody::SUPERTYPE_STAR) {
813 Star *star = new Star(sbody);
814 b = star;
815 } else if ((sbody->GetType() == SystemBody::TYPE_STARPORT_ORBITAL) ||
816 (sbody->GetType() == SystemBody::TYPE_STARPORT_SURFACE)) {
817 SpaceStation *ss = new SpaceStation(sbody);
818 b = ss;
819 } else {
820 Planet *planet = new Planet(sbody);
821 b = planet;
822 // reset this
823 posAccum.clear();
824 }
825 b->SetLabel(sbody->GetName().c_str());
826 b->SetPosition(vector3d(0, 0, 0));
827 AddBody(b);
828 }
829 fId = MakeFramesFor(at_time, sbody, b, fId, posAccum);
830
831 PROFILE_STOP()
832 for (SystemBody *kid : sbody->GetChildren()) {
833 GenBody(at_time, kid, fId, posAccum);
834 }
835 }
836
OnCollision(Body * o1,Body * o2,CollisionContact * c,double relativeVel)837 static bool OnCollision(Body *o1, Body *o2, CollisionContact *c, double relativeVel)
838 {
839 /* XXX: if you create a new class inheriting from Object instead of Body, this code must be updated */
840 if (o1 && !o1->OnCollision(o2, c->geomFlag, relativeVel)) return false;
841 if (o2 && !o2->OnCollision(o1, c->geomFlag, relativeVel)) return false;
842 return true;
843 }
844
hitCallback(CollisionContact * c)845 static void hitCallback(CollisionContact *c)
846 {
847 //Output("OUCH! %x (depth %f)\n", SDL_GetTicks(), c->depth);
848
849 Body *po1 = static_cast<Body *>(c->userData1);
850 Body *po2 = static_cast<Body *>(c->userData2);
851
852 const bool po1_isDynBody = po1->IsType(ObjectType::DYNAMICBODY);
853 const bool po2_isDynBody = po2->IsType(ObjectType::DYNAMICBODY);
854 // collision response
855 assert(po1_isDynBody || po2_isDynBody);
856
857 // Bounce factor
858 const double coeff_rest = 0.35;
859 // Allow stop due to friction
860 const double coeff_slide = 0.700;
861
862 if (po1_isDynBody && po2_isDynBody) {
863 DynamicBody *b1 = static_cast<DynamicBody *>(po1);
864 DynamicBody *b2 = static_cast<DynamicBody *>(po2);
865 const vector3d linVel1 = b1->GetVelocity();
866 const vector3d linVel2 = b2->GetVelocity();
867 const vector3d angVel1 = b1->GetAngVelocity();
868 const vector3d angVel2 = b2->GetAngVelocity();
869
870 const double invMass1 = 1.0 / b1->GetMass();
871 const double invMass2 = 1.0 / b2->GetMass();
872 const vector3d hitPos1 = c->pos - b1->GetPosition();
873 const vector3d hitPos2 = c->pos - b2->GetPosition();
874 const vector3d hitVel1 = linVel1 + angVel1.Cross(hitPos1);
875 const vector3d hitVel2 = linVel2 + angVel2.Cross(hitPos2);
876 const double relVel = (hitVel1 - hitVel2).Dot(c->normal);
877 // moving away so no collision
878 if (relVel > 0) return;
879 if (!OnCollision(po1, po2, c, -relVel)) return;
880 const double invAngInert1 = 1.0 / b1->GetAngularInertia();
881 const double invAngInert2 = 1.0 / b2->GetAngularInertia();
882 const double numerator = -(1.0 + coeff_rest) * relVel;
883 const double term1 = invMass1;
884 const double term2 = invMass2;
885 const double term3 = c->normal.Dot((hitPos1.Cross(c->normal) * invAngInert1).Cross(hitPos1));
886 const double term4 = c->normal.Dot((hitPos2.Cross(c->normal) * invAngInert2).Cross(hitPos2));
887
888 const double j = numerator / (term1 + term2 + term3 + term4);
889 const vector3d force = j * c->normal;
890
891 b1->SetVelocity(linVel1 * (1 - coeff_slide * c->timestep) + force * invMass1);
892 b1->SetAngVelocity(angVel1 + hitPos1.Cross(force) * invAngInert1);
893 b2->SetVelocity(linVel2 * (1 - coeff_slide * c->timestep) - force * invMass2);
894 b2->SetAngVelocity(angVel2 - hitPos2.Cross(force) * invAngInert2);
895 } else {
896 // one body is static
897 vector3d hitNormal;
898 DynamicBody *mover;
899
900 if (po1_isDynBody) {
901 mover = static_cast<DynamicBody *>(po1);
902 hitNormal = c->normal;
903 } else {
904 mover = static_cast<DynamicBody *>(po2);
905 hitNormal = -c->normal;
906 }
907
908 const vector3d linVel1 = mover->GetVelocity();
909 const vector3d angVel1 = mover->GetAngVelocity();
910
911 // step back
912 // mover->UndoTimestep();
913
914 const double invMass1 = 1.0 / mover->GetMass();
915 const vector3d hitPos1 = c->pos - mover->GetPosition();
916 const vector3d hitVel1 = linVel1 + angVel1.Cross(hitPos1);
917 const double relVel = hitVel1.Dot(c->normal);
918 // moving away so no collision
919 if (relVel > 0) return;
920 if (!OnCollision(po1, po2, c, -relVel)) return;
921 const double invAngInert = 1.0 / mover->GetAngularInertia();
922 const double numerator = -(1.0 + coeff_rest) * relVel;
923 const double term1 = invMass1;
924 const double term3 = c->normal.Dot((hitPos1.Cross(c->normal) * invAngInert).Cross(hitPos1));
925
926 const double j = numerator / (term1 + term3);
927 const vector3d force = j * c->normal;
928
929 /*
930 "Linear projection reduces the penetration of two
931 objects by a small percentage, and this is performed
932 after the impulse is applied"
933
934 From:
935 https://gamedevelopment.tutsplus.com/tutorials/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331
936
937 Correction should never be more than c->depth (std::min) and never negative (std::max)
938 NOTE: usually instead of 'c->timestep' you find a 'percent',
939 but here we have a variable timestep and thus the upper limit,
940 which is intended to trigger a collision in the subsequent frame.
941 NOTE2: works (as intendend) at low timestep.
942 Further improvement could be:
943 1) velocity should be projected relative to normal direction,
944 so bouncing and friction may act on the "correct" components
945 (though that a reason for fails and glitches are frames skipped
946 because relVel is quitting before any further calculation)
947 2) with time accel at 10000x you end in space... probably in order
948 for that to works correctly some deeper change should kick in
949 */
950 const float threshold = 0.005;
951
952 vector3d correction = std::min(std::max(c->depth - threshold, 0.0) * c->timestep, c->depth + threshold) * c->normal;
953
954 mover->SetPosition(mover->GetPosition() + correction);
955
956 const float reduction = std::max(1 - coeff_slide * c->timestep, 0.0);
957 vector3d final_vel = linVel1 * reduction + force * invMass1;
958 if (final_vel.LengthSqr() < 0.1) final_vel = vector3d(0.0);
959
960 mover->SetVelocity(final_vel);
961 mover->SetAngVelocity(angVel1 + hitPos1.Cross(force) * invAngInert);
962 }
963 }
964
965 // temporary one-point version
CollideWithTerrain(Body * body,float timeStep)966 static void CollideWithTerrain(Body *body, float timeStep)
967 {
968 PROFILE_SCOPED()
969 if (!body->IsType(ObjectType::DYNAMICBODY))
970 return;
971 DynamicBody *dynBody = static_cast<DynamicBody *>(body);
972 if (!dynBody->IsMoving())
973 return;
974
975 Frame *f = Frame::GetFrame(body->GetFrame());
976 if (!f || !f->GetBody() || f->GetId() != f->GetBody()->GetFrame())
977 return;
978 if (!f->GetBody()->IsType(ObjectType::TERRAINBODY))
979 return;
980 TerrainBody *terrain = static_cast<TerrainBody *>(f->GetBody());
981
982 const Aabb &aabb = dynBody->GetAabb();
983 double altitude = body->GetPosition().Length() + aabb.min.y;
984 if (altitude >= (terrain->GetMaxFeatureRadius() * 2.0))
985 return;
986
987 double terrHeight = terrain->GetTerrainHeight(body->GetPosition().Normalized());
988 if (altitude >= terrHeight)
989 return;
990
991 CollisionContact c(body->GetPosition(), body->GetPosition().Normalized(), terrHeight - altitude, timeStep, static_cast<void *>(body), static_cast<void *>(f->GetBody()));
992 hitCallback(&c);
993 }
994
TimeStep(float step)995 void Space::TimeStep(float step)
996 {
997 PROFILE_SCOPED()
998
999 if (Pi::MustRefreshBackgroundClearFlag())
1000 RefreshBackground();
1001
1002 m_bodyIndexValid = m_sbodyIndexValid = false;
1003
1004 Frame::CollideFrames(&hitCallback);
1005
1006 for (Body *b : m_bodies)
1007 CollideWithTerrain(b, step);
1008
1009 // update frames of reference
1010 for (Body *b : m_bodies)
1011 b->UpdateFrame();
1012
1013 // AI acts here, then move all bodies and frames
1014 for (Body *b : m_bodies)
1015 b->StaticUpdate(step);
1016
1017 Frame::UpdateOrbitRails(m_game->GetTime(), m_game->GetTimeStep());
1018
1019 for (Body *b : m_bodies)
1020 b->TimeStepUpdate(step);
1021
1022 LuaEvent::Emit();
1023 Pi::luaTimer->Tick();
1024
1025 UpdateBodies();
1026
1027 m_bodyNearFinder.Prepare();
1028 }
1029
UpdateBodies()1030 void Space::UpdateBodies()
1031 {
1032 PROFILE_SCOPED()
1033 #ifndef NDEBUG
1034 m_processingFinalizationQueue = true;
1035 #endif
1036
1037 // removing or deleting bodies from space
1038 for (const auto &b : m_assignedBodies) {
1039 auto remove_iterator = m_bodies.end();
1040 for (auto it = m_bodies.begin(); it != m_bodies.end(); ++it) {
1041 if (*it != b.first)
1042 (*it)->NotifyRemoved(b.first);
1043 else
1044 remove_iterator = it;
1045 }
1046 if (remove_iterator != m_bodies.end()) {
1047 *remove_iterator = m_bodies.back();
1048 m_bodies.pop_back();
1049 if (b.second == BodyAssignation::KILL)
1050 delete b.first;
1051 else
1052 b.first->SetFrame(FrameId::Invalid);
1053 }
1054 }
1055
1056 m_assignedBodies.clear();
1057
1058 #ifndef NDEBUG
1059 m_processingFinalizationQueue = false;
1060 #endif
1061 }
1062
1063 static char space[256];
1064
DebugDumpFrame(FrameId fId,bool details,unsigned int indent)1065 static void DebugDumpFrame(FrameId fId, bool details, unsigned int indent)
1066 {
1067 Frame *f = Frame::GetFrame(fId);
1068 Frame *parent = Frame::GetFrame(f->GetParent());
1069
1070 Output("%.*s%2i) %p (%s)%s\n", indent, space, static_cast<int>(fId), static_cast<void *>(f), f->GetLabel().c_str(), f->IsRotFrame() ? " [rotating]" : " [non rotating]");
1071 if (f->GetParent().valid())
1072 Output("%.*s parent %p (%s)\n", indent + 3, space, static_cast<void *>(parent), parent->GetLabel().c_str());
1073 if (f->GetBody())
1074 Output("%.*s body %p (%s)\n", indent + 3, space, static_cast<void *>(f->GetBody()), f->GetBody()->GetLabel().c_str());
1075 if (Body *b = f->GetBody())
1076 Output("%.*s bodyFor %p (%s)\n", indent + 3, space, static_cast<void *>(b), b->GetLabel().c_str());
1077 Output("%.*s distance: %f radius: %f children: %u\n", indent + 3, space, f->GetPosition().Length(), f->GetRadius(), f->GetNumChildren());
1078
1079 for (FrameId kid : f->GetChildren())
1080 DebugDumpFrame(kid, details, indent + 2);
1081 }
1082
DebugDumpFrames(bool details)1083 void Space::DebugDumpFrames(bool details)
1084 {
1085 memset(space, ' ', sizeof(space));
1086
1087 if (m_starSystem)
1088 Output("Frame structure for '%s':\n", m_starSystem->GetName().c_str());
1089 else
1090 Output("Frame structure while in hyperspace:\n");
1091 DebugDumpFrame(m_rootFrameId, details, 3);
1092 }
1093