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 <cstdarg>
23 #include <cstdlib>
24 #include <ctime>
25 
26 #include "Client.h"
27 #include "Fonts.h"
28 #include <Core/FileManager.h>
29 #include <Core/IStream.h>
30 #include <Core/Settings.h>
31 #include <Core/Strings.h>
32 
33 #include "IAudioChunk.h"
34 #include "IAudioDevice.h"
35 
36 #include "CenterMessageView.h"
37 #include "ChatWindow.h"
38 #include "ClientPlayer.h"
39 #include "ClientUI.h"
40 #include "HurtRingView.h"
41 #include "LimboView.h"
42 #include "MapView.h"
43 #include "PaletteView.h"
44 #include "ScoreboardView.h"
45 #include "TCProgressView.h"
46 
47 #include "Corpse.h"
48 #include "ILocalEntity.h"
49 #include "SmokeSpriteEntity.h"
50 
51 #include "GameMap.h"
52 #include "GameMapWrapper.h"
53 #include "Weapon.h"
54 #include "World.h"
55 
56 #include "NetClient.h"
57 
58 DEFINE_SPADES_SETTING(cg_chatBeep, "1");
59 
60 DEFINE_SPADES_SETTING(cg_serverAlert, "1");
61 
62 DEFINE_SPADES_SETTING(cg_skipDeadPlayersWhenDead, "1");
63 
64 SPADES_SETTING(cg_playerName);
65 
66 namespace spades {
67 	namespace client {
68 
Client(IRenderer * r,IAudioDevice * audioDev,const ServerAddress & host,FontManager * fontManager)69 		Client::Client(IRenderer *r, IAudioDevice *audioDev, const ServerAddress &host,
70 		               FontManager *fontManager)
71 		    : playerName(cg_playerName.operator std::string().substr(0, 15)),
72 		      logStream(nullptr),
73 		      hostname(host),
74 		      renderer(r),
75 		      audioDevice(audioDev),
76 
77 
78 		      time(0.f),
79 		      readyToClose(false),
80 
81 
82 
83 		      worldSubFrame(0.f),
84 		      frameToRendererInit(5),
85 		      timeSinceInit(0.f),
86 		      hasLastTool(false),
87 		      lastPosSentTime(0.f),
88 		      lastAliveTime(0.f),
89 		      lastKills(0),
90 		      hasDelayedReload(false),
91 		      localFireVibrationTime(-1.f),
92 		      grenadeVibration(0.f),
93 		      grenadeVibrationSlow(0.f),
94 		      scoreboardVisible(false),
95 		      flashlightOn(false),
96 		      hitFeedbackIconState(0.f),
97 		      hitFeedbackFriendly(false),
98 		      focalLength(20.f),
99 		      targetFocalLength(20.f),
100 		      autoFocusEnabled(true),
101 
102 		      inGameLimbo(false),
103 		      fontManager(fontManager),
104 		      alertDisappearTime(-10000.f),
105 		      lastMyCorpse(nullptr),
106 		      corpseSoftTimeLimit(30.f), // FIXME: this is not used
107 		      corpseSoftLimit(6),
108 		      corpseHardLimit(16),
109 		      nextScreenShotIndex(0),
110 		      nextMapShotIndex(0) {
111 			SPADES_MARK_FUNCTION();
112 			SPLog("Initializing...");
113 
114 			renderer->SetFogDistance(128.f);
115 			renderer->SetFogColor(MakeVector3(.8f, 1.f, 1.f));
116 
117 			chatWindow.reset(new ChatWindow(this, GetRenderer(), fontManager->GetGuiFont(), false));
118 			killfeedWindow.reset(
119 			  new ChatWindow(this, GetRenderer(), fontManager->GetGuiFont(), true));
120 
121 			hurtRingView.reset(new HurtRingView(this));
122 			centerMessageView.reset(new CenterMessageView(this, fontManager->GetLargeFont()));
123 			mapView.reset(new MapView(this, false));
124 			largeMapView.reset(new MapView(this, true));
125 			scoreboard.reset(new ScoreboardView(this));
126 			limbo.reset(new LimboView(this));
127 			paletteView.reset(new PaletteView(this));
128 			tcView.reset(new TCProgressView(this));
129 			scriptedUI.Set(new ClientUI(renderer, audioDev, fontManager, this), false);
130 
131 			renderer->SetGameMap(nullptr);
132 		}
133 
SetWorld(spades::client::World * w)134 		void Client::SetWorld(spades::client::World *w) {
135 			SPADES_MARK_FUNCTION();
136 
137 			if (world.get() == w) {
138 				return;
139 			}
140 
141 			scriptedUI->CloseUI();
142 
143 			RemoveAllCorpses();
144 			RemoveAllLocalEntities();
145 
146 			lastHealth = 0;
147 			lastHurtTime = -100.f;
148 			hurtRingView->ClearAll();
149 			scoreboardVisible = false;
150 			flashlightOn = false;
151 
152 			for (size_t i = 0; i < clientPlayers.size(); i++) {
153 				if (clientPlayers[i]) {
154 					clientPlayers[i]->Invalidate();
155 				}
156 			}
157 			clientPlayers.clear();
158 
159 			if (world) {
160 				world->SetListener(nullptr);
161 				renderer->SetGameMap(nullptr);
162 				audioDevice->SetGameMap(nullptr);
163 				world = nullptr;
164 				map = nullptr;
165 			}
166 			world.reset(w);
167 			if (world) {
168 				SPLog("World set");
169 
170 				// initialize player view objects
171 				clientPlayers.resize(world->GetNumPlayerSlots());
172 				for (size_t i = 0; i < world->GetNumPlayerSlots(); i++) {
173 					Player *p = world->GetPlayer(static_cast<unsigned int>(i));
174 					if (p) {
175 						clientPlayers[i] = new ClientPlayer(p, this);
176 					} else {
177 						clientPlayers[i] = nullptr;
178 					}
179 				}
180 
181 				world->SetListener(this);
182 				map = world->GetMap();
183 				renderer->SetGameMap(map);
184 				audioDevice->SetGameMap(map);
185 				NetLog("------ World Loaded ------");
186 			} else {
187 
188 				SPLog("World removed");
189 				NetLog("------ World Unloaded ------");
190 			}
191 
192 			limbo->SetSelectedTeam(2);
193 			limbo->SetSelectedWeapon(RIFLE_WEAPON);
194 
195 			worldSubFrame = 0.f;
196 			worldSetTime = time;
197 			inGameLimbo = false;
198 		}
199 
~Client()200 		Client::~Client() {
201 			SPADES_MARK_FUNCTION();
202 
203 			NetLog("Disconnecting");
204 
205 			DrawDisconnectScreen();
206 
207 			if (logStream) {
208 				SPLog("Closing netlog");
209 				logStream.reset();
210 			}
211 
212 			if (net) {
213 				SPLog("Disconnecting");
214 				net->Disconnect();
215 				net.reset();
216 			}
217 
218 			SPLog("Disconnected");
219 
220 			RemoveAllLocalEntities();
221 			RemoveAllCorpses();
222 
223 			renderer->SetGameMap(nullptr);
224 			audioDevice->SetGameMap(nullptr);
225 
226 			for (size_t i = 0; i < clientPlayers.size(); i++) {
227 				if (clientPlayers[i]) {
228 					clientPlayers[i]->Invalidate();
229 				}
230 			}
231 			clientPlayers.clear();
232 
233 			scriptedUI->ClientDestroyed();
234 			tcView.reset();
235 			limbo.reset();
236 			scoreboard.reset();
237 			mapView.reset();
238 			largeMapView.reset();
239 			chatWindow.reset();
240 			killfeedWindow.reset();
241 			paletteView.reset();
242 			centerMessageView.reset();
243 			hurtRingView.reset();
244 			world.reset();
245 		}
246 
247 		/** Initiate an initialization which likely to take some time */
DoInit()248 		void Client::DoInit() {
249 			renderer->Init();
250 			SmokeSpriteEntity::Preload(renderer);
251 
252 			renderer->RegisterImage("Textures/Fluid.png");
253 			renderer->RegisterImage("Textures/WaterExpl.png");
254 			renderer->RegisterImage("Gfx/White.tga");
255 			audioDevice->RegisterSound("Sounds/Weapons/Block/Build.opus");
256 			audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal1.opus");
257 			audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal2.opus");
258 			audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal3.opus");
259 			audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal4.opus");
260 			audioDevice->RegisterSound("Sounds/Misc/SwitchMapZoom.opus");
261 			audioDevice->RegisterSound("Sounds/Misc/OpenMap.opus");
262 			audioDevice->RegisterSound("Sounds/Misc/CloseMap.opus");
263 			audioDevice->RegisterSound("Sounds/Player/Flashlight.opus");
264 			audioDevice->RegisterSound("Sounds/Player/Footstep1.opus");
265 			audioDevice->RegisterSound("Sounds/Player/Footstep2.opus");
266 			audioDevice->RegisterSound("Sounds/Player/Footstep3.opus");
267 			audioDevice->RegisterSound("Sounds/Player/Footstep4.opus");
268 			audioDevice->RegisterSound("Sounds/Player/Footstep5.opus");
269 			audioDevice->RegisterSound("Sounds/Player/Footstep6.opus");
270 			audioDevice->RegisterSound("Sounds/Player/Footstep7.opus");
271 			audioDevice->RegisterSound("Sounds/Player/Footstep8.opus");
272 			audioDevice->RegisterSound("Sounds/Player/Wade1.opus");
273 			audioDevice->RegisterSound("Sounds/Player/Wade2.opus");
274 			audioDevice->RegisterSound("Sounds/Player/Wade3.opus");
275 			audioDevice->RegisterSound("Sounds/Player/Wade4.opus");
276 			audioDevice->RegisterSound("Sounds/Player/Wade5.opus");
277 			audioDevice->RegisterSound("Sounds/Player/Wade6.opus");
278 			audioDevice->RegisterSound("Sounds/Player/Wade7.opus");
279 			audioDevice->RegisterSound("Sounds/Player/Wade8.opus");
280 			audioDevice->RegisterSound("Sounds/Player/Run1.opus");
281 			audioDevice->RegisterSound("Sounds/Player/Run2.opus");
282 			audioDevice->RegisterSound("Sounds/Player/Run3.opus");
283 			audioDevice->RegisterSound("Sounds/Player/Run4.opus");
284 			audioDevice->RegisterSound("Sounds/Player/Run5.opus");
285 			audioDevice->RegisterSound("Sounds/Player/Run6.opus");
286 			audioDevice->RegisterSound("Sounds/Player/Run7.opus");
287 			audioDevice->RegisterSound("Sounds/Player/Run8.opus");
288 			audioDevice->RegisterSound("Sounds/Player/Run9.opus");
289 			audioDevice->RegisterSound("Sounds/Player/Run10.opus");
290 			audioDevice->RegisterSound("Sounds/Player/Run11.opus");
291 			audioDevice->RegisterSound("Sounds/Player/Run12.opus");
292 			audioDevice->RegisterSound("Sounds/Player/Jump.opus");
293 			audioDevice->RegisterSound("Sounds/Player/Land.opus");
294 			audioDevice->RegisterSound("Sounds/Player/WaterJump.opus");
295 			audioDevice->RegisterSound("Sounds/Player/WaterLand.opus");
296 			audioDevice->RegisterSound("Sounds/Weapons/SwitchLocal.opus");
297 			audioDevice->RegisterSound("Sounds/Weapons/Switch.opus");
298 			audioDevice->RegisterSound("Sounds/Weapons/Restock.opus");
299 			audioDevice->RegisterSound("Sounds/Weapons/RestockLocal.opus");
300 			audioDevice->RegisterSound("Sounds/Weapons/AimDownSightLocal.opus");
301 			renderer->RegisterImage("Gfx/Ball.png");
302 			renderer->RegisterModel("Models/Player/Dead.kv6");
303 			renderer->RegisterImage("Gfx/Spotlight.png");
304 			renderer->RegisterImage("Gfx/Glare.png");
305 			renderer->RegisterModel("Models/Weapons/Spade/Spade.kv6");
306 			renderer->RegisterModel("Models/Weapons/Block/Block2.kv6");
307 			renderer->RegisterModel("Models/Weapons/Grenade/Grenade.kv6");
308 			renderer->RegisterModel("Models/Weapons/SMG/Weapon.kv6");
309 			renderer->RegisterModel("Models/Weapons/SMG/WeaponNoMagazine.kv6");
310 			renderer->RegisterModel("Models/Weapons/SMG/Magazine.kv6");
311 			renderer->RegisterModel("Models/Weapons/Rifle/Weapon.kv6");
312 			renderer->RegisterModel("Models/Weapons/Rifle/WeaponNoMagazine.kv6");
313 			renderer->RegisterModel("Models/Weapons/Rifle/Magazine.kv6");
314 			renderer->RegisterModel("Models/Weapons/Shotgun/Weapon.kv6");
315 			renderer->RegisterModel("Models/Weapons/Shotgun/WeaponNoPump.kv6");
316 			renderer->RegisterModel("Models/Weapons/Shotgun/Pump.kv6");
317 			renderer->RegisterModel("Models/Player/Arm.kv6");
318 			renderer->RegisterModel("Models/Player/UpperArm.kv6");
319 			renderer->RegisterModel("Models/Player/LegCrouch.kv6");
320 			renderer->RegisterModel("Models/Player/TorsoCrouch.kv6");
321 			renderer->RegisterModel("Models/Player/Leg.kv6");
322 			renderer->RegisterModel("Models/Player/Torso.kv6");
323 			renderer->RegisterModel("Models/Player/Arms.kv6");
324 			renderer->RegisterModel("Models/Player/Head.kv6");
325 			renderer->RegisterModel("Models/MapObjects/Intel.kv6");
326 			renderer->RegisterModel("Models/MapObjects/CheckPoint.kv6");
327 			renderer->RegisterImage("Gfx/Bullet/7.62mm.png");
328 			renderer->RegisterImage("Gfx/Bullet/9mm.png");
329 			renderer->RegisterImage("Gfx/Bullet/12gauge.png");
330 			renderer->RegisterImage("Gfx/CircleGradient.png");
331 			renderer->RegisterImage("Gfx/HurtSprite.png");
332 			renderer->RegisterImage("Gfx/HurtRing2.png");
333 			renderer->RegisterImage("Gfx/Intel.png");
334 			audioDevice->RegisterSound("Sounds/Feedback/Chat.opus");
335 
336 			if (mumbleLink.init())
337 				SPLog("Mumble linked");
338 			else
339 				SPLog("Mumble link failed");
340 
341 			mumbleLink.setContext(hostname.ToString(false));
342 			mumbleLink.setIdentity(playerName);
343 
344 			SPLog("Started connecting to '%s'", hostname.ToString(true).c_str());
345 			net.reset(new NetClient(this));
346 			net->Connect(hostname);
347 
348 			// decide log file name
349 			std::string fn = hostname.ToString(false);
350 			std::string fn2;
351 			{
352 				time_t t;
353 				struct tm tm;
354 				::time(&t);
355 				tm = *localtime(&t);
356 				char buf[256];
357 				sprintf(buf, "%04d%02d%02d%02d%02d%02d_", tm.tm_year + 1900, tm.tm_mon + 1,
358 				        tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
359 				fn2 = buf;
360 			}
361 			for (size_t i = 0; i < fn.size(); i++) {
362 				char c = fn[i];
363 				if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
364 					fn2 += c;
365 				} else {
366 					fn2 += '_';
367 				}
368 			}
369 			fn2 = "NetLogs/" + fn2 + ".log";
370 
371 			try {
372 				logStream.reset(FileManager::OpenForWriting(fn2.c_str()));
373 				SPLog("Netlog Started at '%s'", fn2.c_str());
374 			} catch (const std::exception &ex) {
375 				SPLog("Failed to open netlog file '%s' (%s)", fn2.c_str(), ex.what());
376 			}
377 		}
378 
RunFrame(float dt)379 		void Client::RunFrame(float dt) {
380 			SPADES_MARK_FUNCTION();
381 
382 			fpsCounter.MarkFrame();
383 
384 			if (frameToRendererInit > 0) {
385 				// waiting for renderer initialization
386 
387 				DrawStartupScreen();
388 
389 				frameToRendererInit--;
390 				if (frameToRendererInit == 0) {
391 					DoInit();
392 
393 				} else {
394 					return;
395 				}
396 			}
397 
398 			timeSinceInit += std::min(dt, .03f);
399 
400 			// update network
401 			try {
402 				if (net->GetStatus() == NetClientStatusConnected)
403 					net->DoEvents(0);
404 				else
405 					net->DoEvents(10);
406 			} catch (const std::exception &ex) {
407 				if (net->GetStatus() == NetClientStatusNotConnected) {
408 					SPLog("Disconnected because of error:\n%s", ex.what());
409 					NetLog("Disconnected because of error:\n%s", ex.what());
410 					throw;
411 				} else {
412 					SPLog("Exception while processing network packets (ignored):\n%s", ex.what());
413 				}
414 			}
415 
416 			hurtRingView->Update(dt);
417 			centerMessageView->Update(dt);
418 			mapView->Update(dt);
419 			largeMapView->Update(dt);
420 
421 			UpdateAutoFocus(dt);
422 
423 			if (world) {
424 				UpdateWorld(dt);
425 				mumbleLink.update(world->GetLocalPlayer());
426 			} else {
427 				renderer->SetFogColor(MakeVector3(0.f, 0.f, 0.f));
428 			}
429 
430 			chatWindow->Update(dt);
431 			killfeedWindow->Update(dt);
432 			limbo->Update(dt);
433 
434 			// CreateSceneDefinition also can be used for sounds
435 			SceneDefinition sceneDef = CreateSceneDefinition();
436 			lastSceneDef = sceneDef;
437 
438 			// Update sounds
439 			try {
440 				audioDevice->Respatialize(sceneDef.viewOrigin, sceneDef.viewAxis[2],
441 				                          sceneDef.viewAxis[1]);
442 			} catch (const std::exception &ex) {
443 				SPLog("Audio subsystem returned error (ignored):\n%s", ex.what());
444 			}
445 
446 			// render scene
447 			DrawScene();
448 
449 			// draw 2d
450 			Draw2D();
451 
452 			// draw scripted GUI
453 			scriptedUI->RunFrame(dt);
454 			if (scriptedUI->WantsClientToBeClosed())
455 				readyToClose = true;
456 
457 			// Well done!
458 			renderer->FrameDone();
459 			renderer->Flip();
460 
461 			// reset all "delayed actions" (in case we forget to reset these)
462 			hasDelayedReload = false;
463 
464 			time += dt;
465 		}
466 
IsLimboViewActive()467 		bool Client::IsLimboViewActive() {
468 			if (world) {
469 				if (!world->GetLocalPlayer()) {
470 					return true;
471 				} else if (inGameLimbo) {
472 					return true;
473 				}
474 			}
475 			return false;
476 		}
477 
SpawnPressed()478 		void Client::SpawnPressed() {
479 			WeaponType weap = limbo->GetSelectedWeapon();
480 			int team = limbo->GetSelectedTeam();
481 			inGameLimbo = false;
482 			if (team == 2)
483 				team = 255;
484 
485 			if (!world->GetLocalPlayer() || world->GetLocalPlayer()->GetTeamId() >= 2) {
486 				// join
487 				if (team == 255) {
488 					// weaponId doesn't matter for spectators, but
489 					// NetClient doesn't like invalid weapon ID
490 					weap = WeaponType::RIFLE_WEAPON;
491 				}
492 				net->SendJoin(team, weap, playerName, lastKills);
493 			} else {
494 				Player *p = world->GetLocalPlayer();
495 				if (p->GetTeamId() != team) {
496 					net->SendTeamChange(team);
497 				}
498 				if (team != 2 && p->GetWeapon()->GetWeaponType() != weap) {
499 					net->SendWeaponChange(weap);
500 				}
501 			}
502 		}
503 
ShowAlert(const std::string & contents,AlertType type)504 		void Client::ShowAlert(const std::string &contents, AlertType type) {
505 			float timeout;
506 			switch (type) {
507 				case AlertType::Notice: timeout = 2.5f; break;
508 				case AlertType::Warning: timeout = 3.f; break;
509 				case AlertType::Error: timeout = 3.f; break;
510 			}
511 			ShowAlert(contents, type, timeout);
512 		}
513 
ShowAlert(const std::string & contents,AlertType type,float timeout,bool quiet)514 		void Client::ShowAlert(const std::string &contents, AlertType type, float timeout,
515 		                       bool quiet) {
516 			alertType = type;
517 			alertContents = contents;
518 			alertDisappearTime = time + timeout;
519 			alertAppearTime = time;
520 
521 			if (type != AlertType::Notice && !quiet) {
522 				PlayAlertSound();
523 			}
524 		}
525 
PlayAlertSound()526 		void Client::PlayAlertSound() {
527 			Handle<IAudioChunk> chunk = audioDevice->RegisterSound("Sounds/Feedback/Alert.opus");
528 			audioDevice->PlayLocal(chunk, AudioParam());
529 		}
530 
531 		/** Records chat message/game events to the log file. */
NetLog(const char * format,...)532 		void Client::NetLog(const char *format, ...) {
533 			char buf[4096];
534 			va_list va;
535 			va_start(va, format);
536 			vsnprintf(buf, sizeof(buf), format, va);
537 			va_end(va);
538 			std::string str = buf;
539 
540 			time_t t;
541 			struct tm tm;
542 			::time(&t);
543 			tm = *localtime(&t);
544 
545 			std::string timeStr = asctime(&tm);
546 
547 			// remove '\n' in the end of the result of asctime().
548 			timeStr.resize(timeStr.size() - 1);
549 
550 			snprintf(buf, sizeof(buf), "%s %s\n", timeStr.c_str(), str.c_str());
551 			buf[sizeof(buf) - 1] = 0;
552 
553 			std::string outStr = EscapeControlCharacters(buf);
554 
555 			printf("%s", outStr.c_str());
556 
557 			if (logStream) {
558 				logStream->Write(outStr);
559 				logStream->Flush();
560 			}
561 		}
562 
563 #pragma mark - Snapshots
564 
TakeMapShot()565 		void Client::TakeMapShot() {
566 
567 			try {
568 				std::string name = MapShotPath();
569 				{
570 					std::unique_ptr<IStream> stream(FileManager::OpenForWriting(name.c_str()));
571 					try {
572 						GameMap *map = GetWorld()->GetMap();
573 						if (map == nullptr) {
574 							SPRaise("No map loaded");
575 						}
576 						map->Save(stream.get());
577 					} catch (...) {
578 						throw;
579 					}
580 				}
581 
582 				std::string msg;
583 				msg = _Tr("Client", "Map saved: {0}", name);
584 				ShowAlert(msg, AlertType::Notice);
585 			} catch (const Exception &ex) {
586 				std::string msg;
587 				msg = _Tr("Client", "Saving map failed: ");
588 				msg += ex.GetShortMessage();
589 				ShowAlert(msg, AlertType::Error);
590 				SPLog("Saving map failed: %s", ex.what());
591 			} catch (const std::exception &ex) {
592 				std::string msg;
593 				msg = _Tr("Client", "Saving map failed: ");
594 				msg += ex.what();
595 				ShowAlert(msg, AlertType::Error);
596 				SPLog("Saving map failed: %s", ex.what());
597 			}
598 		}
599 
MapShotPath()600 		std::string Client::MapShotPath() {
601 			char buf[256];
602 			for (int i = 0; i < 10000; i++) {
603 				sprintf(buf, "Mapshots/shot%04d.vxl", nextScreenShotIndex);
604 				if (FileManager::FileExists(buf)) {
605 					nextScreenShotIndex++;
606 					if (nextScreenShotIndex >= 10000)
607 						nextScreenShotIndex = 0;
608 					continue;
609 				}
610 
611 				return buf;
612 			}
613 
614 			SPRaise("No free file name");
615 		}
616 
617 #pragma mark - Chat Messages
618 
PlayerSentChatMessage(spades::client::Player * p,bool global,const std::string & msg)619 		void Client::PlayerSentChatMessage(spades::client::Player *p, bool global,
620 		                                   const std::string &msg) {
621 			{
622 				std::string s;
623 				if (global)
624 					//! Prefix added to global chat messages.
625 					//!
626 					//! Example: [Global] playername (Red) blah blah
627 					//!
628 					//! Crowdin warns that this string shouldn't be translated,
629 					//! but it actually can be.
630 					//! The extra whitespace is not a typo.
631 					s = _Tr("Client", "[Global] ");
632 				s += ChatWindow::TeamColorMessage(p->GetName(), p->GetTeamId());
633 				s += ": ";
634 				s += msg;
635 				chatWindow->AddMessage(s);
636 			}
637 			{
638 				std::string s;
639 				if (global)
640 					s = "[Global] ";
641 				s += p->GetName();
642 				s += ": ";
643 				s += msg;
644 
645 				auto col = p->GetTeamId() < 2 ? world->GetTeam(p->GetTeamId()).color
646 				                              : IntVector3::Make(255, 255, 255);
647 
648 				scriptedUI->RecordChatLog(
649 				  s, MakeVector4(col.x / 255.f, col.y / 255.f, col.z / 255.f, 0.8f));
650 			}
651 			if (global)
652 				NetLog("[Global] %s (%s): %s", p->GetName().c_str(),
653 				       world->GetTeam(p->GetTeamId()).name.c_str(), msg.c_str());
654 			else
655 				NetLog("[Team] %s (%s): %s", p->GetName().c_str(),
656 				       world->GetTeam(p->GetTeamId()).name.c_str(), msg.c_str());
657 
658 			if ((!IsMuted()) && (int)cg_chatBeep) {
659 				Handle<IAudioChunk> chunk = audioDevice->RegisterSound("Sounds/Feedback/Chat.opus");
660 				audioDevice->PlayLocal(chunk, AudioParam());
661 			}
662 		}
663 
ServerSentMessage(const std::string & msg)664 		void Client::ServerSentMessage(const std::string &msg) {
665 			NetLog("%s", msg.c_str());
666 			scriptedUI->RecordChatLog(msg, Vector4::Make(1.f, 1.f, 1.f, 0.8f));
667 
668 			if (cg_serverAlert) {
669 				if (msg.substr(0, 3) == "N% ") {
670 					ShowAlert(msg.substr(3), AlertType::Notice);
671 					return;
672 				}
673 				if (msg.substr(0, 3) == "!% ") {
674 					ShowAlert(msg.substr(3), AlertType::Error);
675 					return;
676 				}
677 				if (msg.substr(0, 3) == "%% ") {
678 					ShowAlert(msg.substr(3), AlertType::Warning);
679 					return;
680 				}
681 				if (msg.substr(0, 3) == "C% ") {
682 					centerMessageView->AddMessage(msg.substr(3));
683 					return;
684 				}
685 			}
686 
687 			chatWindow->AddMessage(msg);
688 		}
689 
690 #pragma mark - Follow / Spectate
691 
FollowNextPlayer(bool reverse)692 		void Client::FollowNextPlayer(bool reverse) {
693 			SPAssert(world->GetLocalPlayer());
694 
695 			auto &localPlayer = *world->GetLocalPlayer();
696 			int myTeam = localPlayer.GetTeamId();
697 
698 			bool localPlayerIsSpectator = localPlayer.IsSpectator();
699 
700 			int nextId = FollowsNonLocalPlayer(GetCameraMode()) ? followedPlayerId :
701 				world->GetLocalPlayerIndex();
702 			do {
703 				reverse ? --nextId : ++nextId;
704 
705 				if (nextId >= static_cast<int>(world->GetNumPlayerSlots()))
706 					nextId = 0;
707 				if (nextId < 0)
708 					nextId = static_cast<int>(world->GetNumPlayerSlots() - 1);
709 
710 				Player *p = world->GetPlayer(nextId);
711 				if (p == nullptr || p->IsSpectator()) {
712 					// Do not follow a non-existent player or spectator
713 					continue;
714 				}
715 
716 				if (!localPlayerIsSpectator && p->GetTeamId() != myTeam) {
717 					continue;
718 				}
719 
720 				if (!localPlayerIsSpectator && cg_skipDeadPlayersWhenDead && !p->IsAlive()) {
721 					// Skip dead players unless the local player is not a spectator
722 					continue;
723 				}
724 
725 				if (p->GetFront().GetPoweredLength() < .01f) {
726 					// Do not follow a player with an invalid state
727 					continue;
728 				}
729 
730 				break;
731 			} while (nextId != followedPlayerId);
732 
733 			followedPlayerId = nextId;
734 			if (followedPlayerId == world->GetLocalPlayerIndex()) {
735 				followCameraState.enabled = false;
736 			} else {
737 				followCameraState.enabled = true;
738 			}
739 		}
740 	}
741 }
742