1 /**
2  * @file
3  * @brief Geoscape/Map management
4  */
5 
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 */
24 
25 #include "../../cl_shared.h"
26 #include "../../ui/ui_dataids.h"
27 #include "../../ui/node/ui_node_geoscape.h"
28 #include "cp_overlay.h"
29 #include "cp_campaign.h"
30 #include "cp_popup.h"
31 #include "cp_mapfightequip.h"
32 #include "cp_geoscape.h"
33 #include "cp_missions.h"
34 #include "cp_ufo.h"
35 #include "cp_time.h"
36 #include "cp_xvi.h"
37 
38 static uiNode_t* geoscapeNode;
39 
40 #ifdef DEBUG
41 static cvar_t* debug_showInterest;
42 #endif
43 
44 #define GLOBE_ROTATE -90
45 #define ZOOM_LIMIT	2.5f
46 
47 /*
48 ==============================================================
49 MULTI SELECTION DEFINITION
50 ==============================================================
51 */
52 
53 #define MULTISELECT_MAXSELECT 6	/**< Maximal count of elements that can be selected at once */
54 
55 /**
56  * @brief Types of elements that can be selected on the map
57  */
58 typedef enum {
59 	MULTISELECT_TYPE_BASE,
60 	MULTISELECT_TYPE_INSTALLATION,
61 	MULTISELECT_TYPE_MISSION,
62 	MULTISELECT_TYPE_AIRCRAFT,
63 	MULTISELECT_TYPE_UFO,
64 	MULTISELECT_TYPE_NONE
65 } multiSelectType_t;
66 
67 
68 /**
69  * @brief Structure to manage the multi selection
70  */
71 typedef struct multiSelect_s {
72 	int nbSelect;						/**< Count of currently selected elements */
73 	multiSelectType_t selectType[MULTISELECT_MAXSELECT];	/**< Type of currently selected elements */
74 	int selectId[MULTISELECT_MAXSELECT];	/**< Identifier of currently selected element */
75 	char popupText[2048];				/**< Text to display in popup_multi_selection menu */
76 } multiSelect_t;
77 
78 static multiSelect_t multiSelect;	/**< Data to manage the multi selection */
79 
80 
81 /*
82 ==============================================================
83 STATIC DEFINITION
84 ==============================================================
85 */
86 
87 /* Functions */
88 static bool GEO_IsPositionSelected(const uiNode_t* node, const vec2_t pos, int x, int y);
89 
90 /* static variables */
91 static char textStandard[2048];		/**< Buffer to display standard text on the geoscape */
92 static int centerOnEventIdx;		/**< Current Event centered on 3D geoscape */
93 
94 /* Colors */
95 static const vec4_t green = {0.0f, 1.0f, 0.0f, 0.8f};
96 static const vec4_t yellow = {1.0f, 0.874f, 0.294f, 1.0f};
97 static const vec4_t red = {1.0f, 0.0f, 0.0f, 0.8f};
98 
99 static const float defaultBaseAngle = -90.0f;	/**< Default angle value for 3D models like bases */
100 
101 static byte* terrainPic;				/**< this is the terrain mask for separating the climate
102 										 * zone and water by different color values */
103 static int terrainWidth, terrainHeight;		/**< the width and height for the terrain pic. */
104 
105 static byte* culturePic;				/**< this is the mask for separating the culture
106 										 * zone and water by different color values */
107 static int cultureWidth, cultureHeight;		/**< the width and height for the culture pic. */
108 
109 static byte* populationPic;				/**< this is the mask for separating the population rate
110 										 * zone and water by different color values */
111 static int populationWidth, populationHeight;		/**< the width and height for the population pic. */
112 
113 static byte* nationsPic;				/**< this is the nation mask - separated
114 										 * by colors given in nations.ufo. */
115 static int nationsWidth, nationsHeight;	/**< the width and height for the nation pic. */
116 
117 
118 /*
119 ==============================================================
120 CLICK ON MAP and MULTI SELECTION FUNCTIONS
121 ==============================================================
122 */
123 
124 /**
125  * @brief Add an element in the multiselection list
126  */
GEO_MultiSelectListAddItem(multiSelectType_t itemType,int itemID,const char * itemDescription,const char * itemName)127 static void GEO_MultiSelectListAddItem (multiSelectType_t itemType, int itemID,
128 	const char* itemDescription, const char* itemName)
129 {
130 	Q_strcat(multiSelect.popupText, sizeof(multiSelect.popupText), "%s\t%s\n", itemDescription, itemName);
131 	multiSelect.selectType[multiSelect.nbSelect] = itemType;
132 	multiSelect.selectId[multiSelect.nbSelect] = itemID;
133 	multiSelect.nbSelect++;
134 }
135 
136 /**
137  * @brief Execute action for 1 element of the multi selection
138  * Param cgi->Cmd_Argv(1) is the element selected in the popup_multi_selection menu
139  */
GEO_MultiSelectExecuteAction_f(void)140 static void GEO_MultiSelectExecuteAction_f (void)
141 {
142 	int selected, id;
143 	aircraft_t* aircraft;
144 	bool multiSelection = false;
145 
146 	if (cgi->Cmd_Argc() < 2) {
147 		/* Direct call from code, not from a popup menu */
148 		selected = 0;
149 	} else {
150 		/* Call from a geoscape popup menu (popup_multi_selection) */
151 		cgi->UI_PopWindow(false);
152 		selected = atoi(cgi->Cmd_Argv(1));
153 		multiSelection = true;
154 	}
155 
156 	if (selected < 0 || selected >= multiSelect.nbSelect)
157 		return;
158 	id = multiSelect.selectId[selected];
159 
160 	/* Execute action on element */
161 	switch (multiSelect.selectType[selected]) {
162 	case MULTISELECT_TYPE_BASE:	/* Select a base */
163 		if (id >= B_GetCount())
164 			break;
165 		GEO_ResetAction();
166 		B_SelectBase(B_GetFoundedBaseByIDX(id));
167 		break;
168 	case MULTISELECT_TYPE_INSTALLATION: {
169 		/* Select an installation */
170 		installation_t* ins = INS_GetByIDX(id);
171 		if (ins) {
172 			GEO_ResetAction();
173 			INS_SelectInstallation(ins);
174 		}
175 		break;
176 	}
177 	case MULTISELECT_TYPE_MISSION: {
178 		mission_t* mission = GEO_GetSelectedMission();
179 		/* Select a mission */
180 		if (ccs.mapAction == MA_INTERCEPT && mission && mission == MIS_GetByIdx(id)) {
181 			CL_DisplayPopupInterceptMission(mission);
182 			return;
183 		}
184 		mission = GEO_SelectMission(MIS_GetByIdx(id));
185 		if (multiSelection) {
186 			/* if we come from a multiSelection menu, there is no need to open this popup twice to go to a mission */
187 			CL_DisplayPopupInterceptMission(mission);
188 			return;
189 		}
190 		break;
191 	}
192 	case MULTISELECT_TYPE_AIRCRAFT: /* Selection of an aircraft */
193 		aircraft = AIR_AircraftGetFromIDX(id);
194 		if (aircraft == nullptr) {
195 			Com_DPrintf(DEBUG_CLIENT, "GEO_MultiSelectExecuteAction: selection of an unknown aircraft idx %i\n", id);
196 			return;
197 		}
198 
199 		if (GEO_IsAircraftSelected(aircraft)) {
200 			/* Selection of an already selected aircraft */
201 			CL_DisplayPopupAircraft(aircraft);	/* Display popup_aircraft */
202 		} else {
203 			/* Selection of an unselected aircraft */
204 			GEO_SelectAircraft(aircraft);
205 			if (multiSelection)
206 				/* if we come from a multiSelection menu, there is no need to open this popup twice to choose an action */
207 				CL_DisplayPopupAircraft(aircraft);
208 		}
209 		break;
210 	case MULTISELECT_TYPE_UFO : /* Selection of a UFO */
211 		/* Get the ufo selected */
212 		if (id < 0 || id >= ccs.numUFOs)
213 			return;
214 		aircraft = UFO_GetByIDX(id);
215 
216 		if (GEO_IsUFOSelected(aircraft)) {
217 			/* Selection of a already selected ufo */
218 			CL_DisplayPopupInterceptUFO(aircraft);
219 		} else {
220 			/* Selection of a unselected ufo */
221 			GEO_SelectUFO(aircraft);
222 			if (multiSelection)
223 				/* if we come from a multiSelection menu, there is no need to open this popup twice to choose an action */
224 				CL_DisplayPopupInterceptUFO(GEO_GetSelectedUFO());
225 		}
226 		break;
227 	case MULTISELECT_TYPE_NONE :	/* Selection of an element that has been removed */
228 		break;
229 	default:
230 		Com_DPrintf(DEBUG_CLIENT, "GEO_MultiSelectExecuteAction: selection of an unknown element type %i\n",
231 				multiSelect.selectType[selected]);
232 		break;
233 	}
234 }
235 
GEO_IsRadarOverlayActivated(void)236 bool GEO_IsRadarOverlayActivated (void)
237 {
238 	return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_RADAR;
239 }
240 
GEO_IsNationOverlayActivated(void)241 static inline bool GEO_IsNationOverlayActivated (void)
242 {
243 	return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_NATION;
244 }
245 
GEO_IsXVIOverlayActivated(void)246 static inline bool GEO_IsXVIOverlayActivated (void)
247 {
248 	return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_XVI;
249 }
250 
251 /**
252  * @brief Click on the map/geoscape
253  * @param[in] node UI Node of the geoscape map
254  * @param[in] x Mouse click X coordinate
255  * @param[in] y Mouse click Y coordinate
256  * @param[in] pos Geoscape (longitude, latitude) coordinate of the click
257  * @return True if the event is used for something
258  */
GEO_Click(const uiNode_t * node,int x,int y,const vec2_t pos)259 bool GEO_Click (const uiNode_t* node, int x, int y, const vec2_t pos)
260 {
261 	aircraft_t* ufo;
262 	base_t* base;
263 
264 	switch (ccs.mapAction) {
265 	case MA_NEWBASE:
266 		/* new base construction */
267 		/** @todo make this a function in cp_base.c - B_BuildBaseAtPos */
268 		if (!MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr))) {
269 			if (B_GetCount() < MAX_BASES) {
270 				Vector2Copy(pos, ccs.newBasePos);
271 				CP_GameTimeStop();
272 				cgi->Cmd_ExecuteString("mn_set_base_title");
273 				cgi->UI_PushWindow("popup_newbase");
274 				return true;
275 			}
276 			return false;
277 		}
278 		break;
279 	case MA_NEWINSTALLATION:
280 		if (!MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr))) {
281 			Vector2Copy(pos, ccs.newBasePos);
282 			CP_GameTimeStop();
283 			cgi->UI_PushWindow("popup_newinstallation");
284 			return true;
285 		}
286 		break;
287 	case MA_UFORADAR:
288 		cgi->UI_PushWindow("popup_intercept_ufo");
289 		break;
290 	default:
291 		break;
292 	}
293 
294 	/* Init data for multi selection */
295 	multiSelect.nbSelect = 0;
296 	OBJZERO(multiSelect.popupText);
297 	/* Get selected missions */
298 	MIS_Foreach(tempMission) {
299 		if (multiSelect.nbSelect >= MULTISELECT_MAXSELECT)
300 			break;
301 		if (tempMission->stage == STAGE_NOT_ACTIVE || !tempMission->onGeoscape)
302 			continue;
303 		if (tempMission->pos && GEO_IsPositionSelected(node, tempMission->pos, x, y))
304 			GEO_MultiSelectListAddItem(MULTISELECT_TYPE_MISSION, MIS_GetIdx(tempMission), _("Mission"), MIS_GetName(tempMission));
305 	}
306 
307 	/* Get selected aircraft which belong */
308 	AIR_Foreach(aircraft) {
309 		if (AIR_IsAircraftOnGeoscape(aircraft) && aircraft->fuel > 0 && GEO_IsPositionSelected(node, aircraft->pos, x, y))
310 			GEO_MultiSelectListAddItem(MULTISELECT_TYPE_AIRCRAFT, aircraft->idx, _("Aircraft"), aircraft->name);
311 	}
312 
313 	/* Get selected bases */
314 	base = nullptr;
315 	while ((base = B_GetNext(base)) != nullptr && multiSelect.nbSelect < MULTISELECT_MAXSELECT) {
316 		if (GEO_IsPositionSelected(node, base->pos, x, y))
317 			GEO_MultiSelectListAddItem(MULTISELECT_TYPE_BASE, base->idx, _("Base"), base->name);
318 	}
319 
320 	/* Get selected installations */
321 	INS_Foreach(installation) {
322 		if (multiSelect.nbSelect >= MULTISELECT_MAXSELECT)
323 			break;
324 		if (GEO_IsPositionSelected(node, installation->pos, x, y))
325 			GEO_MultiSelectListAddItem(MULTISELECT_TYPE_INSTALLATION, installation->idx, _("Installation"), installation->name);
326 	}
327 
328 	/* Get selected ufos */
329 	ufo = nullptr;
330 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr)
331 		if (AIR_IsAircraftOnGeoscape(ufo) && GEO_IsPositionSelected(node, ufo->pos, x, y))
332 			GEO_MultiSelectListAddItem(MULTISELECT_TYPE_UFO, UFO_GetGeoscapeIDX(ufo), _("UFO Sighting"), UFO_GetName(ufo));
333 
334 	if (multiSelect.nbSelect == 1) {
335 		/* Execute directly action for the only one element selected */
336 		cgi->Cmd_ExecuteString("multi_select_click");
337 		return true;
338 	} else if (multiSelect.nbSelect > 1) {
339 		/* Display popup for multi selection */
340 		cgi->UI_RegisterText(TEXT_MULTISELECTION, multiSelect.popupText);
341 		CP_GameTimeStop();
342 		cgi->UI_PushWindow("popup_multi_selection");
343 		return true;
344 	} else {
345 		aircraft_t* aircraft = GEO_GetSelectedAircraft();
346 		/* Nothing selected */
347 		if (!aircraft) {
348 			GEO_ResetAction();
349 			return false;
350 		}
351 
352 		if (AIR_IsAircraftOnGeoscape(aircraft) && AIR_AircraftHasEnoughFuel(aircraft, pos)) {
353 			/* Move the selected aircraft to the position clicked */
354 			GEO_CalcLine(aircraft->pos, pos, &aircraft->route);
355 			aircraft->status = AIR_TRANSIT;
356 			aircraft->aircraftTarget = nullptr;
357 			aircraft->time = 0;
358 			aircraft->point = 0;
359 			return true;
360 		}
361 	}
362 	return false;
363 }
364 
365 
366 /*
367 ==============================================================
368 GEOSCAPE DRAWING AND COORDINATES
369 ==============================================================
370 */
371 
372 /**
373  * @brief Transform a 2D position on the map to screen coordinates.
374  * @param[in] node Menu node
375  * @param[in] pos vector that holds longitude and latitude
376  * @param[out] x normalized (rotated and scaled) x value of mouseclick
377  * @param[out] y normalized (rotated and scaled) y value of mouseclick
378  * @param[out] z z value of the given latitude and longitude - might also be nullptr if not needed
379  * @return true if the point is visible, false else (if it's outside the node or on the wrong side of the earth).
380  */
GEO_3DMapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y,int * z)381 static bool GEO_3DMapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y, int* z)
382 {
383 	vec2_t mid;
384 	vec3_t v, v1, rotationAxis;
385 	const float radius = GLOBE_RADIUS;
386 
387 	PolarToVec(pos, v);
388 
389 	/* rotate the vector to switch of reference frame.
390 	 * We switch from the static frame of the earth to the local frame of the player */
391 	VectorSet(rotationAxis, 0, 0, 1);
392 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
393 	RotatePointAroundVector(v1, rotationAxis, v, - data.angles[PITCH]);
394 
395 	VectorSet(rotationAxis, 0, 1, 0);
396 	RotatePointAroundVector(v, rotationAxis, v1, - data.angles[YAW]);
397 
398 	/* set mid to the coordinates of the center of the globe */
399 	Vector2Set(mid, data.mapPos[0] + data.mapSize[0] / 2.0f, data.mapPos[1] + data.mapSize[1] / 2.0f);
400 
401 	/* We now convert those coordinates relative to the center of the globe to coordinates of the screen
402 	 * (which are relative to the upper left side of the screen) */
403 	*x = (int) (mid[0] - radius * v[1]);
404 	*y = (int) (mid[1] - radius * v[0]);
405 
406 	if (z)
407 		*z = (int) (radius * v[2]);
408 
409 	/* if the point is on the wrong side of the earth, the player cannot see it */
410 	if (v[2] > 0)
411 		return false;
412 
413 	/* if the point is outside the screen, the player cannot see it */
414 	if (*x < data.mapPos[0] && *y < data.mapPos[1]
415 			&& *x > data.mapPos[0] + data.mapSize[0]
416 			&& *y > data.mapPos[1] + data.mapSize[1])
417 		return false;
418 
419 	return true;
420 }
421 
422 /**
423  * @brief Transform a 2D position on the map to screen coordinates.
424  * @param[in] node Menu node
425  * @param[in] pos Position on the map described by longitude and latitude
426  * @param[out] x X coordinate on the screen
427  * @param[out] y Y coordinate on the screen
428  * @returns true if the screen position is within the boundaries of the menu
429  * node. Otherwise returns false.
430  */
GEO_MapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y)431 static bool GEO_MapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y)
432 {
433 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
434 	/* get "raw" position */
435 	float sx = pos[0] / 360 + data.center[0] - 0.5;
436 
437 	/* shift it on screen */
438 	if (sx < -0.5f)
439 		sx += 1.0f;
440 	else if (sx > +0.5f)
441 		sx -= 1.0f;
442 
443 	*x = data.mapPos[0] + 0.5f * data.mapSize[0] - sx * data.mapSize[0] * data.zoom;
444 	*y = data.mapPos[1] + 0.5f * data.mapSize[1] - (pos[1] / 180.0f + data.center[1] - 0.5f) * data.mapSize[1] * data.zoom;
445 
446 	if (*x < data.mapPos[0] && *y < data.mapPos[1]
447 			&& *x > data.mapPos[0] + data.mapSize[0]
448 			&& *y > data.mapPos[1] + data.mapSize[1])
449 		return false;
450 	return true;
451 }
452 
453 /**
454  * @brief Call either GEO_MapToScreen or GEO_3DMapToScreen depending on the geoscape you're using.
455  * @param[in] node Menu node
456  * @param[in] pos Position on the map described by longitude and latitude
457  * @param[out] x Pointer to the X coordinate on the screen
458  * @param[out] y Pointer to the Y coordinate on the screen
459  * @param[out] z Pointer to the Z coordinate on the screen (may be nullptr if not needed)
460  * @returns true if pos corresponds to a point which is visible on the screen. Otherwise returns false.
461  */
GEO_AllMapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y,int * z)462 static bool GEO_AllMapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y, int* z)
463 {
464 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
465 	if (!data.flatgeoscape)
466 		return GEO_3DMapToScreen(node, pos, x, y, z);
467 
468 	if (z)
469 		*z = -10;
470 	return GEO_MapToScreen(node, pos, x, y);
471 }
472 
473 /**
474  * @brief maximum distance (in pixel) to get a valid mouse click
475  * @note this is for a 1024 * 768 screen
476  */
477 #define UI_MAP_DIST_SELECTION 15
478 /**
479  * @brief Tell if the specified position is considered clicked
480  */
GEO_IsPositionSelected(const uiNode_t * node,const vec2_t pos,int x,int y)481 static bool GEO_IsPositionSelected (const uiNode_t* node, const vec2_t pos, int x, int y)
482 {
483 	int msx, msy;
484 
485 	if (GEO_AllMapToScreen(node, pos, &msx, &msy, nullptr))
486 		if (x >= msx - UI_MAP_DIST_SELECTION && x <= msx + UI_MAP_DIST_SELECTION
487 		 && y >= msy - UI_MAP_DIST_SELECTION && y <= msy + UI_MAP_DIST_SELECTION)
488 			return true;
489 
490 	return false;
491 }
492 
493 /**
494  * @brief Draws a 3D marker on geoscape if the player can see it.
495  * @param[in] node Menu node.
496  * @param[in] pos Longitude and latitude of the marker to draw.
497  * @param[in] theta Angle (degree) of the model to the horizontal.
498  * @param[in] model The name of the model of the marker.
499  * @param[in] skin Number of modelskin to draw on marker
500  */
GEO_Draw3DMarkerIfVisible(const uiNode_t * node,const vec2_t pos,float theta,const char * model,int skin)501 static void GEO_Draw3DMarkerIfVisible (const uiNode_t* node, const vec2_t pos, float theta, const char* model, int skin)
502 {
503 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
504 	if (data.flatgeoscape) {
505 		int x, y;
506 		vec3_t screenPos;
507 
508 		GEO_AllMapToScreen(node, pos, &x, &y, nullptr);
509 		VectorSet(screenPos, x, y, 0);
510 		/* models are used on 2D geoscape for aircraft */
511 		cgi->R_Draw2DMapMarkers(screenPos, theta, model, skin);
512 	} else {
513 		cgi->R_Draw3DMapMarkers(data.mapPos, data.mapSize, data.angles, pos, theta, GLOBE_RADIUS, model, skin);
514 	}
515 }
516 
517 /**
518  * @brief Calculate the shortest way to go from start to end on a sphere
519  * @param[in] start The point you start from
520  * @param[in] end The point you go to
521  * @param[out] line Contains the shortest path to go from start to end
522  * @sa GEO_MapDrawLine
523  */
GEO_CalcLine(const vec2_t start,const vec2_t end,mapline_t * line)524 void GEO_CalcLine (const vec2_t start, const vec2_t end, mapline_t* line)
525 {
526 	vec3_t s, e, v;
527 	vec3_t normal;
528 	vec2_t trafo, sa, ea;
529 	float cosTrafo, sinTrafo;
530 	float phiStart, phiEnd, dPhi, phi;
531 	float* p;
532 	int i, n;
533 
534 	/* get plane normal */
535 	PolarToVec(start, s);
536 	PolarToVec(end, e);
537 	/* Procedure below won't work if start is the same like end */
538 	if (VectorEqual(s, e)) {
539 		line->distance = 0;
540 		line->numPoints = 2;
541 		Vector2Set(line->point[0], end[0], end[1]);
542 		Vector2Set(line->point[1], end[0], end[1]);
543 		return;
544 	}
545 
546 	CrossProduct(s, e, normal);
547 	VectorNormalize(normal);
548 
549 	/* get transformation */
550 	VecToPolar(normal, trafo);
551 	cosTrafo = cos(trafo[1] * torad);
552 	sinTrafo = sin(trafo[1] * torad);
553 
554 	sa[0] = start[0] - trafo[0];
555 	sa[1] = start[1];
556 	PolarToVec(sa, s);
557 	ea[0] = end[0] - trafo[0];
558 	ea[1] = end[1];
559 	PolarToVec(ea, e);
560 
561 	phiStart = atan2(s[1], cosTrafo * s[2] - sinTrafo * s[0]);
562 	phiEnd = atan2(e[1], cosTrafo * e[2] - sinTrafo * e[0]);
563 
564 	/* get waypoints */
565 	if (phiEnd < phiStart - M_PI)
566 		phiEnd += 2 * M_PI;
567 	if (phiEnd > phiStart + M_PI)
568 		phiEnd -= 2 * M_PI;
569 
570 	n = (phiEnd - phiStart) / M_PI * LINE_MAXSEG;
571 	if (n > 0)
572 		n = n + 1;
573 	else
574 		n = -n + 1;
575 
576 	line->distance = fabs(phiEnd - phiStart) / n * todeg;
577 	line->numPoints = n + 1;
578 	/* make sure we do not exceed route array size */
579 	assert(line->numPoints <= LINE_MAXPTS);
580 	dPhi = (phiEnd - phiStart) / n;
581 	p = nullptr;
582 	for (phi = phiStart, i = 0; i <= n; phi += dPhi, i++) {
583 		const float* last = p;
584 		p = line->point[i];
585 		VectorSet(v, -sinTrafo * cos(phi), sin(phi), cosTrafo * cos(phi));
586 		VecToPolar(v, p);
587 		p[0] += trafo[0];
588 
589 		if (!last) {
590 			while (p[0] < -180.0)
591 				p[0] += 360.0;
592 			while (p[0] > +180.0)
593 				p[0] -= 360.0;
594 		} else {
595 			while (p[0] - last[0] > +180.0)
596 				p[0] -= 360.0;
597 			while (p[0] - last[0] < -180.0)
598 				p[0] += 360.0;
599 		}
600 	}
601 }
602 
603 /**
604  * @brief Draw a path on a menu node (usually the 2D geoscape map)
605  * @param[in] node The menu node which will be used for drawing dimensions.
606  * This is usually the geoscape menu node.
607  * @param[in] line The path which is to be drawn
608  * @sa GEO_CalcLine
609  */
GEO_MapDrawLine(const uiNode_t * node,const mapline_t * line)610 static void GEO_MapDrawLine (const uiNode_t* node, const mapline_t* line)
611 {
612 	const vec4_t color = {1, 0.5, 0.5, 1};
613 	screenPoint_t pts[LINE_MAXPTS];
614 	screenPoint_t* p;
615 	int i, start, old;
616 
617 	/* draw */
618 	cgi->R_Color(color);
619 	start = 0;
620 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
621 	old = data.mapSize[0] / 2;
622 	for (i = 0, p = pts; i < line->numPoints; i++, p++) {
623 		GEO_MapToScreen(node, line->point[i], &p->x, &p->y);
624 
625 		/* If we cross longitude 180 degree (right/left edge of the screen), draw the first part of the path */
626 		if (i > start && abs(p->x - old) > data.mapSize[0] / 2) {
627 			/* shift last point */
628 			int diff;
629 
630 			if (p->x - old > data.mapSize[0] / 2)
631 				diff = -data.mapSize[0] * data.zoom;
632 			else
633 				diff = data.mapSize[0] * data.zoom;
634 			p->x += diff;
635 
636 			/* wrap around screen border */
637 			cgi->R_DrawLineStrip(i - start, (int*)(&pts));
638 
639 			/* first path of the path is drawn, now we begin the second part of the path */
640 			/* shift first point, continue drawing */
641 			start = i;
642 			pts[0].x = p[-1].x - diff;
643 			pts[0].y = p[-1].y;
644 			p = pts;
645 		}
646 		old = p->x;
647 	}
648 
649 	cgi->R_DrawLineStrip(i - start, (int*)(&pts));
650 	cgi->R_Color(nullptr);
651 }
652 
653 /**
654  * @brief Draw a path on a menu node (usually the 3Dgeoscape map)
655  * @param[in] node The menu node which will be used for drawing dimensions.
656  * This is usually the 3Dgeoscape menu node.
657  * @param[in] line The path which is to be drawn
658  * @sa GEO_CalcLine
659  */
GEO_3DMapDrawLine(const uiNode_t * node,const mapline_t * line)660 static void GEO_3DMapDrawLine (const uiNode_t* node, const mapline_t* line)
661 {
662 	const vec4_t color = {1, 0.5, 0.5, 1};
663 	screenPoint_t pts[LINE_MAXPTS];
664 	int numPoints, start;
665 
666 	start = 0;
667 	numPoints = 0;
668 
669 	/* draw only when the point of the path is visible */
670 	cgi->R_Color(color);
671 	for (int i = 0; i < line->numPoints; i++) {
672 		if (GEO_3DMapToScreen(node, line->point[i], &pts[i].x, &pts[i].y, nullptr))
673 			numPoints++;
674 		else if (!numPoints)
675 			/* the point which is not drawn is at the beginning of the path */
676 			start++;
677 	}
678 
679 	cgi->R_DrawLineStrip(numPoints, (int*)(&pts[start]));
680 	cgi->R_Color(nullptr);
681 }
682 
683 #define CIRCLE_DRAW_POINTS	60
684 /**
685  * @brief Draw equidistant points from a given point on a menu node
686  * @param[in] node The menu node which will be used for drawing dimensions.
687  * This is usually the geoscape menu node.
688  * @param[in] center The latitude and longitude of center point
689  * @param[in] angle The angle defining the distance of the equidistant points to center
690  * @param[in] color The color for drawing
691  * @sa RADAR_DrawCoverage
692  */
GEO_MapDrawEquidistantPoints(const uiNode_t * node,const vec2_t center,const float angle,const vec4_t color)693 static void GEO_MapDrawEquidistantPoints (const uiNode_t* node, const vec2_t center, const float angle, const vec4_t color)
694 {
695 	int i, xCircle, yCircle;
696 	screenPoint_t pts[CIRCLE_DRAW_POINTS + 1];
697 	vec2_t posCircle;
698 	bool oldDraw = false;
699 	int numPoints = 0;
700 	vec3_t initialVector, rotationAxis, currentPoint, centerPos;
701 
702 	cgi->R_Color(color);
703 
704 	/* Set centerPos corresponding to cartesian coordinates of the center point */
705 	PolarToVec(center, centerPos);
706 
707 	/* Find a perpendicular vector to centerPos, and rotate centerPos around it to obtain one point distant of angle from centerPos */
708 	PerpendicularVector(rotationAxis, centerPos);
709 	RotatePointAroundVector(initialVector, rotationAxis, centerPos, angle);
710 
711 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
712 	/* Now, each equidistant point is given by a rotation around centerPos */
713 	for (i = 0; i <= CIRCLE_DRAW_POINTS; i++) {
714 		bool draw = false;
715 		const float degrees = i * 360.0f / (float)CIRCLE_DRAW_POINTS;
716 		RotatePointAroundVector(currentPoint, centerPos, initialVector, degrees);
717 		VecToPolar(currentPoint, posCircle);
718 		if (GEO_AllMapToScreen(node, posCircle, &xCircle, &yCircle, nullptr)) {
719 			draw = true;
720 			if (data.flatgeoscape && numPoints != 0 && abs(pts[numPoints - 1].x - xCircle) > 512)
721 				oldDraw = false;
722 		}
723 
724 		/* if moving from a point of the screen to a distant one, draw the path we already calculated, and begin a new path
725 		 * (to avoid unwanted lines) */
726 		if (draw != oldDraw && i != 0) {
727 			cgi->R_DrawLineStrip(numPoints, (int*)(&pts));
728 			numPoints = 0;
729 		}
730 		/* if the current point is to be drawn, add it to the path */
731 		if (draw) {
732 			pts[numPoints].x = xCircle;
733 			pts[numPoints].y = yCircle;
734 			numPoints++;
735 		}
736 		/* update value of oldDraw */
737 		oldDraw = draw;
738 	}
739 
740 	/* Draw the last path */
741 	cgi->R_DrawLineStrip(numPoints, (int*)(&pts));
742 	cgi->R_Color(nullptr);
743 }
744 
745 /**
746  * @brief Return the angle of a model given its position and destination, on 3D geoscape.
747  * @param[in] start Latitude and longitude of the position of the model.
748  * @param[in] end Latitude and longitude of aimed point.
749  * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
750  * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
751  * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
752  */
GEO_AngleOfPath3D(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)753 static float GEO_AngleOfPath3D (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
754 {
755 	vec3_t start3D, end3D, north3D, ortToDest, ortToPole, v;
756 	const vec2_t northPole = {0.0f, 90.0f};	/**< Position of the north pole (used to know where the 'up' side is) */
757 
758 	PolarToVec(start, start3D);
759 	PolarToVec(end, end3D);
760 	PolarToVec(northPole, north3D);
761 
762 	/* calculate the vector othogonal to movement */
763 	CrossProduct(start3D, end3D, ortToDest);
764 	VectorNormalize(ortToDest);
765 	if (ortVector) {
766 		VectorCopy(ortToDest, ortVector);
767 	}
768 
769 	/* calculate the vector othogonal to north pole (from model location) */
770 	CrossProduct(start3D, north3D, ortToPole);
771 	VectorNormalize(ortToPole);
772 
773 	/**
774 	 * @todo Save the value angle instead of direction: we don't need a vector here,
775 	 * we could just compare angle to current angle.
776 	 */
777 	/* smooth change of direction if the model is not idle */
778 	if (direction) {
779 		VectorSubtract(ortToDest, direction, v);
780 		const float dist = VectorLength(v);
781 		if (dist > 0.01) {
782 			vec3_t rotationAxis;
783 			CrossProduct(direction, ortToDest, rotationAxis);
784 			VectorNormalize(rotationAxis);
785 			RotatePointAroundVector(v, rotationAxis, direction, 5.0);
786 			VectorCopy(v, direction);
787 			VectorSubtract(ortToDest, direction, v);
788 			if (VectorLength(v) < dist)
789 				VectorCopy(direction, ortToDest);
790 			else
791 				VectorCopy(ortToDest, direction);
792 		}
793 	}
794 
795 	/* calculate the angle the model is making at earth surface with north pole direction */
796 	float angle = todeg * acos(DotProduct(ortToDest, ortToPole));
797 	/* with arcos, you only get the absolute value of the angle: get the sign */
798 	CrossProduct(ortToDest, ortToPole, v);
799 	if (DotProduct(start3D, v) < 0)
800 		angle = - angle;
801 
802 	return angle;
803 }
804 
805 /**
806  * @brief Return the angle of a model given its position and destination, on 2D geoscape.
807  * @param[in] start Latitude and longitude of the position of the model.
808  * @param[in] end Latitude and longitude of aimed point.
809  * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
810  * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
811  * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
812  */
GEO_AngleOfPath2D(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)813 static float GEO_AngleOfPath2D (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
814 {
815 	vec3_t start3D, end3D, tangentVector, v, rotationAxis;
816 
817 	/* calculate the vector tangent to movement */
818 	PolarToVec(start, start3D);
819 	PolarToVec(end, end3D);
820 	if (ortVector) {
821 		CrossProduct(start3D, end3D, ortVector);
822 		VectorNormalize(ortVector);
823 		CrossProduct(ortVector, start3D, tangentVector);
824 	} else {
825 		CrossProduct(start3D, end3D, v);
826 		CrossProduct(v, start3D, tangentVector);
827 	}
828 	VectorNormalize(tangentVector);
829 
830 	/* smooth change of direction if the model is not idle */
831 	if (direction) {
832 		VectorSubtract(tangentVector, direction, v);
833 		const float dist = VectorLength(v);
834 		if (dist > 0.01) {
835 			CrossProduct(direction, tangentVector, rotationAxis);
836 			VectorNormalize(rotationAxis);
837 			RotatePointAroundVector(v, rotationAxis, direction, 5.0);
838 			VectorSubtract(tangentVector, direction, v);
839 			if (VectorLength(v) < dist)
840 				VectorCopy(direction, tangentVector);
841 			else
842 				VectorCopy(tangentVector, direction);
843 		}
844 	}
845 
846 	VectorSet(rotationAxis, 0, 0, 1);
847 	RotatePointAroundVector(v, rotationAxis, tangentVector, - start[0]);
848 	VectorSet(rotationAxis, 0, 1, 0);
849 	RotatePointAroundVector(tangentVector, rotationAxis, v, start[1] + 90.0f);
850 
851 	/* calculate the orientation angle of the model around axis perpendicular to the screen */
852 	float angle = todeg * atan(tangentVector[0] / tangentVector[1]);
853 	if (tangentVector[1] > 0)
854 		angle -= 90.0f;
855 	else
856 		angle += 90.0f;
857 
858 	return angle;
859 }
860 
861 /**
862  * @brief Select which function should be used for calculating the direction of model on 2D or 3D geoscape.
863  * @param[in] start Latitude and longitude of the position of the model.
864  * @param[in] end Latitude and longitude of aimed point.
865  * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
866  * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
867  * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
868  */
GEO_AngleOfPath(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)869 float GEO_AngleOfPath (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
870 {
871 	uiNode_t* node = geoscapeNode;
872 	if (!node)
873 		return 0.0f;
874 
875 	const mapExtraData_t &data = UI_MAPEXTRADATA(node);
876 	if (!data.flatgeoscape)
877 		return GEO_AngleOfPath3D(start, end, direction, ortVector);
878 	return GEO_AngleOfPath2D(start, end, direction, ortVector);
879 }
880 
881 /**
882  * @brief Will set the vector for the geoscape position
883  * @param[in] flatgeoscape True for 2D geoscape
884  * @param[out] vector The output vector. A two-dim vector for the flat geoscape, and a three-dim vector for the 3d geoscape
885  * @param[in] objectPos The position vector of the object to transform.
886  */
GEO_ConvertObjectPositionToGeoscapePosition(bool flatgeoscape,float * vector,const vec2_t objectPos)887 static void GEO_ConvertObjectPositionToGeoscapePosition (bool flatgeoscape, float* vector, const vec2_t objectPos)
888 {
889 	if (flatgeoscape)
890 		Vector2Set(vector, objectPos[0], objectPos[1]);
891 	else
892 		VectorSet(vector, objectPos[0], -objectPos[1], 0);
893 }
894 
895 /**
896  * @brief center to a mission
897  */
GEO_GetMissionAngle(bool flatgeoscape,float * vector,int id)898 static void GEO_GetMissionAngle (bool flatgeoscape, float* vector, int id)
899 {
900 	mission_t* mission = MIS_GetByIdx(id);
901 	if (mission == nullptr)
902 		return;
903 	GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, mission->pos);
904 	GEO_SelectMission(mission);
905 }
906 
907 /**
908  * @brief center to a ufo
909  */
GEO_GetUFOAngle(bool flatgeoscape,float * vector,int idx)910 static void GEO_GetUFOAngle (bool flatgeoscape, float* vector, int idx)
911 {
912 	aircraft_t* ufo;
913 
914 	/* Cycle through UFOs (only those visible on geoscape) */
915 	ufo = nullptr;
916 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
917 		if (ufo->idx != idx)
918 			continue;
919 		GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, ufo->pos);
920 		GEO_SelectUFO(ufo);
921 		return;
922 	}
923 }
924 
925 
926 /**
927  * @brief Start center to the selected point
928  */
GEO_StartCenter(uiNode_t * node)929 static void GEO_StartCenter (uiNode_t* node)
930 {
931 	mapExtraData_t &data = UI_MAPEXTRADATA(node);
932 	if (data.flatgeoscape) {
933 		/* case 2D geoscape */
934 		vec2_t diff;
935 
936 		Vector2Set(data.smoothFinal2DGeoscapeCenter, 0.5f - data.smoothFinal2DGeoscapeCenter[0] / 360.0f,
937 				0.5f - data.smoothFinal2DGeoscapeCenter[1] / 180.0f);
938 		if (data.smoothFinal2DGeoscapeCenter[1] < 0.5 / ZOOM_LIMIT)
939 			data.smoothFinal2DGeoscapeCenter[1] = 0.5 / ZOOM_LIMIT;
940 		if (data.smoothFinal2DGeoscapeCenter[1] > 1.0 - 0.5 / ZOOM_LIMIT)
941 			data.smoothFinal2DGeoscapeCenter[1] = 1.0 - 0.5 / ZOOM_LIMIT;
942 		diff[0] = data.smoothFinal2DGeoscapeCenter[0] - data.center[0];
943 		diff[1] = data.smoothFinal2DGeoscapeCenter[1] - data.center[1];
944 		data.smoothDeltaLength = sqrt(diff[0] * diff[0] + diff[1] * diff[1]);
945 	} else {
946 		/* case 3D geoscape */
947 		vec3_t diff;
948 
949 		data.smoothFinalGlobeAngle[1] += GLOBE_ROTATE;
950 		VectorSubtract(data.smoothFinalGlobeAngle, data.angles, diff);
951 		data.smoothDeltaLength = VectorLength(diff);
952 	}
953 
954 	data.smoothFinalZoom = ZOOM_LIMIT;
955 	data.smoothDeltaZoom = fabs(data.smoothFinalZoom - data.zoom);
956 	data.smoothRotation = true;
957 }
958 
959 /**
960  * @brief Start to rotate or shift the globe to the given position
961  * @param[in] pos Longitude and latitude of the position to center on
962  */
GEO_CenterPosition(const vec2_t pos)963 void GEO_CenterPosition (const vec2_t pos)
964 {
965 	uiNode_t* node = geoscapeNode;
966 	if (!node)
967 		return;
968 	mapExtraData_t &data = UI_MAPEXTRADATA(node);
969 	const bool flatgeoscape = data.flatgeoscape;
970 	float* vector;
971 	if (flatgeoscape)
972 		vector = data.smoothFinal2DGeoscapeCenter;
973 	else
974 		vector = data.smoothFinalGlobeAngle;
975 
976 	GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, pos);
977 	GEO_StartCenter(node);
978 }
979 
980 /**
981  * @brief Center the view and select an object from the geoscape
982  */
GEO_SelectObject_f(void)983 static void GEO_SelectObject_f (void)
984 {
985 	uiNode_t* node = geoscapeNode;
986 	if (!node)
987 		return;
988 
989 	if (cgi->Cmd_Argc() != 3) {
990 		Com_Printf("Usage: %s <mission|ufo> <id>\n", cgi->Cmd_Argv(0));
991 		return;
992 	}
993 
994 	const char* type = cgi->Cmd_Argv(1);
995 	const int idx = atoi(cgi->Cmd_Argv(2));
996 	mapExtraData_t &data = UI_MAPEXTRADATA(node);
997 	const bool flatgeoscape = data.flatgeoscape;
998 
999 	float* vector;
1000 	if (flatgeoscape)
1001 		vector = data.smoothFinal2DGeoscapeCenter;
1002 	else
1003 		vector = data.smoothFinalGlobeAngle;
1004 
1005 	if (Q_streq(type, "mission"))
1006 		GEO_GetMissionAngle(flatgeoscape, vector, idx);
1007 	else if (Q_streq(type, "ufo"))
1008 		GEO_GetUFOAngle(flatgeoscape, vector, idx);
1009 	else {
1010 		Com_Printf("GEO_SelectObject_f: type %s unsupported.", type);
1011 		return;
1012 	}
1013 	GEO_StartCenter(node);
1014 }
1015 
1016 /**
1017  * @brief Returns position of the model corresponding to centerOnEventIdx.
1018  * @param[out] pos the position of the object
1019  */
GEO_GetGeoscapeAngle(vec2_t pos)1020 static void GEO_GetGeoscapeAngle (vec2_t pos)
1021 {
1022 	int counter = 0;
1023 	int maxEventIdx;
1024 	const int numMissions = CP_CountMissionOnGeoscape();
1025 	aircraft_t* ufo;
1026 	base_t* base;
1027 	int numBases = B_GetCount();
1028 
1029 	/* If the value of maxEventIdx is too big or to low, restart from beginning */
1030 	maxEventIdx = numMissions + numBases + INS_GetCount() - 1;
1031 	base = nullptr;
1032 	while ((base = B_GetNext(base)) != nullptr) {
1033 		AIR_ForeachFromBase(aircraft, base) {
1034 			if (AIR_IsAircraftOnGeoscape(aircraft))
1035 				maxEventIdx++;
1036 		}
1037 	}
1038 	ufo = nullptr;
1039 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr)
1040 		maxEventIdx++;
1041 
1042 	/* if there's nothing to center the view on, just go to 0,0 pos */
1043 	if (maxEventIdx < 0) {
1044 		Vector2Copy(vec2_origin, pos);
1045 		return;
1046 	}
1047 
1048 	/* check centerOnEventIdx is within the bounds */
1049 	if (centerOnEventIdx < 0)
1050 		centerOnEventIdx = maxEventIdx;
1051 	if (centerOnEventIdx > maxEventIdx)
1052 		centerOnEventIdx = 0;
1053 
1054 	/* Cycle through missions */
1055 	if (centerOnEventIdx < numMissions) {
1056 		MIS_Foreach(mission) {
1057 			if (!mission->onGeoscape)
1058 				continue;
1059 			if (counter == centerOnEventIdx) {
1060 				Vector2Copy(mission->pos, pos);
1061 				GEO_SelectMission(mission);
1062 				return;
1063 			}
1064 			counter++;
1065 		}
1066 	}
1067 	counter = numMissions;
1068 
1069 	/* Cycle through bases */
1070 	if (centerOnEventIdx < numBases + counter) {
1071 		base = nullptr;
1072 		while ((base = B_GetNext(base)) != nullptr) {
1073 			if (counter == centerOnEventIdx) {
1074 				Vector2Copy(base->pos, pos);
1075 				return;
1076 			}
1077 			counter++;
1078 		}
1079 	}
1080 	counter += numBases;
1081 
1082 	/* Cycle through installations */
1083 	if (centerOnEventIdx < INS_GetCount() + counter) {
1084 		INS_Foreach(inst) {
1085 			if (counter == centerOnEventIdx) {
1086 				Vector2Copy(inst->pos, pos);
1087 				return;
1088 			}
1089 			counter++;
1090 		}
1091 	}
1092 	counter += INS_GetCount();
1093 
1094 	/* Cycle through aircraft (only those present on geoscape) */
1095 	AIR_Foreach(aircraft) {
1096 		if (AIR_IsAircraftOnGeoscape(aircraft)) {
1097 			if (centerOnEventIdx == counter) {
1098 				Vector2Copy(aircraft->pos, pos);
1099 				GEO_SelectAircraft(aircraft);
1100 				return;
1101 			}
1102 			counter++;
1103 		}
1104 	}
1105 
1106 	/* Cycle through UFOs (only those visible on geoscape) */
1107 	ufo = nullptr;
1108 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1109 		if (centerOnEventIdx == counter) {
1110 			Vector2Copy(ufo->pos, pos);
1111 			GEO_SelectUFO(ufo);
1112 			return;
1113 		}
1114 		counter++;
1115 	}
1116 }
1117 
1118 /**
1119  * @brief Switch to next model on 2D and 3D geoscape.
1120  * @note Set @c smoothRotation to @c true to allow a smooth rotation in GEO_Draw.
1121  * @note This function sets the value of smoothFinalGlobeAngle (for 3D) or smoothFinal2DGeoscapeCenter (for 2D),
1122  *  which contains the final value that ccs.angles or ccs.centre must respectively take.
1123  */
GEO_CenterOnPoint_f(void)1124 void GEO_CenterOnPoint_f (void)
1125 {
1126 	if (!Q_streq(cgi->UI_GetActiveWindowName(), "geoscape"))
1127 		return;
1128 
1129 	centerOnEventIdx++;
1130 
1131 	uiNode_t* node = geoscapeNode;
1132 	if (!node)
1133 		return;
1134 
1135 	vec2_t pos;
1136 	GEO_GetGeoscapeAngle(pos);
1137 	GEO_CenterPosition(pos);
1138 }
1139 
1140 #define BULLET_SIZE	1
1141 /**
1142  * @brief Draws a bunch of bullets on the geoscape map
1143  * @param[in] node Pointer to the node in which you want to draw the bullets.
1144  * @param[in] pos
1145  * @sa GEO_Draw
1146  */
GEO_DrawBullets(const uiNode_t * node,const vec3_t pos)1147 static void GEO_DrawBullets (const uiNode_t* node, const vec3_t pos)
1148 {
1149 	int x, y;
1150 
1151 	if (GEO_AllMapToScreen(node, pos, &x, &y, nullptr))
1152 		cgi->R_DrawFill(x, y, BULLET_SIZE, BULLET_SIZE, yellow);
1153 }
1154 
1155 /**
1156  * @brief Draws a energy beam on the geoscape map (laser/particle)
1157  * @param[in] node Pointer to the node in which you want to draw.
1158  * @param[in] start Start position of the shot (on geoscape)
1159  * @param[in] end End position of the shot (on geoscape)
1160  * @param[in] color color of the beam
1161  * @sa GEO_Draw
1162  */
GEO_DrawBeam(const uiNode_t * node,const vec3_t start,const vec3_t end,const vec4_t color)1163 static void GEO_DrawBeam (const uiNode_t* node, const vec3_t start, const vec3_t end, const vec4_t color)
1164 {
1165 	int points[4];
1166 
1167 	if (!GEO_AllMapToScreen(node, start, &(points[0]), &(points[1]), nullptr))
1168 		return;
1169 	if (!GEO_AllMapToScreen(node, end, &(points[2]), &(points[3]), nullptr))
1170 		return;
1171 
1172 	cgi->R_Color(color);
1173 	cgi->R_DrawLine(points, 2.0);
1174 	cgi->R_Color(nullptr);
1175 }
1176 
GEO_RenderImage(int x,int y,const char * image)1177 static inline void GEO_RenderImage (int x, int y, const char* image)
1178 {
1179 	cgi->R_DrawImageCentered(x, y, image);
1180 }
1181 
1182 #define SELECT_CIRCLE_RADIUS	1.5f + 3.0f / UI_MAPEXTRADATACONST(node).zoom
1183 
1184 /**
1185  * @brief Draws one mission on the geoscape map (2D and 3D)
1186  * @param[in] node The menu node which will be used for drawing markers.
1187  * @param[in] mission Pointer to the mission to draw.
1188  */
GEO_DrawMapOneMission(const uiNode_t * node,const mission_t * mission)1189 static void GEO_DrawMapOneMission (const uiNode_t* node, const mission_t* mission)
1190 {
1191 	int x, y;
1192 	const bool isCurrentSelectedMission = GEO_IsMissionSelected(mission);
1193 
1194 	if (isCurrentSelectedMission)
1195 		cgi->Cvar_Set("mn_mapdaytime", GEO_IsNight(mission->pos) ? _("Night") : _("Day"));
1196 
1197 	if (!GEO_AllMapToScreen(node, mission->pos, &x, &y, nullptr))
1198 		return;
1199 
1200 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1201 	if (isCurrentSelectedMission) {
1202 		/* Draw circle around the mission */
1203 		if (data.flatgeoscape) {
1204 			if (mission->active) {
1205 				GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1206 			} else {
1207 				GEO_RenderImage(x, y, "pics/geoscape/circle");
1208 			}
1209 		} else {
1210 			if (!mission->active)
1211 				GEO_MapDrawEquidistantPoints(node, mission->pos, SELECT_CIRCLE_RADIUS, yellow);
1212 		}
1213 	}
1214 
1215 	/* Draw mission model (this must be called after drawing the selection circle so that the model is rendered on top of it)*/
1216 	if (!data.flatgeoscape) {
1217 		GEO_RenderImage(x, y, "pics/geoscape/mission");
1218 	} else {
1219 		GEO_Draw3DMarkerIfVisible(node, mission->pos, defaultBaseAngle, MIS_GetModel(mission), 0);
1220 	}
1221 
1222 	cgi->UI_DrawString("f_verysmall", ALIGN_UL, x + 10, y, MIS_GetName(mission));
1223 }
1224 
1225 /**
1226  * @brief Draw only the "wire" Radar coverage.
1227  * @param[in] node The menu node where radar coverage will be drawn.
1228  * @param[in] radar Pointer to the radar that will be drawn.
1229  * @param[in] pos Position of the radar.
1230  */
GEO_DrawRadarLineCoverage(const uiNode_t * node,const radar_t * radar,const vec2_t pos)1231 static void GEO_DrawRadarLineCoverage (const uiNode_t* node, const radar_t* radar, const vec2_t pos)
1232 {
1233 	const vec4_t color = {1., 1., 1., .4};
1234 	GEO_MapDrawEquidistantPoints(node, pos, radar->range, color);
1235 	GEO_MapDrawEquidistantPoints(node, pos, radar->trackingRange, color);
1236 }
1237 
1238 /**
1239  * @brief Draw only the "wire" part of the radar coverage in geoscape
1240  * @param[in] node The menu node where radar coverage will be drawn.
1241  * @param[in] radar Pointer to the radar that will be drawn.
1242  * @param[in] pos Position of the radar.
1243  */
GEO_DrawRadarInMap(const uiNode_t * node,const radar_t * radar,const vec2_t pos)1244 static void GEO_DrawRadarInMap (const uiNode_t* node, const radar_t* radar, const vec2_t pos)
1245 {
1246 	/* Show radar range zones */
1247 	GEO_DrawRadarLineCoverage(node, radar, pos);
1248 
1249 	/* everything below is drawn only if there is at least one detected UFO */
1250 	if (!radar->numUFOs)
1251 		return;
1252 
1253 	/* Draw lines from radar to ufos sensored */
1254 	int x, y;
1255 	const bool display = GEO_AllMapToScreen(node, pos, &x, &y, nullptr);
1256 	if (!display)
1257 		return;
1258 
1259 	screenPoint_t pts[2];
1260 	pts[0].x = x;
1261 	pts[0].y = y;
1262 
1263 	/* Set color */
1264 	const vec4_t color = {1., 1., 1., .3};
1265 	cgi->R_Color(color);
1266 	for (int i = 0; i < radar->numUFOs; i++) {
1267 		const aircraft_t* ufo = radar->ufos[i];
1268 		if (UFO_IsUFOSeenOnGeoscape(ufo) && GEO_AllMapToScreen(node, ufo->pos, &x, &y, nullptr)) {
1269 			pts[1].x = x;
1270 			pts[1].y = y;
1271 			cgi->R_DrawLineStrip(2, (int*)pts);
1272 		}
1273 	}
1274 	cgi->R_Color(nullptr);
1275 }
1276 
1277 /**
1278  * @brief Draws one installation on the geoscape map (2D and 3D)
1279  * @param[in] node The menu node which will be used for drawing markers.
1280  * @param[in] installation Pointer to the installation to draw.
1281  * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1282  * @param[in] font Default font.
1283  * @pre installation is not nullptr.
1284  */
GEO_DrawMapOneInstallation(const uiNode_t * node,const installation_t * installation,bool oneUFOVisible,const char * font)1285 static void GEO_DrawMapOneInstallation (const uiNode_t* node, const installation_t* installation,
1286 	bool oneUFOVisible, const char* font)
1287 {
1288 	const installationTemplate_t* tpl = installation->installationTemplate;
1289 	int x, y;
1290 
1291 	/* Draw weapon range if at least one UFO is visible */
1292 	if (oneUFOVisible && AII_InstallationCanShoot(installation)) {
1293 		int i;
1294 		for (i = 0; i < tpl->maxBatteries; i++) {
1295 			const aircraftSlot_t* slot = &installation->batteries[i].slot;
1296 			if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1297 				GEO_MapDrawEquidistantPoints(node, installation->pos,
1298 					slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1299 			}
1300 		}
1301 	}
1302 
1303 	/* Draw installation radar (only the "wire" style part) */
1304 	if (GEO_IsRadarOverlayActivated())
1305 		GEO_DrawRadarInMap(node, &installation->radar, installation->pos);
1306 
1307 	/* Draw installation */
1308 	if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
1309 		GEO_Draw3DMarkerIfVisible(node, installation->pos, defaultBaseAngle, tpl->model, 0);
1310 	} else if (GEO_MapToScreen(node, installation->pos, &x, &y)) {
1311 		GEO_RenderImage(x, y, tpl->image);
1312 	}
1313 
1314 	/* Draw installation names */
1315 	if (GEO_AllMapToScreen(node, installation->pos, &x, &y, nullptr))
1316 		cgi->UI_DrawString(font, ALIGN_UL, x, y + 10, installation->name);
1317 }
1318 
1319 /**
1320  * @brief Draws one base on the geoscape map (2D and 3D)
1321  * @param[in] node The menu node which will be used for drawing markers.
1322  * @param[in] base Pointer to the base to draw.
1323  * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1324  * @param[in] font Default font.
1325  */
GEO_DrawMapOneBase(const uiNode_t * node,const base_t * base,bool oneUFOVisible,const char * font)1326 static void GEO_DrawMapOneBase (const uiNode_t* node, const base_t* base,
1327 	bool oneUFOVisible, const char* font)
1328 {
1329 	int x, y;
1330 
1331 	/* Draw weapon range if at least one UFO is visible */
1332 	if (oneUFOVisible && AII_BaseCanShoot(base)) {
1333 		int i;
1334 		for (i = 0; i < base->numBatteries; i++) {
1335 			const aircraftSlot_t* slot = &base->batteries[i].slot;
1336 			if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1337 				GEO_MapDrawEquidistantPoints(node, base->pos,
1338 					slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1339 			}
1340 		}
1341 		for (i = 0; i < base->numLasers; i++) {
1342 			const aircraftSlot_t* slot = &base->lasers[i].slot;
1343 			if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1344 				GEO_MapDrawEquidistantPoints(node, base->pos,
1345 					slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1346 			}
1347 		}
1348 	}
1349 
1350 	/* Draw base radar (only the "wire" style part) */
1351 	if (GEO_IsRadarOverlayActivated())
1352 		GEO_DrawRadarInMap(node, &base->radar, base->pos);
1353 
1354 	/* Draw base */
1355 	if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
1356 		if (B_IsUnderAttack(base))
1357 			/* two skins - second skin is for baseattack */
1358 			GEO_Draw3DMarkerIfVisible(node, base->pos, defaultBaseAngle, "geoscape/base", 1);
1359 		else
1360 			GEO_Draw3DMarkerIfVisible(node, base->pos, defaultBaseAngle, "geoscape/base", 0);
1361 	} else if (GEO_MapToScreen(node, base->pos, &x, &y)) {
1362 		if (B_IsUnderAttack(base))
1363 			GEO_RenderImage(x, y, "pics/geoscape/baseattack");
1364 		else
1365 			GEO_RenderImage(x, y, "pics/geoscape/base");
1366 	}
1367 
1368 	/* Draw base names */
1369 	if (GEO_AllMapToScreen(node, base->pos, &x, &y, nullptr))
1370 		cgi->UI_DrawString(font, ALIGN_UL, x, y + 10, base->name);
1371 }
1372 
1373 /**
1374  * @brief Draws health bar for an aircraft (either phalanx or ufo)
1375  * @param[in] node Pointer to the meunode to draw in
1376  * @param[in] aircraft Pointer to the aircraft to draw for
1377  * @note if max health (AIR_STATS_DAMAGE) <= 0 no healthbar drawn
1378  */
GEO_DrawAircraftHealthBar(const uiNode_t * node,const aircraft_t * aircraft)1379 static void GEO_DrawAircraftHealthBar (const uiNode_t* node, const aircraft_t* aircraft)
1380 {
1381 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1382 	const int width = 8 * data.zoom;
1383 	const int height = 1 * data.zoom * 0.9f;
1384 	vec4_t color;
1385 	int centerX;
1386 	int centerY;
1387 	bool visible;
1388 
1389 	if (!aircraft)
1390 		return;
1391 	if (aircraft->stats[AIR_STATS_DAMAGE] <= 0)
1392 		return;
1393 
1394 	if (((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE]) <= .33f) {
1395 		Vector4Copy(red, color);
1396 	} else if (((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE]) <= .75f) {
1397 		Vector4Copy(yellow, color);
1398 	} else {
1399 		Vector4Copy(green, color);
1400 	}
1401 
1402 	if (!data.flatgeoscape)
1403 		visible = GEO_3DMapToScreen(node, aircraft->pos, &centerX, &centerY, nullptr);
1404 	else
1405 		visible = GEO_AllMapToScreen(node, aircraft->pos, &centerX, &centerY, nullptr);
1406 
1407 	if (visible) {
1408 		const vec4_t bordercolor = {1, 1, 1, 1};
1409 		cgi->R_DrawFill(centerX - width / 2 , centerY - 5 * data.zoom, round(width * ((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE])), height, color);
1410 		cgi->R_DrawRect(centerX - width / 2, centerY - 5 * data.zoom, width, height, bordercolor, 1.0, 1);
1411 	}
1412 }
1413 
1414 /**
1415  * @brief Draws one Phalanx aircraft on the geoscape map (2D and 3D)
1416  * @param[in] node The menu node which will be used for drawing markers.
1417  * @param[in] aircraft Pointer to the aircraft to draw.
1418  * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1419  */
GEO_DrawMapOnePhalanxAircraft(const uiNode_t * node,aircraft_t * aircraft,bool oneUFOVisible)1420 static void GEO_DrawMapOnePhalanxAircraft (const uiNode_t* node, aircraft_t* aircraft, bool oneUFOVisible)
1421 {
1422 	float angle;
1423 
1424 	/* Draw aircraft radar (only the "wire" style part) */
1425 	if (GEO_IsRadarOverlayActivated())
1426 		GEO_DrawRadarInMap(node, &aircraft->radar, aircraft->pos);
1427 
1428 	/* Draw only the bigger weapon range on geoscape: more detail will be given on airfight map */
1429 	if (oneUFOVisible)
1430 		GEO_MapDrawEquidistantPoints(node, aircraft->pos, aircraft->stats[AIR_STATS_WRANGE] / 1000.0f, red);
1431 
1432 	const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1433 	/* Draw aircraft route */
1434 	if (aircraft->status >= AIR_TRANSIT) {
1435 		/* aircraft is moving */
1436 		mapline_t path;
1437 
1438 		path.numPoints = aircraft->route.numPoints - aircraft->point;
1439 		/** @todo : check why path.numPoints can be sometime equal to -1 */
1440 		if (path.numPoints > 1) {
1441 			memcpy(path.point, aircraft->pos, sizeof(vec2_t));
1442 			memcpy(path.point + 1, aircraft->route.point + aircraft->point + 1, (path.numPoints - 1) * sizeof(vec2_t));
1443 			if (!data.flatgeoscape)
1444 				GEO_3DMapDrawLine(node, &path);
1445 			else
1446 				GEO_MapDrawLine(node, &path);
1447 		}
1448 		angle = GEO_AngleOfPath(aircraft->pos, aircraft->route.point[aircraft->route.numPoints - 1], aircraft->direction, nullptr);
1449 	} else {
1450 		/* aircraft is idle */
1451 		angle = 0.0f;
1452 	}
1453 
1454 	/* Draw a circle around selected aircraft */
1455 	if (GEO_IsAircraftSelected(aircraft)) {
1456 		int x;
1457 		int y;
1458 
1459 		if (!data.flatgeoscape)
1460 			GEO_MapDrawEquidistantPoints(node, aircraft->pos, SELECT_CIRCLE_RADIUS, yellow);
1461 		else {
1462 			GEO_AllMapToScreen(node, aircraft->pos, &x, &y, nullptr);
1463 			GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1464 		}
1465 
1466 		/* Draw a circle around the ufo pursued by selected aircraft */
1467 		if (aircraft->status == AIR_UFO && GEO_AllMapToScreen(node, aircraft->aircraftTarget->pos, &x, &y, nullptr)) {
1468 			if (!data.flatgeoscape)
1469 				GEO_MapDrawEquidistantPoints(node, aircraft->aircraftTarget->pos, SELECT_CIRCLE_RADIUS, yellow);
1470 			else
1471 				GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1472 		}
1473 	}
1474 
1475 	/* Draw aircraft (this must be called after drawing the selection circle so that the aircraft is drawn on top of it)*/
1476 	GEO_Draw3DMarkerIfVisible(node, aircraft->pos, angle, aircraft->model, 0);
1477 
1478 	/** @todo we should only show healthbar if the aircraft is fighting but it's a slow algo */
1479 	if (oneUFOVisible || cgi->Cvar_GetInteger("debug_showcrafthealth") >= 1)
1480 		GEO_DrawAircraftHealthBar(node, aircraft);
1481 }
1482 
1483 /**
1484  * @brief Assembles a string for a mission that is on the geoscape
1485  * @param[in] mission The mission to get the description for
1486  * @param[out] buffer The target buffer to store the text in
1487  * @param[in] size The size of the target buffer
1488  * @return A pointer to the buffer that was given to this function
1489  */
GEO_GetMissionText(char * buffer,size_t size,const mission_t * mission)1490 static const char* GEO_GetMissionText (char* buffer, size_t size, const mission_t* mission)
1491 {
1492 	assert(mission);
1493 	Com_sprintf(buffer, size, _("Name: %s\nObjective: %s"),
1494 		MIS_GetName(mission), (mission->mapDef) ? _(mission->mapDef->description) : _("Unknown"));
1495 	return buffer;
1496 }
1497 
1498 /**
1499  * @brief Assembles a string for an aircraft that is on the geoscape
1500  * @param[in] aircraft The aircraft to get the description for
1501  * @param[out] buffer The target buffer to store the text in
1502  * @param[in] size The size of the target buffer
1503  * @return A pointer to the buffer that was given to this function
1504  */
GEO_GetAircraftText(char * buffer,size_t size,const aircraft_t * aircraft)1505 static const char* GEO_GetAircraftText (char* buffer, size_t size, const aircraft_t* aircraft)
1506 {
1507 	if (aircraft->status == AIR_UFO) {
1508 		const float distance = GetDistanceOnGlobe(aircraft->pos, aircraft->aircraftTarget->pos);
1509 		Com_sprintf(buffer, size, _("Name:\t%s (%i/%i)\n"), aircraft->name, AIR_GetTeamSize(aircraft), aircraft->maxTeamSize);
1510 		Q_strcat(buffer, size, _("Status:\t%s\n"), AIR_AircraftStatusToName(aircraft));
1511 		if (aircraft->stats[AIR_STATS_DAMAGE] > 0)
1512 			Q_strcat(buffer, size, _("Health:\t%3.0f%%\n"), (double)aircraft->damage * 100 / aircraft->stats[AIR_STATS_DAMAGE]);
1513 		Q_strcat(buffer, size, _("Distance to target:\t\t%.0f\n"), distance);
1514 		Q_strcat(buffer, size, _("Speed:\t%i km/h\n"), AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1515 		Q_strcat(buffer, size, _("Fuel:\t%i/%i\n"), AIR_AircraftMenuStatsValues(aircraft->fuel, AIR_STATS_FUELSIZE),
1516 			AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_FUELSIZE], AIR_STATS_FUELSIZE));
1517 		Q_strcat(buffer, size, _("ETA:\t%sh\n"), CP_SecondConvert((float)SECONDS_PER_HOUR * distance / aircraft->stats[AIR_STATS_SPEED]));
1518 	} else {
1519 		Com_sprintf(buffer, size, _("Name:\t%s (%i/%i)\n"), aircraft->name, AIR_GetTeamSize(aircraft), aircraft->maxTeamSize);
1520 		Q_strcat(buffer, size, _("Status:\t%s\n"), AIR_AircraftStatusToName(aircraft));
1521 		if (aircraft->stats[AIR_STATS_DAMAGE] > 0)
1522 			Q_strcat(buffer, size, _("Health:\t%3.0f%%\n"), (double)aircraft->damage * 100 / aircraft->stats[AIR_STATS_DAMAGE]);
1523 		Q_strcat(buffer, size, _("Speed:\t%i km/h\n"), AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1524 		Q_strcat(buffer, size, _("Fuel:\t%i/%i\n"), AIR_AircraftMenuStatsValues(aircraft->fuel, AIR_STATS_FUELSIZE),
1525 			AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_FUELSIZE], AIR_STATS_FUELSIZE));
1526 		if (aircraft->status != AIR_IDLE) {
1527 			const float distance = GetDistanceOnGlobe(aircraft->pos,
1528 					aircraft->route.point[aircraft->route.numPoints - 1]);
1529 			Q_strcat(buffer, size, _("ETA:\t%sh\n"), CP_SecondConvert((float)SECONDS_PER_HOUR * distance / aircraft->stats[AIR_STATS_SPEED]));
1530 		}
1531 	}
1532 	return buffer;
1533 }
1534 
1535 /**
1536  * @brief Assembles a string for a UFO that is on the geoscape
1537  * @param[in] ufo The UFO to get the description for
1538  * @param[out] buffer The target buffer to store the text in
1539  * @param[in] size The size of the target buffer
1540  * @return A pointer to the buffer that was given to this function
1541  */
GEO_GetUFOText(char * buffer,size_t size,const aircraft_t * ufo)1542 static const char* GEO_GetUFOText (char* buffer, size_t size, const aircraft_t* ufo)
1543 {
1544 	Com_sprintf(buffer, size, "%s\n", UFO_GetName(ufo));
1545 	Q_strcat(buffer, size, _("Speed: %i km/h\n"), AIR_AircraftMenuStatsValues(ufo->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1546 	return buffer;
1547 }
1548 
1549 /**
1550  * @brief Will add missions and UFOs to the geoscape dock panel
1551  */
GEO_UpdateGeoscapeDock(void)1552 void GEO_UpdateGeoscapeDock (void)
1553 {
1554 	char buf[512];
1555 	aircraft_t* ufo;
1556 
1557 	cgi->UI_ExecuteConfunc("clean_geoscape_object");
1558 
1559 	/* draw mission pics */
1560 	MIS_Foreach(mission) {
1561 		if (!mission->onGeoscape)
1562 			continue;
1563 		cgi->UI_ExecuteConfunc("add_geoscape_object mission %i \"%s\" \"%s\n%s\"",
1564 			mission->idx, MIS_GetModel(mission), MIS_GetName(mission),
1565 			(mission->mapDef) ? _(mission->mapDef->description) : "");
1566 	}
1567 
1568 	/* draws ufos */
1569 	ufo = nullptr;
1570 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1571 		const unsigned int ufoIDX = UFO_GetGeoscapeIDX(ufo);
1572 		cgi->UI_ExecuteConfunc("add_geoscape_object ufo %i %s \"%s\"",
1573 				ufoIDX, ufo->model, GEO_GetUFOText(buf, sizeof(buf), ufo));
1574 	}
1575 }
1576 
1577 /**
1578  * @brief Draws all ufos, aircraft, bases and so on to the geoscape map (2D and 3D)
1579  * @param[in] node The menu node which will be used for drawing markers.
1580  * @note This is a drawing function only, called each time a frame is drawn. Therefore
1581  * you should not use this function to calculate eg. the distance between 2 items on the geoscape
1582  * (you should instead calculate it just after one of the items moved -- distance is not
1583  * going to change when you rotate the earth around itself and the time is stopped eg.).
1584  * @sa GEO_Draw
1585  */
GEO_DrawMarkers(const uiNode_t * node)1586 void GEO_DrawMarkers (const uiNode_t* node)
1587 {
1588 	int i;
1589 	const char* font;
1590 	aircraft_t* ufo;
1591 	base_t* base;
1592 
1593 	const vec4_t white = {1.f, 1.f, 1.f, 0.7f};
1594 	int maxInterpolationPoints;
1595 
1596 	assert(node);
1597 
1598 	/* font color on geoscape */
1599 	cgi->R_Color(node->color);
1600 	/* default font */
1601 	font = cgi->UI_GetFontFromNode(node);
1602 
1603 	/* check if at least 1 UFO is visible */
1604 	const bool oneUFOVisible = UFO_GetNextOnGeoscape(nullptr) != nullptr;
1605 
1606 	/* draw mission pics */
1607 	MIS_Foreach(mission) {
1608 		if (!mission->onGeoscape)
1609 			continue;
1610 		GEO_DrawMapOneMission(node, mission);
1611 	}
1612 
1613 	/* draw installations */
1614 	INS_Foreach(installation) {
1615 		GEO_DrawMapOneInstallation(node, installation, oneUFOVisible, font);
1616 	}
1617 
1618 	/* draw bases */
1619 	base = nullptr;
1620 	while ((base = B_GetNext(base)) != nullptr)
1621 		GEO_DrawMapOneBase(node, base, oneUFOVisible, font);
1622 
1623 	/* draw all aircraft */
1624 	AIR_Foreach(aircraft) {
1625 		if (AIR_IsAircraftOnGeoscape(aircraft))
1626 			GEO_DrawMapOnePhalanxAircraft(node, aircraft, oneUFOVisible);
1627 	}
1628 
1629 	/* draws ufos */
1630 	ufo = nullptr;
1631 	while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1632 #ifdef DEBUG
1633 		/* in debug mode you execute set showufos 1 to see the ufos on geoscape */
1634 		if (cgi->Cvar_GetInteger("debug_showufos")) {
1635 			/* Draw ufo route */
1636 			if (!UI_MAPEXTRADATACONST(node).flatgeoscape)
1637 				GEO_3DMapDrawLine(node, &ufo->route);
1638 			else
1639 				GEO_MapDrawLine(node, &ufo->route);
1640 		} else
1641 #endif
1642 		{
1643 			const float angle = GEO_AngleOfPath(ufo->pos, ufo->route.point[ufo->route.numPoints - 1], ufo->direction, nullptr);
1644 			const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1645 
1646 			if (!data.flatgeoscape)
1647 				GEO_MapDrawEquidistantPoints(node, ufo->pos, SELECT_CIRCLE_RADIUS, white);
1648 
1649 			if (GEO_IsUFOSelected(ufo)) {
1650 				if (!data.flatgeoscape) {
1651 					GEO_MapDrawEquidistantPoints(node, ufo->pos, SELECT_CIRCLE_RADIUS, yellow);
1652 				} else {
1653 					int x, y;
1654 					GEO_AllMapToScreen(node, ufo->pos, &x, &y, nullptr);
1655 					GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1656 				}
1657 			}
1658 			GEO_Draw3DMarkerIfVisible(node, ufo->pos, angle, ufo->model, 0);
1659 
1660 			/** @todo we should only show healthbar if aircraft is fighting but it's a slow algo */
1661 			if (RS_IsResearched_ptr(ufo->tech)
1662 			 || cgi->Cvar_GetInteger("debug_showcrafthealth") >= 1)
1663 				GEO_DrawAircraftHealthBar(node, ufo);
1664 		}
1665 	}
1666 
1667 	if (ccs.gameTimeScale > 0)
1668 		maxInterpolationPoints = floor(1.0f / (ccs.frametime * (float)ccs.gameTimeScale));
1669 	else
1670 		maxInterpolationPoints = 0;
1671 
1672 	/* draws projectiles */
1673 	for (i = 0; i < ccs.numProjectiles; i++) {
1674 		aircraftProjectile_t* projectile = &ccs.projectiles[i];
1675 		vec3_t drawPos = {0, 0, 0};
1676 
1677 		if (projectile->hasMoved) {
1678 			projectile->hasMoved = false;
1679 			VectorCopy(projectile->pos[0], drawPos);
1680 		} else {
1681 			if (maxInterpolationPoints > 2 && projectile->numInterpolationPoints < maxInterpolationPoints) {
1682 				/* If a new point hasn't been given and there is at least 3 points need to be filled in then
1683 				 * use linear interpolation to draw the points until a new projectile point is provided.
1684 				 * The reason you need at least 3 points is that acceptable results can be achieved with 2 or less
1685 				 * gaps in points so don't add the overhead of interpolation. */
1686 				const float xInterpolStep = (projectile->projectedPos[0][0] - projectile->pos[0][0]) / (float)maxInterpolationPoints;
1687 				projectile->numInterpolationPoints += 1;
1688 				drawPos[0] = projectile->pos[0][0] + (xInterpolStep * projectile->numInterpolationPoints);
1689 				LinearInterpolation(projectile->pos[0], projectile->projectedPos[0], drawPos[0], drawPos[1]);
1690 			} else {
1691 				VectorCopy(projectile->pos[0], drawPos);
1692 			}
1693 		}
1694 
1695 		if (projectile->bullets) {
1696 			GEO_DrawBullets(node, drawPos);
1697 		} else if (projectile->beam) {
1698 			vec3_t start;
1699 			vec3_t end;
1700 
1701 			if (projectile->attackingAircraft)
1702 				VectorCopy(projectile->attackingAircraft->pos, start);
1703 			else
1704 				VectorCopy(projectile->attackerPos, start);
1705 
1706 			if (projectile->aimedAircraft)
1707 				VectorCopy(projectile->aimedAircraft->pos, end);
1708 			else
1709 				VectorCopy(projectile->idleTarget, end);
1710 
1711 			GEO_DrawBeam(node, start, end, projectile->aircraftItem->craftitem.beamColor);
1712 		} else {
1713 			GEO_Draw3DMarkerIfVisible(node, drawPos, projectile->angle, projectile->aircraftItem->model, 0);
1714 		}
1715 	}
1716 
1717 	const bool showXVI = CP_IsXVIVisible();
1718 	static char buffer[512] = "";
1719 
1720 	/* Draw nation names */
1721 	for (i = 0; i < ccs.numNations; i++) {
1722 		const nation_t* nation = NAT_GetNationByIDX(i);
1723 		int x, y;
1724 		if (GEO_AllMapToScreen(node, nation->pos, &x, &y, nullptr))
1725 			cgi->UI_DrawString("f_verysmall", ALIGN_UC, x , y, _(nation->name));
1726 		if (showXVI) {
1727 			const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
1728 			Q_strcat(buffer, sizeof(buffer), _("%s\t%i%%\n"), _(nation->name), stats->xviInfection);
1729 		}
1730 	}
1731 
1732 	if (showXVI)
1733 		cgi->UI_RegisterText(TEXT_XVI, buffer);
1734 	else
1735 		cgi->UI_ResetData(TEXT_XVI);
1736 
1737 	cgi->R_Color(nullptr);
1738 }
1739 
1740 /**
1741  * @brief Draw the geoscape
1742  * @param[in] data Geoscape status data structure
1743  */
GEO_Draw(geoscapeData_t * data)1744 void GEO_Draw (geoscapeData_t* data)
1745 {
1746 	if (!CP_IsRunning()) {
1747 		data->active = false;
1748 		return;
1749 	}
1750 
1751 	data->active = true;
1752 	data->map = ccs.curCampaign->map;
1753 	data->nationOverlay = GEO_IsNationOverlayActivated();
1754 	data->xviOverlay = GEO_IsXVIOverlayActivated();
1755 	data->radarOverlay = GEO_IsRadarOverlayActivated();
1756 	data->date = ccs.date;
1757 
1758 	geoscapeNode = static_cast<uiNode_t* >(data->geoscapeNode);
1759 
1760 	mission_t* mission = GEO_GetSelectedMission();
1761 	/* display text */
1762 	cgi->UI_ResetData(TEXT_STANDARD);
1763 	switch (ccs.mapAction) {
1764 	case MA_NEWBASE:
1765 		cgi->UI_RegisterText(TEXT_STANDARD, _("Select the desired location of the new base on the map.\n"));
1766 		return;
1767 	case MA_NEWINSTALLATION:
1768 		cgi->UI_RegisterText(TEXT_STANDARD, _("Select the desired location of the new installation on the map.\n"));
1769 		return;
1770 	case MA_BASEATTACK:
1771 		if (mission)
1772 			break;
1773 		cgi->UI_RegisterText(TEXT_STANDARD, _("Aliens are attacking our base at this very moment.\n"));
1774 		return;
1775 	case MA_INTERCEPT:
1776 		if (mission)
1777 			break;
1778 		cgi->UI_RegisterText(TEXT_STANDARD, _("Select ufo or mission on map\n"));
1779 		return;
1780 	case MA_UFORADAR:
1781 		if (mission)
1782 			break;
1783 		cgi->UI_RegisterText(TEXT_STANDARD, _("UFO in radar range\n"));
1784 		return;
1785 	case MA_NONE:
1786 		break;
1787 	}
1788 
1789 	/* Nothing is displayed yet */
1790 	if (mission) {
1791 		cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetMissionText(textStandard, sizeof(textStandard), mission));
1792 	} else if (GEO_GetSelectedAircraft() != nullptr) {
1793 		const aircraft_t* aircraft = GEO_GetSelectedAircraft();
1794 		if (AIR_IsAircraftInBase(aircraft)) {
1795 			cgi->UI_RegisterText(TEXT_STANDARD, nullptr);
1796 			GEO_ResetAction();
1797 			return;
1798 		}
1799 		cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetAircraftText(textStandard, sizeof(textStandard), aircraft));
1800 	} else if (GEO_GetSelectedUFO() != nullptr) {
1801 		cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetUFOText(textStandard, sizeof(textStandard), GEO_GetSelectedUFO()));
1802 	} else {
1803 #ifdef DEBUG
1804 		if (debug_showInterest->integer) {
1805 			static char t[64];
1806 			Com_sprintf(t, lengthof(t), "Interest level: %i\n", ccs.overallInterest);
1807 			cgi->UI_RegisterText(TEXT_STANDARD, t);
1808 		} else
1809 #endif
1810 		cgi->UI_RegisterText(TEXT_STANDARD, "");
1811 	}
1812 }
1813 
1814 /**
1815  * @brief No more special action on the geoscape
1816  */
GEO_ResetAction(void)1817 void GEO_ResetAction (void)
1818 {
1819 	/* don't allow a reset when no base is set up */
1820 	if (B_AtLeastOneExists())
1821 		ccs.mapAction = MA_NONE;
1822 
1823 	GEO_SetInterceptorAircraft(nullptr);
1824 	GEO_SetSelectedMission(nullptr);
1825 	GEO_SetSelectedAircraft(nullptr);
1826 	GEO_SetSelectedUFO(nullptr);
1827 
1828 	if (!radarOverlayWasSet)
1829 		GEO_DeactivateOverlay("radar");
1830 }
1831 
1832 /**
1833  * @brief Select the specified ufo on the geoscape
1834  */
GEO_SelectUFO(aircraft_t * ufo)1835 void GEO_SelectUFO (aircraft_t* ufo)
1836 {
1837 	GEO_ResetAction();
1838 	GEO_SetSelectedUFO(ufo);
1839 }
1840 
1841 /**
1842  * @brief Select the specified aircraft on the geoscape
1843  */
GEO_SelectAircraft(aircraft_t * aircraft)1844 void GEO_SelectAircraft (aircraft_t* aircraft)
1845 {
1846 	GEO_ResetAction();
1847 	GEO_SetSelectedAircraft(aircraft);
1848 }
1849 
1850 /**
1851  * @brief Select the specified mission
1852  * @param[in] mission Pointer to the mission to select
1853  * @return pointer to the selected mission
1854  */
GEO_SelectMission(mission_t * mission)1855 mission_t* GEO_SelectMission (mission_t* mission)
1856 {
1857 	if (!mission || GEO_IsMissionSelected(mission))
1858 		return GEO_GetSelectedMission();
1859 	GEO_ResetAction();
1860 	ccs.mapAction = MA_INTERCEPT;
1861 	GEO_SetSelectedMission(mission);
1862 	return GEO_GetSelectedMission();
1863 }
1864 
1865 /**
1866  * @brief Notify that a mission has been removed
1867  */
GEO_NotifyMissionRemoved(const mission_t * mission)1868 void GEO_NotifyMissionRemoved (const mission_t* mission)
1869 {
1870 	/* Unselect the current selected mission if it's the same */
1871 	if (GEO_IsMissionSelected(mission))
1872 		GEO_ResetAction();
1873 
1874 	GEO_UpdateGeoscapeDock();
1875 }
1876 
1877 /**
1878  * @brief Notify that a UFO has been removed
1879  * @param[in] ufo Pointer to the ufo has been removed
1880  * @param[in] destroyed True if the UFO has been destroyed, false if it's been only set invisible (landed)
1881  */
GEO_NotifyUFORemoved(const aircraft_t * ufo,bool destroyed)1882 void GEO_NotifyUFORemoved (const aircraft_t* ufo, bool destroyed)
1883 {
1884 	GEO_UpdateGeoscapeDock();
1885 
1886 	if (GEO_GetSelectedUFO() == nullptr)
1887 		return;
1888 
1889 	/* Unselect the current selected ufo if it's the same */
1890 	if (GEO_IsUFOSelected(ufo))
1891 		GEO_ResetAction();
1892 	else if (destroyed && ccs.geoscape.selectedUFO > ufo)
1893 		/** @todo convert to linked list */
1894 		ccs.geoscape.selectedUFO--;
1895 }
1896 
1897 /**
1898  * @brief Notify that an aircraft has been removed from game
1899  * @param[in] aircraft Pointer to the aircraft that has been removed
1900  */
GEO_NotifyAircraftRemoved(const aircraft_t * aircraft)1901 void GEO_NotifyAircraftRemoved (const aircraft_t* aircraft)
1902 {
1903 	/* Unselect the current selected ufo if its the same */
1904 	if (GEO_IsAircraftSelected(aircraft) || GEO_IsInterceptorSelected(aircraft))
1905 		GEO_ResetAction();
1906 }
1907 
1908 /**
1909  * @brief Translate nation map color to nation
1910  * @sa GEO_GetColor
1911  * @param[in] pos Map Coordinates to get the nation from
1912  * @return returns the nation pointer with the given color on nationPic at given pos
1913  * @return nullptr if no nation with the given color value was found
1914  * @note The coordinates already have to be transformed to map coordinates via GEO_ScreenToMap
1915  */
GEO_GetNation(const vec2_t pos)1916 nation_t* GEO_GetNation (const vec2_t pos)
1917 {
1918 	int i;
1919 	const byte* color = GEO_GetColor(pos, MAPTYPE_NATIONS, nullptr);
1920 	const vec3_t fcolor = {color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f};
1921 #ifdef PARANOID
1922 	Com_DPrintf(DEBUG_CLIENT, "GEO_GetNation: color value for %.0f:%.0f is r:%i, g:%i, b: %i\n", pos[0], pos[1], color[0], color[1], color[2]);
1923 #endif
1924 	for (i = 0; i < ccs.numNations; i++) {
1925 		nation_t* nation = NAT_GetNationByIDX(i);
1926 		/* compare the first three color values with color value at pos */
1927 		/* 0.02 x 255 = 5.1, which allow a variation of +-5 for each color components */
1928 		if (VectorEqualEpsilon(nation->color, fcolor, 0.02))
1929 			return nation;
1930 	}
1931 	Com_DPrintf(DEBUG_CLIENT, "GEO_GetNation: No nation found at %.0f:%.0f - color: %i:%i:%i\n", pos[0], pos[1], color[0], color[1], color[2]);
1932 	return nullptr;
1933 }
1934 
1935 
1936 /**
1937  * @brief Translate color value to terrain type
1938  * @sa GEO_GetColor
1939  * @param[in] color the color value from the terrain mask
1940  * @return returns the zone name
1941  * @note never may return a null pointer or an empty string
1942  * @note Make sure, that there are textures with the same name in base/textures/tex_terrain
1943  */
GEO_GetTerrainType(const byte * const color)1944 const char* GEO_GetTerrainType (const byte* const color)
1945 {
1946 	if (MapIsDesert(color))
1947 		return "desert";
1948 	else if (MapIsArctic(color))
1949 		return "arctic";
1950 	else if (MapIsWater(color))
1951 		return "water";
1952 	else if (MapIsMountain(color))
1953 		return "mountain";
1954 	else if (MapIsTropical(color))
1955 		return "tropical";
1956 	else if (MapIsCold(color))
1957 		return "cold";
1958 	else if (MapIsWasted(color))
1959 		return "wasted";
1960 	return "grass";
1961 }
1962 
1963 /**
1964  * @brief Translate color value to culture type
1965  * @sa GEO_GetColor
1966  * @param[in] color the color value from the culture mask
1967  * @return returns the zone name
1968  * @note never may return a null pointer or an empty string
1969  */
GEO_GetCultureType(const byte * color)1970 static const char* GEO_GetCultureType (const byte* color)
1971 {
1972 	if (MapIsWater(color))
1973 		return "water";
1974 	else if (MapIsEastern(color))
1975 		return "eastern";
1976 	else if (MapIsWestern(color))
1977 		return "western";
1978 	else if (MapIsOriental(color))
1979 		return "oriental";
1980 	else if (MapIsAfrican(color))
1981 		return "african";
1982 	return "western";
1983 }
1984 
1985 /**
1986  * @brief Translate color value to population type
1987  * @sa GEO_GetColor
1988  * @param[in] color the color value from the population mask
1989  * @return returns the zone name
1990  * @note never may return a null pointer or an empty string
1991  */
GEO_GetPopulationType(const byte * color)1992 static const char* GEO_GetPopulationType (const byte* color)
1993 {
1994 	if (MapIsWater(color))
1995 		return "water";
1996 	else if (MapIsUrban(color))
1997 		return "urban";
1998 	else if (MapIsSuburban(color))
1999 		return "suburban";
2000 	else if (MapIsVillage(color))
2001 		return "village";
2002 	else if (MapIsRural(color))
2003 		return "rural";
2004 	return "nopopulation";
2005 }
2006 
2007 /**
2008  * @brief Determine the terrain type under a given position
2009  * @sa GEO_GetColor
2010  * @param[in] pos Map Coordinates to get the terrain type from
2011  * @param[out] coast GEO_GetColor will set this to true if the given position is a coast line.
2012  * @return returns the zone name
2013  */
GEO_GetTerrainTypeByPos(const vec2_t pos,bool * coast)2014 static inline const char* GEO_GetTerrainTypeByPos (const vec2_t pos, bool *coast)
2015 {
2016 	const byte* color = GEO_GetColor(pos, MAPTYPE_TERRAIN, coast);
2017 	return GEO_GetTerrainType(color);
2018 }
2019 
2020 /**
2021  * @brief Determine the culture type under a given position
2022  * @sa GEO_GetColor
2023  * @param[in] pos Map Coordinates to get the culture type from
2024  * @return returns the zone name
2025  */
GEO_GetCultureTypeByPos(const vec2_t pos)2026 static inline const char* GEO_GetCultureTypeByPos (const vec2_t pos)
2027 {
2028 	const byte* color = GEO_GetColor(pos, MAPTYPE_CULTURE, nullptr);
2029 	return GEO_GetCultureType(color);
2030 }
2031 
2032 /**
2033  * @brief Determine the population type under a given position
2034  * @sa GEO_GetColor
2035  * @param[in] pos Map Coordinates to get the population type from
2036  * @return returns the zone name
2037  */
GEO_GetPopulationTypeByPos(const vec2_t pos)2038 static inline const char* GEO_GetPopulationTypeByPos (const vec2_t pos)
2039 {
2040 	const byte* color = GEO_GetColor(pos, MAPTYPE_POPULATION, nullptr);
2041 	return GEO_GetPopulationType(color);
2042 }
2043 
2044 /**
2045  * @brief Get number of civilian on a map at given position.
2046  * @param[in] pos Position where the mission takes place.
2047  * @return Number of civilian.
2048  * @sa CP_CreateCivilianTeam
2049  */
GEO_GetCivilianNumberByPosition(const vec2_t pos)2050 int GEO_GetCivilianNumberByPosition (const vec2_t pos)
2051 {
2052 	const byte* color = GEO_GetColor(pos, MAPTYPE_POPULATION, nullptr);
2053 
2054 	if (MapIsWater(color))
2055 		cgi->Com_Error(ERR_DROP, "GEO_GetPopulationType: Trying to get number of civilian in a position on water");
2056 
2057 	if (MapIsUrban(color))
2058 		return 10;
2059 	else if (MapIsSuburban(color))
2060 		return 8;
2061 	else if (MapIsVillage(color))
2062 		return 6;
2063 	else if (MapIsRural(color))
2064 		return 4;
2065 	else if (MapIsNopopulation(color))
2066 		return 2;
2067 
2068 	return 0;
2069 }
2070 
2071 /**
2072  * @brief Prints positions parameter in console.
2073  * @param[in] pos Location (latitude, longitude) where you want to check
2074  * @note Used for printing in console, do not translate.
2075  * @sa NAT_ScriptSanityCheck
2076  */
GEO_PrintParameterStringByPos(const vec2_t pos)2077 void GEO_PrintParameterStringByPos (const vec2_t pos)
2078 {
2079 	bool coast = false;
2080 	const char* terrainType = GEO_GetTerrainTypeByPos(pos, &coast);
2081 	const char* cultureType = GEO_GetCultureTypeByPos(pos);
2082 	const char* populationType = GEO_GetPopulationTypeByPos(pos);
2083 
2084 	Com_Printf ("      (Terrain: %s, Culture: %s, Population: %s, Coast: %s)\n",
2085 			terrainType, cultureType, populationType, coast ? "true" : "false");
2086 }
2087 
2088 /**
2089  * @brief Check that a position (in latitude / longitude) is within boundaries.
2090  * @param[in,out] pos Pointer to the 2 elements vector giving the position.
2091  */
GEO_CheckPositionBoundaries(float * pos)2092 void GEO_CheckPositionBoundaries (float* pos)
2093 {
2094 	while (pos[0] > 180.0)
2095 		pos[0] -= 360.0;
2096 	while (pos[0] < -180.0)
2097 		pos[0] += 360.0;
2098 	while (pos[1] > 90.0)
2099 		pos[1] -= 180.0;
2100 	while (pos[1] < -90.0)
2101 		pos[1] += 180.0;
2102 }
2103 
2104 /**
2105  * @brief Check whether given position is Day or Night.
2106  * @param[in] pos Given position.
2107  * @return True if given position is Night.
2108  */
GEO_IsNight(const vec2_t pos)2109 bool GEO_IsNight (const vec2_t pos)
2110 {
2111 	float p, q, a, root, x;
2112 
2113 	/* set p to hours (we don't use ccs.day here because we need a float value) */
2114 	p = (float) ccs.date.sec / SECONDS_PER_DAY;
2115 	/* convert current day to angle (-pi on 1st january, pi on 31 december) */
2116 	q = (ccs.date.day + p) * (2 * M_PI / DAYS_PER_YEAR_AVG) - M_PI;
2117 	p = (0.5 + pos[0] / 360 - p) * (2 * M_PI) - q;
2118 	a = -sin(pos[1] * torad);
2119 	root = sqrt(1.0 - a * a);
2120 	x = sin(p) * root * sin(q) - (a * SIN_ALPHA + cos(p) * root * COS_ALPHA) * cos(q);
2121 	return (x > 0);
2122 }
2123 
2124 /**
2125  * @brief Returns the color value from geoscape of a certain mask (terrain, culture or population) at a given position.
2126  * @param[in] pos vec2_t Value of position on map to get the color value from.
2127  * pos is longitude and latitude
2128  * @param[in] type determine the map to get the color from (there are different masks)
2129  * one for the climatezone (bases made use of this - there are grass, ice and desert
2130  * base tiles available) and one for the nations
2131  * @param[out] coast The function will set this to true if the given position is a coast line.
2132  * This can be @c nullptr if you are not interested in this fact.
2133  * @return Returns the color value at given position.
2134  * @note terrainPic, culturePic and populationPic are pointers to an rgba image in memory
2135  */
GEO_GetColor(const vec2_t pos,mapType_t type,bool * coast)2136 const byte* GEO_GetColor (const vec2_t pos, mapType_t type, bool *coast)
2137 {
2138 	int x, y;
2139 	int width, height;
2140 	const byte* mask;
2141 	const byte* color;
2142 
2143 	switch (type) {
2144 	case MAPTYPE_TERRAIN:
2145 		mask = terrainPic;
2146 		width = terrainWidth;
2147 		height = terrainHeight;
2148 		break;
2149 	case MAPTYPE_CULTURE:
2150 		mask = culturePic;
2151 		width = cultureWidth;
2152 		height = cultureHeight;
2153 		break;
2154 	case MAPTYPE_POPULATION:
2155 		mask = populationPic;
2156 		width = populationWidth;
2157 		height = populationHeight;
2158 		break;
2159 	case MAPTYPE_NATIONS:
2160 		mask = nationsPic;
2161 		width = nationsWidth;
2162 		height = nationsHeight;
2163 		break;
2164 	default:
2165 		cgi->Com_Error(ERR_DROP, "Unknown maptype %i\n", type);
2166 	}
2167 
2168 	assert(mask);
2169 
2170 	/** @todo add EQUAL_EPSILON here? */
2171 	assert(pos[0] >= -180);
2172 	assert(pos[0] <= 180);
2173 	assert(pos[1] >= -90);
2174 	assert(pos[1] <= 90);
2175 
2176 	/* get coordinates */
2177 	x = (180 - pos[0]) / 360 * width;
2178 	x--; /* we start from 0 */
2179 	y = (90 - pos[1]) / 180 * height;
2180 	y--; /* we start from 0 */
2181 	if (x < 0)
2182 		x = 0;
2183 	if (y < 0)
2184 		y = 0;
2185 
2186 	/* 4 => RGBA */
2187 	/* terrainWidth is the width of the image */
2188 	/* this calculation returns the pixel in col x and in row y */
2189 	assert(4 * (x + y * width) < width * height * 4);
2190 	color = mask + 4 * (x + y * width);
2191 	if (coast != nullptr) {
2192 		if (MapIsWater(color)) {
2193 			*coast = false;
2194 		} else {
2195 			/* only check four directions */
2196 			const int gap = 4;
2197 			if (x > gap) {
2198 				const byte* coastCheck = mask + 4 * ((x - gap) + y * width);
2199 				*coast = MapIsWater(coastCheck);
2200 			}
2201 			if (!*coast && x < width - 1 - gap) {
2202 				const byte* coastCheck = mask + 4 * ((x + gap) + y * width);
2203 				*coast = MapIsWater(coastCheck);
2204 			}
2205 
2206 			if (!*coast) {
2207 				if (y > gap) {
2208 					const byte* coastCheck = mask + 4 * (x + (y - gap) * width);
2209 					*coast = MapIsWater(coastCheck);
2210 				}
2211 				if (!*coast && y < height - 1 - gap) {
2212 					const byte* coastCheck = mask + 4 * (x + (y + gap) * width);
2213 					*coast = MapIsWater(coastCheck);
2214 				}
2215 			}
2216 		}
2217 	}
2218 
2219 	return color;
2220 }
2221 
2222 /**
2223  * @brief Minimum distance between a new mission and an existing base.
2224  */
2225 static const float MIN_DIST_BASE = 4.0f;
2226 
2227 /**
2228  * @brief Check if given pos is close to an existing base.
2229  * @return Pointer to the base if one base is closer than MIN_DIST_BASE from pos, nullptr else
2230  */
GEO_PositionCloseToBase(const vec2_t pos)2231 base_t* GEO_PositionCloseToBase (const vec2_t pos)
2232 {
2233 	base_t* base = nullptr;
2234 	while ((base = B_GetNext(base)) != nullptr)
2235 		if (GetDistanceOnGlobe(pos, base->pos) < MIN_DIST_BASE)
2236 			return base;
2237 
2238 	return nullptr;
2239 }
2240 
2241 /**
2242  * @brief Checks for a given location, if it fulfills all criteria given via parameters (terrain, culture, population, nation type)
2243  * @param[in] pos Location to be tested
2244  * @param[in] terrainTypes A linkedList_t containing a list of strings determining the terrain types to be tested for (e.g. "grass") may be nullptr
2245  * @param[in] cultureTypes A linkedList_t containing a list of strings determining the culture types to be tested for (e.g. "western") may be nullptr
2246  * @param[in] populationTypes A linkedList_t containing a list of strings determining the population types to be tested for (e.g. "suburban") may be nullptr
2247  * @param[in] nations A linkedList_t containing a list of strings determining the nations to be tested for (e.g. "asia") may be nullptr
2248  * @return true if a location was found, otherwise false. If the map is over water, return false
2249  * @note The name TCPNTypes comes from terrain, culture, population, nation types
2250  */
GEO_PositionFitsTCPNTypes(const vec2_t pos,const linkedList_t * terrainTypes,const linkedList_t * cultureTypes,const linkedList_t * populationTypes,const linkedList_t * nations)2251 bool GEO_PositionFitsTCPNTypes (const vec2_t pos, const linkedList_t* terrainTypes, const linkedList_t* cultureTypes, const linkedList_t* populationTypes, const linkedList_t* nations)
2252 {
2253 	bool coast = false;
2254 	const char* terrainType = GEO_GetTerrainTypeByPos(pos, &coast);
2255 	const char* cultureType = GEO_GetCultureTypeByPos(pos);
2256 	const char* populationType = GEO_GetPopulationTypeByPos(pos);
2257 
2258 	if (MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr)))
2259 		return false;
2260 
2261 	if (!terrainTypes || cgi->LIST_ContainsString(terrainTypes, terrainType) || (coast && cgi->LIST_ContainsString(terrainTypes, "coast"))) {
2262 		if (!cultureTypes || cgi->LIST_ContainsString(cultureTypes, cultureType)) {
2263 			if (!populationTypes || cgi->LIST_ContainsString(populationTypes, populationType)) {
2264 				const nation_t* nationAtPos = GEO_GetNation(pos);
2265 				if (!nations)
2266 					return true;
2267 				if (nationAtPos && (!nations || cgi->LIST_ContainsString(nations, nationAtPos->id))) {
2268 					return true;
2269 				}
2270 			}
2271 		}
2272 	}
2273 
2274 	return false;
2275 }
2276 
GEO_Shutdown(void)2277 void GEO_Shutdown (void)
2278 {
2279 	Mem_Free(terrainPic);
2280 	terrainPic = nullptr;
2281 
2282 	Mem_Free(culturePic);
2283 	culturePic = nullptr;
2284 
2285 	Mem_Free(populationPic);
2286 	populationPic = nullptr;
2287 
2288 	Mem_Free(nationsPic);
2289 	nationsPic = nullptr;
2290 }
2291 
GEO_Init(const char * map)2292 void GEO_Init (const char* map)
2293 {
2294 	/* load terrain mask */
2295 	cgi->R_LoadImage(va("pics/geoscape/%s_terrain", map), &terrainPic, &terrainWidth, &terrainHeight);
2296 	if (!terrainPic || !terrainWidth || !terrainHeight)
2297 		cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_terrain in pics/geoscape", map);
2298 
2299 	/* load culture mask */
2300 	cgi->R_LoadImage(va("pics/geoscape/%s_culture", map), &culturePic, &cultureWidth, &cultureHeight);
2301 	if (!culturePic || !cultureWidth || !cultureHeight)
2302 		cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_culture in pics/geoscape", map);
2303 
2304 	/* load population mask */
2305 	cgi->R_LoadImage(va("pics/geoscape/%s_population", map), &populationPic, &populationWidth, &populationHeight);
2306 	if (!populationPic || !populationWidth || !populationHeight)
2307 		cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_population in pics/geoscape", map);
2308 
2309 	/* load nations mask */
2310 	cgi->R_LoadImage(va("pics/geoscape/%s_nations", map), &nationsPic, &nationsWidth, &nationsHeight);
2311 	if (!nationsPic || !nationsWidth || !nationsHeight)
2312 		cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_nations in pics/geoscape", map);
2313 }
2314 
GEO_Reset(const char * map)2315 void GEO_Reset (const char* map)
2316 {
2317 	GEO_Shutdown();
2318 	GEO_Init(map);
2319 	GEO_ResetAction();
2320 	GEO_UpdateGeoscapeDock();
2321 }
2322 
2323 /**
2324  * @brief Notify that a UFO disappears on radars
2325  */
GEO_NotifyUFODisappear(const aircraft_t * ufo)2326 void GEO_NotifyUFODisappear (const aircraft_t* ufo)
2327 {
2328 	/* Unselect the currently selected ufo if it's the same */
2329 	if (GEO_IsUFOSelected(ufo))
2330 		GEO_ResetAction();
2331 
2332 	GEO_UpdateGeoscapeDock();
2333 }
2334 
2335 /**
2336  * @brief Switch overlay (turn on / off)
2337  * @param[in] overlayID Name of the overlay you want to switch.
2338  */
GEO_SetOverlay(const char * overlayID)2339 void GEO_SetOverlay (const char* overlayID)
2340 {
2341 	const int value = cgi->Cvar_GetInteger("cl_geoscape_overlay");
2342 	if (Q_streq(overlayID, "nations")) {
2343 		cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_NATION);
2344 		return;
2345 	}
2346 
2347 	/* do nothing while the first base is not build */
2348 	if (!B_AtLeastOneExists())
2349 		return;
2350 
2351 	if (Q_streq(overlayID, "xvi")) {
2352 		cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_XVI);
2353 	} else if (Q_streq(overlayID, "radar")) {
2354 		cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_RADAR);
2355 		if (GEO_IsRadarOverlayActivated())
2356 			RADAR_UpdateWholeRadarOverlay();
2357 	}
2358 }
2359 
2360 /**
2361  * @brief Console command to call GEO_SetOverlay.
2362  */
GEO_SetOverlay_f(void)2363 static void GEO_SetOverlay_f (void)
2364 {
2365 	const char* arg;
2366 
2367 	if (cgi->Cmd_Argc() != 2) {
2368 		Com_Printf("Usage: %s <nations|xvi|radar>\n", cgi->Cmd_Argv(0));
2369 		return;
2370 	}
2371 
2372 	arg = cgi->Cmd_Argv(1);
2373 	GEO_SetOverlay(arg);
2374 
2375 	/* save last decision player took on radar display, in order to be able to restore it later */
2376 	if (Q_streq(arg, "radar"))
2377 		radarOverlayWasSet = GEO_IsRadarOverlayActivated();
2378 }
2379 
2380 /**
2381  * @brief Remove overlay.
2382  * @param[in] overlayID Name of the overlay you want to turn off.
2383  */
GEO_DeactivateOverlay(const char * overlayID)2384 void GEO_DeactivateOverlay (const char* overlayID)
2385 {
2386 	if (Q_streq(overlayID, "nations")) {
2387 		if (GEO_IsNationOverlayActivated())
2388 			GEO_SetOverlay("nations");
2389 	} else if (Q_streq(overlayID, "xvi")) {
2390 		if (GEO_IsXVIOverlayActivated())
2391 			GEO_SetOverlay("xvi");
2392 	} else if (Q_streq(overlayID, "radar")) {
2393 		if (GEO_IsRadarOverlayActivated())
2394 			GEO_SetOverlay("radar");
2395 	}
2396 }
2397 
2398 /**
2399  * @brief Console command to call GEO_DeactivateOverlay.
2400  */
GEO_DeactivateOverlay_f(void)2401 static void GEO_DeactivateOverlay_f (void)
2402 {
2403 	const char* arg;
2404 
2405 	if (cgi->Cmd_Argc() != 2) {
2406 		Com_Printf("Usage: %s <nations|xvi|radar>\n", cgi->Cmd_Argv(0));
2407 		return;
2408 	}
2409 
2410 	arg = cgi->Cmd_Argv(1);
2411 	GEO_DeactivateOverlay(arg);
2412 }
2413 
2414 /**
2415  * @brief Initialise MAP/Geoscape
2416  */
GEO_InitStartup(void)2417 void GEO_InitStartup (void)
2418 {
2419 	cgi->Cmd_AddCommand("multi_select_click", GEO_MultiSelectExecuteAction_f, nullptr);
2420 	cgi->Cmd_AddCommand("map_overlay", GEO_SetOverlay_f, "Set the geoscape overlay");
2421 	cgi->Cmd_AddCommand("map_deactivateoverlay", GEO_DeactivateOverlay_f, "Deactivate overlay");
2422 	cgi->Cmd_AddCommand("map_selectobject", GEO_SelectObject_f, "Select an object and center on it");
2423 	cgi->Cmd_AddCommand("mn_mapaction_reset", GEO_ResetAction, nullptr);
2424 
2425 #ifdef DEBUG
2426 	debug_showInterest = cgi->Cvar_Get("debug_showinterest", "0", CVAR_DEVELOPER, "Shows the global interest value on geoscape");
2427 #endif
2428 }
2429