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