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