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 "SpaceStationType.h"
5 #include "FileSystem.h"
6 #include "Json.h"
7 #include "MathUtil.h"
8 #include "Pi.h"
9 #include "Ship.h"
10 #include "StringF.h"
11 #include "scenegraph/MatrixTransform.h"
12 #include "scenegraph/Model.h"
13
14 #include <algorithm>
15
16 // TODO: Fix the horrible control flow that makes this exception type necessary.
17 struct StationTypeLoadError {};
18
19 std::vector<SpaceStationType> SpaceStationType::surfaceTypes;
20 std::vector<SpaceStationType> SpaceStationType::orbitalTypes;
21
SpaceStationType(const std::string & id_,const std::string & path_)22 SpaceStationType::SpaceStationType(const std::string &id_, const std::string &path_) :
23 id(id_),
24 model(0),
25 modelName(""),
26 angVel(0.f),
27 dockMethod(SURFACE),
28 numDockingPorts(0),
29 numDockingStages(0),
30 numUndockStages(0),
31 shipLaunchStage(3),
32 parkingDistance(0),
33 parkingGapSize(0)
34 {
35 Json data = JsonUtils::LoadJsonDataFile(path_);
36 if (data.is_null()) {
37 Output("couldn't read station def '%s'\n", path_.c_str());
38 throw StationTypeLoadError();
39 }
40
41 modelName = data.value("model", "");
42
43 const std::string type = data.value("type", "");
44 if (type == "surface")
45 dockMethod = SURFACE;
46 else if (type == "orbital")
47 dockMethod = ORBITAL;
48 else {
49 Output("couldn't parse station def '%s': unknown type '%s'\n", path_.c_str(), type.c_str());
50 throw StationTypeLoadError();
51 }
52
53 angVel = data.value("angular_velocity", 0.0f);
54
55 parkingDistance = data.value("parking_distance", 0.0f);
56 parkingGapSize = data.value("parking_gap_size", 0.0f);
57
58 padOffset = data.value("pad_offset", 150.f);
59
60 model = Pi::FindModel(modelName, /* allowPlaceholder = */ false);
61 if (!model) {
62 Output("couldn't initialize station type '%s' because the corresponding model ('%s') could not be found.\n", path_.c_str(), modelName.c_str());
63 throw StationTypeLoadError();
64 }
65 OnSetupComplete();
66 }
67
OnSetupComplete()68 void SpaceStationType::OnSetupComplete()
69 {
70 // Since the model contains (almost) all of the docking information we have to extract that
71 // and then generate any additional locators and information the station will need from it.
72
73 // First we gather the MatrixTransforms that contain the location and orientation of the docking
74 // locators/waypoints. We store some information within the name of these which needs parsing too.
75
76 // Next we build the additional information required for docking ships with SPACE stations
77 // on autopilot - this is the only option for docking with SPACE stations currently.
78 // This mostly means offsetting from one locator to create the next in the sequence.
79
80 // ground stations have a "special-fucking-case" 0 stage launch process
81 shipLaunchStage = ((SURFACE == dockMethod) ? 0 : 3);
82
83 // gather the tags
84 SceneGraph::Model::TVecMT entrance_mts;
85 SceneGraph::Model::TVecMT locator_mts;
86 SceneGraph::Model::TVecMT exit_mts;
87 model->FindTagsByStartOfName("entrance_", entrance_mts);
88 model->FindTagsByStartOfName("loc_", locator_mts);
89 model->FindTagsByStartOfName("exit_", exit_mts);
90
91 Output("%s has:\n %lu entrances,\n %lu pads,\n %lu exits\n", modelName.c_str(), entrance_mts.size(), locator_mts.size(), exit_mts.size());
92
93 // Add the partially initialised ports
94 for (auto apprIter : entrance_mts) {
95 int portId;
96 PiVerify(1 == sscanf(apprIter->GetName().c_str(), "entrance_port%d", &portId));
97 PiVerify(portId > 0);
98
99 SPort new_port;
100 new_port.portId = portId;
101 new_port.name = apprIter->GetName();
102 if (SURFACE == dockMethod) {
103 const vector3f offDir = apprIter->GetTransform().Up().Normalized();
104 new_port.m_approach[1] = apprIter->GetTransform();
105 new_port.m_approach[1].SetTranslate(apprIter->GetTransform().GetTranslate() + (offDir * 500.0f));
106 } else {
107 const vector3f offDir = -apprIter->GetTransform().Back().Normalized();
108 new_port.m_approach[1] = apprIter->GetTransform();
109 new_port.m_approach[1].SetTranslate(apprIter->GetTransform().GetTranslate() + (offDir * 1500.0f));
110 }
111 new_port.m_approach[2] = apprIter->GetTransform();
112 m_ports.push_back(new_port);
113 }
114
115 int bay = 0;
116 for (auto locIter : locator_mts) {
117 int bayStr, portId;
118 int minSize, maxSize;
119 char padname[8];
120 const matrix4x4f &locTransform = locIter->GetTransform();
121
122 ++bay;
123
124 // eg:loc_A001_p01_s0_500_b01
125 PiVerify(5 == sscanf(locIter->GetName().c_str(), "loc_%4s_p%d_s%d_%d_b%d", &padname[0], &portId, &minSize, &maxSize, &bayStr));
126 PiVerify(bay > 0 && portId > 0);
127
128 // find the port and setup the rest of it's information
129 #ifndef NDEBUG
130 bool bFoundPort = false;
131 #endif
132 matrix4x4f approach1(0.0);
133 matrix4x4f approach2(0.0);
134 for (auto &rPort : m_ports) {
135 if (rPort.portId == portId) {
136 rPort.minShipSize = std::min(minSize, rPort.minShipSize);
137 rPort.maxShipSize = std::max(maxSize, rPort.maxShipSize);
138 rPort.bayIDs.push_back(std::make_pair(bay - 1, padname));
139 #ifndef NDEBUG
140 bFoundPort = true;
141 #endif
142 approach1 = rPort.m_approach[1];
143 approach2 = rPort.m_approach[2];
144 break;
145 }
146 }
147 assert(bFoundPort);
148
149 // now build the docking/leaving waypoints
150 if (SURFACE == dockMethod) {
151 // ground stations don't have leaving waypoints.
152 m_portPaths[bay].m_docking[2] = locTransform; // final (docked)
153 numDockingStages = 2;
154 numUndockStages = 1;
155 } else {
156 struct TPointLine {
157 // for reference: http://paulbourke.net/geometry/pointlineplane/
158 static bool ClosestPointOnLine(const vector3f &Point, const vector3f &LineStart, const vector3f &LineEnd, vector3f &Intersection)
159 {
160 const float LineMag = (LineStart - LineEnd).Length();
161
162 const float U = (((Point.x - LineStart.x) * (LineEnd.x - LineStart.x)) +
163 ((Point.y - LineStart.y) * (LineEnd.y - LineStart.y)) +
164 ((Point.z - LineStart.z) * (LineEnd.z - LineStart.z))) /
165 (LineMag * LineMag);
166
167 if (U < 0.0f || U > 1.0f)
168 return false; // closest point does not fall within the line segment
169
170 Intersection.x = LineStart.x + U * (LineEnd.x - LineStart.x);
171 Intersection.y = LineStart.y + U * (LineEnd.y - LineStart.y);
172 Intersection.z = LineStart.z + U * (LineEnd.z - LineStart.z);
173
174 return true;
175 }
176 };
177
178 // create the docking locators
179 // start
180 m_portPaths[bay].m_docking[2] = approach2;
181 m_portPaths[bay].m_docking[2].SetRotationOnly(locTransform.GetOrient());
182 // above the pad
183 vector3f intersectionPos(0.0f);
184 const vector3f approach1Pos = approach1.GetTranslate();
185 const vector3f approach2Pos = approach2.GetTranslate();
186 {
187 const vector3f p0 = locTransform.GetTranslate(); // plane position
188 const vector3f l = (approach2Pos - approach1Pos).Normalized(); // ray direction
189 const vector3f l0 = approach1Pos + (l * 10000.0f);
190
191 if (!TPointLine::ClosestPointOnLine(p0, approach1Pos, l0, intersectionPos)) {
192 Output("No point found on line segment");
193 }
194 }
195 m_portPaths[bay].m_docking[3] = locTransform;
196 m_portPaths[bay].m_docking[3].SetTranslate(intersectionPos);
197 // final (docked)
198 m_portPaths[bay].m_docking[4] = locTransform;
199 numDockingStages = 4;
200
201 // leaving locators ...
202 matrix4x4f orient = locTransform.GetOrient(), EndOrient;
203 if (exit_mts.empty()) {
204 // leaving locators need to face in the opposite direction
205 const matrix4x4f rot = matrix3x3f::Rotate(DEG2RAD(180.0f), orient.Back());
206 orient = orient * rot;
207 orient.SetTranslate(locTransform.GetTranslate());
208 EndOrient = approach2;
209 EndOrient.SetRotationOnly(orient);
210 } else {
211 // leaving locators, use whatever orientation they have
212 orient.SetTranslate(locTransform.GetTranslate());
213 int exitport = 0;
214 for (auto &exitIt : exit_mts) {
215 PiVerify(1 == sscanf(exitIt->GetName().c_str(), "exit_port%d", &exitport));
216 if (exitport == portId) {
217 EndOrient = exitIt->GetTransform();
218 break;
219 }
220 }
221 if (exitport == 0) {
222 EndOrient = approach2;
223 }
224 }
225
226 // create the leaving locators
227 m_portPaths[bay].m_leaving[1] = locTransform; // start - maintain the same orientation and position as when docked.
228 m_portPaths[bay].m_leaving[2] = orient; // above the pad - reorient...
229 m_portPaths[bay].m_leaving[2].SetTranslate(intersectionPos); // ...and translate to new position
230 m_portPaths[bay].m_leaving[3] = EndOrient; // end (on manual after here)
231 numUndockStages = 3;
232 }
233 }
234
235 numDockingPorts = m_portPaths.size();
236
237 // sanity
238 assert(!m_portPaths.empty());
239 assert(numDockingStages > 0);
240 assert(numUndockStages > 0);
241
242 // insanity
243 for (PortPathMap::const_iterator pIt = m_portPaths.begin(), pItEnd = m_portPaths.end(); pIt != pItEnd; ++pIt) {
244 if (Uint32(numDockingStages - 1) < pIt->second.m_docking.size()) {
245 Error(
246 "(%s): numDockingStages (%d) vs number of docking stages (" SIZET_FMT ")\n"
247 "Must have at least the same number of entries as the number of docking stages "
248 "PLUS the docking timeout at the start of the array.",
249 modelName.c_str(), (numDockingStages - 1), pIt->second.m_docking.size());
250
251 } else if (Uint32(numDockingStages - 1) != pIt->second.m_docking.size()) {
252 Warning(
253 "(%s): numDockingStages (%d) vs number of docking stages (" SIZET_FMT ")\n",
254 modelName.c_str(), (numDockingStages - 1), pIt->second.m_docking.size());
255 }
256
257 if (0 != pIt->second.m_leaving.size() && Uint32(numUndockStages) < pIt->second.m_leaving.size()) {
258 Error(
259 "(%s): numUndockStages (%d) vs number of leaving stages (" SIZET_FMT ")\n"
260 "Must have at least the same number of entries as the number of leaving stages.",
261 modelName.c_str(), (numDockingStages - 1), pIt->second.m_docking.size());
262
263 } else if (0 != pIt->second.m_leaving.size() && Uint32(numUndockStages) != pIt->second.m_leaving.size()) {
264 Warning(
265 "(%s): numUndockStages (%d) vs number of leaving stages (" SIZET_FMT ")\n",
266 modelName.c_str(), numUndockStages, pIt->second.m_leaving.size());
267 }
268 }
269 }
270
FindPortByBay(const int zeroBaseBayID) const271 const SpaceStationType::SPort *SpaceStationType::FindPortByBay(const int zeroBaseBayID) const
272 {
273 for (TPorts::const_iterator bayIter = m_ports.begin(), grpEnd = m_ports.end(); bayIter != grpEnd; ++bayIter) {
274 for (auto idIter : (*bayIter).bayIDs) {
275 if (idIter.first == zeroBaseBayID) {
276 return &(*bayIter);
277 }
278 }
279 }
280 // is it safer to return that the bay is locked?
281 return 0;
282 }
283
GetPortByBay(const int zeroBaseBayID)284 SpaceStationType::SPort *SpaceStationType::GetPortByBay(const int zeroBaseBayID)
285 {
286 for (TPorts::iterator bayIter = m_ports.begin(), grpEnd = m_ports.end(); bayIter != grpEnd; ++bayIter) {
287 for (auto idIter : (*bayIter).bayIDs) {
288 if (idIter.first == zeroBaseBayID) {
289 return &(*bayIter);
290 }
291 }
292 }
293 // is it safer to return that the bay is locked?
294 return 0;
295 }
296
GetShipApproachWaypoints(const unsigned int port,const int stage,positionOrient_t & outPosOrient) const297 bool SpaceStationType::GetShipApproachWaypoints(const unsigned int port, const int stage, positionOrient_t &outPosOrient) const
298 {
299 bool gotOrient = false;
300
301 const SPort *pPort = FindPortByBay(port);
302 if (pPort && stage > 0) {
303 TMapBayIDMat::const_iterator stageDataIt = pPort->m_approach.find(stage);
304 if (stageDataIt != pPort->m_approach.end()) {
305 const matrix4x4f &mt = pPort->m_approach.at(stage);
306 outPosOrient.pos = vector3d(mt.GetTranslate());
307 outPosOrient.xaxis = vector3d(mt.GetOrient().VectorX());
308 outPosOrient.yaxis = vector3d(mt.GetOrient().VectorY());
309 outPosOrient.zaxis = vector3d(mt.GetOrient().VectorZ());
310 outPosOrient.xaxis = outPosOrient.xaxis.Normalized();
311 outPosOrient.yaxis = outPosOrient.yaxis.Normalized();
312 outPosOrient.zaxis = outPosOrient.zaxis.Normalized();
313 gotOrient = true;
314 }
315 }
316 return gotOrient;
317 }
318
GetDockAnimStageDuration(const int stage) const319 double SpaceStationType::GetDockAnimStageDuration(const int stage) const
320 {
321 return (stage == 0) ? 300.0 : ((SURFACE == dockMethod) ? 0.0 : 3.0);
322 }
323
GetUndockAnimStageDuration(const int stage) const324 double SpaceStationType::GetUndockAnimStageDuration(const int stage) const
325 {
326 return ((SURFACE == dockMethod) ? 0.0 : 5.0);
327 }
328
GetPosOrient(const SpaceStationType::TMapBayIDMat & bayMap,const int stage,const double t,const vector3d & from,SpaceStationType::positionOrient_t & outPosOrient)329 static bool GetPosOrient(const SpaceStationType::TMapBayIDMat &bayMap, const int stage, const double t, const vector3d &from,
330 SpaceStationType::positionOrient_t &outPosOrient)
331 {
332 bool gotOrient = false;
333
334 vector3d toPos;
335
336 const SpaceStationType::TMapBayIDMat::const_iterator stageDataIt = bayMap.find(stage);
337 const bool bHasStageData = (stageDataIt != bayMap.end());
338 assert(bHasStageData);
339 if (bHasStageData) {
340 const matrix4x4f &mt = stageDataIt->second;
341 outPosOrient.xaxis = vector3d(mt.GetOrient().VectorX()).Normalized();
342 outPosOrient.yaxis = vector3d(mt.GetOrient().VectorY()).Normalized();
343 outPosOrient.zaxis = vector3d(mt.GetOrient().VectorZ()).Normalized();
344 toPos = vector3d(mt.GetTranslate());
345 gotOrient = true;
346 }
347
348 if (gotOrient) {
349 vector3d pos = MathUtil::mix<vector3d, double>(from, toPos, t);
350 outPosOrient.pos = pos;
351 }
352
353 return gotOrient;
354 }
355
356 /* when ship is on rails it returns true and fills outPosOrient.
357 * when ship has been released (or docked) it returns false.
358 * Note station animations may continue for any number of stages after
359 * ship has been released and is under player control again */
GetDockAnimPositionOrient(const unsigned int port,int stage,double t,const vector3d & from,positionOrient_t & outPosOrient,const Ship * ship) const360 bool SpaceStationType::GetDockAnimPositionOrient(const unsigned int port, int stage, double t, const vector3d &from, positionOrient_t &outPosOrient, const Ship *ship) const
361 {
362 assert(ship);
363 if (stage < -shipLaunchStage) {
364 stage = -shipLaunchStage;
365 t = 1.0;
366 }
367 if (stage > numDockingStages || !stage) {
368 stage = numDockingStages;
369 t = 1.0;
370 }
371 // note case for stageless launch (shipLaunchStage==0)
372
373 bool gotOrient = false;
374
375 assert(port <= m_portPaths.size());
376 const PortPath &rPortPath = m_portPaths.at(port + 1);
377 if (stage < 0) {
378 const int leavingStage = (-1 * stage);
379 gotOrient = GetPosOrient(rPortPath.m_leaving, leavingStage, t, from, outPosOrient);
380 const vector3d up = outPosOrient.yaxis.Normalized() * ship->GetLandingPosOffset();
381 outPosOrient.pos = outPosOrient.pos - up;
382 } else if (stage > 0) {
383 gotOrient = GetPosOrient(rPortPath.m_docking, stage, t, from, outPosOrient);
384 const vector3d up = outPosOrient.yaxis.Normalized() * ship->GetLandingPosOffset();
385 outPosOrient.pos = outPosOrient.pos - up;
386 }
387
388 return gotOrient;
389 }
390
391 /*static*/
Init()392 void SpaceStationType::Init()
393 {
394 PROFILE_SCOPED()
395 static bool isInitted = false;
396 if (isInitted)
397 return;
398 isInitted = true;
399
400 // load all station definitions
401 namespace fs = FileSystem;
402 for (fs::FileEnumerator files(fs::gameDataFiles, "stations", 0); !files.Finished(); files.Next()) {
403 const fs::FileInfo &info = files.Current();
404 if (ends_with_ci(info.GetPath(), ".json")) {
405 const std::string id(info.GetName().substr(0, info.GetName().size() - 5));
406 try {
407 SpaceStationType st = SpaceStationType(id, info.GetPath());
408 switch (st.dockMethod) {
409 case SURFACE: surfaceTypes.push_back(st); break;
410 case ORBITAL: orbitalTypes.push_back(st); break;
411 }
412 } catch (StationTypeLoadError) {
413 // TODO: Actual error handling would be nice.
414 Error("Error while loading Space Station data (check stdout/output.txt).\n");
415 }
416 }
417 }
418 }
419
420 /*static*/
RandomStationType(Random & random,const bool bIsGround)421 const SpaceStationType *SpaceStationType::RandomStationType(Random &random, const bool bIsGround)
422 {
423 if (bIsGround) {
424 return &surfaceTypes[random.Int32(SpaceStationType::surfaceTypes.size())];
425 }
426
427 return &orbitalTypes[random.Int32(SpaceStationType::orbitalTypes.size())];
428 }
429
430 /*static*/
FindByName(const std::string & name)431 const SpaceStationType *SpaceStationType::FindByName(const std::string &name)
432 {
433 for (auto &sst : surfaceTypes)
434 if (sst.id == name)
435 return &sst;
436 for (auto &sst : orbitalTypes)
437 if (sst.id == name)
438 return &sst;
439 return nullptr;
440 }
441