1 /*
2  Copyright (c) 2013 yvt
3  based on code of pysnip (c) Mathias Kaerlev 2011-2012.
4 
5  This file is part of OpenSpades.
6 
7  OpenSpades is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  OpenSpades is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with OpenSpades.  If not, see <http://www.gnu.org/licenses/>.
19 
20  */
21 
22 #include <cstdlib>
23 
24 #include "Client.h"
25 
26 #include <Core/Bitmap.h>
27 #include <Core/ConcurrentDispatch.h>
28 #include <Core/FileManager.h>
29 #include <Core/Settings.h>
30 #include <Core/Strings.h>
31 
32 #include "IAudioChunk.h"
33 #include "IAudioDevice.h"
34 
35 #include "CenterMessageView.h"
36 #include "ChatWindow.h"
37 #include "ClientPlayer.h"
38 #include "ClientUI.h"
39 #include "Corpse.h"
40 #include "FallingBlock.h"
41 #include "Fonts.h"
42 #include "HurtRingView.h"
43 #include "IFont.h"
44 #include "ILocalEntity.h"
45 #include "LimboView.h"
46 #include "MapView.h"
47 #include "PaletteView.h"
48 #include "ParticleSpriteEntity.h"
49 #include "ScoreboardView.h"
50 #include "SmokeSpriteEntity.h"
51 #include "TCProgressView.h"
52 #include "Tracer.h"
53 #include "IGameMode.h"
54 #include "CTFGameMode.h"
55 
56 #include "GameMap.h"
57 #include "Grenade.h"
58 #include "Weapon.h"
59 #include "World.h"
60 
61 #include "NetClient.h"
62 
63 DEFINE_SPADES_SETTING(cg_hitIndicator, "1");
64 DEFINE_SPADES_SETTING(cg_debugAim, "0");
65 SPADES_SETTING(cg_keyReloadWeapon);
66 SPADES_SETTING(cg_keyJump);
67 SPADES_SETTING(cg_keyAttack);
68 SPADES_SETTING(cg_keyAltAttack);
69 SPADES_SETTING(cg_keyCrouch);
70 DEFINE_SPADES_SETTING(cg_screenshotFormat, "jpeg");
71 DEFINE_SPADES_SETTING(cg_stats, "0");
72 DEFINE_SPADES_SETTING(cg_hideHud, "0");
73 DEFINE_SPADES_SETTING(cg_playerNames, "2");
74 DEFINE_SPADES_SETTING(cg_playerNameX, "0");
75 DEFINE_SPADES_SETTING(cg_playerNameY, "0");
76 
77 namespace spades {
78 	namespace client {
79 
80 		enum class ScreenshotFormat { Jpeg, Targa, Png };
81 
82 		namespace {
GetScreenshotFormat()83 			ScreenshotFormat GetScreenshotFormat() {
84 				if (EqualsIgnoringCase(cg_screenshotFormat, "jpeg")) {
85 					return ScreenshotFormat::Jpeg;
86 				} else if (EqualsIgnoringCase(cg_screenshotFormat, "targa")) {
87 					return ScreenshotFormat::Targa;
88 				} else if (EqualsIgnoringCase(cg_screenshotFormat, "png")) {
89 					return ScreenshotFormat::Png;
90 				} else {
91 					SPRaise("Invalid screenshot format: %s", cg_screenshotFormat.CString());
92 				}
93 			}
94 
TranslateKeyName(const std::string & name)95 			std::string TranslateKeyName(const std::string &name) {
96 				if (name == "LeftMouseButton") {
97 					return "LMB";
98 				} else if (name == "RightMouseButton") {
99 					return "RMB";
100 				} else if (name.empty()) {
101 					return _Tr("Client", "Unbound");
102 				} else {
103 					return name;
104 				}
105 			}
106 		}
107 
TakeScreenShot(bool sceneOnly)108 		void Client::TakeScreenShot(bool sceneOnly) {
109 			SceneDefinition sceneDef = CreateSceneDefinition();
110 			lastSceneDef = sceneDef;
111 
112 			// render scene
113 			flashDlights = flashDlightsOld;
114 			DrawScene();
115 
116 			// draw 2d
117 			if (!sceneOnly)
118 				Draw2D();
119 
120 			// Well done!
121 			renderer->FrameDone();
122 
123 			Handle<Bitmap> bmp(renderer->ReadBitmap(), false);
124 			// force 100% opacity
125 
126 			uint32_t *pixels = bmp->GetPixels();
127 			for (size_t i = bmp->GetWidth() * bmp->GetHeight(); i > 0; i--) {
128 				*(pixels++) |= 0xff000000UL;
129 			}
130 
131 			try {
132 				std::string name = ScreenShotPath();
133 				bmp->Save(name);
134 
135 				std::string msg;
136 				if (sceneOnly)
137 					msg = _Tr("Client", "Sceneshot saved: {0}", name);
138 				else
139 					msg = _Tr("Client", "Screenshot saved: {0}", name);
140 				ShowAlert(msg, AlertType::Notice);
141 			} catch (const Exception &ex) {
142 				std::string msg;
143 				msg = _Tr("Client", "Screenshot failed: ");
144 				msg += ex.GetShortMessage();
145 				ShowAlert(msg, AlertType::Error);
146 				SPLog("Screenshot failed: %s", ex.what());
147 			} catch (const std::exception &ex) {
148 				std::string msg;
149 				msg = _Tr("Client", "Screenshot failed: ");
150 				msg += ex.what();
151 				ShowAlert(msg, AlertType::Error);
152 				SPLog("Screenshot failed: %s", ex.what());
153 			}
154 		}
155 
ScreenShotPath()156 		std::string Client::ScreenShotPath() {
157 			char bufJpeg[256];
158 			char bufTarga[256];
159 			char bufPng[256];
160 			for (int i = 0; i < 10000; i++) {
161 				sprintf(bufJpeg, "Screenshots/shot%04d.jpg", nextScreenShotIndex);
162 				sprintf(bufTarga, "Screenshots/shot%04d.tga", nextScreenShotIndex);
163 				sprintf(bufPng, "Screenshots/shot%04d.png", nextScreenShotIndex);
164 				if (FileManager::FileExists(bufJpeg) || FileManager::FileExists(bufTarga) ||
165 				    FileManager::FileExists(bufPng)) {
166 					nextScreenShotIndex++;
167 					if (nextScreenShotIndex >= 10000)
168 						nextScreenShotIndex = 0;
169 					continue;
170 				}
171 
172 				switch (GetScreenshotFormat()) {
173 					case ScreenshotFormat::Jpeg: return bufJpeg;
174 					case ScreenshotFormat::Targa: return bufTarga;
175 					case ScreenshotFormat::Png: return bufPng;
176 				}
177 				SPAssert(false);
178 			}
179 
180 			SPRaise("No free file name");
181 		}
182 
183 #pragma mark - HUD Drawings
184 
DrawSplash()185 		void Client::DrawSplash() {
186 			Handle<IImage> img;
187 			Vector2 siz;
188 			Vector2 scrSize = {renderer->ScreenWidth(), renderer->ScreenHeight()};
189 
190 			renderer->SetColorAlphaPremultiplied(MakeVector4(0, 0, 0, 1));
191 			img = renderer->RegisterImage("Gfx/White.tga");
192 			renderer->DrawImage(img, AABB2(0, 0, scrSize.x, scrSize.y));
193 
194 			renderer->SetColorAlphaPremultiplied(MakeVector4(1, 1, 1, 1.));
195 			img = renderer->RegisterImage("Gfx/Title/Logo.png");
196 
197 			siz = MakeVector2(img->GetWidth(), img->GetHeight());
198 			siz *= std::min(1.f, scrSize.x / siz.x * 0.5f);
199 			siz *= std::min(1.f, scrSize.y / siz.y);
200 
201 			renderer->DrawImage(
202 			  img, AABB2((scrSize.x - siz.x) * .5f, (scrSize.y - siz.y) * .5f, siz.x, siz.y));
203 		}
204 
DrawStartupScreen()205 		void Client::DrawStartupScreen() {
206 			Handle<IImage> img;
207 			Vector2 scrSize = {renderer->ScreenWidth(), renderer->ScreenHeight()};
208 
209 			renderer->SetColorAlphaPremultiplied(MakeVector4(0, 0, 0, 1.));
210 			img = renderer->RegisterImage("Gfx/White.tga");
211 			renderer->DrawImage(img, AABB2(0, 0, scrSize.x, scrSize.y));
212 
213 			DrawSplash();
214 
215 			IFont *font = fontManager->GetGuiFont();
216 			std::string str = _Tr("Client", "NOW LOADING");
217 			Vector2 size = font->Measure(str);
218 			Vector2 pos = MakeVector2(scrSize.x - 16.f, scrSize.y - 16.f);
219 			pos -= size;
220 			font->DrawShadow(str, pos, 1.f, MakeVector4(1, 1, 1, 1), MakeVector4(0, 0, 0, 0.5));
221 
222 			renderer->FrameDone();
223 			renderer->Flip();
224 		}
225 
DrawDisconnectScreen()226 		void Client::DrawDisconnectScreen() {}
227 
DrawHurtSprites()228 		void Client::DrawHurtSprites() {
229 			float per = (world->GetTime() - lastHurtTime) / 1.5f;
230 			if (per > 1.f)
231 				return;
232 			if (per < 0.f)
233 				return;
234 			Handle<IImage> img = renderer->RegisterImage("Gfx/HurtSprite.png");
235 
236 			Vector2 scrSize = {renderer->ScreenWidth(), renderer->ScreenHeight()};
237 			Vector2 scrCenter = scrSize * .5f;
238 			float radius = scrSize.GetLength() * .5f;
239 
240 			for (size_t i = 0; i < hurtSprites.size(); i++) {
241 				HurtSprite &spr = hurtSprites[i];
242 				float alpha = spr.strength - per;
243 				if (alpha < 0.f)
244 					continue;
245 				if (alpha > 1.f)
246 					alpha = 1.f;
247 
248 				Vector2 radDir = {cosf(spr.angle), sinf(spr.angle)};
249 				Vector2 angDir = {-sinf(spr.angle), cosf(spr.angle)};
250 				float siz = spr.scale * radius;
251 				Vector2 base = radDir * radius + scrCenter;
252 				Vector2 centVect = radDir * (-siz);
253 				Vector2 sideVect1 = angDir * (siz * 4.f * (spr.horzShift));
254 				Vector2 sideVect2 = angDir * (siz * 4.f * (spr.horzShift - 1.f));
255 
256 				Vector2 v1 = base + centVect + sideVect1;
257 				Vector2 v2 = base + centVect + sideVect2;
258 				Vector2 v3 = base + sideVect1;
259 
260 				renderer->SetColorAlphaPremultiplied(MakeVector4(0.f, 0.f, 0.f, alpha));
261 				renderer->DrawImage(img, v1, v2, v3,
262 				                    AABB2(0, 8.f, img->GetWidth(), img->GetHeight()));
263 			}
264 		}
265 
DrawHurtScreenEffect()266 		void Client::DrawHurtScreenEffect() {
267 			SPADES_MARK_FUNCTION();
268 
269 			float scrWidth = renderer->ScreenWidth();
270 			float scrHeight = renderer->ScreenHeight();
271 			float wTime = world->GetTime();
272 			Player *p = GetWorld()->GetLocalPlayer();
273 			if (wTime < lastHurtTime + .35f && wTime >= lastHurtTime) {
274 				float per = (wTime - lastHurtTime) / .35f;
275 				per = 1.f - per;
276 				per *= .3f + (1.f - p->GetHealth() / 100.f) * .7f;
277 				per = std::min(per, 0.9f);
278 				per = 1.f - per;
279 				Vector3 color = {1.f, per, per};
280 				renderer->MultiplyScreenColor(color);
281 				renderer->SetColorAlphaPremultiplied(
282 				  MakeVector4((1.f - per) * .1f, 0, 0, (1.f - per) * .1f));
283 				renderer->DrawImage(renderer->RegisterImage("Gfx/White.tga"),
284 				                    AABB2(0, 0, scrWidth, scrHeight));
285 			}
286 		}
287 
DrawHottrackedPlayerName()288 		void Client::DrawHottrackedPlayerName() {
289 			SPADES_MARK_FUNCTION();
290 
291 			if ((int)cg_playerNames == 0)
292 				return;
293 
294 			Player *p = GetWorld()->GetLocalPlayer();
295 
296 			hitTag_t tag = hit_None;
297 			Player *hottracked = HotTrackedPlayer(&tag);
298 			if (hottracked) {
299 				Vector3 posxyz = Project(hottracked->GetEye());
300 				Vector2 pos = {posxyz.x, posxyz.y};
301 				char buf[64];
302 				if ((int)cg_playerNames == 1) {
303 					float dist = (hottracked->GetEye() - p->GetEye()).GetLength();
304 					int idist = (int)floorf(dist + .5f);
305 					sprintf(buf, "%s [%d%s]", hottracked->GetName().c_str(), idist,
306 					        (idist == 1) ? "block" : "blocks");
307 				} else
308 					sprintf(buf, "%s", hottracked->GetName().c_str());
309 
310 				pos.y += (int)cg_playerNameY;
311 				pos.x += (int)cg_playerNameX;
312 
313 				IFont *font = fontManager->GetGuiFont();
314 				Vector2 size = font->Measure(buf);
315 				pos.x -= size.x * .5f;
316 				pos.y -= size.y;
317 				font->DrawShadow(buf, pos, 1.f, MakeVector4(1, 1, 1, 1), MakeVector4(0, 0, 0, 0.5));
318 			}
319 		}
320 
DrawDebugAim()321 		void Client::DrawDebugAim() {
322 			SPADES_MARK_FUNCTION();
323 
324 			// float scrWidth = renderer->ScreenWidth();
325 			// float scrHeight = renderer->ScreenHeight();
326 			// float wTime = world->GetTime();
327 			Player &p = GetCameraTargetPlayer();
328 			// IFont *font;
329 
330 			Weapon &w = *p.GetWeapon();
331 			float spread = w.GetSpread();
332 
333 			AABB2 boundary(0, 0, 0, 0);
334 			for (int i = 0; i < 8; i++) {
335 				Vector3 vec = p.GetFront();
336 				if (i & 1)
337 					vec.x += spread;
338 				else
339 					vec.x -= spread;
340 				if (i & 2)
341 					vec.y += spread;
342 				else
343 					vec.y -= spread;
344 				if (i & 4)
345 					vec.z += spread;
346 				else
347 					vec.z -= spread;
348 
349 				Vector3 viewPos;
350 				viewPos.x = Vector3::Dot(vec, p.GetRight());
351 				viewPos.y = Vector3::Dot(vec, p.GetUp());
352 				viewPos.z = Vector3::Dot(vec, p.GetFront());
353 
354 				Vector2 p;
355 				p.x = viewPos.x / viewPos.z;
356 				p.y = viewPos.y / viewPos.z;
357 				boundary.min.x = std::min(boundary.min.x, p.x);
358 				boundary.min.y = std::min(boundary.min.y, p.y);
359 				boundary.max.x = std::max(boundary.max.x, p.x);
360 				boundary.max.y = std::max(boundary.max.y, p.y);
361 			}
362 
363 			Handle<IImage> img = renderer->RegisterImage("Gfx/White.tga");
364 			boundary.min *= renderer->ScreenHeight() * .5f;
365 			boundary.max *= renderer->ScreenHeight() * .5f;
366 			boundary.min /= tanf(lastSceneDef.fovY * .5f);
367 			boundary.max /= tanf(lastSceneDef.fovY * .5f);
368 			IntVector3 cent;
369 			cent.x = (int)(renderer->ScreenWidth() * .5f);
370 			cent.y = (int)(renderer->ScreenHeight() * .5f);
371 
372 			IntVector3 p1 = cent;
373 			IntVector3 p2 = cent;
374 
375 			p1.x += (int)floorf(boundary.min.x);
376 			p1.y += (int)floorf(boundary.min.y);
377 			p2.x += (int)ceilf(boundary.max.x);
378 			p2.y += (int)ceilf(boundary.max.y);
379 
380 			renderer->SetColorAlphaPremultiplied(MakeVector4(0, 0, 0, 1));
381 			renderer->DrawImage(img, AABB2(p1.x - 2, p1.y - 2, p2.x - p1.x + 4, 1));
382 			renderer->DrawImage(img, AABB2(p1.x - 2, p1.y - 2, 1, p2.y - p1.y + 4));
383 			renderer->DrawImage(img, AABB2(p1.x - 2, p2.y + 1, p2.x - p1.x + 4, 1));
384 			renderer->DrawImage(img, AABB2(p2.x + 1, p1.y - 2, 1, p2.y - p1.y + 4));
385 
386 			renderer->SetColorAlphaPremultiplied(MakeVector4(1, 1, 1, 1));
387 			renderer->DrawImage(img, AABB2(p1.x - 1, p1.y - 1, p2.x - p1.x + 2, 1));
388 			renderer->DrawImage(img, AABB2(p1.x - 1, p1.y - 1, 1, p2.y - p1.y + 2));
389 			renderer->DrawImage(img, AABB2(p1.x - 1, p2.y, p2.x - p1.x + 2, 1));
390 			renderer->DrawImage(img, AABB2(p2.x, p1.y - 1, 1, p2.y - p1.y + 2));
391 		}
392 
DrawFirstPersonHUD()393 		void Client::DrawFirstPersonHUD() {
394 			SPADES_MARK_FUNCTION();
395 
396 			float scrWidth = renderer->ScreenWidth();
397 			float scrHeight = renderer->ScreenHeight();
398 
399 			Player &player = GetCameraTargetPlayer();
400 			int playerId = GetCameraTargetPlayerId();
401 
402 			clientPlayers[playerId]->Draw2D();
403 
404 			if (cg_hitIndicator && hitFeedbackIconState > 0.f && !cg_hideHud) {
405 				Handle<IImage> img(renderer->RegisterImage("Gfx/HitFeedback.png"), false);
406 				Vector2 pos = {scrWidth * .5f, scrHeight * .5f};
407 				pos.x -= img->GetWidth() * .5f;
408 				pos.y -= img->GetHeight() * .5f;
409 
410 				float op = hitFeedbackIconState;
411 				Vector4 color;
412 				if (hitFeedbackFriendly) {
413 					color = MakeVector4(0.02f, 1.f, 0.02f, 1.f);
414 				} else {
415 					color = MakeVector4(1.f, 0.02f, 0.04f, 1.f);
416 				}
417 				color *= op;
418 
419 				renderer->SetColorAlphaPremultiplied(color);
420 
421 				renderer->DrawImage(img, pos);
422 			}
423 
424 			// If the player has the intel, display an intel icon
425 			IGameMode &mode = *world->GetMode();
426 			if (mode.ModeType() == IGameMode::m_CTF) {
427 				auto &ctfMode = static_cast<CTFGameMode &>(mode);
428 				if (ctfMode.PlayerHasIntel(*world, player)) {
429 					Handle<IImage> img(renderer->RegisterImage("Gfx/Intel.png"), false);
430 
431 					// Strobe
432 					Vector4 color {1.0f, 1.0f, 1.0f, 1.0f};
433 					color *= std::fabs(std::sin(world->GetTime() * 2.0f));
434 
435 					renderer->SetColorAlphaPremultiplied(color);
436 
437 					renderer->DrawImage(img, Vector2{scrWidth - 260.f, scrHeight - 64.0f});
438 				}
439 			}
440 
441 			if (cg_debugAim && player.GetTool() == Player::ToolWeapon && player.IsAlive()) {
442 				DrawDebugAim();
443 			}
444 		}
445 
DrawJoinedAlivePlayerHUD()446 		void Client::DrawJoinedAlivePlayerHUD() {
447 			SPADES_MARK_FUNCTION();
448 
449 			float scrWidth = renderer->ScreenWidth();
450 			float scrHeight = renderer->ScreenHeight();
451 			Player *p = GetWorld()->GetLocalPlayer();
452 			IFont *font;
453 
454 			// Draw damage rings
455 			if (!cg_hideHud)
456 				hurtRingView->Draw();
457 
458 			if (!cg_hideHud) {
459 				// Draw ammo amount
460 				// (Note: this cannot be displayed for a spectated player --- the server
461 				//        does not submit sufficient information)
462 				Weapon *weap = p->GetWeapon();
463 				Handle<IImage> ammoIcon;
464 				float iconWidth, iconHeight;
465 				float spacing = 2.f;
466 				int stockNum;
467 				int warnLevel;
468 
469 				if (p->IsToolWeapon()) {
470 					switch (weap->GetWeaponType()) {
471 						case RIFLE_WEAPON:
472 							ammoIcon = renderer->RegisterImage("Gfx/Bullet/7.62mm.png");
473 							iconWidth = 6.f;
474 							iconHeight = iconWidth * 4.f;
475 							break;
476 						case SMG_WEAPON:
477 							ammoIcon = renderer->RegisterImage("Gfx/Bullet/9mm.png");
478 							iconWidth = 4.f;
479 							iconHeight = iconWidth * 4.f;
480 							break;
481 						case SHOTGUN_WEAPON:
482 							ammoIcon = renderer->RegisterImage("Gfx/Bullet/12gauge.png");
483 							iconWidth = 30.f;
484 							iconHeight = iconWidth / 4.f;
485 							spacing = -6.f;
486 							break;
487 						default: SPInvalidEnum("weap->GetWeaponType()", weap->GetWeaponType());
488 					}
489 
490 					int clipSize = weap->GetClipSize();
491 					int clip = weap->GetAmmo();
492 
493 					clipSize = std::max(clipSize, clip);
494 
495 					for (int i = 0; i < clipSize; i++) {
496 						float x = scrWidth - 16.f - (float)(i + 1) * (iconWidth + spacing);
497 						float y = scrHeight - 16.f - iconHeight;
498 
499 						if (clip >= i + 1) {
500 							renderer->SetColorAlphaPremultiplied(MakeVector4(1, 1, 1, 1));
501 						} else {
502 							renderer->SetColorAlphaPremultiplied(MakeVector4(0.4, 0.4, 0.4, 1));
503 						}
504 
505 						renderer->DrawImage(ammoIcon, AABB2(x, y, iconWidth, iconHeight));
506 					}
507 
508 					stockNum = weap->GetStock();
509 					warnLevel = weap->GetMaxStock() / 3;
510 				} else {
511 					iconHeight = 0.f;
512 					warnLevel = 0;
513 
514 					switch (p->GetTool()) {
515 						case Player::ToolSpade:
516 						case Player::ToolBlock: stockNum = p->GetNumBlocks(); break;
517 						case Player::ToolGrenade: stockNum = p->GetNumGrenades(); break;
518 						default: SPInvalidEnum("p->GetTool()", p->GetTool());
519 					}
520 				}
521 
522 				Vector4 numberColor = {1, 1, 1, 1};
523 
524 				if (stockNum == 0) {
525 					numberColor.y = 0.3f;
526 					numberColor.z = 0.3f;
527 				} else if (stockNum <= warnLevel) {
528 					numberColor.z = 0.3f;
529 				}
530 
531 				char buf[64];
532 				sprintf(buf, "%d", stockNum);
533 				font = fontManager->GetSquareDesignFont();
534 				std::string stockStr = buf;
535 				Vector2 size = font->Measure(stockStr);
536 				Vector2 pos = MakeVector2(scrWidth - 16.f, scrHeight - 16.f - iconHeight);
537 				pos -= size;
538 				font->DrawShadow(stockStr, pos, 1.f, numberColor, MakeVector4(0, 0, 0, 0.5));
539 
540 				// draw "press ... to reload"
541 				{
542 					std::string msg = "";
543 
544 					switch (p->GetTool()) {
545 						case Player::ToolBlock:
546 							if (p->GetNumBlocks() == 0) {
547 								msg = _Tr("Client", "Out of Block");
548 							}
549 							break;
550 						case Player::ToolGrenade:
551 							if (p->GetNumGrenades() == 0) {
552 								msg = _Tr("Client", "Out of Grenade");
553 							}
554 							break;
555 						case Player::ToolWeapon: {
556 							Weapon *weap = p->GetWeapon();
557 							if (weap->IsReloading() || p->IsAwaitingReloadCompletion()) {
558 								msg = _Tr("Client", "Reloading");
559 							} else if (weap->GetAmmo() == 0 && weap->GetStock() == 0) {
560 								msg = _Tr("Client", "Out of Ammo");
561 							} else if (weap->GetStock() > 0 &&
562 							           weap->GetAmmo() < weap->GetClipSize() / 4) {
563 								msg = _Tr("Client", "Press [{0}] to Reload",
564 								          TranslateKeyName(cg_keyReloadWeapon));
565 							}
566 						} break;
567 						default:;
568 							// no message
569 					}
570 
571 					if (!msg.empty()) {
572 						font = fontManager->GetGuiFont();
573 						Vector2 size = font->Measure(msg);
574 						Vector2 pos = MakeVector2((scrWidth - size.x) * .5f, scrHeight * 2.f / 3.f);
575 						font->DrawShadow(msg, pos, 1.f, MakeVector4(1, 1, 1, 1),
576 						                 MakeVector4(0, 0, 0, 0.5));
577 					}
578 				}
579 
580 				if (p->GetTool() == Player::ToolBlock) {
581 					paletteView->Draw();
582 				}
583 
584 				// draw map
585 				mapView->Draw();
586 
587 				DrawHealth();
588 			}
589 		}
590 
DrawDeadPlayerHUD()591 		void Client::DrawDeadPlayerHUD() {
592 			SPADES_MARK_FUNCTION();
593 
594 			Player *p = GetWorld()->GetLocalPlayer();
595 			IFont *font;
596 			float scrWidth = renderer->ScreenWidth();
597 			float scrHeight = renderer->ScreenHeight();
598 
599 			if (!cg_hideHud) {
600 				// draw respawn tme
601 				if (!p->IsAlive()) {
602 					std::string msg;
603 
604 					float secs = p->GetRespawnTime() - world->GetTime();
605 
606 					if (secs > 0.f)
607 						msg = _Tr("Client", "You will respawn in: {0}", (int)ceilf(secs));
608 					else
609 						msg = _Tr("Client", "Waiting for respawn");
610 
611 					if (!msg.empty()) {
612 						font = fontManager->GetGuiFont();
613 						Vector2 size = font->Measure(msg);
614 						Vector2 pos = MakeVector2((scrWidth - size.x) * .5f, scrHeight / 3.f);
615 
616 						font->DrawShadow(msg, pos, 1.f, MakeVector4(1, 1, 1, 1),
617 						                 MakeVector4(0, 0, 0, 0.5));
618 					}
619 				}
620 			}
621 		}
622 
DrawSpectateHUD()623 		void Client::DrawSpectateHUD() {
624 			SPADES_MARK_FUNCTION();
625 
626 			if (cg_hideHud) {
627 				return;
628 			}
629 
630 			IFont &font = *fontManager->GetGuiFont();
631 			float scrWidth = renderer->ScreenWidth();
632 
633 			float textX = scrWidth - 8.0f;
634 			float textY = 256.0f + 32.0f;
635 
636 			auto addLine = [&](const std::string &text) {
637 				Vector2 size = font.Measure(text);
638 				Vector2 pos = MakeVector2(textX, textY);
639 				pos.x -= size.x;
640 				textY += 20.0f;
641 				font.DrawShadow(text, pos, 1.f, MakeVector4(1, 1, 1, 1), MakeVector4(0, 0, 0, 0.5));
642 			};
643 
644 			if (HasTargetPlayer(GetCameraMode())) {
645 				addLine(_Tr("Client", "Following {0}",
646 				            world->GetPlayerPersistent(GetCameraTargetPlayerId()).name));
647 			}
648 
649 			textY += 10.0f;
650 
651 			// Help messages (make sure to synchronize these with the keyboard input handler)
652 			if (FollowsNonLocalPlayer(GetCameraMode())) {
653 				if (GetCameraTargetPlayer().IsAlive()) {
654 					addLine(_Tr("Client", "[{0}] Cycle camera mode", TranslateKeyName(cg_keyJump)));
655 				}
656 				addLine(_Tr("Client", "[{0}/{1}] Next/previous player",
657 				            TranslateKeyName(cg_keyAttack), TranslateKeyName(cg_keyAltAttack)));
658 
659 				if (GetWorld()->GetLocalPlayer()->IsSpectator()) {
660 					addLine(_Tr("Client", "[{0}] Unfollow", TranslateKeyName(cg_keyReloadWeapon)));
661 				}
662 			} else {
663 				addLine(_Tr("Client", "[{0}/{1}] Follow a player", TranslateKeyName(cg_keyAttack),
664 				            TranslateKeyName(cg_keyAltAttack)));
665 			}
666 
667 			if (GetCameraMode() == ClientCameraMode::Free) {
668 				addLine(_Tr("Client", "[{0}/{1}] Go up/down", TranslateKeyName(cg_keyJump),
669 				            TranslateKeyName(cg_keyCrouch)));
670 			}
671 
672 			mapView->Draw();
673 		}
674 
DrawAlert()675 		void Client::DrawAlert() {
676 			SPADES_MARK_FUNCTION();
677 
678 			IFont *font = fontManager->GetGuiFont();
679 			float scrWidth = renderer->ScreenWidth();
680 			float scrHeight = renderer->ScreenHeight();
681 			auto &r = renderer;
682 
683 			const float fadeOutTime = 1.f;
684 
685 			float fade = 1.f - (time - alertDisappearTime) / fadeOutTime;
686 			fade = std::min(fade, 1.f);
687 			if (fade <= 0.f) {
688 				return;
689 			}
690 
691 			float borderFade = 1.f - (time - alertAppearTime) * 1.5f;
692 			borderFade = std::max(std::min(borderFade, 1.f), 0.f);
693 			borderFade *= fade;
694 
695 			Handle<IImage> alertIcon(renderer->RegisterImage("Gfx/AlertIcon.png"), false);
696 
697 			Vector2 textSize = font->Measure(alertContents);
698 			Vector2 contentsSize = textSize;
699 			contentsSize.y = std::max(contentsSize.y, 16.f);
700 			if (alertType != AlertType::Notice) {
701 				contentsSize.x += 22.f;
702 			}
703 
704 			// add margin
705 			const float margin = 8.f;
706 			contentsSize.x += margin * 2.f;
707 			contentsSize.y += margin * 2.f;
708 
709 			contentsSize.x = floorf(contentsSize.x);
710 			contentsSize.y = floorf(contentsSize.y);
711 
712 			Vector2 pos = (Vector2(scrWidth, scrHeight) - contentsSize) * Vector2(0.5f, 0.7f);
713 			pos.y += 40.f;
714 
715 			pos.x = floorf(pos.x);
716 			pos.y = floorf(pos.y);
717 
718 			Vector4 color;
719 
720 			// draw border
721 			switch (alertType) {
722 				case AlertType::Notice: color = Vector4(0.f, 0.f, 0.f, 0.f); break;
723 				case AlertType::Warning: color = Vector4(1.f, 1.f, 0.f, .7f); break;
724 				case AlertType::Error: color = Vector4(1.f, 0.f, 0.f, .7f); break;
725 			}
726 			color *= borderFade;
727 			r->SetColorAlphaPremultiplied(color);
728 
729 			const float border = 1.f;
730 			r->DrawImage(nullptr, AABB2(pos.x - border, pos.y - border,
731 			                            contentsSize.x + border * 2.f, border));
732 			r->DrawImage(nullptr, AABB2(pos.x - border, pos.y + contentsSize.y,
733 			                            contentsSize.x + border * 2.f, border));
734 
735 			r->DrawImage(nullptr, AABB2(pos.x - border, pos.y, border, contentsSize.y));
736 			r->DrawImage(nullptr, AABB2(pos.x + contentsSize.x, pos.y, border, contentsSize.y));
737 
738 			// fill background
739 			color = Vector4(0.f, 0.f, 0.f, fade * 0.5f);
740 			r->SetColorAlphaPremultiplied(color);
741 			r->DrawImage(nullptr, AABB2(pos.x, pos.y, contentsSize.x, contentsSize.y));
742 
743 			// draw icon
744 			switch (alertType) {
745 				case AlertType::Notice: color = Vector4(0.f, 0.f, 0.f, 0.f); break;
746 				case AlertType::Warning: color = Vector4(1.f, 1.f, 0.f, 1.f); break;
747 				case AlertType::Error: color = Vector4(1.f, 0.f, 0.f, 1.f); break;
748 			}
749 			color *= fade;
750 			r->SetColorAlphaPremultiplied(color);
751 
752 			r->DrawImage(alertIcon,
753 			             Vector2(pos.x + margin, pos.y + (contentsSize.y - 16.f) * 0.5f));
754 
755 			// draw text
756 			color = Vector4(1.f, 1.f, 1.f, 1.f);
757 			color *= fade;
758 
759 			font->DrawShadow(alertContents, Vector2(pos.x + contentsSize.x - textSize.x - margin,
760 			                                        pos.y + (contentsSize.y - textSize.y) * 0.5f),
761 			                 1.f, color, Vector4(0.f, 0.f, 0.f, fade * 0.5f));
762 		}
763 
DrawHealth()764 		void Client::DrawHealth() {
765 			SPADES_MARK_FUNCTION();
766 
767 			Player *p = GetWorld()->GetLocalPlayer();
768 			IFont *font;
769 			// float scrWidth = renderer->ScreenWidth();
770 			float scrHeight = renderer->ScreenHeight();
771 
772 			std::string str = std::to_string(p->GetHealth());
773 
774 			Vector4 numberColor = {1, 1, 1, 1};
775 
776 			if (p->GetHealth() == 0) {
777 				numberColor.y = 0.3f;
778 				numberColor.z = 0.3f;
779 			} else if (p->GetHealth() <= 50) {
780 				numberColor.z = 0.3f;
781 			}
782 
783 			font = fontManager->GetSquareDesignFont();
784 			Vector2 size = font->Measure(str);
785 			Vector2 pos = MakeVector2(16.f, scrHeight - 16.f);
786 			pos.y -= size.y;
787 			font->DrawShadow(str, pos, 1.f, numberColor, MakeVector4(0, 0, 0, 0.5));
788 		}
789 
Draw2DWithWorld()790 		void Client::Draw2DWithWorld() {
791 			SPADES_MARK_FUNCTION();
792 
793 			for (auto &ent : localEntities) {
794 				ent->Render2D();
795 			}
796 
797 			Player *p = GetWorld()->GetLocalPlayer();
798 			if (p) {
799 				DrawHurtSprites();
800 				DrawHurtScreenEffect();
801 				DrawHottrackedPlayerName();
802 
803 				if (!cg_hideHud) {
804 					tcView->Draw();
805 
806 					if (IsFirstPerson(GetCameraMode())) {
807 						DrawFirstPersonHUD();
808 					}
809 				}
810 
811 				if (p->GetTeamId() < 2) {
812 					// player is not spectator
813 					if (p->IsAlive()) {
814 						DrawJoinedAlivePlayerHUD();
815 					} else {
816 						DrawDeadPlayerHUD();
817 						DrawSpectateHUD();
818 					}
819 				} else {
820 					DrawSpectateHUD();
821 				}
822 
823 				if (!cg_hideHud) {
824 					DrawAlert();
825 
826 					chatWindow->Draw();
827 					killfeedWindow->Draw();
828 				}
829 
830 				// large map view should come in front
831 				largeMapView->Draw();
832 
833 				// --- end "player is there" render
834 			} else {
835 				// world exists, but no local player: not joined
836 
837 				scoreboard->Draw();
838 
839 				DrawAlert();
840 			}
841 
842 			if (!cg_hideHud)
843 				centerMessageView->Draw();
844 
845 			if (scoreboardVisible || !p)
846 				scoreboard->Draw();
847 
848 			if (IsLimboViewActive())
849 				limbo->Draw();
850 		}
851 
Draw2DWithoutWorld()852 		void Client::Draw2DWithoutWorld() {
853 			SPADES_MARK_FUNCTION();
854 			// no world; loading?
855 			float scrWidth = renderer->ScreenWidth();
856 			float scrHeight = renderer->ScreenHeight();
857 			IFont *font;
858 
859 			DrawSplash();
860 
861 			Handle<IImage> img;
862 
863 			std::string msg = net->GetStatusString();
864 			font = fontManager->GetGuiFont();
865 			Vector2 textSize = font->Measure(msg);
866 			font->Draw(msg, MakeVector2(scrWidth - 16.f, scrHeight - 24.f) - textSize, 1.f,
867 			           MakeVector4(1, 1, 1, 0.95f));
868 
869 			img = renderer->RegisterImage("Gfx/White.tga");
870 			float pos = timeSinceInit / 3.6f;
871 			pos -= floorf(pos);
872 			pos = 1.f - pos * 2.0f;
873 			for (float v = 0; v < 0.6f; v += 0.14f) {
874 				float p = pos + v;
875 				if (p < 0.01f || p > .99f)
876 					continue;
877 				p = asin(p * 2.f - 1.f);
878 				p = p / (float)M_PI + 0.5f;
879 
880 				float op = p * (1.f - p) * 4.f;
881 				renderer->SetColorAlphaPremultiplied(MakeVector4(op, op, op, op));
882 				renderer->DrawImage(
883 				  img, AABB2(scrWidth - 236.f + p * 234.f, scrHeight - 18.f, 4.f, 4.f));
884 			}
885 
886 			DrawAlert();
887 		}
888 
DrawStats()889 		void Client::DrawStats() {
890 			SPADES_MARK_FUNCTION();
891 
892 			if (!cg_stats)
893 				return;
894 
895 			char buf[256];
896 			std::string str;
897 
898 			{
899 				auto fps = fpsCounter.GetFps();
900 				if (fps == 0.0)
901 					str += "--.-- fps";
902 				else {
903 					sprintf(buf, "%.02f fps", fps);
904 					str += buf;
905 				}
906 			}
907 			{
908 				// Display world updates per second
909 				auto ups = upsCounter.GetFps();
910 				if (ups == 0.0)
911 					str += ", --.-- ups";
912 				else {
913 					sprintf(buf, ", %.02f ups", ups);
914 					str += buf;
915 				}
916 			}
917 
918 			if (net) {
919 				auto ping = net->GetPing();
920 				auto upbps = net->GetUplinkBps();
921 				auto downbps = net->GetDownlinkBps();
922 				sprintf(buf, ", ping: %dms, up/down: %.02f/%.02fkbps", ping, upbps / 1000.0,
923 				        downbps / 1000.0);
924 				str += buf;
925 			}
926 
927 			float scrWidth = renderer->ScreenWidth();
928 			float scrHeight = renderer->ScreenHeight();
929 			IFont *font = fontManager->GetGuiFont();
930 			float margin = 5.f;
931 
932 			IRenderer *r = renderer;
933 			auto size = font->Measure(str);
934 			size += Vector2(margin * 2.f, margin * 2.f);
935 
936 			auto pos = (Vector2(scrWidth, scrHeight) - size) * Vector2(0.5f, 1.f);
937 
938 			r->SetColorAlphaPremultiplied(Vector4(0.f, 0.f, 0.f, 0.5f));
939 			r->DrawImage(nullptr, AABB2(pos.x, pos.y, size.x, size.y));
940 			font->DrawShadow(str, pos + Vector2(margin, margin), 1.f, Vector4(1.f, 1.f, 1.f, 1.f),
941 			                 Vector4(0.f, 0.f, 0.f, 0.5f));
942 		}
943 
Draw2D()944 		void Client::Draw2D() {
945 			SPADES_MARK_FUNCTION();
946 
947 			if (GetWorld()) {
948 				Draw2DWithWorld();
949 			} else {
950 				Draw2DWithoutWorld();
951 			}
952 
953 			DrawStats();
954 		}
955 	}
956 }
957