| 1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
| 2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
| 3 | #include "gamecontext.h" |
| 4 | |
| 5 | #include "entities/character.h" |
| 6 | #include "gamemodes/ddnet.h" |
| 7 | #include "gamemodes/mod.h" |
| 8 | #include "player.h" |
| 9 | #include "score.h" |
| 10 | #include "teeinfo.h" |
| 11 | |
| 12 | #include <antibot/antibot_data.h> |
| 13 | |
| 14 | #include <base/aio.h> |
| 15 | #include <base/dbg.h> |
| 16 | #include <base/fs.h> |
| 17 | #include <base/io.h> |
| 18 | #include <base/logger.h> |
| 19 | #include <base/math.h> |
| 20 | #include <base/mem.h> |
| 21 | #include <base/secure.h> |
| 22 | #include <base/str.h> |
| 23 | #include <base/time.h> |
| 24 | |
| 25 | #include <engine/console.h> |
| 26 | #include <engine/engine.h> |
| 27 | #include <engine/map.h> |
| 28 | #include <engine/server/server.h> |
| 29 | #include <engine/shared/config.h> |
| 30 | #include <engine/shared/datafile.h> |
| 31 | #include <engine/shared/json.h> |
| 32 | #include <engine/shared/linereader.h> |
| 33 | #include <engine/shared/memheap.h> |
| 34 | #include <engine/shared/protocol.h> |
| 35 | #include <engine/shared/protocolglue.h> |
| 36 | #include <engine/storage.h> |
| 37 | |
| 38 | #include <generated/protocol.h> |
| 39 | #include <generated/protocol7.h> |
| 40 | #include <generated/protocolglue.h> |
| 41 | |
| 42 | #include <game/collision.h> |
| 43 | #include <game/gamecore.h> |
| 44 | #include <game/mapitems.h> |
| 45 | #include <game/version.h> |
| 46 | |
| 47 | #include <vector> |
| 48 | |
| 49 | // Not thread-safe! |
| 50 | class CClientChatLogger : public ILogger |
| 51 | { |
| 52 | CGameContext *m_pGameServer; |
| 53 | int m_ClientId; |
| 54 | ILogger *m_pOuterLogger; |
| 55 | |
| 56 | public: |
| 57 | CClientChatLogger(CGameContext *pGameServer, int ClientId, ILogger *pOuterLogger) : |
| 58 | m_pGameServer(pGameServer), |
| 59 | m_ClientId(ClientId), |
| 60 | m_pOuterLogger(pOuterLogger) |
| 61 | { |
| 62 | } |
| 63 | void Log(const CLogMessage *pMessage) override; |
| 64 | }; |
| 65 | |
| 66 | void CClientChatLogger::Log(const CLogMessage *pMessage) |
| 67 | { |
| 68 | if(str_comp(a: pMessage->m_aSystem, b: "chatresp" ) == 0) |
| 69 | { |
| 70 | if(m_Filter.Filters(pMessage)) |
| 71 | { |
| 72 | return; |
| 73 | } |
| 74 | m_pGameServer->SendChatTarget(To: m_ClientId, pText: pMessage->Message()); |
| 75 | } |
| 76 | else |
| 77 | { |
| 78 | m_pOuterLogger->Log(pMessage); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | CGameContext::CGameContext(bool Resetting) : |
| 83 | m_Mutes("mutes" ), |
| 84 | m_VoteMutes("votemutes" ) |
| 85 | { |
| 86 | m_Resetting = false; |
| 87 | m_pServer = nullptr; |
| 88 | |
| 89 | for(auto &pPlayer : m_apPlayers) |
| 90 | pPlayer = nullptr; |
| 91 | |
| 92 | mem_zero(block: &m_aLastPlayerInput, size: sizeof(m_aLastPlayerInput)); |
| 93 | std::fill(first: std::begin(arr&: m_aPlayerHasInput), last: std::end(arr&: m_aPlayerHasInput), value: false); |
| 94 | |
| 95 | m_pController = nullptr; |
| 96 | |
| 97 | m_pVoteOptionFirst = nullptr; |
| 98 | m_pVoteOptionLast = nullptr; |
| 99 | m_LastMapVote = 0; |
| 100 | |
| 101 | m_SqlRandomMapResult = nullptr; |
| 102 | |
| 103 | m_pLoadMapInfoResult = nullptr; |
| 104 | m_aMapInfoMessage[0] = '\0'; |
| 105 | |
| 106 | m_pScore = nullptr; |
| 107 | |
| 108 | m_VoteCreator = -1; |
| 109 | m_VoteType = VOTE_TYPE_UNKNOWN; |
| 110 | m_VoteCloseTime = 0; |
| 111 | m_VoteUpdate = false; |
| 112 | m_VotePos = 0; |
| 113 | m_aVoteDescription[0] = '\0'; |
| 114 | m_aSixupVoteDescription[0] = '\0'; |
| 115 | m_aVoteCommand[0] = '\0'; |
| 116 | m_aVoteReason[0] = '\0'; |
| 117 | m_NumVoteOptions = 0; |
| 118 | m_VoteEnforce = VOTE_ENFORCE_UNKNOWN; |
| 119 | |
| 120 | m_LatestLog = 0; |
| 121 | mem_zero(block: &m_aLogs, size: sizeof(m_aLogs)); |
| 122 | |
| 123 | if(!Resetting) |
| 124 | { |
| 125 | m_pMap = CreateMap(); |
| 126 | |
| 127 | for(auto &pSavedTee : m_apSavedTees) |
| 128 | pSavedTee = nullptr; |
| 129 | |
| 130 | for(auto &pSavedTeam : m_apSavedTeams) |
| 131 | pSavedTeam = nullptr; |
| 132 | |
| 133 | std::fill(first: std::begin(arr&: m_aTeamMapping), last: std::end(arr&: m_aTeamMapping), value: -1); |
| 134 | |
| 135 | m_NonEmptySince = 0; |
| 136 | m_pVoteOptionHeap = new CHeap(); |
| 137 | } |
| 138 | |
| 139 | m_aDeleteTempfile[0] = 0; |
| 140 | m_TeeHistorianActive = false; |
| 141 | } |
| 142 | |
| 143 | CGameContext::~CGameContext() |
| 144 | { |
| 145 | for(auto &pPlayer : m_apPlayers) |
| 146 | delete pPlayer; |
| 147 | |
| 148 | if(!m_Resetting) |
| 149 | { |
| 150 | m_pMap->Unload(); |
| 151 | m_pMap = nullptr; |
| 152 | |
| 153 | for(auto &pSavedTee : m_apSavedTees) |
| 154 | delete pSavedTee; |
| 155 | |
| 156 | for(auto &pSavedTeam : m_apSavedTeams) |
| 157 | delete pSavedTeam; |
| 158 | |
| 159 | delete m_pVoteOptionHeap; |
| 160 | } |
| 161 | |
| 162 | delete m_pScore; |
| 163 | m_pScore = nullptr; |
| 164 | } |
| 165 | |
| 166 | void CGameContext::Clear() |
| 167 | { |
| 168 | CHeap *pVoteOptionHeap = m_pVoteOptionHeap; |
| 169 | CVoteOptionServer *pVoteOptionFirst = m_pVoteOptionFirst; |
| 170 | CVoteOptionServer *pVoteOptionLast = m_pVoteOptionLast; |
| 171 | int NumVoteOptions = m_NumVoteOptions; |
| 172 | CTuningParams Tuning = m_aTuningList[0]; |
| 173 | CMutes Mutes = m_Mutes; |
| 174 | CMutes VoteMutes = m_VoteMutes; |
| 175 | std::unique_ptr<IMap> pMap; |
| 176 | std::swap(x&: pMap, y&: m_pMap); |
| 177 | |
| 178 | m_Resetting = true; |
| 179 | this->~CGameContext(); |
| 180 | new(this) CGameContext(true); |
| 181 | |
| 182 | m_pVoteOptionHeap = pVoteOptionHeap; |
| 183 | m_pVoteOptionFirst = pVoteOptionFirst; |
| 184 | m_pVoteOptionLast = pVoteOptionLast; |
| 185 | m_NumVoteOptions = NumVoteOptions; |
| 186 | m_aTuningList[0] = Tuning; |
| 187 | m_Mutes = Mutes; |
| 188 | m_VoteMutes = VoteMutes; |
| 189 | std::swap(x&: pMap, y&: m_pMap); |
| 190 | } |
| 191 | |
| 192 | void CGameContext::TeeHistorianWrite(const void *pData, int DataSize, void *pUser) |
| 193 | { |
| 194 | CGameContext *pSelf = (CGameContext *)pUser; |
| 195 | aio_write(aio: pSelf->m_pTeeHistorianFile, buffer: pData, size: DataSize); |
| 196 | } |
| 197 | |
| 198 | void CGameContext::CommandCallback(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser) |
| 199 | { |
| 200 | CGameContext *pSelf = (CGameContext *)pUser; |
| 201 | if(pSelf->m_TeeHistorianActive) |
| 202 | { |
| 203 | pSelf->m_TeeHistorian.RecordConsoleCommand(ClientId, FlagMask, pCmd, pResult); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | CNetObj_PlayerInput CGameContext::GetLastPlayerInput(int ClientId) const |
| 208 | { |
| 209 | dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "invalid ClientId" ); |
| 210 | return m_aLastPlayerInput[ClientId]; |
| 211 | } |
| 212 | |
| 213 | CCharacter *CGameContext::GetPlayerChar(int ClientId) |
| 214 | { |
| 215 | if(ClientId < 0 || ClientId >= MAX_CLIENTS || !m_apPlayers[ClientId]) |
| 216 | return nullptr; |
| 217 | return m_apPlayers[ClientId]->GetCharacter(); |
| 218 | } |
| 219 | |
| 220 | const CCharacter *CGameContext::GetPlayerChar(int ClientId) const |
| 221 | { |
| 222 | if(ClientId < 0 || ClientId >= MAX_CLIENTS || !m_apPlayers[ClientId]) |
| 223 | return nullptr; |
| 224 | return m_apPlayers[ClientId]->GetCharacter(); |
| 225 | } |
| 226 | |
| 227 | const CPlayer *CGameContext::FindPlayerByName(const char *pName) const |
| 228 | { |
| 229 | std::optional<int> ClientId = FindClientIdByName(pName); |
| 230 | if(!ClientId.has_value()) |
| 231 | return nullptr; |
| 232 | return m_apPlayers[ClientId.value()]; |
| 233 | } |
| 234 | |
| 235 | CPlayer *CGameContext::FindPlayerByName(const char *pName) |
| 236 | { |
| 237 | std::optional<int> ClientId = FindClientIdByName(pName); |
| 238 | if(!ClientId.has_value()) |
| 239 | return nullptr; |
| 240 | return m_apPlayers[ClientId.value()]; |
| 241 | } |
| 242 | |
| 243 | std::optional<int> CGameContext::FindClientIdByName(const char *pName) const |
| 244 | { |
| 245 | if(!pName) |
| 246 | return std::nullopt; |
| 247 | |
| 248 | for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) |
| 249 | { |
| 250 | if(!Server()->ClientIngame(ClientId)) |
| 251 | continue; |
| 252 | if(str_comp(a: pName, b: Server()->ClientName(ClientId))) |
| 253 | continue; |
| 254 | |
| 255 | return ClientId; |
| 256 | } |
| 257 | return std::nullopt; |
| 258 | } |
| 259 | |
| 260 | bool CGameContext::EmulateBug(int Bug) const |
| 261 | { |
| 262 | return m_MapBugs.Contains(Bug); |
| 263 | } |
| 264 | |
| 265 | void CGameContext::FillAntibot(CAntibotRoundData *pData) |
| 266 | { |
| 267 | if(!pData->m_Map.m_pTiles) |
| 268 | { |
| 269 | Collision()->FillAntibot(pMapData: &pData->m_Map); |
| 270 | } |
| 271 | pData->m_Tick = Server()->Tick(); |
| 272 | mem_zero(block: pData->m_aCharacters, size: sizeof(pData->m_aCharacters)); |
| 273 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 274 | { |
| 275 | CAntibotCharacterData *pChar = &pData->m_aCharacters[i]; |
| 276 | for(auto &LatestInput : pChar->m_aLatestInputs) |
| 277 | { |
| 278 | LatestInput.m_Direction = 0; |
| 279 | LatestInput.m_TargetX = -1; |
| 280 | LatestInput.m_TargetY = -1; |
| 281 | LatestInput.m_Jump = -1; |
| 282 | LatestInput.m_Fire = -1; |
| 283 | LatestInput.m_Hook = -1; |
| 284 | LatestInput.m_PlayerFlags = -1; |
| 285 | LatestInput.m_WantedWeapon = -1; |
| 286 | LatestInput.m_NextWeapon = -1; |
| 287 | LatestInput.m_PrevWeapon = -1; |
| 288 | } |
| 289 | pChar->m_Alive = false; |
| 290 | pChar->m_Pause = false; |
| 291 | pChar->m_Team = -1; |
| 292 | |
| 293 | pChar->m_Pos = vec2(-1, -1); |
| 294 | pChar->m_Vel = vec2(0, 0); |
| 295 | pChar->m_Angle = -1; |
| 296 | pChar->m_HookedPlayer = -1; |
| 297 | pChar->m_SpawnTick = -1; |
| 298 | pChar->m_WeaponChangeTick = -1; |
| 299 | |
| 300 | if(m_apPlayers[i]) |
| 301 | { |
| 302 | str_copy(dst: pChar->m_aName, src: Server()->ClientName(ClientId: i), dst_size: sizeof(pChar->m_aName)); |
| 303 | CCharacter *pGameChar = m_apPlayers[i]->GetCharacter(); |
| 304 | pChar->m_Alive = (bool)pGameChar; |
| 305 | pChar->m_Pause = m_apPlayers[i]->IsPaused(); |
| 306 | pChar->m_Team = m_apPlayers[i]->GetTeam(); |
| 307 | if(pGameChar) |
| 308 | { |
| 309 | pGameChar->FillAntibot(pData: pChar); |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | void CGameContext::CreateDamageInd(vec2 Pos, float Angle, int Amount, CClientMask Mask) |
| 316 | { |
| 317 | float a = 3 * pi / 2 + Angle; |
| 318 | float s = a - pi / 3; |
| 319 | float e = a + pi / 3; |
| 320 | for(int i = 0; i < Amount; i++) |
| 321 | { |
| 322 | float f = mix(a: s, b: e, amount: (i + 1) / (float)(Amount + 1)); |
| 323 | CNetEvent_DamageInd *pEvent = m_Events.Create<CNetEvent_DamageInd>(Mask); |
| 324 | if(pEvent) |
| 325 | { |
| 326 | pEvent->m_X = (int)Pos.x; |
| 327 | pEvent->m_Y = (int)Pos.y; |
| 328 | pEvent->m_Angle = (int)(f * 256.0f); |
| 329 | } |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | void CGameContext::CreateHammerHit(vec2 Pos, CClientMask Mask) |
| 334 | { |
| 335 | CNetEvent_HammerHit *pEvent = m_Events.Create<CNetEvent_HammerHit>(Mask); |
| 336 | if(pEvent) |
| 337 | { |
| 338 | pEvent->m_X = (int)Pos.x; |
| 339 | pEvent->m_Y = (int)Pos.y; |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | void CGameContext::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask) |
| 344 | { |
| 345 | // create the event |
| 346 | CNetEvent_Explosion *pEvent = m_Events.Create<CNetEvent_Explosion>(Mask); |
| 347 | if(pEvent) |
| 348 | { |
| 349 | pEvent->m_X = (int)Pos.x; |
| 350 | pEvent->m_Y = (int)Pos.y; |
| 351 | } |
| 352 | |
| 353 | // deal damage |
| 354 | CEntity *apEnts[MAX_CLIENTS]; |
| 355 | float Radius = 135.0f; |
| 356 | float InnerRadius = 48.0f; |
| 357 | int Num = m_World.FindEntities(Pos, Radius, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER); |
| 358 | CClientMask TeamMask = CClientMask().set(); |
| 359 | for(int i = 0; i < Num; i++) |
| 360 | { |
| 361 | auto *pChr = static_cast<CCharacter *>(apEnts[i]); |
| 362 | vec2 Diff = pChr->m_Pos - Pos; |
| 363 | vec2 ForceDir(0, 1); |
| 364 | float l = length(a: Diff); |
| 365 | if(l) |
| 366 | ForceDir = normalize(v: Diff); |
| 367 | l = 1 - std::clamp(val: (l - InnerRadius) / (Radius - InnerRadius), lo: 0.0f, hi: 1.0f); |
| 368 | float Strength; |
| 369 | if(Owner == -1 || !m_apPlayers[Owner] || !m_apPlayers[Owner]->m_TuneZone) |
| 370 | Strength = GlobalTuning()->m_ExplosionStrength; |
| 371 | else |
| 372 | Strength = TuningList()[m_apPlayers[Owner]->m_TuneZone].m_ExplosionStrength; |
| 373 | |
| 374 | float Dmg = Strength * l; |
| 375 | if(!(int)Dmg) |
| 376 | continue; |
| 377 | |
| 378 | if((GetPlayerChar(ClientId: Owner) ? !GetPlayerChar(ClientId: Owner)->GrenadeHitDisabled() : g_Config.m_SvHit) || NoDamage || Owner == pChr->GetPlayer()->GetCid()) |
| 379 | { |
| 380 | if(Owner != -1 && pChr->IsAlive() && !pChr->CanCollide(ClientId: Owner)) |
| 381 | continue; |
| 382 | if(Owner == -1 && ActivatedTeam != -1 && pChr->IsAlive() && pChr->Team() != ActivatedTeam) |
| 383 | continue; |
| 384 | |
| 385 | // Explode at most once per team |
| 386 | int PlayerTeam = pChr->Team(); |
| 387 | if((GetPlayerChar(ClientId: Owner) ? GetPlayerChar(ClientId: Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit) || NoDamage) |
| 388 | { |
| 389 | if(PlayerTeam == TEAM_SUPER) |
| 390 | continue; |
| 391 | if(!TeamMask.test(position: PlayerTeam)) |
| 392 | continue; |
| 393 | TeamMask.reset(pos: PlayerTeam); |
| 394 | } |
| 395 | |
| 396 | pChr->TakeDamage(Force: ForceDir * Dmg * 2, Dmg: (int)Dmg, From: Owner, Weapon); |
| 397 | } |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | void CGameContext::CreatePlayerSpawn(vec2 Pos, CClientMask Mask) |
| 402 | { |
| 403 | CNetEvent_Spawn *pEvent = m_Events.Create<CNetEvent_Spawn>(Mask); |
| 404 | if(pEvent) |
| 405 | { |
| 406 | pEvent->m_X = (int)Pos.x; |
| 407 | pEvent->m_Y = (int)Pos.y; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | void CGameContext::CreateDeath(vec2 Pos, int ClientId, CClientMask Mask) |
| 412 | { |
| 413 | CNetEvent_Death *pEvent = m_Events.Create<CNetEvent_Death>(Mask); |
| 414 | if(pEvent) |
| 415 | { |
| 416 | pEvent->m_X = (int)Pos.x; |
| 417 | pEvent->m_Y = (int)Pos.y; |
| 418 | pEvent->m_ClientId = ClientId; |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | void CGameContext::CreateBirthdayEffect(vec2 Pos, CClientMask Mask) |
| 423 | { |
| 424 | CNetEvent_Birthday *pEvent = m_Events.Create<CNetEvent_Birthday>(Mask); |
| 425 | if(pEvent) |
| 426 | { |
| 427 | pEvent->m_X = (int)Pos.x; |
| 428 | pEvent->m_Y = (int)Pos.y; |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | void CGameContext::CreateFinishEffect(vec2 Pos, CClientMask Mask) |
| 433 | { |
| 434 | CNetEvent_Finish *pEvent = m_Events.Create<CNetEvent_Finish>(Mask); |
| 435 | if(pEvent) |
| 436 | { |
| 437 | pEvent->m_X = (int)Pos.x; |
| 438 | pEvent->m_Y = (int)Pos.y; |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | void CGameContext::CreateSound(vec2 Pos, int Sound, CClientMask Mask) |
| 443 | { |
| 444 | if(Sound < 0) |
| 445 | return; |
| 446 | |
| 447 | // create a sound |
| 448 | CNetEvent_SoundWorld *pEvent = m_Events.Create<CNetEvent_SoundWorld>(Mask); |
| 449 | if(pEvent) |
| 450 | { |
| 451 | pEvent->m_X = (int)Pos.x; |
| 452 | pEvent->m_Y = (int)Pos.y; |
| 453 | pEvent->m_SoundId = Sound; |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | void CGameContext::CreateSoundGlobal(int Sound, int Target) const |
| 458 | { |
| 459 | if(Sound < 0) |
| 460 | return; |
| 461 | |
| 462 | CNetMsg_Sv_SoundGlobal Msg; |
| 463 | Msg.m_SoundId = Sound; |
| 464 | if(Target == -2) |
| 465 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId: -1); |
| 466 | else |
| 467 | { |
| 468 | int Flag = MSGFLAG_VITAL; |
| 469 | if(Target != -1) |
| 470 | Flag |= MSGFLAG_NORECORD; |
| 471 | Server()->SendPackMsg(pMsg: &Msg, Flags: Flag, ClientId: Target); |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | void CGameContext::SnapSwitchers(int SnappingClient) |
| 476 | { |
| 477 | if(Switchers().empty()) |
| 478 | return; |
| 479 | |
| 480 | CPlayer *pPlayer = SnappingClient != SERVER_DEMO_CLIENT ? m_apPlayers[SnappingClient] : nullptr; |
| 481 | int Team = pPlayer && pPlayer->GetCharacter() ? pPlayer->GetCharacter()->Team() : 0; |
| 482 | |
| 483 | if(pPlayer && (pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->SpectatorId() != SPEC_FREEVIEW && m_apPlayers[pPlayer->SpectatorId()] && m_apPlayers[pPlayer->SpectatorId()]->GetCharacter()) |
| 484 | Team = m_apPlayers[pPlayer->SpectatorId()]->GetCharacter()->Team(); |
| 485 | |
| 486 | if(Team == TEAM_SUPER) |
| 487 | return; |
| 488 | |
| 489 | int SentTeam = Team; |
| 490 | if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) |
| 491 | SentTeam = 0; |
| 492 | |
| 493 | CNetObj_SwitchState SwitchState = {}; |
| 494 | |
| 495 | SwitchState.m_HighestSwitchNumber = std::clamp(val: (int)Switchers().size() - 1, lo: 0, hi: 255); |
| 496 | std::fill(first: std::begin(arr&: SwitchState.m_aStatus), last: std::end(arr&: SwitchState.m_aStatus), value: 0); |
| 497 | |
| 498 | std::vector<std::pair<int, int>> vEndTicks; // <EndTick, SwitchNumber> |
| 499 | |
| 500 | for(int i = 0; i <= SwitchState.m_HighestSwitchNumber; i++) |
| 501 | { |
| 502 | int Status = (int)Switchers()[i].m_aStatus[Team]; |
| 503 | SwitchState.m_aStatus[i / 32] |= (Status << (i % 32)); |
| 504 | |
| 505 | int EndTick = Switchers()[i].m_aEndTick[Team]; |
| 506 | if(EndTick > 0 && EndTick < Server()->Tick() + 3 * Server()->TickSpeed() && Switchers()[i].m_aLastUpdateTick[Team] < Server()->Tick()) |
| 507 | { |
| 508 | // only keep track of EndTicks that have less than three second left and are not currently being updated by a player being present on a switch tile, to limit how often these are sent |
| 509 | vEndTicks.emplace_back(args&: Switchers()[i].m_aEndTick[Team], args&: i); |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | // send the endtick of switchers that are about to toggle back (up to four, prioritizing those with the earliest endticks) |
| 514 | std::fill(first: std::begin(arr&: SwitchState.m_aSwitchNumbers), last: std::end(arr&: SwitchState.m_aSwitchNumbers), value: 0); |
| 515 | std::fill(first: std::begin(arr&: SwitchState.m_aEndTicks), last: std::end(arr&: SwitchState.m_aEndTicks), value: 0); |
| 516 | |
| 517 | std::sort(first: vEndTicks.begin(), last: vEndTicks.end()); |
| 518 | const int NumTimedSwitchers = minimum(a: (int)vEndTicks.size(), b: (int)std::size(SwitchState.m_aEndTicks)); |
| 519 | |
| 520 | for(int i = 0; i < NumTimedSwitchers; i++) |
| 521 | { |
| 522 | SwitchState.m_aSwitchNumbers[i] = vEndTicks[i].second; |
| 523 | SwitchState.m_aEndTicks[i] = vEndTicks[i].first; |
| 524 | } |
| 525 | |
| 526 | Server()->SnapNewItem(Id: SentTeam, Data: SwitchState); |
| 527 | } |
| 528 | |
| 529 | void CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapId, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber) const |
| 530 | { |
| 531 | if(Context.GetClientVersion() >= VERSION_DDNET_MULTI_LASER) |
| 532 | { |
| 533 | CNetObj_DDNetLaser Laser = {}; |
| 534 | Laser.m_ToX = (int)To.x; |
| 535 | Laser.m_ToY = (int)To.y; |
| 536 | Laser.m_FromX = (int)From.x; |
| 537 | Laser.m_FromY = (int)From.y; |
| 538 | Laser.m_StartTick = StartTick; |
| 539 | Laser.m_Owner = Owner; |
| 540 | Laser.m_Type = LaserType; |
| 541 | Laser.m_Subtype = Subtype; |
| 542 | Laser.m_SwitchNumber = SwitchNumber; |
| 543 | Laser.m_Flags = 0; |
| 544 | Server()->SnapNewItem(Id: SnapId, Data: Laser); |
| 545 | } |
| 546 | else |
| 547 | { |
| 548 | CNetObj_Laser Laser = {}; |
| 549 | Laser.m_X = (int)To.x; |
| 550 | Laser.m_Y = (int)To.y; |
| 551 | Laser.m_FromX = (int)From.x; |
| 552 | Laser.m_FromY = (int)From.y; |
| 553 | Laser.m_StartTick = StartTick; |
| 554 | Server()->SnapNewItem(Id: SnapId, Data: Laser); |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | void CGameContext::SnapPickup(const CSnapContext &Context, int SnapId, const vec2 &Pos, int Type, int SubType, int SwitchNumber, int Flags) const |
| 559 | { |
| 560 | if(Context.IsSixup()) |
| 561 | { |
| 562 | protocol7::CNetObj_Pickup Pickup = {}; |
| 563 | Pickup.m_X = (int)Pos.x; |
| 564 | Pickup.m_Y = (int)Pos.y; |
| 565 | Pickup.m_Type = PickupType_SixToSeven(Type6: Type, SubType6: SubType); |
| 566 | Server()->SnapNewItem(Id: SnapId, Data: Pickup); |
| 567 | } |
| 568 | else if(Context.GetClientVersion() >= VERSION_DDNET_ENTITY_NETOBJS) |
| 569 | { |
| 570 | CNetObj_DDNetPickup Pickup = {}; |
| 571 | Pickup.m_X = (int)Pos.x; |
| 572 | Pickup.m_Y = (int)Pos.y; |
| 573 | Pickup.m_Type = Type; |
| 574 | Pickup.m_Subtype = SubType; |
| 575 | Pickup.m_SwitchNumber = SwitchNumber; |
| 576 | Pickup.m_Flags = Flags; |
| 577 | Server()->SnapNewItem(Id: SnapId, Data: Pickup); |
| 578 | } |
| 579 | else |
| 580 | { |
| 581 | CNetObj_Pickup Pickup = {}; |
| 582 | |
| 583 | Pickup.m_X = (int)Pos.x; |
| 584 | Pickup.m_Y = (int)Pos.y; |
| 585 | |
| 586 | Pickup.m_Type = Type; |
| 587 | if(Context.GetClientVersion() < VERSION_DDNET_WEAPON_SHIELDS) |
| 588 | { |
| 589 | if(Type >= POWERUP_ARMOR_SHOTGUN && Type <= POWERUP_ARMOR_LASER) |
| 590 | { |
| 591 | Pickup.m_Type = POWERUP_ARMOR; |
| 592 | } |
| 593 | } |
| 594 | Pickup.m_Subtype = SubType; |
| 595 | |
| 596 | Server()->SnapNewItem(Id: SnapId, Data: Pickup); |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | void CGameContext::CallVote(int ClientId, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc) |
| 601 | { |
| 602 | // check if a vote is already running |
| 603 | if(m_VoteCloseTime) |
| 604 | return; |
| 605 | |
| 606 | int64_t Now = Server()->Tick(); |
| 607 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 608 | |
| 609 | if(!pPlayer) |
| 610 | return; |
| 611 | |
| 612 | SendChat(ClientId: -1, Team: TEAM_ALL, pText: pChatmsg, SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 613 | if(!pSixupDesc) |
| 614 | pSixupDesc = pDesc; |
| 615 | |
| 616 | m_VoteCreator = ClientId; |
| 617 | StartVote(pDesc, pCommand: pCmd, pReason, pSixupDesc); |
| 618 | pPlayer->m_Vote = 1; |
| 619 | pPlayer->m_VotePos = m_VotePos = 1; |
| 620 | pPlayer->m_LastVoteCall = Now; |
| 621 | |
| 622 | CNetMsg_Sv_YourVote Msg = {.m_Voted: pPlayer->m_Vote}; |
| 623 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 624 | } |
| 625 | |
| 626 | void CGameContext::SendChatTarget(int To, const char *pText, int VersionFlags) const |
| 627 | { |
| 628 | CNetMsg_Sv_Chat Msg; |
| 629 | Msg.m_Team = 0; |
| 630 | Msg.m_ClientId = -1; |
| 631 | Msg.m_pMessage = pText; |
| 632 | |
| 633 | if(g_Config.m_SvDemoChat) |
| 634 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId: SERVER_DEMO_CLIENT); |
| 635 | |
| 636 | if(To == -1) |
| 637 | { |
| 638 | for(int i = 0; i < Server()->MaxClients(); i++) |
| 639 | { |
| 640 | if(!((Server()->IsSixup(ClientId: i) && (VersionFlags & FLAG_SIXUP)) || |
| 641 | (!Server()->IsSixup(ClientId: i) && (VersionFlags & FLAG_SIX)))) |
| 642 | continue; |
| 643 | |
| 644 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 645 | } |
| 646 | } |
| 647 | else |
| 648 | { |
| 649 | if(!((Server()->IsSixup(ClientId: To) && (VersionFlags & FLAG_SIXUP)) || |
| 650 | (!Server()->IsSixup(ClientId: To) && (VersionFlags & FLAG_SIX)))) |
| 651 | return; |
| 652 | |
| 653 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: To); |
| 654 | } |
| 655 | } |
| 656 | |
| 657 | void CGameContext::SendChatTeam(int Team, const char *pText) const |
| 658 | { |
| 659 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 660 | if(m_apPlayers[i] != nullptr && GetDDRaceTeam(ClientId: i) == Team) |
| 661 | SendChatTarget(To: i, pText); |
| 662 | } |
| 663 | |
| 664 | void CGameContext::SendChat(int ChatterClientId, int Team, const char *pText, int SpamProtectionClientId, int VersionFlags) |
| 665 | { |
| 666 | dbg_assert(ChatterClientId >= -1 && ChatterClientId < MAX_CLIENTS, "ChatterClientId invalid: %d" , ChatterClientId); |
| 667 | |
| 668 | if(SpamProtectionClientId >= 0 && SpamProtectionClientId < MAX_CLIENTS) |
| 669 | if(ProcessSpamProtection(ClientId: SpamProtectionClientId)) |
| 670 | return; |
| 671 | |
| 672 | char aText[256]; |
| 673 | str_copy(dst: aText, src: pText, dst_size: sizeof(aText)); |
| 674 | const char *pTeamString = Team == TEAM_ALL ? "chat" : "teamchat" ; |
| 675 | if(ChatterClientId == -1) |
| 676 | { |
| 677 | log_info(pTeamString, "*** %s" , aText); |
| 678 | } |
| 679 | else |
| 680 | { |
| 681 | log_info(pTeamString, "%d:%d:%s: %s" , ChatterClientId, Team, Server()->ClientName(ChatterClientId), aText); |
| 682 | } |
| 683 | |
| 684 | if(Team == TEAM_ALL) |
| 685 | { |
| 686 | CNetMsg_Sv_Chat Msg; |
| 687 | Msg.m_Team = 0; |
| 688 | Msg.m_ClientId = ChatterClientId; |
| 689 | Msg.m_pMessage = aText; |
| 690 | |
| 691 | // pack one for the recording only |
| 692 | if(g_Config.m_SvDemoChat) |
| 693 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId: SERVER_DEMO_CLIENT); |
| 694 | |
| 695 | // send to the clients |
| 696 | for(int i = 0; i < Server()->MaxClients(); i++) |
| 697 | { |
| 698 | if(!m_apPlayers[i]) |
| 699 | continue; |
| 700 | bool Send = (Server()->IsSixup(ClientId: i) && (VersionFlags & FLAG_SIXUP)) || |
| 701 | (!Server()->IsSixup(ClientId: i) && (VersionFlags & FLAG_SIX)); |
| 702 | |
| 703 | if(!m_apPlayers[i]->m_DND && Send) |
| 704 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 705 | } |
| 706 | |
| 707 | char aBuf[sizeof(aText) + 8]; |
| 708 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Chat: %s" , aText); |
| 709 | LogEvent(Description: aBuf, ClientId: ChatterClientId); |
| 710 | } |
| 711 | else |
| 712 | { |
| 713 | CTeamsCore *pTeams = &m_pController->Teams().m_Core; |
| 714 | CNetMsg_Sv_Chat Msg; |
| 715 | Msg.m_Team = 1; |
| 716 | Msg.m_ClientId = ChatterClientId; |
| 717 | Msg.m_pMessage = aText; |
| 718 | |
| 719 | // pack one for the recording only |
| 720 | if(g_Config.m_SvDemoChat) |
| 721 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId: SERVER_DEMO_CLIENT); |
| 722 | |
| 723 | // send to the clients |
| 724 | for(int i = 0; i < Server()->MaxClients(); i++) |
| 725 | { |
| 726 | if(m_apPlayers[i] != nullptr) |
| 727 | { |
| 728 | if(Team == TEAM_SPECTATORS) |
| 729 | { |
| 730 | if(m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS) |
| 731 | { |
| 732 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 733 | } |
| 734 | } |
| 735 | else |
| 736 | { |
| 737 | if(pTeams->Team(ClientId: i) == Team && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS) |
| 738 | { |
| 739 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 740 | } |
| 741 | } |
| 742 | } |
| 743 | } |
| 744 | } |
| 745 | } |
| 746 | |
| 747 | void CGameContext::SendStartWarning(int ClientId, const char *pMessage) |
| 748 | { |
| 749 | CCharacter *pChr = GetPlayerChar(ClientId); |
| 750 | if(pChr && pChr->m_LastStartWarning < Server()->Tick() - 3 * Server()->TickSpeed()) |
| 751 | { |
| 752 | SendChatTarget(To: ClientId, pText: pMessage); |
| 753 | pChr->m_LastStartWarning = Server()->Tick(); |
| 754 | } |
| 755 | } |
| 756 | |
| 757 | void CGameContext::SendEmoticon(int ClientId, int Emoticon, int TargetClientId) const |
| 758 | { |
| 759 | CNetMsg_Sv_Emoticon Msg; |
| 760 | Msg.m_ClientId = ClientId; |
| 761 | Msg.m_Emoticon = Emoticon; |
| 762 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: TargetClientId); |
| 763 | } |
| 764 | |
| 765 | void CGameContext::SendWeaponPickup(int ClientId, int Weapon) const |
| 766 | { |
| 767 | CNetMsg_Sv_WeaponPickup Msg; |
| 768 | Msg.m_Weapon = Weapon; |
| 769 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 770 | } |
| 771 | |
| 772 | void CGameContext::SendMotd(int ClientId) const |
| 773 | { |
| 774 | CNetMsg_Sv_Motd Msg; |
| 775 | Msg.m_pMessage = g_Config.m_SvMotd; |
| 776 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 777 | } |
| 778 | |
| 779 | void CGameContext::SendSettings(int ClientId) const |
| 780 | { |
| 781 | protocol7::CNetMsg_Sv_ServerSettings Msg; |
| 782 | Msg.m_KickVote = g_Config.m_SvVoteKick; |
| 783 | Msg.m_KickMin = g_Config.m_SvVoteKickMin; |
| 784 | Msg.m_SpecVote = g_Config.m_SvVoteSpectate; |
| 785 | Msg.m_TeamLock = 0; |
| 786 | Msg.m_TeamBalance = 0; |
| 787 | Msg.m_PlayerSlots = Server()->MaxClients() - g_Config.m_SvSpectatorSlots; |
| 788 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 789 | } |
| 790 | |
| 791 | void CGameContext::SendServerAlert(const char *pMessage) |
| 792 | { |
| 793 | for(int ClientId = 0; ClientId < Server()->MaxClients(); ClientId++) |
| 794 | { |
| 795 | if(!m_apPlayers[ClientId]) |
| 796 | { |
| 797 | continue; |
| 798 | } |
| 799 | |
| 800 | if(m_apPlayers[ClientId]->GetClientVersion() >= VERSION_DDNET_IMPORTANT_ALERT) |
| 801 | { |
| 802 | CNetMsg_Sv_ServerAlert Msg; |
| 803 | Msg.m_pMessage = pMessage; |
| 804 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 805 | } |
| 806 | else |
| 807 | { |
| 808 | char aBroadcastText[1024 + 32]; |
| 809 | str_copy(dst&: aBroadcastText, src: "SERVER ALERT\n\n" ); |
| 810 | str_append(dst&: aBroadcastText, src: pMessage); |
| 811 | SendBroadcast(pText: aBroadcastText, ClientId, IsImportant: true); |
| 812 | } |
| 813 | } |
| 814 | |
| 815 | // Record server alert to demos exactly once |
| 816 | // TODO: Workaround https://github.com/ddnet/ddnet/issues/11144 by using client ID 0, |
| 817 | // otherwise the message is recorded multiple times. |
| 818 | CNetMsg_Sv_ServerAlert Msg; |
| 819 | Msg.m_pMessage = pMessage; |
| 820 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId: 0); |
| 821 | } |
| 822 | |
| 823 | void CGameContext::SendModeratorAlert(const char *pMessage, int ToClientId) |
| 824 | { |
| 825 | dbg_assert(in_range(ToClientId, 0, MAX_CLIENTS - 1), "SendImportantAlert ToClientId invalid: %d" , ToClientId); |
| 826 | dbg_assert(m_apPlayers[ToClientId] != nullptr, "Client not online: %d" , ToClientId); |
| 827 | |
| 828 | if(m_apPlayers[ToClientId]->GetClientVersion() >= VERSION_DDNET_IMPORTANT_ALERT) |
| 829 | { |
| 830 | CNetMsg_Sv_ModeratorAlert Msg; |
| 831 | Msg.m_pMessage = pMessage; |
| 832 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: ToClientId); |
| 833 | } |
| 834 | else |
| 835 | { |
| 836 | char aBroadcastText[1024 + 32]; |
| 837 | str_copy(dst&: aBroadcastText, src: "MODERATOR ALERT\n\n" ); |
| 838 | str_append(dst&: aBroadcastText, src: pMessage); |
| 839 | SendBroadcast(pText: aBroadcastText, ClientId: ToClientId, IsImportant: true); |
| 840 | log_info("moderator_alert" , "Notice: player uses an old client version and may not see moderator alerts: %s (ID %d)" , Server()->ClientName(ToClientId), ToClientId); |
| 841 | } |
| 842 | } |
| 843 | |
| 844 | void CGameContext::SendBroadcast(const char *pText, int ClientId, bool IsImportant) |
| 845 | { |
| 846 | CNetMsg_Sv_Broadcast Msg; |
| 847 | Msg.m_pMessage = pText; |
| 848 | |
| 849 | if(ClientId == -1) |
| 850 | { |
| 851 | dbg_assert(IsImportant, "broadcast messages to all players must be important" ); |
| 852 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 853 | |
| 854 | for(auto &pPlayer : m_apPlayers) |
| 855 | { |
| 856 | if(pPlayer) |
| 857 | { |
| 858 | pPlayer->m_LastBroadcastImportance = true; |
| 859 | pPlayer->m_LastBroadcast = Server()->Tick(); |
| 860 | } |
| 861 | } |
| 862 | return; |
| 863 | } |
| 864 | |
| 865 | if(!m_apPlayers[ClientId]) |
| 866 | return; |
| 867 | |
| 868 | if(!IsImportant && m_apPlayers[ClientId]->m_LastBroadcastImportance && m_apPlayers[ClientId]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10) |
| 869 | return; |
| 870 | |
| 871 | // Broadcasts to individual players are not recorded in demos |
| 872 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 873 | m_apPlayers[ClientId]->m_LastBroadcast = Server()->Tick(); |
| 874 | m_apPlayers[ClientId]->m_LastBroadcastImportance = IsImportant; |
| 875 | } |
| 876 | |
| 877 | void CGameContext::SendSkinChange7(int ClientId) |
| 878 | { |
| 879 | dbg_assert(in_range(ClientId, 0, MAX_CLIENTS - 1), "Invalid ClientId: %d" , ClientId); |
| 880 | dbg_assert(m_apPlayers[ClientId] != nullptr, "Client not online: %d" , ClientId); |
| 881 | |
| 882 | const CTeeInfo &Info = m_apPlayers[ClientId]->m_TeeInfos; |
| 883 | protocol7::CNetMsg_Sv_SkinChange Msg; |
| 884 | Msg.m_ClientId = ClientId; |
| 885 | for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) |
| 886 | { |
| 887 | Msg.m_apSkinPartNames[Part] = Info.m_aaSkinPartNames[Part]; |
| 888 | Msg.m_aSkinPartColors[Part] = Info.m_aSkinPartColors[Part]; |
| 889 | Msg.m_aUseCustomColors[Part] = Info.m_aUseCustomColors[Part]; |
| 890 | } |
| 891 | |
| 892 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1); |
| 893 | } |
| 894 | |
| 895 | void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc) |
| 896 | { |
| 897 | // reset votes |
| 898 | m_VoteEnforce = VOTE_ENFORCE_UNKNOWN; |
| 899 | for(auto &pPlayer : m_apPlayers) |
| 900 | { |
| 901 | if(pPlayer) |
| 902 | { |
| 903 | pPlayer->m_Vote = 0; |
| 904 | pPlayer->m_VotePos = 0; |
| 905 | } |
| 906 | } |
| 907 | |
| 908 | // start vote |
| 909 | m_VoteCloseTime = time_get() + time_freq() * g_Config.m_SvVoteTime; |
| 910 | str_copy(dst: m_aVoteDescription, src: pDesc, dst_size: sizeof(m_aVoteDescription)); |
| 911 | str_copy(dst: m_aSixupVoteDescription, src: pSixupDesc, dst_size: sizeof(m_aSixupVoteDescription)); |
| 912 | str_copy(dst: m_aVoteCommand, src: pCommand, dst_size: sizeof(m_aVoteCommand)); |
| 913 | str_copy(dst: m_aVoteReason, src: pReason, dst_size: sizeof(m_aVoteReason)); |
| 914 | SendVoteSet(ClientId: -1); |
| 915 | m_VoteUpdate = true; |
| 916 | } |
| 917 | |
| 918 | void CGameContext::EndVote() |
| 919 | { |
| 920 | m_VoteCloseTime = 0; |
| 921 | SendVoteSet(ClientId: -1); |
| 922 | } |
| 923 | |
| 924 | void CGameContext::SendVoteSet(int ClientId) |
| 925 | { |
| 926 | ::CNetMsg_Sv_VoteSet Msg6; |
| 927 | protocol7::CNetMsg_Sv_VoteSet Msg7; |
| 928 | |
| 929 | Msg7.m_ClientId = m_VoteCreator; |
| 930 | if(m_VoteCloseTime) |
| 931 | { |
| 932 | Msg6.m_Timeout = Msg7.m_Timeout = (m_VoteCloseTime - time_get()) / time_freq(); |
| 933 | Msg6.m_pDescription = m_aVoteDescription; |
| 934 | Msg6.m_pReason = Msg7.m_pReason = m_aVoteReason; |
| 935 | |
| 936 | Msg7.m_pDescription = m_aSixupVoteDescription; |
| 937 | if(IsKickVote()) |
| 938 | Msg7.m_Type = protocol7::VOTE_START_KICK; |
| 939 | else if(IsSpecVote()) |
| 940 | Msg7.m_Type = protocol7::VOTE_START_SPEC; |
| 941 | else if(IsOptionVote()) |
| 942 | Msg7.m_Type = protocol7::VOTE_START_OP; |
| 943 | else |
| 944 | Msg7.m_Type = protocol7::VOTE_UNKNOWN; |
| 945 | } |
| 946 | else |
| 947 | { |
| 948 | Msg6.m_Timeout = Msg7.m_Timeout = 0; |
| 949 | Msg6.m_pDescription = Msg7.m_pDescription = "" ; |
| 950 | Msg6.m_pReason = Msg7.m_pReason = "" ; |
| 951 | |
| 952 | if(m_VoteEnforce == VOTE_ENFORCE_NO || m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN) |
| 953 | Msg7.m_Type = protocol7::VOTE_END_FAIL; |
| 954 | else if(m_VoteEnforce == VOTE_ENFORCE_YES || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) |
| 955 | Msg7.m_Type = protocol7::VOTE_END_PASS; |
| 956 | else if(m_VoteEnforce == VOTE_ENFORCE_ABORT || m_VoteEnforce == VOTE_ENFORCE_CANCEL) |
| 957 | Msg7.m_Type = protocol7::VOTE_END_ABORT; |
| 958 | else |
| 959 | Msg7.m_Type = protocol7::VOTE_UNKNOWN; |
| 960 | |
| 961 | if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) |
| 962 | Msg7.m_ClientId = -1; |
| 963 | } |
| 964 | |
| 965 | if(ClientId == -1) |
| 966 | { |
| 967 | for(int i = 0; i < Server()->MaxClients(); i++) |
| 968 | { |
| 969 | if(!m_apPlayers[i]) |
| 970 | continue; |
| 971 | if(!Server()->IsSixup(ClientId: i)) |
| 972 | Server()->SendPackMsg(pMsg: &Msg6, Flags: MSGFLAG_VITAL, ClientId: i); |
| 973 | else |
| 974 | Server()->SendPackMsg(pMsg: &Msg7, Flags: MSGFLAG_VITAL, ClientId: i); |
| 975 | } |
| 976 | } |
| 977 | else |
| 978 | { |
| 979 | if(!Server()->IsSixup(ClientId)) |
| 980 | Server()->SendPackMsg(pMsg: &Msg6, Flags: MSGFLAG_VITAL, ClientId); |
| 981 | else |
| 982 | Server()->SendPackMsg(pMsg: &Msg7, Flags: MSGFLAG_VITAL, ClientId); |
| 983 | } |
| 984 | } |
| 985 | |
| 986 | void CGameContext::SendVoteStatus(int ClientId, int Total, int Yes, int No) |
| 987 | { |
| 988 | if(ClientId == -1) |
| 989 | { |
| 990 | for(int i = 0; i < MAX_CLIENTS; ++i) |
| 991 | if(Server()->ClientIngame(ClientId: i)) |
| 992 | SendVoteStatus(ClientId: i, Total, Yes, No); |
| 993 | return; |
| 994 | } |
| 995 | |
| 996 | if(Total > VANILLA_MAX_CLIENTS && m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetClientVersion() <= VERSION_DDRACE) |
| 997 | { |
| 998 | Yes = (Yes * VANILLA_MAX_CLIENTS) / (float)Total; |
| 999 | No = (No * VANILLA_MAX_CLIENTS) / (float)Total; |
| 1000 | Total = VANILLA_MAX_CLIENTS; |
| 1001 | } |
| 1002 | |
| 1003 | CNetMsg_Sv_VoteStatus Msg = {.m_Yes: 0}; |
| 1004 | Msg.m_Total = Total; |
| 1005 | Msg.m_Yes = Yes; |
| 1006 | Msg.m_No = No; |
| 1007 | Msg.m_Pass = Total - (Yes + No); |
| 1008 | |
| 1009 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 1010 | } |
| 1011 | |
| 1012 | void CGameContext::AbortVoteKickOnDisconnect(int ClientId) |
| 1013 | { |
| 1014 | if(m_VoteCloseTime && ((str_startswith(str: m_aVoteCommand, prefix: "kick " ) && str_toint(str: &m_aVoteCommand[5]) == ClientId) || |
| 1015 | (str_startswith(str: m_aVoteCommand, prefix: "set_team " ) && str_toint(str: &m_aVoteCommand[9]) == ClientId))) |
| 1016 | m_VoteEnforce = VOTE_ENFORCE_ABORT; |
| 1017 | } |
| 1018 | |
| 1019 | void CGameContext::CheckPureTuning() |
| 1020 | { |
| 1021 | // might not be created yet during start up |
| 1022 | if(!m_pController) |
| 1023 | return; |
| 1024 | |
| 1025 | if(str_comp(a: m_pController->m_pGameType, b: "DM" ) == 0 || |
| 1026 | str_comp(a: m_pController->m_pGameType, b: "TDM" ) == 0 || |
| 1027 | str_comp(a: m_pController->m_pGameType, b: "CTF" ) == 0) |
| 1028 | { |
| 1029 | if(mem_comp(a: &CTuningParams::DEFAULT, b: &m_aTuningList[0], size: sizeof(CTuningParams)) != 0) |
| 1030 | { |
| 1031 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: "resetting tuning due to pure server" ); |
| 1032 | m_aTuningList[0] = CTuningParams::DEFAULT; |
| 1033 | } |
| 1034 | } |
| 1035 | } |
| 1036 | |
| 1037 | void CGameContext::SendTuningParams(int ClientId, int Zone) |
| 1038 | { |
| 1039 | if(ClientId == -1) |
| 1040 | { |
| 1041 | for(int i = 0; i < MAX_CLIENTS; ++i) |
| 1042 | { |
| 1043 | if(m_apPlayers[i]) |
| 1044 | { |
| 1045 | if(m_apPlayers[i]->GetCharacter()) |
| 1046 | { |
| 1047 | if(m_apPlayers[i]->GetCharacter()->m_TuneZone == Zone) |
| 1048 | SendTuningParams(ClientId: i, Zone); |
| 1049 | } |
| 1050 | else if(m_apPlayers[i]->m_TuneZone == Zone) |
| 1051 | { |
| 1052 | SendTuningParams(ClientId: i, Zone); |
| 1053 | } |
| 1054 | } |
| 1055 | } |
| 1056 | return; |
| 1057 | } |
| 1058 | |
| 1059 | CheckPureTuning(); |
| 1060 | |
| 1061 | dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "Invalid ClientId: %d" , ClientId); |
| 1062 | dbg_assert(m_apPlayers[ClientId], "client %d without player" , ClientId); |
| 1063 | |
| 1064 | CTuningParams Params = m_aTuningList[Zone]; |
| 1065 | |
| 1066 | CCharacter *pCharacter = m_apPlayers[ClientId]->GetCharacter(); |
| 1067 | int NeededFakeTuning = pCharacter ? pCharacter->NeededFaketuning() : 0; |
| 1068 | |
| 1069 | if(NeededFakeTuning & FAKETUNE_SOLO) |
| 1070 | { |
| 1071 | Params.m_PlayerCollision = 0; |
| 1072 | Params.m_PlayerHooking = 0; |
| 1073 | } |
| 1074 | |
| 1075 | if(NeededFakeTuning & FAKETUNE_NOCOLL) |
| 1076 | { |
| 1077 | Params.m_PlayerCollision = 0; |
| 1078 | } |
| 1079 | |
| 1080 | if(NeededFakeTuning & FAKETUNE_NOHOOK) |
| 1081 | { |
| 1082 | Params.m_PlayerHooking = 0; |
| 1083 | } |
| 1084 | |
| 1085 | if(NeededFakeTuning & FAKETUNE_NOJUMP) |
| 1086 | { |
| 1087 | Params.m_GroundJumpImpulse = 0; |
| 1088 | } |
| 1089 | |
| 1090 | if(NeededFakeTuning & FAKETUNE_JETPACK) |
| 1091 | { |
| 1092 | Params.m_JetpackStrength = 0; |
| 1093 | } |
| 1094 | |
| 1095 | if(NeededFakeTuning & FAKETUNE_NOHAMMER) |
| 1096 | { |
| 1097 | Params.m_HammerStrength = 0; |
| 1098 | } |
| 1099 | |
| 1100 | CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS); |
| 1101 | const int *pParams = Params.NetworkArray(); |
| 1102 | for(int i = 0; i < CTuningParams::Num(); i++) |
| 1103 | { |
| 1104 | static_assert(offsetof(CTuningParams, m_LaserDamage) / sizeof(CTuneParam) == 30); |
| 1105 | if(i == 30 && Server()->IsSixup(ClientId)) // laser_damage was removed in 0.7 |
| 1106 | { |
| 1107 | continue; |
| 1108 | } |
| 1109 | Msg.AddInt(i: pParams[i]); |
| 1110 | } |
| 1111 | Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 1112 | } |
| 1113 | |
| 1114 | void CGameContext::OnPreTickTeehistorian() |
| 1115 | { |
| 1116 | if(!m_TeeHistorianActive) |
| 1117 | return; |
| 1118 | |
| 1119 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1120 | { |
| 1121 | if(m_apPlayers[i] != nullptr) |
| 1122 | m_TeeHistorian.RecordPlayerTeam(ClientId: i, Team: GetDDRaceTeam(ClientId: i)); |
| 1123 | else |
| 1124 | m_TeeHistorian.RecordPlayerTeam(ClientId: i, Team: 0); |
| 1125 | } |
| 1126 | for(int i = 0; i < TEAM_SUPER; i++) |
| 1127 | { |
| 1128 | m_TeeHistorian.RecordTeamPractice(Team: i, Practice: m_pController->Teams().IsPractice(Team: i)); |
| 1129 | } |
| 1130 | } |
| 1131 | |
| 1132 | void CGameContext::OnTick() |
| 1133 | { |
| 1134 | // check tuning |
| 1135 | CheckPureTuning(); |
| 1136 | |
| 1137 | if(m_TeeHistorianActive) |
| 1138 | { |
| 1139 | int Error = aio_error(aio: m_pTeeHistorianFile); |
| 1140 | if(Error) |
| 1141 | { |
| 1142 | dbg_msg(sys: "teehistorian" , fmt: "error writing to file, err=%d" , Error); |
| 1143 | Server()->SetErrorShutdown("teehistorian io error" ); |
| 1144 | } |
| 1145 | |
| 1146 | if(!m_TeeHistorian.Starting()) |
| 1147 | { |
| 1148 | m_TeeHistorian.EndInputs(); |
| 1149 | m_TeeHistorian.EndTick(); |
| 1150 | } |
| 1151 | m_TeeHistorian.BeginTick(Tick: Server()->Tick()); |
| 1152 | m_TeeHistorian.BeginPlayers(); |
| 1153 | } |
| 1154 | |
| 1155 | // copy tuning |
| 1156 | *m_World.GetTuning(i: 0) = m_aTuningList[0]; |
| 1157 | m_World.Tick(); |
| 1158 | |
| 1159 | UpdatePlayerMaps(); |
| 1160 | |
| 1161 | m_pController->Tick(); |
| 1162 | |
| 1163 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1164 | { |
| 1165 | if(m_apPlayers[i]) |
| 1166 | { |
| 1167 | // send vote options |
| 1168 | ProgressVoteOptions(ClientId: i); |
| 1169 | |
| 1170 | m_apPlayers[i]->Tick(); |
| 1171 | m_apPlayers[i]->PostTick(); |
| 1172 | } |
| 1173 | } |
| 1174 | |
| 1175 | for(auto &pPlayer : m_apPlayers) |
| 1176 | { |
| 1177 | if(pPlayer) |
| 1178 | pPlayer->PostPostTick(); |
| 1179 | } |
| 1180 | |
| 1181 | // update voting |
| 1182 | if(m_VoteCloseTime) |
| 1183 | { |
| 1184 | // abort the kick-vote on player-leave |
| 1185 | if(m_VoteEnforce == VOTE_ENFORCE_ABORT) |
| 1186 | { |
| 1187 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote aborted" ); |
| 1188 | EndVote(); |
| 1189 | } |
| 1190 | else if(m_VoteEnforce == VOTE_ENFORCE_CANCEL) |
| 1191 | { |
| 1192 | char aBuf[64]; |
| 1193 | if(m_VoteCreator == -1) |
| 1194 | { |
| 1195 | str_copy(dst&: aBuf, src: "Vote canceled" ); |
| 1196 | } |
| 1197 | else |
| 1198 | { |
| 1199 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' canceled their vote" , Server()->ClientName(ClientId: m_VoteCreator)); |
| 1200 | } |
| 1201 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf); |
| 1202 | EndVote(); |
| 1203 | } |
| 1204 | else |
| 1205 | { |
| 1206 | int Total = 0, Yes = 0, No = 0; |
| 1207 | bool Veto = false, VetoStop = false; |
| 1208 | if(m_VoteUpdate) |
| 1209 | { |
| 1210 | // count votes |
| 1211 | const NETADDR *apAddresses[MAX_CLIENTS] = {nullptr}; |
| 1212 | const NETADDR *pFirstAddress = nullptr; |
| 1213 | bool SinglePlayer = true; |
| 1214 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1215 | { |
| 1216 | if(m_apPlayers[i]) |
| 1217 | { |
| 1218 | apAddresses[i] = Server()->ClientAddr(ClientId: i); |
| 1219 | if(!pFirstAddress) |
| 1220 | { |
| 1221 | pFirstAddress = apAddresses[i]; |
| 1222 | } |
| 1223 | else if(SinglePlayer && net_addr_comp_noport(a: pFirstAddress, b: apAddresses[i]) != 0) |
| 1224 | { |
| 1225 | SinglePlayer = false; |
| 1226 | } |
| 1227 | } |
| 1228 | } |
| 1229 | |
| 1230 | // remember checked players, only the first player with a specific ip will be handled |
| 1231 | bool aVoteChecked[MAX_CLIENTS] = {false}; |
| 1232 | int64_t Now = Server()->Tick(); |
| 1233 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1234 | { |
| 1235 | if(!m_apPlayers[i] || aVoteChecked[i]) |
| 1236 | continue; |
| 1237 | |
| 1238 | if((IsKickVote() || IsSpecVote()) && (m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS || |
| 1239 | (GetPlayerChar(ClientId: m_VoteCreator) && GetPlayerChar(ClientId: i) && |
| 1240 | GetPlayerChar(ClientId: m_VoteCreator)->Team() != GetPlayerChar(ClientId: i)->Team()))) |
| 1241 | continue; |
| 1242 | |
| 1243 | if(m_apPlayers[i]->IsAfk() && i != m_VoteCreator) |
| 1244 | continue; |
| 1245 | |
| 1246 | // can't vote in kick and spec votes in the beginning after joining |
| 1247 | if((IsKickVote() || IsSpecVote()) && Now < m_apPlayers[i]->m_FirstVoteTick) |
| 1248 | continue; |
| 1249 | |
| 1250 | // connecting clients with spoofed ips can clog slots without being ingame |
| 1251 | if(!Server()->ClientIngame(ClientId: i)) |
| 1252 | continue; |
| 1253 | |
| 1254 | // don't count votes by blacklisted clients |
| 1255 | if(g_Config.m_SvDnsblVote && !m_pServer->DnsblWhite(ClientId: i) && !SinglePlayer) |
| 1256 | continue; |
| 1257 | |
| 1258 | int CurVote = m_apPlayers[i]->m_Vote; |
| 1259 | int CurVotePos = m_apPlayers[i]->m_VotePos; |
| 1260 | |
| 1261 | // only allow IPs to vote once, but keep veto ability |
| 1262 | // check for more players with the same ip (only use the vote of the one who voted first) |
| 1263 | for(int j = i + 1; j < MAX_CLIENTS; j++) |
| 1264 | { |
| 1265 | if(!m_apPlayers[j] || aVoteChecked[j] || net_addr_comp_noport(a: apAddresses[j], b: apAddresses[i]) != 0) |
| 1266 | continue; |
| 1267 | |
| 1268 | // count the latest vote by this ip |
| 1269 | if(CurVotePos < m_apPlayers[j]->m_VotePos) |
| 1270 | { |
| 1271 | CurVote = m_apPlayers[j]->m_Vote; |
| 1272 | CurVotePos = m_apPlayers[j]->m_VotePos; |
| 1273 | } |
| 1274 | |
| 1275 | aVoteChecked[j] = true; |
| 1276 | } |
| 1277 | |
| 1278 | Total++; |
| 1279 | if(CurVote > 0) |
| 1280 | Yes++; |
| 1281 | else if(CurVote < 0) |
| 1282 | No++; |
| 1283 | |
| 1284 | // veto right for players who have been active on server for long and who're not afk |
| 1285 | if(!IsKickVote() && !IsSpecVote() && g_Config.m_SvVoteVetoTime) |
| 1286 | { |
| 1287 | // look through all players with same IP again, including the current player |
| 1288 | for(int j = i; j < MAX_CLIENTS; j++) |
| 1289 | { |
| 1290 | // no need to check ip address of current player |
| 1291 | if(i != j && (!m_apPlayers[j] || net_addr_comp_noport(a: apAddresses[j], b: apAddresses[i]) != 0)) |
| 1292 | continue; |
| 1293 | |
| 1294 | if(m_apPlayers[j] && !m_apPlayers[j]->IsAfk() && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && |
| 1295 | ((Server()->Tick() - m_apPlayers[j]->m_JoinTick) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime || |
| 1296 | (m_apPlayers[j]->GetCharacter() && m_apPlayers[j]->GetCharacter()->m_DDRaceState == ERaceState::STARTED && |
| 1297 | (Server()->Tick() - m_apPlayers[j]->GetCharacter()->m_StartTime) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime))) |
| 1298 | { |
| 1299 | if(CurVote == 0) |
| 1300 | Veto = true; |
| 1301 | else if(CurVote < 0) |
| 1302 | VetoStop = true; |
| 1303 | break; |
| 1304 | } |
| 1305 | } |
| 1306 | } |
| 1307 | } |
| 1308 | |
| 1309 | if(g_Config.m_SvVoteMaxTotal && Total > g_Config.m_SvVoteMaxTotal && |
| 1310 | (IsKickVote() || IsSpecVote())) |
| 1311 | Total = g_Config.m_SvVoteMaxTotal; |
| 1312 | |
| 1313 | if((Yes > Total / (100.0f / g_Config.m_SvVoteYesPercentage)) && !Veto) |
| 1314 | m_VoteEnforce = VOTE_ENFORCE_YES; |
| 1315 | else if(No >= Total - Total / (100.0f / g_Config.m_SvVoteYesPercentage)) |
| 1316 | m_VoteEnforce = VOTE_ENFORCE_NO; |
| 1317 | |
| 1318 | if(VetoStop) |
| 1319 | m_VoteEnforce = VOTE_ENFORCE_NO; |
| 1320 | |
| 1321 | m_VoteWillPass = Yes > (Yes + No) / (100.0f / g_Config.m_SvVoteYesPercentage); |
| 1322 | } |
| 1323 | |
| 1324 | if(time_get() > m_VoteCloseTime && !g_Config.m_SvVoteMajority) |
| 1325 | m_VoteEnforce = (m_VoteWillPass && !Veto) ? VOTE_ENFORCE_YES : VOTE_ENFORCE_NO; |
| 1326 | |
| 1327 | // / Ensure minimum time for vote to end when moderating. |
| 1328 | if(m_VoteEnforce == VOTE_ENFORCE_YES && !(PlayerModerating() && |
| 1329 | (IsKickVote() || IsSpecVote()) && time_get() < m_VoteCloseTime)) |
| 1330 | { |
| 1331 | Server()->SetRconCid(IServer::RCON_CID_VOTE); |
| 1332 | Console()->ExecuteLine(pStr: m_aVoteCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED); |
| 1333 | Server()->SetRconCid(IServer::RCON_CID_SERV); |
| 1334 | EndVote(); |
| 1335 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote passed" , SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 1336 | |
| 1337 | if(m_VoteCreator != -1 && m_apPlayers[m_VoteCreator] && !IsKickVote() && !IsSpecVote()) |
| 1338 | m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0; |
| 1339 | } |
| 1340 | else if(m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) |
| 1341 | { |
| 1342 | Server()->SetRconCid(IServer::RCON_CID_VOTE); |
| 1343 | Console()->ExecuteLine(pStr: m_aVoteCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED); |
| 1344 | Server()->SetRconCid(IServer::RCON_CID_SERV); |
| 1345 | EndVote(); |
| 1346 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote passed enforced by authorized player" , SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 1347 | |
| 1348 | if(m_VoteCreator != -1 && m_apPlayers[m_VoteCreator]) |
| 1349 | m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0; |
| 1350 | } |
| 1351 | else if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN) |
| 1352 | { |
| 1353 | EndVote(); |
| 1354 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote failed enforced by authorized player" , SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 1355 | } |
| 1356 | else if(m_VoteEnforce == VOTE_ENFORCE_NO || (time_get() > m_VoteCloseTime && g_Config.m_SvVoteMajority)) |
| 1357 | { |
| 1358 | EndVote(); |
| 1359 | if(VetoStop || (m_VoteWillPass && Veto)) |
| 1360 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote failed because of veto. Find an empty server instead" , SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 1361 | else |
| 1362 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: "Vote failed" , SpamProtectionClientId: -1, VersionFlags: FLAG_SIX); |
| 1363 | } |
| 1364 | else if(m_VoteUpdate) |
| 1365 | { |
| 1366 | m_VoteUpdate = false; |
| 1367 | SendVoteStatus(ClientId: -1, Total, Yes, No); |
| 1368 | } |
| 1369 | } |
| 1370 | } |
| 1371 | |
| 1372 | if(Server()->Tick() % (Server()->TickSpeed() / 2) == 0) |
| 1373 | { |
| 1374 | m_Mutes.UnmuteExpired(); |
| 1375 | m_VoteMutes.UnmuteExpired(); |
| 1376 | } |
| 1377 | |
| 1378 | if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0) |
| 1379 | { |
| 1380 | const char *pLine = Server()->GetAnnouncementLine(); |
| 1381 | if(pLine) |
| 1382 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: pLine); |
| 1383 | } |
| 1384 | |
| 1385 | for(auto &Switcher : Switchers()) |
| 1386 | { |
| 1387 | for(int j = 0; j < NUM_DDRACE_TEAMS; ++j) |
| 1388 | { |
| 1389 | if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDOPEN) |
| 1390 | { |
| 1391 | Switcher.m_aStatus[j] = false; |
| 1392 | Switcher.m_aEndTick[j] = 0; |
| 1393 | Switcher.m_aType[j] = TILE_SWITCHCLOSE; |
| 1394 | } |
| 1395 | else if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDCLOSE) |
| 1396 | { |
| 1397 | Switcher.m_aStatus[j] = true; |
| 1398 | Switcher.m_aEndTick[j] = 0; |
| 1399 | Switcher.m_aType[j] = TILE_SWITCHOPEN; |
| 1400 | } |
| 1401 | } |
| 1402 | } |
| 1403 | |
| 1404 | if(m_SqlRandomMapResult != nullptr && m_SqlRandomMapResult->m_Completed) |
| 1405 | { |
| 1406 | if(m_SqlRandomMapResult->m_Success) |
| 1407 | { |
| 1408 | if(m_SqlRandomMapResult->m_ClientId != -1 && m_apPlayers[m_SqlRandomMapResult->m_ClientId] && m_SqlRandomMapResult->m_aMessage[0] != '\0') |
| 1409 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: m_SqlRandomMapResult->m_aMessage); |
| 1410 | if(m_SqlRandomMapResult->m_aMap[0] != '\0') |
| 1411 | Server()->ChangeMap(pMap: m_SqlRandomMapResult->m_aMap); |
| 1412 | else |
| 1413 | m_LastMapVote = 0; |
| 1414 | } |
| 1415 | m_SqlRandomMapResult = nullptr; |
| 1416 | } |
| 1417 | |
| 1418 | // check for map info result from database |
| 1419 | if(m_pLoadMapInfoResult != nullptr && m_pLoadMapInfoResult->m_Completed) |
| 1420 | { |
| 1421 | if(m_pLoadMapInfoResult->m_Success && m_pLoadMapInfoResult->m_Data.m_aaMessages[0][0] != '\0') |
| 1422 | { |
| 1423 | str_copy(dst&: m_aMapInfoMessage, src: m_pLoadMapInfoResult->m_Data.m_aaMessages[0]); |
| 1424 | CNetMsg_Sv_MapInfo MapInfoMsg; |
| 1425 | MapInfoMsg.m_pDescription = m_aMapInfoMessage; |
| 1426 | Server()->SendPackMsg(pMsg: &MapInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1); |
| 1427 | } |
| 1428 | m_pLoadMapInfoResult = nullptr; |
| 1429 | } |
| 1430 | |
| 1431 | // Record player position at the end of the tick |
| 1432 | if(m_TeeHistorianActive) |
| 1433 | { |
| 1434 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1435 | { |
| 1436 | if(m_apPlayers[i] && m_apPlayers[i]->GetCharacter()) |
| 1437 | { |
| 1438 | CNetObj_CharacterCore Char; |
| 1439 | m_apPlayers[i]->GetCharacter()->GetCore().Write(pObjCore: &Char); |
| 1440 | m_TeeHistorian.RecordPlayer(ClientId: i, pChar: &Char); |
| 1441 | } |
| 1442 | else |
| 1443 | { |
| 1444 | m_TeeHistorian.RecordDeadPlayer(ClientId: i); |
| 1445 | } |
| 1446 | } |
| 1447 | m_TeeHistorian.EndPlayers(); |
| 1448 | m_TeeHistorian.BeginInputs(); |
| 1449 | } |
| 1450 | // Warning: do not put code in this function directly above or below this comment |
| 1451 | } |
| 1452 | |
| 1453 | void CGameContext::PreInputClients(int ClientId, bool *pClients) |
| 1454 | { |
| 1455 | if(!pClients || !m_apPlayers[ClientId]) |
| 1456 | return; |
| 1457 | |
| 1458 | CCharacter *pInputChr = m_apPlayers[ClientId]->GetCharacter(); |
| 1459 | if(!pInputChr || m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsAfk()) |
| 1460 | return; |
| 1461 | |
| 1462 | for(int Id = 0; Id < MAX_CLIENTS; Id++) |
| 1463 | { |
| 1464 | if(ClientId == Id) |
| 1465 | continue; |
| 1466 | |
| 1467 | CPlayer *pPlayer = m_apPlayers[Id]; |
| 1468 | if(!pPlayer) |
| 1469 | continue; |
| 1470 | |
| 1471 | if(Server()->GetClientVersion(ClientId: Id) < VERSION_DDNET_PREINPUT) |
| 1472 | continue; |
| 1473 | |
| 1474 | if(pPlayer->GetTeam() == TEAM_SPECTATORS || GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: Id) || pPlayer->IsAfk()) |
| 1475 | continue; |
| 1476 | |
| 1477 | if(!pInputChr->CanSnapCharacter(SnappingClient: Id) || pInputChr->NetworkClipped(SnappingClient: Id)) |
| 1478 | continue; |
| 1479 | |
| 1480 | pClients[Id] = true; |
| 1481 | } |
| 1482 | } |
| 1483 | |
| 1484 | // Server hooks |
| 1485 | void CGameContext::OnClientPrepareInput(int ClientId, void *pInput) |
| 1486 | { |
| 1487 | CNetObj_PlayerInput *pPlayerInput = static_cast<CNetObj_PlayerInput *>(pInput); |
| 1488 | |
| 1489 | if(Server()->IsSixup(ClientId)) |
| 1490 | pPlayerInput->m_PlayerFlags = PlayerFlags_SevenToSix(Flags: pPlayerInput->m_PlayerFlags); |
| 1491 | } |
| 1492 | |
| 1493 | void CGameContext::OnClientDirectInput(int ClientId, const void *pInput) |
| 1494 | { |
| 1495 | const CNetObj_PlayerInput *pPlayerInput = static_cast<const CNetObj_PlayerInput *>(pInput); |
| 1496 | |
| 1497 | if(!m_pController->IsGamePaused()) |
| 1498 | m_apPlayers[ClientId]->OnDirectInput(pNewInput: pPlayerInput); |
| 1499 | |
| 1500 | int Flags = pPlayerInput->m_PlayerFlags; |
| 1501 | if((Flags & 256) || (Flags & 512)) |
| 1502 | { |
| 1503 | Server()->Kick(ClientId, pReason: "please update your client or use DDNet client" ); |
| 1504 | } |
| 1505 | } |
| 1506 | |
| 1507 | void CGameContext::OnClientPredictedInput(int ClientId, const void *pInput) |
| 1508 | { |
| 1509 | const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput); |
| 1510 | |
| 1511 | if(pApplyInput == nullptr) |
| 1512 | { |
| 1513 | // early return if no input at all has been sent by a player |
| 1514 | if(!m_aPlayerHasInput[ClientId]) |
| 1515 | { |
| 1516 | return; |
| 1517 | } |
| 1518 | // set to last sent input when no new input has been sent |
| 1519 | pApplyInput = &m_aLastPlayerInput[ClientId]; |
| 1520 | } |
| 1521 | |
| 1522 | if(!m_pController->IsGamePaused()) |
| 1523 | m_apPlayers[ClientId]->OnPredictedInput(pNewInput: pApplyInput); |
| 1524 | } |
| 1525 | |
| 1526 | void CGameContext::OnClientPredictedEarlyInput(int ClientId, const void *pInput) |
| 1527 | { |
| 1528 | const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput); |
| 1529 | |
| 1530 | if(pApplyInput == nullptr) |
| 1531 | { |
| 1532 | // early return if no input at all has been sent by a player |
| 1533 | if(!m_aPlayerHasInput[ClientId]) |
| 1534 | { |
| 1535 | return; |
| 1536 | } |
| 1537 | // set to last sent input when no new input has been sent |
| 1538 | pApplyInput = &m_aLastPlayerInput[ClientId]; |
| 1539 | } |
| 1540 | else |
| 1541 | { |
| 1542 | // Store input in this function and not in `OnClientPredictedInput`, |
| 1543 | // because this function is called on all inputs, while |
| 1544 | // `OnClientPredictedInput` is only called on the first input of each |
| 1545 | // tick. |
| 1546 | mem_copy(dest: &m_aLastPlayerInput[ClientId], source: pApplyInput, size: sizeof(m_aLastPlayerInput[ClientId])); |
| 1547 | m_aPlayerHasInput[ClientId] = true; |
| 1548 | } |
| 1549 | |
| 1550 | if(!m_pController->IsGamePaused()) |
| 1551 | m_apPlayers[ClientId]->OnPredictedEarlyInput(pNewInput: pApplyInput); |
| 1552 | |
| 1553 | if(m_TeeHistorianActive) |
| 1554 | { |
| 1555 | m_TeeHistorian.RecordPlayerInput(ClientId, UniqueClientId: m_apPlayers[ClientId]->GetUniqueCid(), pInput: pApplyInput); |
| 1556 | } |
| 1557 | } |
| 1558 | |
| 1559 | const CVoteOptionServer *CGameContext::GetVoteOption(int Index) const |
| 1560 | { |
| 1561 | const CVoteOptionServer *pCurrent; |
| 1562 | for(pCurrent = m_pVoteOptionFirst; |
| 1563 | Index > 0 && pCurrent; |
| 1564 | Index--, pCurrent = pCurrent->m_pNext) |
| 1565 | ; |
| 1566 | |
| 1567 | if(Index > 0) |
| 1568 | return nullptr; |
| 1569 | return pCurrent; |
| 1570 | } |
| 1571 | |
| 1572 | void CGameContext::ProgressVoteOptions(int ClientId) |
| 1573 | { |
| 1574 | CPlayer *pPl = m_apPlayers[ClientId]; |
| 1575 | |
| 1576 | if(pPl->m_SendVoteIndex == -1) |
| 1577 | return; // we didn't start sending options yet |
| 1578 | |
| 1579 | if(pPl->m_SendVoteIndex > m_NumVoteOptions) |
| 1580 | return; // shouldn't happen / fail silently |
| 1581 | |
| 1582 | int VotesLeft = m_NumVoteOptions - pPl->m_SendVoteIndex; |
| 1583 | int NumVotesToSend = minimum(a: g_Config.m_SvSendVotesPerTick, b: VotesLeft); |
| 1584 | |
| 1585 | if(!VotesLeft) |
| 1586 | { |
| 1587 | // player has up to date vote option list |
| 1588 | return; |
| 1589 | } |
| 1590 | |
| 1591 | // build vote option list msg |
| 1592 | int CurIndex = 0; |
| 1593 | |
| 1594 | CNetMsg_Sv_VoteOptionListAdd OptionMsg; |
| 1595 | OptionMsg.m_pDescription0 = "" ; |
| 1596 | OptionMsg.m_pDescription1 = "" ; |
| 1597 | OptionMsg.m_pDescription2 = "" ; |
| 1598 | OptionMsg.m_pDescription3 = "" ; |
| 1599 | OptionMsg.m_pDescription4 = "" ; |
| 1600 | OptionMsg.m_pDescription5 = "" ; |
| 1601 | OptionMsg.m_pDescription6 = "" ; |
| 1602 | OptionMsg.m_pDescription7 = "" ; |
| 1603 | OptionMsg.m_pDescription8 = "" ; |
| 1604 | OptionMsg.m_pDescription9 = "" ; |
| 1605 | OptionMsg.m_pDescription10 = "" ; |
| 1606 | OptionMsg.m_pDescription11 = "" ; |
| 1607 | OptionMsg.m_pDescription12 = "" ; |
| 1608 | OptionMsg.m_pDescription13 = "" ; |
| 1609 | OptionMsg.m_pDescription14 = "" ; |
| 1610 | |
| 1611 | // get current vote option by index |
| 1612 | const CVoteOptionServer *pCurrent = GetVoteOption(Index: pPl->m_SendVoteIndex); |
| 1613 | |
| 1614 | while(CurIndex < NumVotesToSend && pCurrent != nullptr) |
| 1615 | { |
| 1616 | switch(CurIndex) |
| 1617 | { |
| 1618 | case 0: OptionMsg.m_pDescription0 = pCurrent->m_aDescription; break; |
| 1619 | case 1: OptionMsg.m_pDescription1 = pCurrent->m_aDescription; break; |
| 1620 | case 2: OptionMsg.m_pDescription2 = pCurrent->m_aDescription; break; |
| 1621 | case 3: OptionMsg.m_pDescription3 = pCurrent->m_aDescription; break; |
| 1622 | case 4: OptionMsg.m_pDescription4 = pCurrent->m_aDescription; break; |
| 1623 | case 5: OptionMsg.m_pDescription5 = pCurrent->m_aDescription; break; |
| 1624 | case 6: OptionMsg.m_pDescription6 = pCurrent->m_aDescription; break; |
| 1625 | case 7: OptionMsg.m_pDescription7 = pCurrent->m_aDescription; break; |
| 1626 | case 8: OptionMsg.m_pDescription8 = pCurrent->m_aDescription; break; |
| 1627 | case 9: OptionMsg.m_pDescription9 = pCurrent->m_aDescription; break; |
| 1628 | case 10: OptionMsg.m_pDescription10 = pCurrent->m_aDescription; break; |
| 1629 | case 11: OptionMsg.m_pDescription11 = pCurrent->m_aDescription; break; |
| 1630 | case 12: OptionMsg.m_pDescription12 = pCurrent->m_aDescription; break; |
| 1631 | case 13: OptionMsg.m_pDescription13 = pCurrent->m_aDescription; break; |
| 1632 | case 14: OptionMsg.m_pDescription14 = pCurrent->m_aDescription; break; |
| 1633 | } |
| 1634 | |
| 1635 | CurIndex++; |
| 1636 | pCurrent = pCurrent->m_pNext; |
| 1637 | } |
| 1638 | |
| 1639 | // send msg |
| 1640 | if(pPl->m_SendVoteIndex == 0) |
| 1641 | { |
| 1642 | CNetMsg_Sv_VoteOptionGroupStart StartMsg; |
| 1643 | Server()->SendPackMsg(pMsg: &StartMsg, Flags: MSGFLAG_VITAL, ClientId); |
| 1644 | } |
| 1645 | |
| 1646 | OptionMsg.m_NumOptions = NumVotesToSend; |
| 1647 | Server()->SendPackMsg(pMsg: &OptionMsg, Flags: MSGFLAG_VITAL, ClientId); |
| 1648 | |
| 1649 | pPl->m_SendVoteIndex += NumVotesToSend; |
| 1650 | |
| 1651 | if(pPl->m_SendVoteIndex == m_NumVoteOptions) |
| 1652 | { |
| 1653 | CNetMsg_Sv_VoteOptionGroupEnd EndMsg; |
| 1654 | Server()->SendPackMsg(pMsg: &EndMsg, Flags: MSGFLAG_VITAL, ClientId); |
| 1655 | } |
| 1656 | } |
| 1657 | |
| 1658 | void CGameContext::OnClientEnter(int ClientId) |
| 1659 | { |
| 1660 | if(m_TeeHistorianActive) |
| 1661 | { |
| 1662 | m_TeeHistorian.RecordPlayerReady(ClientId); |
| 1663 | } |
| 1664 | m_pController->OnPlayerConnect(pPlayer: m_apPlayers[ClientId]); |
| 1665 | |
| 1666 | { |
| 1667 | CNetMsg_Sv_CommandInfoGroupStart Msg; |
| 1668 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1669 | } |
| 1670 | for(const IConsole::ICommandInfo *pCmd = Console()->FirstCommandInfo(ClientId, FlagMask: CFGFLAG_CHAT); |
| 1671 | pCmd; pCmd = Console()->NextCommandInfo(pInfo: pCmd, ClientId, FlagMask: CFGFLAG_CHAT)) |
| 1672 | { |
| 1673 | const char *pName = pCmd->Name(); |
| 1674 | |
| 1675 | if(Server()->IsSixup(ClientId)) |
| 1676 | { |
| 1677 | if(!str_comp_nocase(a: pName, b: "w" ) || !str_comp_nocase(a: pName, b: "whisper" )) |
| 1678 | continue; |
| 1679 | |
| 1680 | if(!str_comp_nocase(a: pName, b: "r" )) |
| 1681 | pName = "rescue" ; |
| 1682 | |
| 1683 | protocol7::CNetMsg_Sv_CommandInfo Msg; |
| 1684 | Msg.m_pName = pName; |
| 1685 | Msg.m_pArgsFormat = pCmd->Params(); |
| 1686 | Msg.m_pHelpText = pCmd->Help(); |
| 1687 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1688 | } |
| 1689 | else |
| 1690 | { |
| 1691 | CNetMsg_Sv_CommandInfo Msg; |
| 1692 | Msg.m_pName = pName; |
| 1693 | Msg.m_pArgsFormat = pCmd->Params(); |
| 1694 | Msg.m_pHelpText = pCmd->Help(); |
| 1695 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1696 | } |
| 1697 | } |
| 1698 | { |
| 1699 | CNetMsg_Sv_CommandInfoGroupEnd Msg; |
| 1700 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1701 | } |
| 1702 | |
| 1703 | { |
| 1704 | int Empty = -1; |
| 1705 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 1706 | { |
| 1707 | if(Server()->ClientSlotEmpty(ClientId: i)) |
| 1708 | { |
| 1709 | Empty = i; |
| 1710 | break; |
| 1711 | } |
| 1712 | } |
| 1713 | CNetMsg_Sv_Chat Msg; |
| 1714 | Msg.m_Team = 0; |
| 1715 | Msg.m_ClientId = Empty; |
| 1716 | Msg.m_pMessage = "Do you know someone who uses a bot? Please report them to the moderators." ; |
| 1717 | m_apPlayers[ClientId]->m_EligibleForFinishCheck = time_get(); |
| 1718 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1719 | } |
| 1720 | |
| 1721 | IServer::CClientInfo Info; |
| 1722 | if(Server()->GetClientInfo(ClientId, pInfo: &Info) && Info.m_GotDDNetVersion) |
| 1723 | { |
| 1724 | if(OnClientDDNetVersionKnown(ClientId)) |
| 1725 | return; // kicked |
| 1726 | } |
| 1727 | |
| 1728 | if(!Server()->ClientPrevIngame(ClientId)) |
| 1729 | { |
| 1730 | if(g_Config.m_SvWelcome[0] != 0) |
| 1731 | SendChatTarget(To: ClientId, pText: g_Config.m_SvWelcome); |
| 1732 | |
| 1733 | if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF) |
| 1734 | { |
| 1735 | if(g_Config.m_SvShowOthers) |
| 1736 | SendChatTarget(To: ClientId, pText: "You can see other players. To disable this use DDNet client and type /showothers" ); |
| 1737 | |
| 1738 | m_apPlayers[ClientId]->m_ShowOthers = g_Config.m_SvShowOthersDefault; |
| 1739 | } |
| 1740 | } |
| 1741 | m_VoteUpdate = true; |
| 1742 | |
| 1743 | // send active vote |
| 1744 | if(m_VoteCloseTime) |
| 1745 | SendVoteSet(ClientId); |
| 1746 | |
| 1747 | Server()->ExpireServerInfo(); |
| 1748 | |
| 1749 | // send map info if loaded from database |
| 1750 | if(m_aMapInfoMessage[0] != '\0') |
| 1751 | { |
| 1752 | CNetMsg_Sv_MapInfo MapInfoMsg; |
| 1753 | MapInfoMsg.m_pDescription = m_aMapInfoMessage; |
| 1754 | Server()->SendPackMsg(pMsg: &MapInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1755 | } |
| 1756 | |
| 1757 | CPlayer *pNewPlayer = m_apPlayers[ClientId]; |
| 1758 | mem_zero(block: &m_aLastPlayerInput[ClientId], size: sizeof(m_aLastPlayerInput[ClientId])); |
| 1759 | m_aPlayerHasInput[ClientId] = false; |
| 1760 | |
| 1761 | // new info for others |
| 1762 | protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg; |
| 1763 | NewClientInfoMsg.m_ClientId = ClientId; |
| 1764 | NewClientInfoMsg.m_Local = 0; |
| 1765 | NewClientInfoMsg.m_Team = pNewPlayer->GetTeam(); |
| 1766 | NewClientInfoMsg.m_pName = Server()->ClientName(ClientId); |
| 1767 | NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientId); |
| 1768 | NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientId); |
| 1769 | NewClientInfoMsg.m_Silent = false; |
| 1770 | |
| 1771 | for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) |
| 1772 | { |
| 1773 | NewClientInfoMsg.m_apSkinPartNames[p] = pNewPlayer->m_TeeInfos.m_aaSkinPartNames[p]; |
| 1774 | NewClientInfoMsg.m_aUseCustomColors[p] = pNewPlayer->m_TeeInfos.m_aUseCustomColors[p]; |
| 1775 | NewClientInfoMsg.m_aSkinPartColors[p] = pNewPlayer->m_TeeInfos.m_aSkinPartColors[p]; |
| 1776 | } |
| 1777 | |
| 1778 | // update client infos (others before local) |
| 1779 | for(int i = 0; i < Server()->MaxClients(); ++i) |
| 1780 | { |
| 1781 | if(i == ClientId || !m_apPlayers[i] || !Server()->ClientIngame(ClientId: i)) |
| 1782 | continue; |
| 1783 | |
| 1784 | CPlayer *pPlayer = m_apPlayers[i]; |
| 1785 | |
| 1786 | if(Server()->IsSixup(ClientId: i)) |
| 1787 | Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 1788 | |
| 1789 | if(Server()->IsSixup(ClientId)) |
| 1790 | { |
| 1791 | // existing infos for new player |
| 1792 | protocol7::CNetMsg_Sv_ClientInfo ClientInfoMsg; |
| 1793 | ClientInfoMsg.m_ClientId = i; |
| 1794 | ClientInfoMsg.m_Local = 0; |
| 1795 | ClientInfoMsg.m_Team = pPlayer->GetTeam(); |
| 1796 | ClientInfoMsg.m_pName = Server()->ClientName(ClientId: i); |
| 1797 | ClientInfoMsg.m_pClan = Server()->ClientClan(ClientId: i); |
| 1798 | ClientInfoMsg.m_Country = Server()->ClientCountry(ClientId: i); |
| 1799 | ClientInfoMsg.m_Silent = 0; |
| 1800 | |
| 1801 | for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) |
| 1802 | { |
| 1803 | ClientInfoMsg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p]; |
| 1804 | ClientInfoMsg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p]; |
| 1805 | ClientInfoMsg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p]; |
| 1806 | } |
| 1807 | |
| 1808 | Server()->SendPackMsg(pMsg: &ClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1809 | } |
| 1810 | } |
| 1811 | |
| 1812 | // local info |
| 1813 | if(Server()->IsSixup(ClientId)) |
| 1814 | { |
| 1815 | NewClientInfoMsg.m_Local = 1; |
| 1816 | Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 1817 | } |
| 1818 | |
| 1819 | // initial chat delay |
| 1820 | if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientId]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed()) |
| 1821 | { |
| 1822 | char aBuf[128]; |
| 1823 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "This server has an initial chat delay, you will need to wait %d seconds before talking." , g_Config.m_SvChatInitialDelay); |
| 1824 | SendChatTarget(To: ClientId, pText: aBuf); |
| 1825 | m_Mutes.Mute(pAddr: Server()->ClientAddr(ClientId), Seconds: g_Config.m_SvChatInitialDelay, pReason: "Initial chat delay" , pClientName: Server()->ClientName(ClientId), InitialDelay: true); |
| 1826 | } |
| 1827 | |
| 1828 | LogEvent(Description: "Connect" , ClientId); |
| 1829 | } |
| 1830 | |
| 1831 | bool CGameContext::OnClientDataPersist(int ClientId, void *pData) |
| 1832 | { |
| 1833 | CPersistentClientData *pPersistent = (CPersistentClientData *)pData; |
| 1834 | if(!m_apPlayers[ClientId]) |
| 1835 | { |
| 1836 | return false; |
| 1837 | } |
| 1838 | pPersistent->m_IsSpectator = m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS; |
| 1839 | pPersistent->m_IsAfk = m_apPlayers[ClientId]->IsAfk(); |
| 1840 | pPersistent->m_LastWhisperTo = m_apPlayers[ClientId]->m_LastWhisperTo; |
| 1841 | return true; |
| 1842 | } |
| 1843 | |
| 1844 | void CGameContext::OnClientConnected(int ClientId, void *pData) |
| 1845 | { |
| 1846 | CPersistentClientData *pPersistentData = (CPersistentClientData *)pData; |
| 1847 | bool Spec = false; |
| 1848 | bool Afk = true; |
| 1849 | int LastWhisperTo = -1; |
| 1850 | if(pPersistentData) |
| 1851 | { |
| 1852 | Spec = pPersistentData->m_IsSpectator; |
| 1853 | Afk = pPersistentData->m_IsAfk; |
| 1854 | LastWhisperTo = pPersistentData->m_LastWhisperTo; |
| 1855 | } |
| 1856 | else |
| 1857 | { |
| 1858 | // new player connected, clear whispers waiting for the old player with this id |
| 1859 | for(auto &pPlayer : m_apPlayers) |
| 1860 | { |
| 1861 | if(pPlayer && pPlayer->m_LastWhisperTo == ClientId) |
| 1862 | pPlayer->m_LastWhisperTo = -1; |
| 1863 | } |
| 1864 | } |
| 1865 | |
| 1866 | { |
| 1867 | bool Empty = true; |
| 1868 | for(auto &pPlayer : m_apPlayers) |
| 1869 | { |
| 1870 | // connecting clients with spoofed ips can clog slots without being ingame |
| 1871 | if(pPlayer && Server()->ClientIngame(ClientId: pPlayer->GetCid())) |
| 1872 | { |
| 1873 | Empty = false; |
| 1874 | break; |
| 1875 | } |
| 1876 | } |
| 1877 | if(Empty) |
| 1878 | { |
| 1879 | m_NonEmptySince = Server()->Tick(); |
| 1880 | } |
| 1881 | } |
| 1882 | |
| 1883 | // Check which team the player should be on |
| 1884 | const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(NotThisId: ClientId); |
| 1885 | CreatePlayer(ClientId, StartTeam, Afk, LastWhisperTo); |
| 1886 | |
| 1887 | SendMotd(ClientId); |
| 1888 | SendSettings(ClientId); |
| 1889 | |
| 1890 | Server()->ExpireServerInfo(); |
| 1891 | } |
| 1892 | |
| 1893 | void CGameContext::OnClientDrop(int ClientId, const char *pReason) |
| 1894 | { |
| 1895 | LogEvent(Description: "Disconnect" , ClientId); |
| 1896 | |
| 1897 | AbortVoteKickOnDisconnect(ClientId); |
| 1898 | m_pController->OnPlayerDisconnect(pPlayer: m_apPlayers[ClientId], pReason); |
| 1899 | delete m_apPlayers[ClientId]; |
| 1900 | m_apPlayers[ClientId] = nullptr; |
| 1901 | |
| 1902 | delete m_apSavedTeams[ClientId]; |
| 1903 | m_apSavedTeams[ClientId] = nullptr; |
| 1904 | |
| 1905 | delete m_apSavedTees[ClientId]; |
| 1906 | m_apSavedTees[ClientId] = nullptr; |
| 1907 | |
| 1908 | m_aTeamMapping[ClientId] = -1; |
| 1909 | |
| 1910 | if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && PracticeByDefault()) |
| 1911 | m_pController->Teams().SetPractice(Team: GetDDRaceTeam(ClientId), Enabled: true); |
| 1912 | |
| 1913 | m_VoteUpdate = true; |
| 1914 | if(m_VoteCreator == ClientId) |
| 1915 | { |
| 1916 | m_VoteCreator = -1; |
| 1917 | } |
| 1918 | |
| 1919 | // update spectator modes |
| 1920 | for(auto &pPlayer : m_apPlayers) |
| 1921 | { |
| 1922 | if(pPlayer && pPlayer->SpectatorId() == ClientId) |
| 1923 | pPlayer->SetSpectatorId(SPEC_FREEVIEW); |
| 1924 | } |
| 1925 | |
| 1926 | // update conversation targets |
| 1927 | for(auto &pPlayer : m_apPlayers) |
| 1928 | { |
| 1929 | if(pPlayer && pPlayer->m_LastWhisperTo == ClientId) |
| 1930 | pPlayer->m_LastWhisperTo = -1; |
| 1931 | } |
| 1932 | |
| 1933 | protocol7::CNetMsg_Sv_ClientDrop Msg; |
| 1934 | Msg.m_ClientId = ClientId; |
| 1935 | Msg.m_pReason = pReason; |
| 1936 | Msg.m_Silent = false; |
| 1937 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1); |
| 1938 | |
| 1939 | Server()->ExpireServerInfo(); |
| 1940 | } |
| 1941 | |
| 1942 | void CGameContext::TeehistorianRecordAntibot(const void *pData, int DataSize) |
| 1943 | { |
| 1944 | if(m_TeeHistorianActive) |
| 1945 | { |
| 1946 | m_TeeHistorian.RecordAntibot(pData, DataSize); |
| 1947 | } |
| 1948 | } |
| 1949 | |
| 1950 | void CGameContext::TeehistorianRecordPlayerJoin(int ClientId, bool Sixup) |
| 1951 | { |
| 1952 | if(m_TeeHistorianActive) |
| 1953 | { |
| 1954 | m_TeeHistorian.RecordPlayerJoin(ClientId, Protocol: !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7); |
| 1955 | } |
| 1956 | } |
| 1957 | |
| 1958 | void CGameContext::TeehistorianRecordPlayerDrop(int ClientId, const char *pReason) |
| 1959 | { |
| 1960 | if(m_TeeHistorianActive) |
| 1961 | { |
| 1962 | m_TeeHistorian.RecordPlayerDrop(ClientId, pReason); |
| 1963 | } |
| 1964 | } |
| 1965 | |
| 1966 | void CGameContext::TeehistorianRecordPlayerRejoin(int ClientId) |
| 1967 | { |
| 1968 | if(m_TeeHistorianActive) |
| 1969 | { |
| 1970 | m_TeeHistorian.RecordPlayerRejoin(ClientId); |
| 1971 | } |
| 1972 | } |
| 1973 | |
| 1974 | void CGameContext::TeehistorianRecordPlayerName(int ClientId, const char *pName) |
| 1975 | { |
| 1976 | if(m_TeeHistorianActive) |
| 1977 | { |
| 1978 | m_TeeHistorian.RecordPlayerName(ClientId, pName); |
| 1979 | } |
| 1980 | } |
| 1981 | |
| 1982 | void CGameContext::TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks) |
| 1983 | { |
| 1984 | if(m_TeeHistorianActive) |
| 1985 | { |
| 1986 | m_TeeHistorian.RecordPlayerFinish(ClientId, TimeTicks); |
| 1987 | } |
| 1988 | } |
| 1989 | |
| 1990 | void CGameContext::TeehistorianRecordTeamFinish(int TeamId, int TimeTicks) |
| 1991 | { |
| 1992 | if(m_TeeHistorianActive) |
| 1993 | { |
| 1994 | m_TeeHistorian.RecordTeamFinish(TeamId, TimeTicks); |
| 1995 | } |
| 1996 | } |
| 1997 | |
| 1998 | void CGameContext::TeehistorianRecordAuthLogin(int ClientId, int Level, const char *pAuthName) |
| 1999 | { |
| 2000 | if(m_TeeHistorianActive) |
| 2001 | { |
| 2002 | m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName); |
| 2003 | } |
| 2004 | } |
| 2005 | |
| 2006 | bool CGameContext::OnClientDDNetVersionKnown(int ClientId) |
| 2007 | { |
| 2008 | IServer::CClientInfo Info; |
| 2009 | dbg_assert(Server()->GetClientInfo(ClientId, &Info), "failed to get client info" ); |
| 2010 | int ClientVersion = Info.m_DDNetVersion; |
| 2011 | dbg_msg(sys: "ddnet" , fmt: "cid=%d version=%d" , ClientId, ClientVersion); |
| 2012 | |
| 2013 | if(m_TeeHistorianActive) |
| 2014 | { |
| 2015 | if(Info.m_pConnectionId && Info.m_pDDNetVersionStr) |
| 2016 | { |
| 2017 | m_TeeHistorian.RecordDDNetVersion(ClientId, ConnectionId: *Info.m_pConnectionId, DDNetVersion: ClientVersion, pDDNetVersionStr: Info.m_pDDNetVersionStr); |
| 2018 | } |
| 2019 | else |
| 2020 | { |
| 2021 | m_TeeHistorian.RecordDDNetVersionOld(ClientId, DDNetVersion: ClientVersion); |
| 2022 | } |
| 2023 | } |
| 2024 | |
| 2025 | // Autoban known bot versions. |
| 2026 | if(g_Config.m_SvBannedVersions[0] != '\0' && IsVersionBanned(Version: ClientVersion)) |
| 2027 | { |
| 2028 | Server()->Kick(ClientId, pReason: "unsupported client" ); |
| 2029 | return true; |
| 2030 | } |
| 2031 | |
| 2032 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2033 | if(ClientVersion >= VERSION_DDNET_GAMETICK) |
| 2034 | pPlayer->m_TimerType = g_Config.m_SvDefaultTimerType; |
| 2035 | |
| 2036 | // First update the teams state. |
| 2037 | m_pController->Teams().SendTeamsState(ClientId); |
| 2038 | |
| 2039 | // Then send records. |
| 2040 | SendRecord(ClientId); |
| 2041 | |
| 2042 | // And report correct tunings. |
| 2043 | if(ClientVersion < VERSION_DDNET_EARLY_VERSION) |
| 2044 | SendTuningParams(ClientId, Zone: pPlayer->m_TuneZone); |
| 2045 | |
| 2046 | // Tell old clients to update. |
| 2047 | if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0') |
| 2048 | SendBroadcast(pText: g_Config.m_SvClientSuggestionOld, ClientId); |
| 2049 | // Tell known bot clients that they're botting and we know it. |
| 2050 | if(((ClientVersion >= 15 && ClientVersion < 100) || ClientVersion == 502) && g_Config.m_SvClientSuggestionBot[0] != '\0') |
| 2051 | SendBroadcast(pText: g_Config.m_SvClientSuggestionBot, ClientId); |
| 2052 | |
| 2053 | return false; |
| 2054 | } |
| 2055 | |
| 2056 | void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId) |
| 2057 | { |
| 2058 | if(Server()->IsSixup(ClientId) && *pMsgId < OFFSET_UUID) |
| 2059 | { |
| 2060 | void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(Type: *pMsgId, pUnpacker); |
| 2061 | if(!pRawMsg) |
| 2062 | return nullptr; |
| 2063 | |
| 2064 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2065 | static char s_aRawMsg[1024]; |
| 2066 | |
| 2067 | if(*pMsgId == protocol7::NETMSGTYPE_CL_SAY) |
| 2068 | { |
| 2069 | protocol7::CNetMsg_Cl_Say *pMsg7 = (protocol7::CNetMsg_Cl_Say *)pRawMsg; |
| 2070 | // Should probably use a placement new to start the lifetime of the object to avoid future weirdness |
| 2071 | ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg; |
| 2072 | |
| 2073 | if(pMsg7->m_Mode == protocol7::CHAT_WHISPER) |
| 2074 | { |
| 2075 | if(!CheckClientId(ClientId: pMsg7->m_Target) || !Server()->ClientIngame(ClientId: pMsg7->m_Target)) |
| 2076 | return nullptr; |
| 2077 | if(ProcessSpamProtection(ClientId)) |
| 2078 | return nullptr; |
| 2079 | |
| 2080 | WhisperId(ClientId, VictimId: pMsg7->m_Target, pMessage: pMsg7->m_pMessage); |
| 2081 | return nullptr; |
| 2082 | } |
| 2083 | else |
| 2084 | { |
| 2085 | pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM; |
| 2086 | pMsg->m_pMessage = pMsg7->m_pMessage; |
| 2087 | } |
| 2088 | } |
| 2089 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_STARTINFO) |
| 2090 | { |
| 2091 | protocol7::CNetMsg_Cl_StartInfo *pMsg7 = (protocol7::CNetMsg_Cl_StartInfo *)pRawMsg; |
| 2092 | ::CNetMsg_Cl_StartInfo *pMsg = (::CNetMsg_Cl_StartInfo *)s_aRawMsg; |
| 2093 | |
| 2094 | pMsg->m_pName = pMsg7->m_pName; |
| 2095 | pMsg->m_pClan = pMsg7->m_pClan; |
| 2096 | pMsg->m_Country = pMsg7->m_Country; |
| 2097 | |
| 2098 | pPlayer->m_TeeInfos = CTeeInfo(pMsg7->m_apSkinPartNames, pMsg7->m_aUseCustomColors, pMsg7->m_aSkinPartColors); |
| 2099 | pPlayer->m_TeeInfos.FromSixup(); |
| 2100 | |
| 2101 | str_copy(dst: s_aRawMsg + sizeof(*pMsg), src: pPlayer->m_TeeInfos.m_aSkinName, dst_size: sizeof(s_aRawMsg) - sizeof(*pMsg)); |
| 2102 | |
| 2103 | pMsg->m_pSkin = s_aRawMsg + sizeof(*pMsg); |
| 2104 | pMsg->m_UseCustomColor = pPlayer->m_TeeInfos.m_UseCustomColor; |
| 2105 | pMsg->m_ColorBody = pPlayer->m_TeeInfos.m_ColorBody; |
| 2106 | pMsg->m_ColorFeet = pPlayer->m_TeeInfos.m_ColorFeet; |
| 2107 | } |
| 2108 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_SKINCHANGE) |
| 2109 | { |
| 2110 | protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg; |
| 2111 | if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && |
| 2112 | pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick()) |
| 2113 | return nullptr; |
| 2114 | |
| 2115 | pPlayer->m_LastChangeInfo = Server()->Tick(); |
| 2116 | |
| 2117 | CTeeInfo Info(pMsg->m_apSkinPartNames, pMsg->m_aUseCustomColors, pMsg->m_aSkinPartColors); |
| 2118 | Info.FromSixup(); |
| 2119 | pPlayer->m_TeeInfos = Info; |
| 2120 | SendSkinChange7(ClientId); |
| 2121 | |
| 2122 | return nullptr; |
| 2123 | } |
| 2124 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE) |
| 2125 | { |
| 2126 | protocol7::CNetMsg_Cl_SetSpectatorMode *pMsg7 = (protocol7::CNetMsg_Cl_SetSpectatorMode *)pRawMsg; |
| 2127 | ::CNetMsg_Cl_SetSpectatorMode *pMsg = (::CNetMsg_Cl_SetSpectatorMode *)s_aRawMsg; |
| 2128 | |
| 2129 | if(pMsg7->m_SpecMode == protocol7::SPEC_FREEVIEW) |
| 2130 | pMsg->m_SpectatorId = SPEC_FREEVIEW; |
| 2131 | else if(pMsg7->m_SpecMode == protocol7::SPEC_PLAYER) |
| 2132 | pMsg->m_SpectatorId = pMsg7->m_SpectatorId; |
| 2133 | else |
| 2134 | pMsg->m_SpectatorId = SPEC_FREEVIEW; // Probably not needed |
| 2135 | } |
| 2136 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM) |
| 2137 | { |
| 2138 | protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg; |
| 2139 | ::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg; |
| 2140 | |
| 2141 | pMsg->m_Team = pMsg7->m_Team; |
| 2142 | } |
| 2143 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_COMMAND) |
| 2144 | { |
| 2145 | protocol7::CNetMsg_Cl_Command *pMsg7 = (protocol7::CNetMsg_Cl_Command *)pRawMsg; |
| 2146 | ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg; |
| 2147 | |
| 2148 | str_format(buffer: s_aRawMsg + sizeof(*pMsg), buffer_size: sizeof(s_aRawMsg) - sizeof(*pMsg), format: "/%s %s" , pMsg7->m_pName, pMsg7->m_pArguments); |
| 2149 | pMsg->m_pMessage = s_aRawMsg + sizeof(*pMsg); |
| 2150 | pMsg->m_Team = 0; |
| 2151 | |
| 2152 | *pMsgId = NETMSGTYPE_CL_SAY; |
| 2153 | return s_aRawMsg; |
| 2154 | } |
| 2155 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_CALLVOTE) |
| 2156 | { |
| 2157 | protocol7::CNetMsg_Cl_CallVote *pMsg7 = (protocol7::CNetMsg_Cl_CallVote *)pRawMsg; |
| 2158 | |
| 2159 | if(pMsg7->m_Force) |
| 2160 | { |
| 2161 | if(!Server()->IsRconAuthed(ClientId)) |
| 2162 | { |
| 2163 | return nullptr; |
| 2164 | } |
| 2165 | char aCommand[IConsole::CMDLINE_LENGTH]; |
| 2166 | str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "force_vote \"%s\" \"%s\" \"%s\"" , pMsg7->m_pType, pMsg7->m_pValue, pMsg7->m_pReason); |
| 2167 | Console()->ExecuteLine(pStr: aCommand, ClientId, InterpretSemicolons: false); |
| 2168 | return nullptr; |
| 2169 | } |
| 2170 | |
| 2171 | ::CNetMsg_Cl_CallVote *pMsg = (::CNetMsg_Cl_CallVote *)s_aRawMsg; |
| 2172 | pMsg->m_pValue = pMsg7->m_pValue; |
| 2173 | pMsg->m_pReason = pMsg7->m_pReason; |
| 2174 | pMsg->m_pType = pMsg7->m_pType; |
| 2175 | } |
| 2176 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_EMOTICON) |
| 2177 | { |
| 2178 | protocol7::CNetMsg_Cl_Emoticon *pMsg7 = (protocol7::CNetMsg_Cl_Emoticon *)pRawMsg; |
| 2179 | ::CNetMsg_Cl_Emoticon *pMsg = (::CNetMsg_Cl_Emoticon *)s_aRawMsg; |
| 2180 | |
| 2181 | pMsg->m_Emoticon = pMsg7->m_Emoticon; |
| 2182 | } |
| 2183 | else if(*pMsgId == protocol7::NETMSGTYPE_CL_VOTE) |
| 2184 | { |
| 2185 | protocol7::CNetMsg_Cl_Vote *pMsg7 = (protocol7::CNetMsg_Cl_Vote *)pRawMsg; |
| 2186 | ::CNetMsg_Cl_Vote *pMsg = (::CNetMsg_Cl_Vote *)s_aRawMsg; |
| 2187 | |
| 2188 | pMsg->m_Vote = pMsg7->m_Vote; |
| 2189 | } |
| 2190 | |
| 2191 | *pMsgId = Msg_SevenToSix(a: *pMsgId); |
| 2192 | |
| 2193 | return s_aRawMsg; |
| 2194 | } |
| 2195 | else |
| 2196 | return m_NetObjHandler.SecureUnpackMsg(Type: *pMsgId, pUnpacker); |
| 2197 | } |
| 2198 | |
| 2199 | void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size) |
| 2200 | { |
| 2201 | str_copy(dst: pCensoredMessage, src: pMessage, dst_size: Size); |
| 2202 | |
| 2203 | for(auto &Item : m_vCensorlist) |
| 2204 | { |
| 2205 | char *pCurLoc = pCensoredMessage; |
| 2206 | do |
| 2207 | { |
| 2208 | pCurLoc = (char *)str_utf8_find_nocase(haystack: pCurLoc, needle: Item.c_str()); |
| 2209 | if(pCurLoc) |
| 2210 | { |
| 2211 | for(int i = 0; i < (int)Item.length(); i++) |
| 2212 | { |
| 2213 | pCurLoc[i] = '*'; |
| 2214 | } |
| 2215 | pCurLoc++; |
| 2216 | } |
| 2217 | } while(pCurLoc); |
| 2218 | } |
| 2219 | } |
| 2220 | |
| 2221 | void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId) |
| 2222 | { |
| 2223 | if(m_TeeHistorianActive) |
| 2224 | { |
| 2225 | if(m_NetObjHandler.TeeHistorianRecordMsg(Type: MsgId)) |
| 2226 | { |
| 2227 | m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize()); |
| 2228 | } |
| 2229 | } |
| 2230 | |
| 2231 | void *pRawMsg = PreProcessMsg(pMsgId: &MsgId, pUnpacker, ClientId); |
| 2232 | |
| 2233 | if(!pRawMsg) |
| 2234 | return; |
| 2235 | |
| 2236 | if(Server()->ClientIngame(ClientId)) |
| 2237 | { |
| 2238 | switch(MsgId) |
| 2239 | { |
| 2240 | case NETMSGTYPE_CL_SAY: |
| 2241 | OnSayNetMessage(pMsg: static_cast<CNetMsg_Cl_Say *>(pRawMsg), ClientId, pUnpacker); |
| 2242 | break; |
| 2243 | case NETMSGTYPE_CL_CALLVOTE: |
| 2244 | OnCallVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_CallVote *>(pRawMsg), ClientId); |
| 2245 | break; |
| 2246 | case NETMSGTYPE_CL_VOTE: |
| 2247 | OnVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_Vote *>(pRawMsg), ClientId); |
| 2248 | break; |
| 2249 | case NETMSGTYPE_CL_SETTEAM: |
| 2250 | OnSetTeamNetMessage(pMsg: static_cast<CNetMsg_Cl_SetTeam *>(pRawMsg), ClientId); |
| 2251 | break; |
| 2252 | case NETMSGTYPE_CL_ISDDNETLEGACY: |
| 2253 | OnIsDDNetLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_IsDDNetLegacy *>(pRawMsg), ClientId, pUnpacker); |
| 2254 | break; |
| 2255 | case NETMSGTYPE_CL_SHOWOTHERSLEGACY: |
| 2256 | OnShowOthersLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthersLegacy *>(pRawMsg), ClientId); |
| 2257 | break; |
| 2258 | case NETMSGTYPE_CL_SHOWOTHERS: |
| 2259 | OnShowOthersNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthers *>(pRawMsg), ClientId); |
| 2260 | break; |
| 2261 | case NETMSGTYPE_CL_SHOWDISTANCE: |
| 2262 | OnShowDistanceNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowDistance *>(pRawMsg), ClientId); |
| 2263 | break; |
| 2264 | case NETMSGTYPE_CL_CAMERAINFO: |
| 2265 | OnCameraInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_CameraInfo *>(pRawMsg), ClientId); |
| 2266 | break; |
| 2267 | case NETMSGTYPE_CL_SETSPECTATORMODE: |
| 2268 | OnSetSpectatorModeNetMessage(pMsg: static_cast<CNetMsg_Cl_SetSpectatorMode *>(pRawMsg), ClientId); |
| 2269 | break; |
| 2270 | case NETMSGTYPE_CL_CHANGEINFO: |
| 2271 | OnChangeInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_ChangeInfo *>(pRawMsg), ClientId); |
| 2272 | break; |
| 2273 | case NETMSGTYPE_CL_EMOTICON: |
| 2274 | OnEmoticonNetMessage(pMsg: static_cast<CNetMsg_Cl_Emoticon *>(pRawMsg), ClientId); |
| 2275 | break; |
| 2276 | case NETMSGTYPE_CL_KILL: |
| 2277 | OnKillNetMessage(pMsg: static_cast<CNetMsg_Cl_Kill *>(pRawMsg), ClientId); |
| 2278 | break; |
| 2279 | case NETMSGTYPE_CL_ENABLESPECTATORCOUNT: |
| 2280 | OnEnableSpectatorCountNetMessage(pMsg: static_cast<CNetMsg_Cl_EnableSpectatorCount *>(pRawMsg), ClientId); |
| 2281 | default: |
| 2282 | break; |
| 2283 | } |
| 2284 | } |
| 2285 | if(MsgId == NETMSGTYPE_CL_STARTINFO) |
| 2286 | { |
| 2287 | OnStartInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientId); |
| 2288 | } |
| 2289 | } |
| 2290 | |
| 2291 | void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker) |
| 2292 | { |
| 2293 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2294 | bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get(); |
| 2295 | if(Check && str_comp(a: pMsg->m_pMessage, b: "xd sure chillerbot.png is lyfe" ) == 0 && pMsg->m_Team == 0) |
| 2296 | { |
| 2297 | if(m_TeeHistorianActive) |
| 2298 | { |
| 2299 | m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize()); |
| 2300 | } |
| 2301 | |
| 2302 | pPlayer->m_NotEligibleForFinish = true; |
| 2303 | dbg_msg(sys: "hack" , fmt: "bot detected, cid=%d" , ClientId); |
| 2304 | return; |
| 2305 | } |
| 2306 | int Team = pMsg->m_Team; |
| 2307 | |
| 2308 | // trim right and set maximum length to 256 utf8-characters |
| 2309 | int Length = 0; |
| 2310 | const char *p = pMsg->m_pMessage; |
| 2311 | const char *pEnd = nullptr; |
| 2312 | while(*p) |
| 2313 | { |
| 2314 | const char *pStrOld = p; |
| 2315 | int Code = str_utf8_decode(ptr: &p); |
| 2316 | |
| 2317 | // check if unicode is not empty |
| 2318 | if(!str_utf8_isspace(code: Code)) |
| 2319 | { |
| 2320 | pEnd = nullptr; |
| 2321 | } |
| 2322 | else if(pEnd == nullptr) |
| 2323 | pEnd = pStrOld; |
| 2324 | |
| 2325 | if(++Length >= 256) |
| 2326 | { |
| 2327 | *(const_cast<char *>(p)) = 0; |
| 2328 | break; |
| 2329 | } |
| 2330 | } |
| 2331 | if(pEnd != nullptr) |
| 2332 | *(const_cast<char *>(pEnd)) = 0; |
| 2333 | |
| 2334 | // drop empty and autocreated spam messages (more than 32 characters per second) |
| 2335 | if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick()))) |
| 2336 | return; |
| 2337 | |
| 2338 | int GameTeam = GetDDRaceTeam(ClientId: pPlayer->GetCid()); |
| 2339 | if(Team) |
| 2340 | Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? TEAM_SPECTATORS : GameTeam); |
| 2341 | else |
| 2342 | Team = TEAM_ALL; |
| 2343 | |
| 2344 | if(pMsg->m_pMessage[0] == '/') |
| 2345 | { |
| 2346 | const char *pWhisper; |
| 2347 | if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "w " ))) |
| 2348 | { |
| 2349 | Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper)); |
| 2350 | } |
| 2351 | else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "whisper " ))) |
| 2352 | { |
| 2353 | Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper)); |
| 2354 | } |
| 2355 | else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "c " ))) |
| 2356 | { |
| 2357 | Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper)); |
| 2358 | } |
| 2359 | else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "converse " ))) |
| 2360 | { |
| 2361 | Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper)); |
| 2362 | } |
| 2363 | else |
| 2364 | { |
| 2365 | if(g_Config.m_SvSpamprotection && !str_startswith(str: pMsg->m_pMessage + 1, prefix: "timeout " ) && pPlayer->m_aLastCommands[0] && pPlayer->m_aLastCommands[0] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[1] && pPlayer->m_aLastCommands[1] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[2] && pPlayer->m_aLastCommands[2] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[3] && pPlayer->m_aLastCommands[3] + Server()->TickSpeed() > Server()->Tick()) |
| 2366 | return; |
| 2367 | |
| 2368 | int64_t Now = Server()->Tick(); |
| 2369 | pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now; |
| 2370 | pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4; |
| 2371 | |
| 2372 | Console()->SetFlagMask(CFGFLAG_CHAT); |
| 2373 | { |
| 2374 | CClientChatLogger Logger(this, ClientId, log_get_scope_logger()); |
| 2375 | CLogScope Scope(&Logger); |
| 2376 | Console()->ExecuteLine(pStr: pMsg->m_pMessage + 1, ClientId, InterpretSemicolons: false); |
| 2377 | } |
| 2378 | // m_apPlayers[ClientId] can be NULL, if the player used a |
| 2379 | // timeout code and replaced another client. |
| 2380 | char aBuf[256]; |
| 2381 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d used %s" , ClientId, pMsg->m_pMessage); |
| 2382 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "chat-command" , pStr: aBuf); |
| 2383 | |
| 2384 | Console()->SetFlagMask(CFGFLAG_SERVER); |
| 2385 | } |
| 2386 | } |
| 2387 | else |
| 2388 | { |
| 2389 | pPlayer->UpdatePlaytime(); |
| 2390 | char aCensoredMessage[256]; |
| 2391 | CensorMessage(pCensoredMessage: aCensoredMessage, pMessage: pMsg->m_pMessage, Size: sizeof(aCensoredMessage)); |
| 2392 | SendChat(ChatterClientId: ClientId, Team, pText: aCensoredMessage, SpamProtectionClientId: ClientId); |
| 2393 | } |
| 2394 | } |
| 2395 | |
| 2396 | void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId) |
| 2397 | { |
| 2398 | if(RateLimitPlayerVote(ClientId) || m_VoteCloseTime) |
| 2399 | return; |
| 2400 | |
| 2401 | m_apPlayers[ClientId]->UpdatePlaytime(); |
| 2402 | |
| 2403 | m_VoteType = VOTE_TYPE_UNKNOWN; |
| 2404 | char aChatmsg[512] = {0}; |
| 2405 | char aDesc[VOTE_DESC_LENGTH] = {0}; |
| 2406 | char aSixupDesc[VOTE_DESC_LENGTH] = {0}; |
| 2407 | char aCmd[VOTE_CMD_LENGTH] = {0}; |
| 2408 | char aReason[VOTE_REASON_LENGTH] = "No reason given" ; |
| 2409 | if(pMsg->m_pReason[0]) |
| 2410 | { |
| 2411 | str_copy(dst: aReason, src: pMsg->m_pReason, dst_size: sizeof(aReason)); |
| 2412 | } |
| 2413 | |
| 2414 | if(str_comp_nocase(a: pMsg->m_pType, b: "option" ) == 0) |
| 2415 | { |
| 2416 | CVoteOptionServer *pOption = m_pVoteOptionFirst; |
| 2417 | while(pOption) |
| 2418 | { |
| 2419 | if(str_comp_nocase(a: pMsg->m_pValue, b: pOption->m_aDescription) == 0) |
| 2420 | { |
| 2421 | if(!Console()->LineIsValid(pStr: pOption->m_aCommand)) |
| 2422 | { |
| 2423 | SendChatTarget(To: ClientId, pText: "Invalid option" ); |
| 2424 | return; |
| 2425 | } |
| 2426 | if((str_find(haystack: pOption->m_aCommand, needle: "sv_map " ) != nullptr || str_find(haystack: pOption->m_aCommand, needle: "change_map " ) != nullptr || str_find(haystack: pOption->m_aCommand, needle: "random_map" ) != nullptr || str_find(haystack: pOption->m_aCommand, needle: "random_unfinished_map" ) != nullptr) && RateLimitPlayerMapVote(ClientId)) |
| 2427 | { |
| 2428 | return; |
| 2429 | } |
| 2430 | |
| 2431 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s' (%s)" , Server()->ClientName(ClientId), |
| 2432 | pOption->m_aDescription, aReason); |
| 2433 | str_copy(dst&: aDesc, src: pOption->m_aDescription); |
| 2434 | |
| 2435 | if((str_endswith(str: pOption->m_aCommand, suffix: "random_map" ) || str_endswith(str: pOption->m_aCommand, suffix: "random_unfinished_map" ))) |
| 2436 | { |
| 2437 | if(str_length(str: aReason) == 1 && aReason[0] >= '0' && aReason[0] <= '5') |
| 2438 | { |
| 2439 | int Stars = aReason[0] - '0'; |
| 2440 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d" , pOption->m_aCommand, Stars); |
| 2441 | } |
| 2442 | else if(str_length(str: aReason) == 3 && aReason[1] == '-' && aReason[0] >= '0' && aReason[0] <= '5' && aReason[2] >= '0' && aReason[2] <= '5') |
| 2443 | { |
| 2444 | int Start = aReason[0] - '0'; |
| 2445 | int End = aReason[2] - '0'; |
| 2446 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d %d" , pOption->m_aCommand, Start, End); |
| 2447 | } |
| 2448 | else |
| 2449 | { |
| 2450 | str_copy(dst&: aCmd, src: pOption->m_aCommand); |
| 2451 | } |
| 2452 | } |
| 2453 | else |
| 2454 | { |
| 2455 | str_copy(dst&: aCmd, src: pOption->m_aCommand); |
| 2456 | } |
| 2457 | |
| 2458 | m_LastMapVote = time_get(); |
| 2459 | break; |
| 2460 | } |
| 2461 | |
| 2462 | pOption = pOption->m_pNext; |
| 2463 | } |
| 2464 | |
| 2465 | if(!pOption) |
| 2466 | { |
| 2467 | if(!Server()->IsRconAuthedAdmin(ClientId)) // allow admins to call any vote they want |
| 2468 | { |
| 2469 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' isn't an option on this server" , pMsg->m_pValue); |
| 2470 | SendChatTarget(To: ClientId, pText: aChatmsg); |
| 2471 | return; |
| 2472 | } |
| 2473 | else |
| 2474 | { |
| 2475 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s'" , Server()->ClientName(ClientId), pMsg->m_pValue); |
| 2476 | str_copy(dst&: aDesc, src: pMsg->m_pValue); |
| 2477 | str_copy(dst&: aCmd, src: pMsg->m_pValue); |
| 2478 | } |
| 2479 | } |
| 2480 | |
| 2481 | m_VoteType = VOTE_TYPE_OPTION; |
| 2482 | } |
| 2483 | else if(str_comp_nocase(a: pMsg->m_pType, b: "kick" ) == 0) |
| 2484 | { |
| 2485 | if(!g_Config.m_SvVoteKick && !Server()->IsRconAuthed(ClientId)) // allow admins to call kick votes even if they are forbidden |
| 2486 | { |
| 2487 | SendChatTarget(To: ClientId, pText: "Server does not allow voting to kick players" ); |
| 2488 | return; |
| 2489 | } |
| 2490 | if(!Server()->IsRconAuthed(ClientId) && time_get() < m_apPlayers[ClientId]->m_LastKickVote + (time_freq() * g_Config.m_SvVoteKickDelay)) |
| 2491 | { |
| 2492 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "There's a %d second wait time between kick votes for each player please wait %d second(s)" , |
| 2493 | g_Config.m_SvVoteKickDelay, |
| 2494 | (int)((m_apPlayers[ClientId]->m_LastKickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq())); |
| 2495 | SendChatTarget(To: ClientId, pText: aChatmsg); |
| 2496 | return; |
| 2497 | } |
| 2498 | |
| 2499 | if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientId)) |
| 2500 | { |
| 2501 | const NETADDR *apAddresses[MAX_CLIENTS]; |
| 2502 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 2503 | { |
| 2504 | if(m_apPlayers[i]) |
| 2505 | { |
| 2506 | apAddresses[i] = Server()->ClientAddr(ClientId: i); |
| 2507 | } |
| 2508 | } |
| 2509 | int NumPlayers = 0; |
| 2510 | for(int i = 0; i < MAX_CLIENTS; ++i) |
| 2511 | { |
| 2512 | if(m_apPlayers[i] && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: i)) |
| 2513 | { |
| 2514 | NumPlayers++; |
| 2515 | for(int j = 0; j < i; j++) |
| 2516 | { |
| 2517 | if(m_apPlayers[j] && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: j)) |
| 2518 | { |
| 2519 | if(!net_addr_comp_noport(a: apAddresses[i], b: apAddresses[j])) |
| 2520 | { |
| 2521 | NumPlayers--; |
| 2522 | break; |
| 2523 | } |
| 2524 | } |
| 2525 | } |
| 2526 | } |
| 2527 | } |
| 2528 | |
| 2529 | if(NumPlayers < g_Config.m_SvVoteKickMin) |
| 2530 | { |
| 2531 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "Kick voting requires %d players" , g_Config.m_SvVoteKickMin); |
| 2532 | SendChatTarget(To: ClientId, pText: aChatmsg); |
| 2533 | return; |
| 2534 | } |
| 2535 | } |
| 2536 | |
| 2537 | int KickId = str_toint(str: pMsg->m_pValue); |
| 2538 | |
| 2539 | if(KickId < 0 || KickId >= MAX_CLIENTS || !m_apPlayers[KickId]) |
| 2540 | { |
| 2541 | SendChatTarget(To: ClientId, pText: "Invalid client id to kick" ); |
| 2542 | return; |
| 2543 | } |
| 2544 | if(KickId == ClientId) |
| 2545 | { |
| 2546 | SendChatTarget(To: ClientId, pText: "You can't kick yourself" ); |
| 2547 | return; |
| 2548 | } |
| 2549 | if(!Server()->ReverseTranslate(Target&: KickId, Client: ClientId)) |
| 2550 | { |
| 2551 | return; |
| 2552 | } |
| 2553 | int Authed = Server()->GetAuthedState(ClientId); |
| 2554 | int KickedAuthed = Server()->GetAuthedState(ClientId: KickId); |
| 2555 | if(KickedAuthed > Authed) |
| 2556 | { |
| 2557 | SendChatTarget(To: ClientId, pText: "You can't kick authorized players" ); |
| 2558 | char aBufKick[128]; |
| 2559 | str_format(buffer: aBufKick, buffer_size: sizeof(aBufKick), format: "'%s' called for vote to kick you" , Server()->ClientName(ClientId)); |
| 2560 | SendChatTarget(To: KickId, pText: aBufKick); |
| 2561 | return; |
| 2562 | } |
| 2563 | |
| 2564 | // Don't allow kicking if a player has no character |
| 2565 | if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: KickId)) |
| 2566 | { |
| 2567 | SendChatTarget(To: ClientId, pText: "You can kick only your team member" ); |
| 2568 | return; |
| 2569 | } |
| 2570 | |
| 2571 | if(GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: KickId)) |
| 2572 | { |
| 2573 | if(!g_Config.m_SvVoteKickMuteTime) |
| 2574 | { |
| 2575 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called for vote to mute '%s' (%s)" , Server()->ClientName(ClientId), Server()->ClientName(ClientId: KickId), aReason); |
| 2576 | str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s" , KickId, Server()->ClientName(ClientId: KickId)); |
| 2577 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "muteid %d %d Muted by vote" , KickId, g_Config.m_SvVoteKickMuteTime); |
| 2578 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Mute '%s'" , Server()->ClientName(ClientId: KickId)); |
| 2579 | } |
| 2580 | else |
| 2581 | { |
| 2582 | SendChatTarget(To: ClientId, pText: "You can kick only your team member" ); |
| 2583 | return; |
| 2584 | } |
| 2585 | } |
| 2586 | else |
| 2587 | { |
| 2588 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called for vote to kick '%s' (%s)" , Server()->ClientName(ClientId), Server()->ClientName(ClientId: KickId), aReason); |
| 2589 | str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s" , KickId, Server()->ClientName(ClientId: KickId)); |
| 2590 | if(!GetDDRaceTeam(ClientId)) |
| 2591 | { |
| 2592 | if(!g_Config.m_SvVoteKickBantime) |
| 2593 | { |
| 2594 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "kick %d Kicked by vote" , KickId); |
| 2595 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Kick '%s'" , Server()->ClientName(ClientId: KickId)); |
| 2596 | } |
| 2597 | else |
| 2598 | { |
| 2599 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "ban %s %d Banned by vote" , Server()->ClientAddrString(ClientId: KickId, IncludePort: false), g_Config.m_SvVoteKickBantime); |
| 2600 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Ban '%s'" , Server()->ClientName(ClientId: KickId)); |
| 2601 | } |
| 2602 | } |
| 2603 | else |
| 2604 | { |
| 2605 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "uninvite %d %d; set_team_ddr %d 0" , KickId, GetDDRaceTeam(ClientId: KickId), KickId); |
| 2606 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to team 0" , Server()->ClientName(ClientId: KickId)); |
| 2607 | } |
| 2608 | } |
| 2609 | m_apPlayers[ClientId]->m_LastKickVote = time_get(); |
| 2610 | m_VoteType = VOTE_TYPE_KICK; |
| 2611 | m_VoteVictim = KickId; |
| 2612 | } |
| 2613 | else if(str_comp_nocase(a: pMsg->m_pType, b: "spectate" ) == 0) |
| 2614 | { |
| 2615 | if(!g_Config.m_SvVoteSpectate) |
| 2616 | { |
| 2617 | SendChatTarget(To: ClientId, pText: "Server does not allow voting to move players to spectators" ); |
| 2618 | return; |
| 2619 | } |
| 2620 | |
| 2621 | int SpectateId = str_toint(str: pMsg->m_pValue); |
| 2622 | |
| 2623 | if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !m_apPlayers[SpectateId] || m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS) |
| 2624 | { |
| 2625 | SendChatTarget(To: ClientId, pText: "Invalid client id to move to spectators" ); |
| 2626 | return; |
| 2627 | } |
| 2628 | if(SpectateId == ClientId) |
| 2629 | { |
| 2630 | SendChatTarget(To: ClientId, pText: "You can't move yourself to spectators" ); |
| 2631 | return; |
| 2632 | } |
| 2633 | int Authed = Server()->GetAuthedState(ClientId); |
| 2634 | int SpectateAuthed = Server()->GetAuthedState(ClientId: SpectateId); |
| 2635 | if(SpectateAuthed > Authed) |
| 2636 | { |
| 2637 | SendChatTarget(To: ClientId, pText: "You can't move authorized players to spectators" ); |
| 2638 | char aBufSpectate[128]; |
| 2639 | str_format(buffer: aBufSpectate, buffer_size: sizeof(aBufSpectate), format: "'%s' called for vote to move you to spectators" , Server()->ClientName(ClientId)); |
| 2640 | SendChatTarget(To: SpectateId, pText: aBufSpectate); |
| 2641 | return; |
| 2642 | } |
| 2643 | if(!Server()->ReverseTranslate(Target&: SpectateId, Client: ClientId)) |
| 2644 | { |
| 2645 | return; |
| 2646 | } |
| 2647 | |
| 2648 | if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: SpectateId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: SpectateId)) |
| 2649 | { |
| 2650 | SendChatTarget(To: ClientId, pText: "You can only move your team member to spectators" ); |
| 2651 | return; |
| 2652 | } |
| 2653 | |
| 2654 | str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s" , SpectateId, Server()->ClientName(ClientId: SpectateId)); |
| 2655 | if(g_Config.m_SvPauseable && g_Config.m_SvVotePause) |
| 2656 | { |
| 2657 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called for vote to pause '%s' for %d seconds (%s)" , Server()->ClientName(ClientId), Server()->ClientName(ClientId: SpectateId), g_Config.m_SvVotePauseTime, aReason); |
| 2658 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Pause '%s' (%ds)" , Server()->ClientName(ClientId: SpectateId), g_Config.m_SvVotePauseTime); |
| 2659 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "uninvite %d %d; force_pause %d %d" , SpectateId, GetDDRaceTeam(ClientId: SpectateId), SpectateId, g_Config.m_SvVotePauseTime); |
| 2660 | } |
| 2661 | else |
| 2662 | { |
| 2663 | str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called for vote to move '%s' to spectators (%s)" , Server()->ClientName(ClientId), Server()->ClientName(ClientId: SpectateId), aReason); |
| 2664 | str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to spectators" , Server()->ClientName(ClientId: SpectateId)); |
| 2665 | str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "uninvite %d %d; set_team %d -1 %d" , SpectateId, GetDDRaceTeam(ClientId: SpectateId), SpectateId, g_Config.m_SvVoteSpectateRejoindelay); |
| 2666 | } |
| 2667 | m_VoteType = VOTE_TYPE_SPECTATE; |
| 2668 | m_VoteVictim = SpectateId; |
| 2669 | } |
| 2670 | |
| 2671 | if(aCmd[0] && str_comp_nocase(a: aCmd, b: "info" ) != 0) |
| 2672 | CallVote(ClientId, pDesc: aDesc, pCmd: aCmd, pReason: aReason, pChatmsg: aChatmsg, pSixupDesc: aSixupDesc[0] ? aSixupDesc : nullptr); |
| 2673 | } |
| 2674 | |
| 2675 | void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId) |
| 2676 | { |
| 2677 | if(!m_VoteCloseTime) |
| 2678 | return; |
| 2679 | |
| 2680 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2681 | |
| 2682 | if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick()) |
| 2683 | return; |
| 2684 | |
| 2685 | pPlayer->m_LastVoteTry = Server()->Tick(); |
| 2686 | pPlayer->UpdatePlaytime(); |
| 2687 | |
| 2688 | if(!pMsg->m_Vote) |
| 2689 | return; |
| 2690 | |
| 2691 | // Allow the vote creator to cancel the vote |
| 2692 | if(pPlayer->GetCid() == m_VoteCreator && pMsg->m_Vote == -1) |
| 2693 | { |
| 2694 | m_VoteEnforce = VOTE_ENFORCE_CANCEL; |
| 2695 | return; |
| 2696 | } |
| 2697 | |
| 2698 | pPlayer->m_Vote = pMsg->m_Vote; |
| 2699 | pPlayer->m_VotePos = ++m_VotePos; |
| 2700 | m_VoteUpdate = true; |
| 2701 | |
| 2702 | CNetMsg_Sv_YourVote Msg = {.m_Voted: pMsg->m_Vote}; |
| 2703 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 2704 | } |
| 2705 | |
| 2706 | void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId) |
| 2707 | { |
| 2708 | if(m_pController->IsGamePaused()) |
| 2709 | return; |
| 2710 | |
| 2711 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2712 | |
| 2713 | if(pPlayer->GetTeam() == pMsg->m_Team) |
| 2714 | return; |
| 2715 | if(g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick()) |
| 2716 | return; |
| 2717 | |
| 2718 | // Kill Protection |
| 2719 | CCharacter *pChr = pPlayer->GetCharacter(); |
| 2720 | if(pChr) |
| 2721 | { |
| 2722 | int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed(); |
| 2723 | if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED) |
| 2724 | { |
| 2725 | SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to join the spectators, first type /kill" ); |
| 2726 | return; |
| 2727 | } |
| 2728 | } |
| 2729 | |
| 2730 | if(pPlayer->m_TeamChangeTick > Server()->Tick()) |
| 2731 | { |
| 2732 | pPlayer->m_LastSetTeam = Server()->Tick(); |
| 2733 | int TimeLeft = (pPlayer->m_TeamChangeTick - Server()->Tick()) / Server()->TickSpeed(); |
| 2734 | char aTime[32]; |
| 2735 | str_time(centisecs: (int64_t)TimeLeft * 100, format: ETimeFormat::HOURS, buffer: aTime, buffer_size: sizeof(aTime)); |
| 2736 | char aBuf[128]; |
| 2737 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Time to wait before changing team: %s" , aTime); |
| 2738 | SendBroadcast(pText: aBuf, ClientId); |
| 2739 | return; |
| 2740 | } |
| 2741 | |
| 2742 | // Switch team on given client and kill/respawn them |
| 2743 | char aTeamJoinError[512]; |
| 2744 | if(m_pController->CanJoinTeam(Team: pMsg->m_Team, NotThisId: ClientId, pErrorReason: aTeamJoinError, ErrorReasonSize: sizeof(aTeamJoinError))) |
| 2745 | { |
| 2746 | if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS) |
| 2747 | m_VoteUpdate = true; |
| 2748 | m_pController->DoTeamChange(pPlayer, Team: pMsg->m_Team); |
| 2749 | pPlayer->m_TeamChangeTick = Server()->Tick(); |
| 2750 | } |
| 2751 | else |
| 2752 | SendBroadcast(pText: aTeamJoinError, ClientId); |
| 2753 | } |
| 2754 | |
| 2755 | void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker) |
| 2756 | { |
| 2757 | IServer::CClientInfo Info; |
| 2758 | if(Server()->GetClientInfo(ClientId, pInfo: &Info) && Info.m_GotDDNetVersion) |
| 2759 | { |
| 2760 | return; |
| 2761 | } |
| 2762 | int DDNetVersion = pUnpacker->GetInt(); |
| 2763 | if(pUnpacker->Error() || DDNetVersion < 0) |
| 2764 | { |
| 2765 | DDNetVersion = VERSION_DDRACE; |
| 2766 | } |
| 2767 | Server()->SetClientDDNetVersion(ClientId, DDNetVersion); |
| 2768 | OnClientDDNetVersionKnown(ClientId); |
| 2769 | } |
| 2770 | |
| 2771 | void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId) |
| 2772 | { |
| 2773 | if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault) |
| 2774 | { |
| 2775 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2776 | pPlayer->m_ShowOthers = pMsg->m_Show; |
| 2777 | } |
| 2778 | } |
| 2779 | |
| 2780 | void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId) |
| 2781 | { |
| 2782 | if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault) |
| 2783 | { |
| 2784 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2785 | pPlayer->m_ShowOthers = pMsg->m_Show; |
| 2786 | } |
| 2787 | } |
| 2788 | |
| 2789 | void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId) |
| 2790 | { |
| 2791 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2792 | pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y); |
| 2793 | } |
| 2794 | |
| 2795 | void CGameContext::OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId) |
| 2796 | { |
| 2797 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2798 | pPlayer->m_CameraInfo.Write(pMsg); |
| 2799 | } |
| 2800 | |
| 2801 | void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId) |
| 2802 | { |
| 2803 | if(m_pController->IsGamePaused()) |
| 2804 | return; |
| 2805 | |
| 2806 | int SpectatorId = std::clamp(val: pMsg->m_SpectatorId, lo: (int)SPEC_FOLLOW, hi: MAX_CLIENTS - 1); |
| 2807 | if(SpectatorId >= 0) |
| 2808 | if(!Server()->ReverseTranslate(Target&: SpectatorId, Client: ClientId)) |
| 2809 | return; |
| 2810 | |
| 2811 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2812 | if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick())) |
| 2813 | return; |
| 2814 | |
| 2815 | pPlayer->m_LastSetSpectatorMode = Server()->Tick(); |
| 2816 | pPlayer->UpdatePlaytime(); |
| 2817 | if(SpectatorId >= 0 && (!m_apPlayers[SpectatorId] || m_apPlayers[SpectatorId]->GetTeam() == TEAM_SPECTATORS)) |
| 2818 | SendChatTarget(To: ClientId, pText: "Invalid spectator id used" ); |
| 2819 | else |
| 2820 | pPlayer->SetSpectatorId(SpectatorId); |
| 2821 | } |
| 2822 | |
| 2823 | void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId) |
| 2824 | { |
| 2825 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2826 | if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick()) |
| 2827 | return; |
| 2828 | |
| 2829 | bool SixupNeedsUpdate = false; |
| 2830 | |
| 2831 | pPlayer->m_LastChangeInfo = Server()->Tick(); |
| 2832 | pPlayer->UpdatePlaytime(); |
| 2833 | |
| 2834 | if(g_Config.m_SvSpamprotection) |
| 2835 | { |
| 2836 | CNetMsg_Sv_ChangeInfoCooldown ChangeInfoCooldownMsg; |
| 2837 | ChangeInfoCooldownMsg.m_WaitUntil = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay; |
| 2838 | Server()->SendPackMsg(pMsg: &ChangeInfoCooldownMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 2839 | } |
| 2840 | |
| 2841 | // set infos |
| 2842 | if(Server()->WouldClientNameChange(ClientId, pNameRequest: pMsg->m_pName) && !ProcessSpamProtection(ClientId)) |
| 2843 | { |
| 2844 | char aOldName[MAX_NAME_LENGTH]; |
| 2845 | str_copy(dst: aOldName, src: Server()->ClientName(ClientId), dst_size: sizeof(aOldName)); |
| 2846 | |
| 2847 | Server()->SetClientName(ClientId, pName: pMsg->m_pName); |
| 2848 | |
| 2849 | char aChatText[256]; |
| 2850 | str_format(buffer: aChatText, buffer_size: sizeof(aChatText), format: "'%s' changed name to '%s'" , aOldName, Server()->ClientName(ClientId)); |
| 2851 | SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aChatText); |
| 2852 | |
| 2853 | // reload scores |
| 2854 | Score()->PlayerData(Id: ClientId)->Reset(); |
| 2855 | Server()->SetClientScore(ClientId, Score: std::nullopt); |
| 2856 | Score()->LoadPlayerData(ClientId); |
| 2857 | |
| 2858 | SixupNeedsUpdate = true; |
| 2859 | |
| 2860 | LogEvent(Description: "Name change" , ClientId); |
| 2861 | } |
| 2862 | |
| 2863 | if(Server()->WouldClientClanChange(ClientId, pClanRequest: pMsg->m_pClan)) |
| 2864 | { |
| 2865 | SixupNeedsUpdate = true; |
| 2866 | Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan); |
| 2867 | } |
| 2868 | |
| 2869 | if(Server()->ClientCountry(ClientId) != pMsg->m_Country) |
| 2870 | SixupNeedsUpdate = true; |
| 2871 | Server()->SetClientCountry(ClientId, Country: pMsg->m_Country); |
| 2872 | |
| 2873 | str_copy(dst: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin, dst_size: sizeof(pPlayer->m_TeeInfos.m_aSkinName)); |
| 2874 | pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor; |
| 2875 | pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody; |
| 2876 | pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet; |
| 2877 | if(!Server()->IsSixup(ClientId)) |
| 2878 | pPlayer->m_TeeInfos.ToSixup(); |
| 2879 | |
| 2880 | if(SixupNeedsUpdate) |
| 2881 | { |
| 2882 | protocol7::CNetMsg_Sv_ClientDrop Drop; |
| 2883 | Drop.m_ClientId = ClientId; |
| 2884 | Drop.m_pReason = "" ; |
| 2885 | Drop.m_Silent = true; |
| 2886 | |
| 2887 | protocol7::CNetMsg_Sv_ClientInfo Info; |
| 2888 | Info.m_ClientId = ClientId; |
| 2889 | Info.m_pName = Server()->ClientName(ClientId); |
| 2890 | Info.m_Country = pMsg->m_Country; |
| 2891 | Info.m_pClan = pMsg->m_pClan; |
| 2892 | Info.m_Local = 0; |
| 2893 | Info.m_Silent = true; |
| 2894 | Info.m_Team = pPlayer->GetTeam(); |
| 2895 | |
| 2896 | for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) |
| 2897 | { |
| 2898 | Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p]; |
| 2899 | Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p]; |
| 2900 | Info.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p]; |
| 2901 | } |
| 2902 | |
| 2903 | for(int i = 0; i < Server()->MaxClients(); i++) |
| 2904 | { |
| 2905 | if(i != ClientId) |
| 2906 | { |
| 2907 | Server()->SendPackMsg(pMsg: &Drop, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 2908 | Server()->SendPackMsg(pMsg: &Info, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i); |
| 2909 | } |
| 2910 | } |
| 2911 | } |
| 2912 | else |
| 2913 | { |
| 2914 | SendSkinChange7(ClientId); |
| 2915 | } |
| 2916 | |
| 2917 | Server()->ExpireServerInfo(); |
| 2918 | } |
| 2919 | |
| 2920 | void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId) |
| 2921 | { |
| 2922 | if(m_pController->IsGamePaused()) |
| 2923 | return; |
| 2924 | |
| 2925 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 2926 | |
| 2927 | auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) { |
| 2928 | return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000); |
| 2929 | }; |
| 2930 | |
| 2931 | if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmote, (int64_t)g_Config.m_SvEmoticonMsDelay)) |
| 2932 | return; |
| 2933 | |
| 2934 | CCharacter *pChr = pPlayer->GetCharacter(); |
| 2935 | |
| 2936 | // player needs a character to send emotes |
| 2937 | if(!pChr) |
| 2938 | return; |
| 2939 | |
| 2940 | pPlayer->m_LastEmote = Server()->Tick(); |
| 2941 | pPlayer->UpdatePlaytime(); |
| 2942 | |
| 2943 | // check if the global emoticon is prevented and emotes are only send to nearby players |
| 2944 | if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmoteGlobal, (int64_t)g_Config.m_SvGlobalEmoticonMsDelay)) |
| 2945 | { |
| 2946 | for(int i = 0; i < MAX_CLIENTS; ++i) |
| 2947 | { |
| 2948 | if(m_apPlayers[i] && pChr->CanSnapCharacter(SnappingClient: i) && pChr->IsSnappingCharacterInView(SnappingClientId: i)) |
| 2949 | { |
| 2950 | SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: i); |
| 2951 | } |
| 2952 | } |
| 2953 | } |
| 2954 | else |
| 2955 | { |
| 2956 | // else send emoticons to all players |
| 2957 | pPlayer->m_LastEmoteGlobal = Server()->Tick(); |
| 2958 | SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: -1); |
| 2959 | } |
| 2960 | |
| 2961 | if(g_Config.m_SvEmotionalTees == 1 && pPlayer->m_EyeEmoteEnabled) |
| 2962 | { |
| 2963 | int EmoteType = EMOTE_NORMAL; |
| 2964 | switch(pMsg->m_Emoticon) |
| 2965 | { |
| 2966 | case EMOTICON_EXCLAMATION: |
| 2967 | case EMOTICON_GHOST: |
| 2968 | case EMOTICON_QUESTION: |
| 2969 | case EMOTICON_WTF: |
| 2970 | EmoteType = EMOTE_SURPRISE; |
| 2971 | break; |
| 2972 | case EMOTICON_DOTDOT: |
| 2973 | case EMOTICON_DROP: |
| 2974 | case EMOTICON_ZZZ: |
| 2975 | EmoteType = EMOTE_BLINK; |
| 2976 | break; |
| 2977 | case EMOTICON_EYES: |
| 2978 | case EMOTICON_HEARTS: |
| 2979 | case EMOTICON_MUSIC: |
| 2980 | EmoteType = EMOTE_HAPPY; |
| 2981 | break; |
| 2982 | case EMOTICON_OOP: |
| 2983 | case EMOTICON_SORRY: |
| 2984 | case EMOTICON_SUSHI: |
| 2985 | EmoteType = EMOTE_PAIN; |
| 2986 | break; |
| 2987 | case EMOTICON_DEVILTEE: |
| 2988 | case EMOTICON_SPLATTEE: |
| 2989 | case EMOTICON_ZOMG: |
| 2990 | EmoteType = EMOTE_ANGRY; |
| 2991 | break; |
| 2992 | default: |
| 2993 | break; |
| 2994 | } |
| 2995 | pChr->SetEmote(Emote: EmoteType, Tick: Server()->Tick() + 2 * Server()->TickSpeed()); |
| 2996 | } |
| 2997 | } |
| 2998 | |
| 2999 | void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId) |
| 3000 | { |
| 3001 | if(m_pController->IsGamePaused()) |
| 3002 | return; |
| 3003 | |
| 3004 | if(IsRunningKickOrSpecVote(ClientId) && GetDDRaceTeam(ClientId)) |
| 3005 | { |
| 3006 | SendChatTarget(To: ClientId, pText: "You are running a vote please try again after the vote is done!" ); |
| 3007 | return; |
| 3008 | } |
| 3009 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 3010 | if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick()) |
| 3011 | return; |
| 3012 | if(pPlayer->IsPaused()) |
| 3013 | return; |
| 3014 | |
| 3015 | CCharacter *pChr = pPlayer->GetCharacter(); |
| 3016 | if(!pChr) |
| 3017 | return; |
| 3018 | |
| 3019 | // Kill Protection |
| 3020 | int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed(); |
| 3021 | if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED) |
| 3022 | { |
| 3023 | SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to kill, type /kill" ); |
| 3024 | return; |
| 3025 | } |
| 3026 | |
| 3027 | pPlayer->m_LastKill = Server()->Tick(); |
| 3028 | pPlayer->KillCharacter(Weapon: WEAPON_SELF); |
| 3029 | pPlayer->Respawn(); |
| 3030 | } |
| 3031 | |
| 3032 | void CGameContext::OnEnableSpectatorCountNetMessage(const CNetMsg_Cl_EnableSpectatorCount *pMsg, int ClientId) |
| 3033 | { |
| 3034 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 3035 | if(!pPlayer) |
| 3036 | return; |
| 3037 | |
| 3038 | pPlayer->m_EnableSpectatorCount = pMsg->m_Enable; |
| 3039 | } |
| 3040 | |
| 3041 | void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId) |
| 3042 | { |
| 3043 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 3044 | |
| 3045 | if(pPlayer->m_IsReady) |
| 3046 | return; |
| 3047 | |
| 3048 | pPlayer->m_LastChangeInfo = Server()->Tick(); |
| 3049 | |
| 3050 | // set start infos |
| 3051 | Server()->SetClientName(ClientId, pName: pMsg->m_pName); |
| 3052 | // trying to set client name can delete the player object, check if it still exists |
| 3053 | if(!m_apPlayers[ClientId]) |
| 3054 | { |
| 3055 | return; |
| 3056 | } |
| 3057 | Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan); |
| 3058 | // trying to set client clan can delete the player object, check if it still exists |
| 3059 | if(!m_apPlayers[ClientId]) |
| 3060 | { |
| 3061 | return; |
| 3062 | } |
| 3063 | Server()->SetClientCountry(ClientId, Country: pMsg->m_Country); |
| 3064 | str_copy(dst: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin, dst_size: sizeof(pPlayer->m_TeeInfos.m_aSkinName)); |
| 3065 | pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor; |
| 3066 | pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody; |
| 3067 | pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet; |
| 3068 | if(!Server()->IsSixup(ClientId)) |
| 3069 | pPlayer->m_TeeInfos.ToSixup(); |
| 3070 | |
| 3071 | // send clear vote options |
| 3072 | CNetMsg_Sv_VoteClearOptions ClearMsg; |
| 3073 | Server()->SendPackMsg(pMsg: &ClearMsg, Flags: MSGFLAG_VITAL, ClientId); |
| 3074 | |
| 3075 | // begin sending vote options |
| 3076 | pPlayer->m_SendVoteIndex = 0; |
| 3077 | |
| 3078 | // send tuning parameters to client |
| 3079 | SendTuningParams(ClientId, Zone: pPlayer->m_TuneZone); |
| 3080 | |
| 3081 | // client is ready to enter |
| 3082 | pPlayer->m_IsReady = true; |
| 3083 | CNetMsg_Sv_ReadyToEnter ReadyMsg; |
| 3084 | Server()->SendPackMsg(pMsg: &ReadyMsg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); |
| 3085 | |
| 3086 | Server()->ExpireServerInfo(); |
| 3087 | } |
| 3088 | |
| 3089 | void CGameContext::ConTuneParam(IConsole::IResult *pResult, void *pUserData) |
| 3090 | { |
| 3091 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3092 | const char *pParamName = pResult->GetString(Index: 0); |
| 3093 | |
| 3094 | char aBuf[256]; |
| 3095 | if(pResult->NumArguments() == 2) |
| 3096 | { |
| 3097 | float NewValue = pResult->GetFloat(Index: 1); |
| 3098 | if(pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue)) |
| 3099 | { |
| 3100 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f" , pParamName, NewValue); |
| 3101 | pSelf->SendTuningParams(ClientId: -1); |
| 3102 | } |
| 3103 | else |
| 3104 | { |
| 3105 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s" , pParamName); |
| 3106 | } |
| 3107 | } |
| 3108 | else |
| 3109 | { |
| 3110 | float Value; |
| 3111 | if(pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &Value)) |
| 3112 | { |
| 3113 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f" , pParamName, Value); |
| 3114 | } |
| 3115 | else |
| 3116 | { |
| 3117 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s" , pParamName); |
| 3118 | } |
| 3119 | } |
| 3120 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3121 | } |
| 3122 | |
| 3123 | void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData) |
| 3124 | { |
| 3125 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3126 | const char *pParamName = pResult->GetString(Index: 0); |
| 3127 | float OldValue; |
| 3128 | |
| 3129 | char aBuf[256]; |
| 3130 | if(!pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &OldValue)) |
| 3131 | { |
| 3132 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s" , pParamName); |
| 3133 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3134 | return; |
| 3135 | } |
| 3136 | |
| 3137 | float NewValue = absolute(a: OldValue - pResult->GetFloat(Index: 1)) < 0.0001f ? pResult->GetFloat(Index: 2) : pResult->GetFloat(Index: 1); |
| 3138 | |
| 3139 | pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue); |
| 3140 | pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue); |
| 3141 | |
| 3142 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f" , pParamName, NewValue); |
| 3143 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3144 | pSelf->SendTuningParams(ClientId: -1); |
| 3145 | } |
| 3146 | |
| 3147 | void CGameContext::ConTuneReset(IConsole::IResult *pResult, void *pUserData) |
| 3148 | { |
| 3149 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3150 | if(pResult->NumArguments()) |
| 3151 | { |
| 3152 | const char *pParamName = pResult->GetString(Index: 0); |
| 3153 | float DefaultValue = 0.0f; |
| 3154 | char aBuf[256]; |
| 3155 | |
| 3156 | if(CTuningParams::DEFAULT.Get(pName: pParamName, pValue: &DefaultValue) && pSelf->GlobalTuning()->Set(pName: pParamName, Value: DefaultValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &DefaultValue)) |
| 3157 | { |
| 3158 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s reset to %.2f" , pParamName, DefaultValue); |
| 3159 | pSelf->SendTuningParams(ClientId: -1); |
| 3160 | } |
| 3161 | else |
| 3162 | { |
| 3163 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s" , pParamName); |
| 3164 | } |
| 3165 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3166 | } |
| 3167 | else |
| 3168 | { |
| 3169 | pSelf->ResetTuning(); |
| 3170 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: "Tuning reset" ); |
| 3171 | } |
| 3172 | } |
| 3173 | |
| 3174 | void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData) |
| 3175 | { |
| 3176 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3177 | char aBuf[256]; |
| 3178 | for(int i = 0; i < CTuningParams::Num(); i++) |
| 3179 | { |
| 3180 | float Value; |
| 3181 | pSelf->GlobalTuning()->Get(Index: i, pValue: &Value); |
| 3182 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f" , CTuningParams::Name(Index: i), Value); |
| 3183 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3184 | } |
| 3185 | } |
| 3186 | |
| 3187 | void CGameContext::ConTuneZone(IConsole::IResult *pResult, void *pUserData) |
| 3188 | { |
| 3189 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3190 | int List = pResult->GetInteger(Index: 0); |
| 3191 | const char *pParamName = pResult->GetString(Index: 1); |
| 3192 | float NewValue = pResult->GetFloat(Index: 2); |
| 3193 | |
| 3194 | if(List >= 0 && List < TuneZone::NUM) |
| 3195 | { |
| 3196 | char aBuf[256]; |
| 3197 | if(pSelf->TuningList()[List].Set(pName: pParamName, Value: NewValue) && pSelf->TuningList()[List].Get(pName: pParamName, pValue: &NewValue)) |
| 3198 | { |
| 3199 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s in zone %d changed to %.2f" , pParamName, List, NewValue); |
| 3200 | pSelf->SendTuningParams(ClientId: -1, Zone: List); |
| 3201 | } |
| 3202 | else |
| 3203 | { |
| 3204 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s" , pParamName); |
| 3205 | } |
| 3206 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3207 | } |
| 3208 | } |
| 3209 | |
| 3210 | void CGameContext::ConTuneDumpZone(IConsole::IResult *pResult, void *pUserData) |
| 3211 | { |
| 3212 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3213 | int List = pResult->GetInteger(Index: 0); |
| 3214 | char aBuf[256]; |
| 3215 | if(List >= 0 && List < TuneZone::NUM) |
| 3216 | { |
| 3217 | for(int i = 0; i < CTuningParams::Num(); i++) |
| 3218 | { |
| 3219 | float Value; |
| 3220 | pSelf->TuningList()[List].Get(Index: i, pValue: &Value); |
| 3221 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "zone %d: %s %.2f" , List, CTuningParams::Name(Index: i), Value); |
| 3222 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3223 | } |
| 3224 | } |
| 3225 | } |
| 3226 | |
| 3227 | void CGameContext::ConTuneResetZone(IConsole::IResult *pResult, void *pUserData) |
| 3228 | { |
| 3229 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3230 | if(pResult->NumArguments()) |
| 3231 | { |
| 3232 | int List = pResult->GetInteger(Index: 0); |
| 3233 | if(List >= 0 && List < TuneZone::NUM) |
| 3234 | { |
| 3235 | pSelf->TuningList()[List] = CTuningParams::DEFAULT; |
| 3236 | char aBuf[256]; |
| 3237 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Tunezone %d reset" , List); |
| 3238 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: aBuf); |
| 3239 | pSelf->SendTuningParams(ClientId: -1, Zone: List); |
| 3240 | } |
| 3241 | } |
| 3242 | else |
| 3243 | { |
| 3244 | for(int i = 0; i < TuneZone::NUM; i++) |
| 3245 | { |
| 3246 | *(pSelf->TuningList() + i) = CTuningParams::DEFAULT; |
| 3247 | pSelf->SendTuningParams(ClientId: -1, Zone: i); |
| 3248 | } |
| 3249 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning" , pStr: "All Tunezones reset" ); |
| 3250 | } |
| 3251 | } |
| 3252 | |
| 3253 | void CGameContext::ConTuneSetZoneMsgEnter(IConsole::IResult *pResult, void *pUserData) |
| 3254 | { |
| 3255 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3256 | if(pResult->NumArguments()) |
| 3257 | { |
| 3258 | int List = pResult->GetInteger(Index: 0); |
| 3259 | if(List >= 0 && List < TuneZone::NUM) |
| 3260 | { |
| 3261 | str_copy(dst: pSelf->m_aaZoneEnterMsg[List], src: pResult->GetString(Index: 1), dst_size: sizeof(pSelf->m_aaZoneEnterMsg[List])); |
| 3262 | } |
| 3263 | } |
| 3264 | } |
| 3265 | |
| 3266 | void CGameContext::ConTuneSetZoneMsgLeave(IConsole::IResult *pResult, void *pUserData) |
| 3267 | { |
| 3268 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3269 | if(pResult->NumArguments()) |
| 3270 | { |
| 3271 | int List = pResult->GetInteger(Index: 0); |
| 3272 | if(List >= 0 && List < TuneZone::NUM) |
| 3273 | { |
| 3274 | str_copy(dst: pSelf->m_aaZoneLeaveMsg[List], src: pResult->GetString(Index: 1), dst_size: sizeof(pSelf->m_aaZoneLeaveMsg[List])); |
| 3275 | } |
| 3276 | } |
| 3277 | } |
| 3278 | |
| 3279 | void CGameContext::ConMapbug(IConsole::IResult *pResult, void *pUserData) |
| 3280 | { |
| 3281 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3282 | |
| 3283 | if(pSelf->m_pController) |
| 3284 | { |
| 3285 | log_info("mapbugs" , "can't add map bugs after the game started" ); |
| 3286 | return; |
| 3287 | } |
| 3288 | |
| 3289 | const char *pMapBugName = pResult->GetString(Index: 0); |
| 3290 | switch(pSelf->m_MapBugs.Update(pBug: pMapBugName)) |
| 3291 | { |
| 3292 | case EMapBugUpdate::OK: |
| 3293 | break; |
| 3294 | case EMapBugUpdate::OVERRIDDEN: |
| 3295 | log_info("mapbugs" , "map-internal setting overridden by database" ); |
| 3296 | break; |
| 3297 | case EMapBugUpdate::NOTFOUND: |
| 3298 | log_info("mapbugs" , "unknown map bug '%s', ignoring" , pMapBugName); |
| 3299 | break; |
| 3300 | default: |
| 3301 | dbg_assert_failed("unreachable" ); |
| 3302 | } |
| 3303 | } |
| 3304 | |
| 3305 | void CGameContext::ConSwitchOpen(IConsole::IResult *pResult, void *pUserData) |
| 3306 | { |
| 3307 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3308 | int Switch = pResult->GetInteger(Index: 0); |
| 3309 | |
| 3310 | if(in_range(a: Switch, upper: (int)pSelf->Switchers().size() - 1)) |
| 3311 | { |
| 3312 | pSelf->Switchers()[Switch].m_Initial = false; |
| 3313 | char aBuf[256]; |
| 3314 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "switch %d opened by default" , Switch); |
| 3315 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3316 | } |
| 3317 | } |
| 3318 | |
| 3319 | void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData) |
| 3320 | { |
| 3321 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3322 | |
| 3323 | pSelf->m_pController->SetGamePaused(!pSelf->m_pController->IsGamePaused()); |
| 3324 | } |
| 3325 | |
| 3326 | void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData) |
| 3327 | { |
| 3328 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3329 | pSelf->m_pController->ChangeMap(pToMap: pResult->GetString(Index: 0)); |
| 3330 | } |
| 3331 | |
| 3332 | void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData) |
| 3333 | { |
| 3334 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3335 | |
| 3336 | const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId; |
| 3337 | int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1; |
| 3338 | int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars; |
| 3339 | |
| 3340 | if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5)) |
| 3341 | return; |
| 3342 | |
| 3343 | pSelf->m_pScore->RandomMap(ClientId, MinStars, MaxStars); |
| 3344 | } |
| 3345 | |
| 3346 | void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData) |
| 3347 | { |
| 3348 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3349 | |
| 3350 | const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId; |
| 3351 | int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1; |
| 3352 | int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars; |
| 3353 | |
| 3354 | if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5)) |
| 3355 | return; |
| 3356 | |
| 3357 | pSelf->m_pScore->RandomUnfinishedMap(ClientId, MinStars, MaxStars); |
| 3358 | } |
| 3359 | |
| 3360 | void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData) |
| 3361 | { |
| 3362 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3363 | if(pResult->NumArguments()) |
| 3364 | pSelf->m_pController->DoWarmup(Seconds: pResult->GetInteger(Index: 0)); |
| 3365 | else |
| 3366 | pSelf->m_pController->StartRound(); |
| 3367 | } |
| 3368 | |
| 3369 | static void UnescapeNewlines(char *pBuf) |
| 3370 | { |
| 3371 | int i, j; |
| 3372 | for(i = 0, j = 0; pBuf[i]; i++, j++) |
| 3373 | { |
| 3374 | if(pBuf[i] == '\\' && pBuf[i + 1] == 'n') |
| 3375 | { |
| 3376 | pBuf[j] = '\n'; |
| 3377 | i++; |
| 3378 | } |
| 3379 | else if(i != j) |
| 3380 | { |
| 3381 | pBuf[j] = pBuf[i]; |
| 3382 | } |
| 3383 | } |
| 3384 | pBuf[j] = '\0'; |
| 3385 | } |
| 3386 | |
| 3387 | void CGameContext::ConServerAlert(IConsole::IResult *pResult, void *pUserData) |
| 3388 | { |
| 3389 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3390 | |
| 3391 | char aBuf[1024]; |
| 3392 | str_copy(dst: aBuf, src: pResult->GetString(Index: 0), dst_size: sizeof(aBuf)); |
| 3393 | UnescapeNewlines(pBuf: aBuf); |
| 3394 | |
| 3395 | pSelf->SendServerAlert(pMessage: aBuf); |
| 3396 | } |
| 3397 | |
| 3398 | void CGameContext::ConModAlert(IConsole::IResult *pResult, void *pUserData) |
| 3399 | { |
| 3400 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3401 | |
| 3402 | const int Victim = pResult->GetVictim(); |
| 3403 | if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim]) |
| 3404 | { |
| 3405 | log_info("moderator_alert" , "Client ID not found: %d" , Victim); |
| 3406 | return; |
| 3407 | } |
| 3408 | |
| 3409 | char aBuf[1024]; |
| 3410 | str_copy(dst: aBuf, src: pResult->GetString(Index: 1), dst_size: sizeof(aBuf)); |
| 3411 | UnescapeNewlines(pBuf: aBuf); |
| 3412 | |
| 3413 | pSelf->SendModeratorAlert(pMessage: aBuf, ToClientId: Victim); |
| 3414 | } |
| 3415 | |
| 3416 | void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData) |
| 3417 | { |
| 3418 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3419 | |
| 3420 | char aBuf[1024]; |
| 3421 | str_copy(dst: aBuf, src: pResult->GetString(Index: 0), dst_size: sizeof(aBuf)); |
| 3422 | UnescapeNewlines(pBuf: aBuf); |
| 3423 | |
| 3424 | pSelf->SendBroadcast(pText: aBuf, ClientId: -1); |
| 3425 | } |
| 3426 | |
| 3427 | void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData) |
| 3428 | { |
| 3429 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3430 | pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: pResult->GetString(Index: 0)); |
| 3431 | } |
| 3432 | |
| 3433 | void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData) |
| 3434 | { |
| 3435 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3436 | int Team = pResult->GetInteger(Index: 1); |
| 3437 | if(!pSelf->m_pController->IsValidTeam(Team)) |
| 3438 | { |
| 3439 | log_info("server" , "Invalid Team: %d" , Team); |
| 3440 | return; |
| 3441 | } |
| 3442 | |
| 3443 | int ClientId = std::clamp(val: pResult->GetInteger(Index: 0), lo: 0, hi: (int)MAX_CLIENTS - 1); |
| 3444 | int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(Index: 2) : 0; |
| 3445 | if(!pSelf->m_apPlayers[ClientId]) |
| 3446 | return; |
| 3447 | |
| 3448 | char aBuf[256]; |
| 3449 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "moved client %d to the %s" , ClientId, pSelf->m_pController->GetTeamName(Team)); |
| 3450 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3451 | |
| 3452 | pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: false); // reset /spec and /pause to allow rejoin |
| 3453 | pSelf->m_apPlayers[ClientId]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60; |
| 3454 | pSelf->m_pController->DoTeamChange(pPlayer: pSelf->m_apPlayers[ClientId], Team); |
| 3455 | if(Team == TEAM_SPECTATORS) |
| 3456 | pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: true); |
| 3457 | } |
| 3458 | |
| 3459 | void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData) |
| 3460 | { |
| 3461 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3462 | int Team = pResult->GetInteger(Index: 0); |
| 3463 | if(!pSelf->m_pController->IsValidTeam(Team)) |
| 3464 | { |
| 3465 | log_info("server" , "Invalid Team: %d" , Team); |
| 3466 | return; |
| 3467 | } |
| 3468 | |
| 3469 | char aBuf[256]; |
| 3470 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "All players were moved to the %s" , pSelf->m_pController->GetTeamName(Team)); |
| 3471 | pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf); |
| 3472 | |
| 3473 | for(auto &pPlayer : pSelf->m_apPlayers) |
| 3474 | if(pPlayer) |
| 3475 | pSelf->m_pController->DoTeamChange(pPlayer, Team, DoChatMsg: false); |
| 3476 | } |
| 3477 | |
| 3478 | void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData) |
| 3479 | { |
| 3480 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3481 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 3482 | { |
| 3483 | if(!pSelf->GetPlayerChar(ClientId: i)) |
| 3484 | continue; |
| 3485 | |
| 3486 | CCharacter *pChar = pSelf->GetPlayerChar(ClientId: i); |
| 3487 | |
| 3488 | // Save the tee individually |
| 3489 | pSelf->m_apSavedTees[i] = new CSaveHotReloadTee(); |
| 3490 | pSelf->m_apSavedTees[i]->Save(pChr: pChar, AddPenalty: false); |
| 3491 | |
| 3492 | // Save the team state |
| 3493 | pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(ClientId: i); |
| 3494 | if(pSelf->m_aTeamMapping[i] == TEAM_SUPER) |
| 3495 | pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper; |
| 3496 | |
| 3497 | if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]) |
| 3498 | continue; |
| 3499 | |
| 3500 | pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]] = new CSaveTeam(); |
| 3501 | pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]->Save(pGameServer: pSelf, Team: pSelf->m_aTeamMapping[i], Dry: true, Force: true); |
| 3502 | } |
| 3503 | pSelf->Server()->ReloadMap(); |
| 3504 | } |
| 3505 | |
| 3506 | void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData) |
| 3507 | { |
| 3508 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3509 | const char *pDescription = pResult->GetString(Index: 0); |
| 3510 | const char *pCommand = pResult->GetString(Index: 1); |
| 3511 | |
| 3512 | pSelf->AddVote(pDescription, pCommand); |
| 3513 | } |
| 3514 | |
| 3515 | void CGameContext::AddVote(const char *pDescription, const char *pCommand) |
| 3516 | { |
| 3517 | if(m_NumVoteOptions == MAX_VOTE_OPTIONS) |
| 3518 | { |
| 3519 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: "maximum number of vote options reached" ); |
| 3520 | return; |
| 3521 | } |
| 3522 | |
| 3523 | // check for valid option |
| 3524 | if(!Console()->LineIsValid(pStr: pCommand) || str_length(str: pCommand) >= VOTE_CMD_LENGTH) |
| 3525 | { |
| 3526 | char aBuf[256]; |
| 3527 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid command '%s'" , pCommand); |
| 3528 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3529 | return; |
| 3530 | } |
| 3531 | while(*pDescription == ' ') |
| 3532 | pDescription++; |
| 3533 | if(str_length(str: pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0) |
| 3534 | { |
| 3535 | char aBuf[256]; |
| 3536 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid option '%s'" , pDescription); |
| 3537 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3538 | return; |
| 3539 | } |
| 3540 | |
| 3541 | // check for duplicate entry |
| 3542 | CVoteOptionServer *pOption = m_pVoteOptionFirst; |
| 3543 | while(pOption) |
| 3544 | { |
| 3545 | if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0) |
| 3546 | { |
| 3547 | char aBuf[256]; |
| 3548 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' already exists" , pDescription); |
| 3549 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3550 | return; |
| 3551 | } |
| 3552 | pOption = pOption->m_pNext; |
| 3553 | } |
| 3554 | |
| 3555 | // add the option |
| 3556 | ++m_NumVoteOptions; |
| 3557 | int Len = str_length(str: pCommand); |
| 3558 | |
| 3559 | pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer)); |
| 3560 | pOption->m_pNext = nullptr; |
| 3561 | pOption->m_pPrev = m_pVoteOptionLast; |
| 3562 | if(pOption->m_pPrev) |
| 3563 | pOption->m_pPrev->m_pNext = pOption; |
| 3564 | m_pVoteOptionLast = pOption; |
| 3565 | if(!m_pVoteOptionFirst) |
| 3566 | m_pVoteOptionFirst = pOption; |
| 3567 | |
| 3568 | str_copy(dst: pOption->m_aDescription, src: pDescription, dst_size: sizeof(pOption->m_aDescription)); |
| 3569 | str_copy(dst: pOption->m_aCommand, src: pCommand, dst_size: Len + 1); |
| 3570 | } |
| 3571 | |
| 3572 | void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData) |
| 3573 | { |
| 3574 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3575 | const char *pDescription = pResult->GetString(Index: 0); |
| 3576 | |
| 3577 | // check for valid option |
| 3578 | CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; |
| 3579 | while(pOption) |
| 3580 | { |
| 3581 | if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0) |
| 3582 | break; |
| 3583 | pOption = pOption->m_pNext; |
| 3584 | } |
| 3585 | if(!pOption) |
| 3586 | { |
| 3587 | char aBuf[256]; |
| 3588 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' does not exist" , pDescription); |
| 3589 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3590 | return; |
| 3591 | } |
| 3592 | |
| 3593 | // start reloading vote option list |
| 3594 | // clear vote options |
| 3595 | CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg; |
| 3596 | pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1); |
| 3597 | |
| 3598 | // reset sending of vote options |
| 3599 | for(auto &pPlayer : pSelf->m_apPlayers) |
| 3600 | { |
| 3601 | if(pPlayer) |
| 3602 | pPlayer->m_SendVoteIndex = 0; |
| 3603 | } |
| 3604 | |
| 3605 | // TODO: improve this |
| 3606 | // remove the option |
| 3607 | --pSelf->m_NumVoteOptions; |
| 3608 | |
| 3609 | CHeap *pVoteOptionHeap = new CHeap(); |
| 3610 | CVoteOptionServer *pVoteOptionFirst = nullptr; |
| 3611 | CVoteOptionServer *pVoteOptionLast = nullptr; |
| 3612 | int NumVoteOptions = pSelf->m_NumVoteOptions; |
| 3613 | for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext) |
| 3614 | { |
| 3615 | if(pSrc == pOption) |
| 3616 | continue; |
| 3617 | |
| 3618 | // copy option |
| 3619 | int Len = str_length(str: pSrc->m_aCommand); |
| 3620 | CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer)); |
| 3621 | pDst->m_pNext = nullptr; |
| 3622 | pDst->m_pPrev = pVoteOptionLast; |
| 3623 | if(pDst->m_pPrev) |
| 3624 | pDst->m_pPrev->m_pNext = pDst; |
| 3625 | pVoteOptionLast = pDst; |
| 3626 | if(!pVoteOptionFirst) |
| 3627 | pVoteOptionFirst = pDst; |
| 3628 | |
| 3629 | str_copy(dst: pDst->m_aDescription, src: pSrc->m_aDescription, dst_size: sizeof(pDst->m_aDescription)); |
| 3630 | str_copy(dst: pDst->m_aCommand, src: pSrc->m_aCommand, dst_size: Len + 1); |
| 3631 | } |
| 3632 | |
| 3633 | // clean up |
| 3634 | delete pSelf->m_pVoteOptionHeap; |
| 3635 | pSelf->m_pVoteOptionHeap = pVoteOptionHeap; |
| 3636 | pSelf->m_pVoteOptionFirst = pVoteOptionFirst; |
| 3637 | pSelf->m_pVoteOptionLast = pVoteOptionLast; |
| 3638 | pSelf->m_NumVoteOptions = NumVoteOptions; |
| 3639 | } |
| 3640 | |
| 3641 | void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData) |
| 3642 | { |
| 3643 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3644 | const char *pType = pResult->GetString(Index: 0); |
| 3645 | const char *pValue = pResult->GetString(Index: 1); |
| 3646 | const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(Index: 2)[0] ? pResult->GetString(Index: 2) : "No reason given" ; |
| 3647 | char aBuf[128] = {0}; |
| 3648 | |
| 3649 | if(str_comp_nocase(a: pType, b: "option" ) == 0) |
| 3650 | { |
| 3651 | CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; |
| 3652 | while(pOption) |
| 3653 | { |
| 3654 | if(str_comp_nocase(a: pValue, b: pOption->m_aDescription) == 0) |
| 3655 | { |
| 3656 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "authorized player forced server option '%s' (%s)" , pValue, pReason); |
| 3657 | pSelf->SendChatTarget(To: -1, pText: aBuf, VersionFlags: FLAG_SIX); |
| 3658 | pSelf->m_VoteCreator = pResult->m_ClientId; |
| 3659 | pSelf->Console()->ExecuteLine(pStr: pOption->m_aCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED); |
| 3660 | break; |
| 3661 | } |
| 3662 | |
| 3663 | pOption = pOption->m_pNext; |
| 3664 | } |
| 3665 | |
| 3666 | if(!pOption) |
| 3667 | { |
| 3668 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' isn't an option on this server" , pValue); |
| 3669 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: aBuf); |
| 3670 | return; |
| 3671 | } |
| 3672 | } |
| 3673 | else if(str_comp_nocase(a: pType, b: "kick" ) == 0) |
| 3674 | { |
| 3675 | int KickId = str_toint(str: pValue); |
| 3676 | if(KickId < 0 || KickId >= MAX_CLIENTS || !pSelf->m_apPlayers[KickId]) |
| 3677 | { |
| 3678 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: "Invalid client id to kick" ); |
| 3679 | return; |
| 3680 | } |
| 3681 | |
| 3682 | if(!g_Config.m_SvVoteKickBantime) |
| 3683 | { |
| 3684 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "kick %d %s" , KickId, pReason); |
| 3685 | pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false); |
| 3686 | } |
| 3687 | else |
| 3688 | { |
| 3689 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ban %s %d %s" , pSelf->Server()->ClientAddrString(ClientId: KickId, IncludePort: false), g_Config.m_SvVoteKickBantime, pReason); |
| 3690 | pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false); |
| 3691 | } |
| 3692 | } |
| 3693 | else if(str_comp_nocase(a: pType, b: "spectate" ) == 0) |
| 3694 | { |
| 3695 | int SpectateId = str_toint(str: pValue); |
| 3696 | if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateId] || pSelf->m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS) |
| 3697 | { |
| 3698 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: "Invalid client id to move" ); |
| 3699 | return; |
| 3700 | } |
| 3701 | |
| 3702 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' was moved to spectator (%s)" , pSelf->Server()->ClientName(ClientId: SpectateId), pReason); |
| 3703 | pSelf->SendChatTarget(To: -1, pText: aBuf); |
| 3704 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "set_team %d -1 %d" , SpectateId, g_Config.m_SvVoteSpectateRejoindelay); |
| 3705 | pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false); |
| 3706 | } |
| 3707 | } |
| 3708 | |
| 3709 | void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData) |
| 3710 | { |
| 3711 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3712 | |
| 3713 | CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg; |
| 3714 | pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1); |
| 3715 | pSelf->m_pVoteOptionHeap->Reset(); |
| 3716 | pSelf->m_pVoteOptionFirst = nullptr; |
| 3717 | pSelf->m_pVoteOptionLast = nullptr; |
| 3718 | pSelf->m_NumVoteOptions = 0; |
| 3719 | |
| 3720 | // reset sending of vote options |
| 3721 | for(auto &pPlayer : pSelf->m_apPlayers) |
| 3722 | { |
| 3723 | if(pPlayer) |
| 3724 | pPlayer->m_SendVoteIndex = 0; |
| 3725 | } |
| 3726 | } |
| 3727 | |
| 3728 | struct CMapNameItem |
| 3729 | { |
| 3730 | char m_aName[IO_MAX_PATH_LENGTH - 4]; |
| 3731 | bool m_IsDirectory; |
| 3732 | |
| 3733 | static bool CompareFilenameAscending(const CMapNameItem Lhs, const CMapNameItem Rhs) |
| 3734 | { |
| 3735 | if(str_comp(a: Rhs.m_aName, b: ".." ) == 0) |
| 3736 | return false; |
| 3737 | if(str_comp(a: Lhs.m_aName, b: ".." ) == 0) |
| 3738 | return true; |
| 3739 | if(Lhs.m_IsDirectory != Rhs.m_IsDirectory) |
| 3740 | return Lhs.m_IsDirectory; |
| 3741 | return str_comp_filenames(a: Lhs.m_aName, b: Rhs.m_aName) < 0; |
| 3742 | } |
| 3743 | }; |
| 3744 | |
| 3745 | void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData) |
| 3746 | { |
| 3747 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3748 | |
| 3749 | std::vector<CMapNameItem> vMapList; |
| 3750 | const char *pDirectory = pResult->GetString(Index: 0); |
| 3751 | |
| 3752 | // Don't allow moving to parent directories |
| 3753 | if(str_find_nocase(haystack: pDirectory, needle: ".." )) |
| 3754 | return; |
| 3755 | |
| 3756 | char aPath[IO_MAX_PATH_LENGTH] = "maps/" ; |
| 3757 | str_append(dst: aPath, src: pDirectory, dst_size: sizeof(aPath)); |
| 3758 | pSelf->Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: aPath, pfnCallback: MapScan, pUser: &vMapList); |
| 3759 | std::sort(first: vMapList.begin(), last: vMapList.end(), comp: CMapNameItem::CompareFilenameAscending); |
| 3760 | |
| 3761 | for(auto &Item : vMapList) |
| 3762 | { |
| 3763 | if(!str_comp(a: Item.m_aName, b: ".." ) && (!str_comp(a: aPath, b: "maps/" ))) |
| 3764 | continue; |
| 3765 | |
| 3766 | char aDescription[VOTE_DESC_LENGTH]; |
| 3767 | str_format(buffer: aDescription, buffer_size: sizeof(aDescription), format: "%s: %s%s" , Item.m_IsDirectory ? "Directory" : "Map" , Item.m_aName, Item.m_IsDirectory ? "/" : "" ); |
| 3768 | |
| 3769 | char aCommand[VOTE_CMD_LENGTH]; |
| 3770 | char aOptionEscaped[IO_MAX_PATH_LENGTH * 2]; |
| 3771 | char *pDst = aOptionEscaped; |
| 3772 | str_escape(dst: &pDst, src: Item.m_aName, end: aOptionEscaped + sizeof(aOptionEscaped)); |
| 3773 | |
| 3774 | char aDirectory[IO_MAX_PATH_LENGTH] = "" ; |
| 3775 | if(pResult->NumArguments()) |
| 3776 | str_copy(dst&: aDirectory, src: pDirectory); |
| 3777 | |
| 3778 | if(!str_comp(a: Item.m_aName, b: ".." )) |
| 3779 | { |
| 3780 | fs_parent_dir(path: aDirectory); |
| 3781 | str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"" , aDirectory); |
| 3782 | } |
| 3783 | else if(Item.m_IsDirectory) |
| 3784 | { |
| 3785 | str_append(dst: aDirectory, src: "/" , dst_size: sizeof(aDirectory)); |
| 3786 | str_append(dst: aDirectory, src: aOptionEscaped, dst_size: sizeof(aDirectory)); |
| 3787 | |
| 3788 | str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"" , aDirectory); |
| 3789 | } |
| 3790 | else |
| 3791 | str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "change_map \"%s%s%s\"" , pDirectory, pDirectory[0] == '\0' ? "" : "/" , aOptionEscaped); |
| 3792 | |
| 3793 | pSelf->AddVote(pDescription: aDescription, pCommand: aCommand); |
| 3794 | } |
| 3795 | |
| 3796 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server" , pStr: "added maps to votes" ); |
| 3797 | } |
| 3798 | |
| 3799 | int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData) |
| 3800 | { |
| 3801 | if((!IsDir && !str_endswith(str: pName, suffix: ".map" )) || !str_comp(a: pName, b: "." )) |
| 3802 | return 0; |
| 3803 | |
| 3804 | CMapNameItem Item; |
| 3805 | Item.m_IsDirectory = IsDir; |
| 3806 | if(!IsDir) |
| 3807 | str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pName, truncation_len: str_length(str: pName) - str_length(str: ".map" )); |
| 3808 | else |
| 3809 | str_copy(dst&: Item.m_aName, src: pName); |
| 3810 | static_cast<std::vector<CMapNameItem> *>(pUserData)->push_back(x: Item); |
| 3811 | |
| 3812 | return 0; |
| 3813 | } |
| 3814 | |
| 3815 | void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData) |
| 3816 | { |
| 3817 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3818 | |
| 3819 | if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "yes" ) == 0) |
| 3820 | pSelf->ForceVote(Success: true); |
| 3821 | else if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "no" ) == 0) |
| 3822 | pSelf->ForceVote(Success: false); |
| 3823 | } |
| 3824 | |
| 3825 | void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData) |
| 3826 | { |
| 3827 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3828 | |
| 3829 | int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : 0; |
| 3830 | static const int s_EntriesPerPage = 20; |
| 3831 | const int Start = Page * s_EntriesPerPage; |
| 3832 | const int End = (Page + 1) * s_EntriesPerPage; |
| 3833 | |
| 3834 | char aBuf[512]; |
| 3835 | int Count = 0; |
| 3836 | for(CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; pOption; pOption = pOption->m_pNext, Count++) |
| 3837 | { |
| 3838 | if(Count < Start || Count >= End) |
| 3839 | { |
| 3840 | continue; |
| 3841 | } |
| 3842 | |
| 3843 | str_copy(dst&: aBuf, src: "add_vote \"" ); |
| 3844 | char *pDst = aBuf + str_length(str: aBuf); |
| 3845 | str_escape(dst: &pDst, src: pOption->m_aDescription, end: aBuf + sizeof(aBuf)); |
| 3846 | str_append(dst&: aBuf, src: "\" \"" ); |
| 3847 | pDst = aBuf + str_length(str: aBuf); |
| 3848 | str_escape(dst: &pDst, src: pOption->m_aCommand, end: aBuf + sizeof(aBuf)); |
| 3849 | str_append(dst&: aBuf, src: "\"" ); |
| 3850 | |
| 3851 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes" , pStr: aBuf); |
| 3852 | } |
| 3853 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d %s, showing entries %d - %d" , Count, Count == 1 ? "vote" : "votes" , Start, End - 1); |
| 3854 | pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes" , pStr: aBuf); |
| 3855 | } |
| 3856 | |
| 3857 | void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
| 3858 | { |
| 3859 | pfnCallback(pResult, pCallbackUserData); |
| 3860 | if(pResult->NumArguments()) |
| 3861 | { |
| 3862 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3863 | pSelf->SendMotd(ClientId: -1); |
| 3864 | } |
| 3865 | } |
| 3866 | |
| 3867 | void CGameContext::ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
| 3868 | { |
| 3869 | pfnCallback(pResult, pCallbackUserData); |
| 3870 | if(pResult->NumArguments()) |
| 3871 | { |
| 3872 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3873 | pSelf->SendSettings(ClientId: -1); |
| 3874 | } |
| 3875 | } |
| 3876 | |
| 3877 | void CGameContext::ConchainPracticeByDefaultUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) |
| 3878 | { |
| 3879 | const int OldValue = g_Config.m_SvPracticeByDefault; |
| 3880 | pfnCallback(pResult, pCallbackUserData); |
| 3881 | |
| 3882 | if(pResult->NumArguments() && g_Config.m_SvTestingCommands) |
| 3883 | { |
| 3884 | CGameContext *pSelf = (CGameContext *)pUserData; |
| 3885 | |
| 3886 | if(pSelf->m_pController == nullptr) |
| 3887 | return; |
| 3888 | |
| 3889 | const int Enable = pResult->GetInteger(Index: 0); |
| 3890 | if(Enable == OldValue) |
| 3891 | return; |
| 3892 | |
| 3893 | char aBuf[256]; |
| 3894 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Practice is %s by default." , Enable ? "enabled" : "disabled" ); |
| 3895 | if(Enable) |
| 3896 | str_append(dst&: aBuf, src: " Join a team and /unpractice to turn it off for your team." ); |
| 3897 | |
| 3898 | pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf); |
| 3899 | |
| 3900 | for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++) |
| 3901 | { |
| 3902 | if(Team == TEAM_FLOCK || pSelf->m_pController->Teams().TeamSize(Team) == 0) |
| 3903 | { |
| 3904 | pSelf->m_pController->Teams().SetPractice(Team, Enabled: Enable); |
| 3905 | } |
| 3906 | } |
| 3907 | } |
| 3908 | } |
| 3909 | |
| 3910 | void CGameContext::OnConsoleInit() |
| 3911 | { |
| 3912 | m_pServer = Kernel()->RequestInterface<IServer>(); |
| 3913 | m_pConfigManager = Kernel()->RequestInterface<IConfigManager>(); |
| 3914 | m_pConfig = m_pConfigManager->Values(); |
| 3915 | m_pConsole = Kernel()->RequestInterface<IConsole>(); |
| 3916 | m_pEngine = Kernel()->RequestInterface<IEngine>(); |
| 3917 | m_pStorage = Kernel()->RequestInterface<IStorage>(); |
| 3918 | |
| 3919 | Console()->Register(pName: "tune" , pParams: "s[tuning] ?f[value]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConTuneParam, pUser: this, pHelp: "Tune variable to value or show current value" ); |
| 3920 | Console()->Register(pName: "toggle_tune" , pParams: "s[tuning] f[value 1] f[value 2]" , Flags: CFGFLAG_SERVER, pfnFunc: ConToggleTuneParam, pUser: this, pHelp: "Toggle tune variable" ); |
| 3921 | Console()->Register(pName: "tune_reset" , pParams: "?s[tuning]" , Flags: CFGFLAG_SERVER, pfnFunc: ConTuneReset, pUser: this, pHelp: "Reset all or one tuning variable to default" ); |
| 3922 | Console()->Register(pName: "tunes" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConTunes, pUser: this, pHelp: "List all tuning variables and their values" ); |
| 3923 | Console()->Register(pName: "tune_zone" , pParams: "i[zone] s[tuning] f[value]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConTuneZone, pUser: this, pHelp: "Tune in zone a variable to value" ); |
| 3924 | Console()->Register(pName: "tune_zone_dump" , pParams: "i[zone]" , Flags: CFGFLAG_SERVER, pfnFunc: ConTuneDumpZone, pUser: this, pHelp: "Dump zone tuning in zone x" ); |
| 3925 | Console()->Register(pName: "tune_zone_reset" , pParams: "?i[zone]" , Flags: CFGFLAG_SERVER, pfnFunc: ConTuneResetZone, pUser: this, pHelp: "Reset zone tuning in zone x or in all zones" ); |
| 3926 | Console()->Register(pName: "tune_zone_enter" , pParams: "i[zone] r[message]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConTuneSetZoneMsgEnter, pUser: this, pHelp: "Which message to display on zone enter; use 0 for normal area" ); |
| 3927 | Console()->Register(pName: "tune_zone_leave" , pParams: "i[zone] r[message]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConTuneSetZoneMsgLeave, pUser: this, pHelp: "Which message to display on zone leave; use 0 for normal area" ); |
| 3928 | Console()->Register(pName: "mapbug" , pParams: "s[mapbug]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConMapbug, pUser: this, pHelp: "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)" ); |
| 3929 | Console()->Register(pName: "switch_open" , pParams: "i[switch]" , Flags: CFGFLAG_SERVER | CFGFLAG_GAME, pfnFunc: ConSwitchOpen, pUser: this, pHelp: "Whether a switch is deactivated by default (otherwise activated)" ); |
| 3930 | Console()->Register(pName: "pause_game" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConPause, pUser: this, pHelp: "Pause/unpause game" ); |
| 3931 | Console()->Register(pName: "change_map" , pParams: "r[map]" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConChangeMap, pUser: this, pHelp: "Change map" ); |
| 3932 | Console()->Register(pName: "random_map" , pParams: "?i[stars] ?i[max stars]" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRandomMap, pUser: this, pHelp: "Random map" ); |
| 3933 | Console()->Register(pName: "random_unfinished_map" , pParams: "?i[stars] ?i[max stars]" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRandomUnfinishedMap, pUser: this, pHelp: "Random unfinished map" ); |
| 3934 | Console()->Register(pName: "restart" , pParams: "?i[seconds]" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRestart, pUser: this, pHelp: "Restart in x seconds (0 = abort)" ); |
| 3935 | Console()->Register(pName: "server_alert" , pParams: "r[message]" , Flags: CFGFLAG_SERVER, pfnFunc: ConServerAlert, pUser: this, pHelp: "Send a server alert message to all players" ); |
| 3936 | Console()->Register(pName: "mod_alert" , pParams: "v[id] r[message]" , Flags: CFGFLAG_SERVER, pfnFunc: ConModAlert, pUser: this, pHelp: "Send a moderator alert message to player" ); |
| 3937 | Console()->Register(pName: "broadcast" , pParams: "r[message]" , Flags: CFGFLAG_SERVER, pfnFunc: ConBroadcast, pUser: this, pHelp: "Broadcast message" ); |
| 3938 | Console()->Register(pName: "say" , pParams: "r[message]" , Flags: CFGFLAG_SERVER, pfnFunc: ConSay, pUser: this, pHelp: "Say in chat" ); |
| 3939 | Console()->Register(pName: "set_team" , pParams: "i[id] i[team-id] ?i[delay in minutes]" , Flags: CFGFLAG_SERVER, pfnFunc: ConSetTeam, pUser: this, pHelp: "Set team for a player (spectators = -1, game = 0)" ); |
| 3940 | Console()->Register(pName: "set_team_all" , pParams: "i[team-id]" , Flags: CFGFLAG_SERVER, pfnFunc: ConSetTeamAll, pUser: this, pHelp: "Set team for all players (spectators = -1, game = 0)" ); |
| 3941 | Console()->Register(pName: "hot_reload" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConHotReload, pUser: this, pHelp: "Reload the map while preserving the state of tees and teams" ); |
| 3942 | Console()->Register(pName: "reload_censorlist" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConReloadCensorlist, pUser: this, pHelp: "Reload the censorlist" ); |
| 3943 | |
| 3944 | Console()->Register(pName: "add_vote" , pParams: "s[name] r[command]" , Flags: CFGFLAG_SERVER, pfnFunc: ConAddVote, pUser: this, pHelp: "Add a voting option" ); |
| 3945 | Console()->Register(pName: "remove_vote" , pParams: "r[name]" , Flags: CFGFLAG_SERVER, pfnFunc: ConRemoveVote, pUser: this, pHelp: "remove a voting option" ); |
| 3946 | Console()->Register(pName: "force_vote" , pParams: "s[name] s[command] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConForceVote, pUser: this, pHelp: "Force a voting option" ); |
| 3947 | Console()->Register(pName: "clear_votes" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConClearVotes, pUser: this, pHelp: "Clears the voting options" ); |
| 3948 | Console()->Register(pName: "add_map_votes" , pParams: "?s[directory]" , Flags: CFGFLAG_SERVER, pfnFunc: ConAddMapVotes, pUser: this, pHelp: "Automatically adds voting options for all maps" ); |
| 3949 | Console()->Register(pName: "vote" , pParams: "r['yes'|'no']" , Flags: CFGFLAG_SERVER, pfnFunc: ConVote, pUser: this, pHelp: "Force a vote to yes/no" ); |
| 3950 | Console()->Register(pName: "votes" , pParams: "?i[page]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVotes, pUser: this, pHelp: "Show all votes (page 0 by default, 20 entries per page)" ); |
| 3951 | Console()->Register(pName: "dump_antibot" , pParams: "" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConDumpAntibot, pUser: this, pHelp: "Dumps the antibot status" ); |
| 3952 | Console()->Register(pName: "antibot" , pParams: "r[command]" , Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConAntibot, pUser: this, pHelp: "Sends a command to the antibot" ); |
| 3953 | |
| 3954 | Console()->Chain(pName: "sv_motd" , pfnChainFunc: ConchainSpecialMotdupdate, pUser: this); |
| 3955 | |
| 3956 | Console()->Chain(pName: "sv_vote_kick" , pfnChainFunc: ConchainSettingUpdate, pUser: this); |
| 3957 | Console()->Chain(pName: "sv_vote_kick_min" , pfnChainFunc: ConchainSettingUpdate, pUser: this); |
| 3958 | Console()->Chain(pName: "sv_vote_spectate" , pfnChainFunc: ConchainSettingUpdate, pUser: this); |
| 3959 | Console()->Chain(pName: "sv_spectator_slots" , pfnChainFunc: ConchainSettingUpdate, pUser: this); |
| 3960 | |
| 3961 | RegisterDDRaceCommands(); |
| 3962 | RegisterChatCommands(); |
| 3963 | } |
| 3964 | |
| 3965 | void CGameContext::RegisterDDRaceCommands() |
| 3966 | { |
| 3967 | Console()->Register(pName: "kill_pl" , pParams: "v[id] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConKillPlayer, pUser: this, pHelp: "Kills a player and announces the kill" ); |
| 3968 | Console()->Register(pName: "totele" , pParams: "i[number]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i" ); |
| 3969 | Console()->Register(pName: "totelecp" , pParams: "i[number]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i" ); |
| 3970 | Console()->Register(pName: "tele" , pParams: "?i[id] ?i[id]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConTeleport, pUser: this, pHelp: "Teleports player i (or you) to player i (or you to where you look at)" ); |
| 3971 | Console()->Register(pName: "addweapon" , pParams: "i[weapon-id]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConAddWeapon, pUser: this, pHelp: "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)" ); |
| 3972 | Console()->Register(pName: "removeweapon" , pParams: "i[weapon-id]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConRemoveWeapon, pUser: this, pHelp: "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)" ); |
| 3973 | Console()->Register(pName: "shotgun" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConShotgun, pUser: this, pHelp: "Gives a shotgun to you" ); |
| 3974 | Console()->Register(pName: "grenade" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGrenade, pUser: this, pHelp: "Gives a grenade launcher to you" ); |
| 3975 | Console()->Register(pName: "laser" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you" ); |
| 3976 | Console()->Register(pName: "rifle" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you" ); |
| 3977 | Console()->Register(pName: "jetpack" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConJetpack, pUser: this, pHelp: "Gives jetpack to you" ); |
| 3978 | Console()->Register(pName: "setjumps" , pParams: "i[jumps]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSetJumps, pUser: this, pHelp: "Gives you as many jumps as you specify" ); |
| 3979 | Console()->Register(pName: "weapons" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConWeapons, pUser: this, pHelp: "Gives all weapons to you" ); |
| 3980 | Console()->Register(pName: "unshotgun" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnShotgun, pUser: this, pHelp: "Removes the shotgun from you" ); |
| 3981 | Console()->Register(pName: "ungrenade" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you" ); |
| 3982 | Console()->Register(pName: "unlaser" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you" ); |
| 3983 | Console()->Register(pName: "unrifle" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you" ); |
| 3984 | Console()->Register(pName: "unjetpack" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnJetpack, pUser: this, pHelp: "Removes the jetpack from you" ); |
| 3985 | Console()->Register(pName: "unweapons" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnWeapons, pUser: this, pHelp: "Removes all weapons from you" ); |
| 3986 | Console()->Register(pName: "ninja" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConNinja, pUser: this, pHelp: "Makes you a ninja" ); |
| 3987 | Console()->Register(pName: "unninja" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnNinja, pUser: this, pHelp: "Removes ninja from you" ); |
| 3988 | Console()->Register(pName: "super" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSuper, pUser: this, pHelp: "Makes you super" ); |
| 3989 | Console()->Register(pName: "unsuper" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConUnSuper, pUser: this, pHelp: "Removes super from you" ); |
| 3990 | Console()->Register(pName: "invincible" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToggleInvincible, pUser: this, pHelp: "Toggles invincible mode" ); |
| 3991 | Console()->Register(pName: "infinite_jump" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessJump, pUser: this, pHelp: "Gives you infinite jump" ); |
| 3992 | Console()->Register(pName: "uninfinite_jump" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you" ); |
| 3993 | Console()->Register(pName: "endless_hook" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessHook, pUser: this, pHelp: "Gives you endless hook" ); |
| 3994 | Console()->Register(pName: "unendless_hook" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you" ); |
| 3995 | Console()->Register(pName: "setswitch" , pParams: "i[switch] ?i['0'|'1'] ?i[seconds]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSetSwitch, pUser: this, pHelp: "Toggle or set the switch on or off for the specified time (or indefinitely by default)" ); |
| 3996 | Console()->Register(pName: "solo" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSolo, pUser: this, pHelp: "Puts you into solo part" ); |
| 3997 | Console()->Register(pName: "unsolo" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnSolo, pUser: this, pHelp: "Puts you out of solo part" ); |
| 3998 | Console()->Register(pName: "freeze" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConFreeze, pUser: this, pHelp: "Puts you into freeze" ); |
| 3999 | Console()->Register(pName: "unfreeze" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnfreeze, pUser: this, pHelp: "Puts you out of freeze" ); |
| 4000 | Console()->Register(pName: "deep" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConDeep, pUser: this, pHelp: "Puts you into deep freeze" ); |
| 4001 | Console()->Register(pName: "undeep" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnDeep, pUser: this, pHelp: "Puts you out of deep freeze" ); |
| 4002 | Console()->Register(pName: "livefreeze" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLiveFreeze, pUser: this, pHelp: "Makes you live frozen" ); |
| 4003 | Console()->Register(pName: "unlivefreeze" , pParams: "" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze" ); |
| 4004 | Console()->Register(pName: "left" , pParams: "?i[tiles]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoLeft, pUser: this, pHelp: "Makes you move 1 tile left" ); |
| 4005 | Console()->Register(pName: "right" , pParams: "?i[tiles]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoRight, pUser: this, pHelp: "Makes you move 1 tile right" ); |
| 4006 | Console()->Register(pName: "up" , pParams: "?i[tiles]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoUp, pUser: this, pHelp: "Makes you move 1 tile up" ); |
| 4007 | Console()->Register(pName: "down" , pParams: "?i[tiles]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoDown, pUser: this, pHelp: "Makes you move 1 tile down" ); |
| 4008 | |
| 4009 | Console()->Register(pName: "move" , pParams: "i[x] i[y]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConMove, pUser: this, pHelp: "Moves to the tile with x/y-number ii" ); |
| 4010 | Console()->Register(pName: "move_raw" , pParams: "i[x] i[y]" , Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConMoveRaw, pUser: this, pHelp: "Moves to the point with x/y-coordinates ii" ); |
| 4011 | Console()->Register(pName: "force_pause" , pParams: "v[id] i[seconds]" , Flags: CFGFLAG_SERVER, pfnFunc: ConForcePause, pUser: this, pHelp: "Force i to pause for i seconds" ); |
| 4012 | Console()->Register(pName: "force_unpause" , pParams: "v[id]" , Flags: CFGFLAG_SERVER, pfnFunc: ConForcePause, pUser: this, pHelp: "Set force-pause timer of i to 0." ); |
| 4013 | |
| 4014 | Console()->Register(pName: "set_team_ddr" , pParams: "v[id] i[team]" , Flags: CFGFLAG_SERVER, pfnFunc: ConSetDDRTeam, pUser: this, pHelp: "Set ddrace team for a player" ); |
| 4015 | Console()->Register(pName: "uninvite" , pParams: "v[id] i[team]" , Flags: CFGFLAG_SERVER, pfnFunc: ConUninvite, pUser: this, pHelp: "Uninvite player from team" ); |
| 4016 | |
| 4017 | Console()->Register(pName: "mute" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConMute, pUser: this, pHelp: "Deprecated. Use either 'muteid <client_id> <seconds> <reason>' or 'muteip <ip> <seconds> <reason>'" ); |
| 4018 | Console()->Register(pName: "muteid" , pParams: "v[id] i[seconds] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConMuteId, pUser: this, pHelp: "Mute player with client ID" ); |
| 4019 | Console()->Register(pName: "muteip" , pParams: "s[ip] i[seconds] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConMuteIp, pUser: this, pHelp: "Mute player with IP address" ); |
| 4020 | Console()->Register(pName: "unmute" , pParams: "i[index]" , Flags: CFGFLAG_SERVER, pfnFunc: ConUnmute, pUser: this, pHelp: "Unmute player with list index" ); |
| 4021 | Console()->Register(pName: "unmuteid" , pParams: "v[id]" , Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteId, pUser: this, pHelp: "Unmute player with client ID" ); |
| 4022 | Console()->Register(pName: "unmuteip" , pParams: "s[ip]" , Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteIp, pUser: this, pHelp: "Unmute player with IP address" ); |
| 4023 | Console()->Register(pName: "mutes" , pParams: "?i[page]" , Flags: CFGFLAG_SERVER, pfnFunc: ConMutes, pUser: this, pHelp: "Show list of mutes (page 1 by default, 20 entries per page)" ); |
| 4024 | |
| 4025 | Console()->Register(pName: "vote_mute" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteMute, pUser: this, pHelp: "Deprecated. Use either 'vote_muteid <client_id> <seconds> <reason>' or 'vote_muteip <ip> <seconds> <reason>'" ); |
| 4026 | Console()->Register(pName: "vote_muteid" , pParams: "v[id] i[seconds] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteMuteId, pUser: this, pHelp: "Remove right to vote from player with client ID" ); |
| 4027 | Console()->Register(pName: "vote_muteip" , pParams: "s[ip] i[seconds] ?r[reason]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteMuteIp, pUser: this, pHelp: "Remove right to vote from player with IP address" ); |
| 4028 | Console()->Register(pName: "vote_unmute" , pParams: "i[index]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteUnmute, pUser: this, pHelp: "Give back right to vote to player with list index" ); |
| 4029 | Console()->Register(pName: "vote_unmuteid" , pParams: "v[id]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteUnmuteId, pUser: this, pHelp: "Give back right to vote to player with client ID" ); |
| 4030 | Console()->Register(pName: "vote_unmuteip" , pParams: "s[ip]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteUnmuteIp, pUser: this, pHelp: "Give back right to vote to player with IP address" ); |
| 4031 | Console()->Register(pName: "vote_mutes" , pParams: "?i[page]" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteMutes, pUser: this, pHelp: "Show list of vote mutes (page 1 by default, 20 entries per page)" ); |
| 4032 | |
| 4033 | Console()->Register(pName: "moderate" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConModerate, pUser: this, pHelp: "Enables/disables active moderator mode for the player" ); |
| 4034 | Console()->Register(pName: "vote_no" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConVoteNo, pUser: this, pHelp: "Same as \"vote no\"" ); |
| 4035 | Console()->Register(pName: "save_dry" , pParams: "" , Flags: CFGFLAG_SERVER, pfnFunc: ConDrySave, pUser: this, pHelp: "Dump the current savestring" ); |
| 4036 | Console()->Register(pName: "dump_log" , pParams: "?i[seconds]" , Flags: CFGFLAG_SERVER, pfnFunc: ConDumpLog, pUser: this, pHelp: "Show logs of the last i seconds" ); |
| 4037 | |
| 4038 | Console()->Chain(pName: "sv_practice_by_default" , pfnChainFunc: ConchainPracticeByDefaultUpdate, pUser: this); |
| 4039 | } |
| 4040 | |
| 4041 | void CGameContext::RegisterChatCommands() |
| 4042 | { |
| 4043 | Console()->Register(pName: "credits" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCredits, pUser: this, pHelp: "Shows the credits of the DDNet mod" ); |
| 4044 | Console()->Register(pName: "rules" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConRules, pUser: this, pHelp: "Shows the server rules" ); |
| 4045 | Console()->Register(pName: "emote" , pParams: "?s[emote name] i[duration in seconds]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConEyeEmote, pUser: this, pHelp: "Sets your tee's eye emote" ); |
| 4046 | Console()->Register(pName: "eyeemote" , pParams: "?s['on'|'off'|'toggle']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSetEyeEmote, pUser: this, pHelp: "Toggles use of standard eye-emotes on/off, eyeemote s, where s = on for on, off for off, toggle for toggle and nothing to show current status" ); |
| 4047 | Console()->Register(pName: "settings" , pParams: "?s[configname]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSettings, pUser: this, pHelp: "Shows gameplay information for this server" ); |
| 4048 | Console()->Register(pName: "help" , pParams: "?r[command]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConHelp, pUser: this, pHelp: "Shows help to command r, general help if left blank" ); |
| 4049 | Console()->Register(pName: "info" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConInfo, pUser: this, pHelp: "Shows info about this server" ); |
| 4050 | Console()->Register(pName: "list" , pParams: "?s[filter]" , Flags: CFGFLAG_CHAT, pfnFunc: ConList, pUser: this, pHelp: "List connected players with optional case-insensitive substring matching filter" ); |
| 4051 | Console()->Register(pName: "w" , pParams: "s[player name] r[message]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhisper, pUser: this, pHelp: "Whisper something to someone (private message)" ); |
| 4052 | Console()->Register(pName: "whisper" , pParams: "s[player name] r[message]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhisper, pUser: this, pHelp: "Whisper something to someone (private message)" ); |
| 4053 | Console()->Register(pName: "c" , pParams: "r[message]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConConverse, pUser: this, pHelp: "Converse with the last person you whispered to (private message)" ); |
| 4054 | Console()->Register(pName: "converse" , pParams: "r[message]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConConverse, pUser: this, pHelp: "Converse with the last person you whispered to (private message)" ); |
| 4055 | Console()->Register(pName: "pause" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePause, pUser: this, pHelp: "Toggles pause" ); |
| 4056 | Console()->Register(pName: "spec" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConToggleSpec, pUser: this, pHelp: "Toggles spec (if not available behaves as /pause)" ); |
| 4057 | Console()->Register(pName: "pausevoted" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePauseVoted, pUser: this, pHelp: "Toggles pause on the currently voted player" ); |
| 4058 | Console()->Register(pName: "specvoted" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConToggleSpecVoted, pUser: this, pHelp: "Toggles spec on the currently voted player" ); |
| 4059 | Console()->Register(pName: "dnd" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConDND, pUser: this, pHelp: "Toggle Do Not Disturb (no chat and server messages)" ); |
| 4060 | Console()->Register(pName: "whispers" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhispers, pUser: this, pHelp: "Toggle receiving whispers" ); |
| 4061 | Console()->Register(pName: "mapinfo" , pParams: "?r[map]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConMapInfo, pUser: this, pHelp: "Show info about the map with name r gives (current map by default)" ); |
| 4062 | Console()->Register(pName: "timeout" , pParams: "?s[code]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimeout, pUser: this, pHelp: "Set timeout protection code s" ); |
| 4063 | Console()->Register(pName: "practice" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPractice, pUser: this, pHelp: "Enable cheats for your current team's run, but you can't earn a rank" ); |
| 4064 | Console()->Register(pName: "unpractice" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConUnPractice, pUser: this, pHelp: "Kills team and disables practice mode" ); |
| 4065 | Console()->Register(pName: "practicecmdlist" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPracticeCmdList, pUser: this, pHelp: "List all commands that are available in practice mode" ); |
| 4066 | Console()->Register(pName: "swap" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSwap, pUser: this, pHelp: "Request to swap your tee with another team member" ); |
| 4067 | Console()->Register(pName: "cancelswap" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCancelSwap, pUser: this, pHelp: "Cancel your swap request" ); |
| 4068 | Console()->Register(pName: "save" , pParams: "?r[code]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSave, pUser: this, pHelp: "Save team with code r." ); |
| 4069 | Console()->Register(pName: "load" , pParams: "?r[code]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConLoad, pUser: this, pHelp: "Load with code r. /load to check your existing saves" ); |
| 4070 | Console()->Register(pName: "map" , pParams: "?r[map]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConMap, pUser: this, pHelp: "Vote a map by name" ); |
| 4071 | |
| 4072 | Console()->Register(pName: "rankteam" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamRank, pUser: this, pHelp: "Shows the team rank of player with name r (your team rank by default)" ); |
| 4073 | Console()->Register(pName: "teamrank" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamRank, pUser: this, pHelp: "Shows the team rank of player with name r (your team rank by default)" ); |
| 4074 | |
| 4075 | Console()->Register(pName: "rank" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConRank, pUser: this, pHelp: "Shows the rank of player with name r (your rank by default)" ); |
| 4076 | Console()->Register(pName: "top5team" , pParams: "?s[player name] ?i[rank to start with]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamTop5, pUser: this, pHelp: "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)" ); |
| 4077 | Console()->Register(pName: "teamtop5" , pParams: "?s[player name] ?i[rank to start with]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamTop5, pUser: this, pHelp: "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)" ); |
| 4078 | Console()->Register(pName: "top" , pParams: "?i[rank to start with]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTop, pUser: this, pHelp: "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)" ); |
| 4079 | Console()->Register(pName: "top5" , pParams: "?i[rank to start with]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTop, pUser: this, pHelp: "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)" ); |
| 4080 | Console()->Register(pName: "times" , pParams: "?s[player name] ?i[number of times to skip]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimes, pUser: this, pHelp: "/times ?s?i shows last 5 times of the server or of a player beginning with name s starting with time i (i = 1 by default, -1 for first)" ); |
| 4081 | Console()->Register(pName: "points" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPoints, pUser: this, pHelp: "Shows the global points of a player beginning with name r (your rank by default)" ); |
| 4082 | Console()->Register(pName: "top5points" , pParams: "?i[number]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTopPoints, pUser: this, pHelp: "Shows five points of the global point ladder beginning with rank i (1 by default)" ); |
| 4083 | Console()->Register(pName: "timecp" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimeCP, pUser: this, pHelp: "Set your checkpoints based on another player" ); |
| 4084 | |
| 4085 | Console()->Register(pName: "team" , pParams: "?i[id]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeam, pUser: this, pHelp: "Lets you join team i (shows your team if left blank)" ); |
| 4086 | Console()->Register(pName: "lock" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConLock, pUser: this, pHelp: "Toggle team lock so no one else can join and so the team restarts when a player dies. /lock 0 to unlock, /lock 1 to lock" ); |
| 4087 | Console()->Register(pName: "unlock" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConUnlock, pUser: this, pHelp: "Unlock a team" ); |
| 4088 | Console()->Register(pName: "invite" , pParams: "r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConInvite, pUser: this, pHelp: "Invite a person to a locked team" ); |
| 4089 | Console()->Register(pName: "join" , pParams: "r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConJoin, pUser: this, pHelp: "Join the team of the specified player" ); |
| 4090 | Console()->Register(pName: "team0mode" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeam0Mode, pUser: this, pHelp: "Toggle team between team 0 and team mode. This mode will make your team behave like team 0." ); |
| 4091 | |
| 4092 | Console()->Register(pName: "showothers" , pParams: "?i['0'|'1'|'2']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConShowOthers, pUser: this, pHelp: "Whether to show players from other teams or not (off by default), optional i = 0 for off, i = 1 for on, i = 2 for own team only" ); |
| 4093 | Console()->Register(pName: "showall" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConShowAll, pUser: this, pHelp: "Whether to show players at any distance (off by default), optional i = 0 for off else for on" ); |
| 4094 | Console()->Register(pName: "specteam" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSpecTeam, pUser: this, pHelp: "Whether to show players from other teams when spectating (on by default), optional i = 0 for off else for on" ); |
| 4095 | Console()->Register(pName: "ninjajetpack" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConNinjaJetpack, pUser: this, pHelp: "Whether to use ninja jetpack or not. Makes jetpack look more awesome" ); |
| 4096 | Console()->Register(pName: "saytime" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConSayTime, pUser: this, pHelp: "Privately messages someone's current time in this current running race (your time by default)" ); |
| 4097 | Console()->Register(pName: "saytimeall" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConSayTimeAll, pUser: this, pHelp: "Publicly messages everyone your current time in this current running race" ); |
| 4098 | Console()->Register(pName: "time" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTime, pUser: this, pHelp: "Privately shows you your current time in this current running race in the broadcast message" ); |
| 4099 | Console()->Register(pName: "timer" , pParams: "?s['gametimer'|'broadcast'|'both'|'none'|'cycle']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSetTimerType, pUser: this, pHelp: "Personal Setting of showing time in either broadcast or game/round timer, timer s, where s = broadcast for broadcast, gametimer for game/round timer, cycle for cycle, both for both, none for no timer and nothing to show current status" ); |
| 4100 | Console()->Register(pName: "r" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescue, pUser: this, pHelp: "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)" ); |
| 4101 | Console()->Register(pName: "rescue" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescue, pUser: this, pHelp: "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)" ); |
| 4102 | Console()->Register(pName: "back" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConBack, pUser: this, pHelp: "Teleport yourself to the last auto rescue position before you died (use sv_rescue 1 to enable this feature)" ); |
| 4103 | Console()->Register(pName: "rescuemode" , pParams: "?r['auto'|'manual']" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescueMode, pUser: this, pHelp: "Sets one of the two rescue modes (auto or manual). Prints current mode if no arguments provided" ); |
| 4104 | Console()->Register(pName: "tp" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleTo, pUser: this, pHelp: "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name" ); |
| 4105 | Console()->Register(pName: "teleport" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleTo, pUser: this, pHelp: "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name" ); |
| 4106 | Console()->Register(pName: "tpxy" , pParams: "s[x] s[y]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleXY, pUser: this, pHelp: "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right" ); |
| 4107 | Console()->Register(pName: "lasttp" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConLastTele, pUser: this, pHelp: "Teleport yourself to the last location you teleported to" ); |
| 4108 | Console()->Register(pName: "tc" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleCursor, pUser: this, pHelp: "Teleport yourself to player or to where you are spectating/or looking if no player name is given" ); |
| 4109 | Console()->Register(pName: "telecursor" , pParams: "?r[player name]" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleCursor, pUser: this, pHelp: "Teleport yourself to player or to where you are spectating/or looking if no player name is given" ); |
| 4110 | Console()->Register(pName: "totele" , pParams: "i[number]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i" ); |
| 4111 | Console()->Register(pName: "totelecp" , pParams: "i[number]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i" ); |
| 4112 | Console()->Register(pName: "unsolo" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnSolo, pUser: this, pHelp: "Puts you out of solo part" ); |
| 4113 | Console()->Register(pName: "solo" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSolo, pUser: this, pHelp: "Puts you into solo part" ); |
| 4114 | Console()->Register(pName: "undeep" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnDeep, pUser: this, pHelp: "Puts you out of deep freeze" ); |
| 4115 | Console()->Register(pName: "deep" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeDeep, pUser: this, pHelp: "Puts you into deep freeze" ); |
| 4116 | Console()->Register(pName: "unlivefreeze" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze" ); |
| 4117 | Console()->Register(pName: "livefreeze" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLiveFreeze, pUser: this, pHelp: "Makes you live frozen" ); |
| 4118 | Console()->Register(pName: "addweapon" , pParams: "i[weapon-id]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeAddWeapon, pUser: this, pHelp: "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)" ); |
| 4119 | Console()->Register(pName: "removeweapon" , pParams: "i[weapon-id]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeRemoveWeapon, pUser: this, pHelp: "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)" ); |
| 4120 | Console()->Register(pName: "shotgun" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeShotgun, pUser: this, pHelp: "Gives a shotgun to you" ); |
| 4121 | Console()->Register(pName: "grenade" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeGrenade, pUser: this, pHelp: "Gives a grenade launcher to you" ); |
| 4122 | Console()->Register(pName: "laser" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you" ); |
| 4123 | Console()->Register(pName: "rifle" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you" ); |
| 4124 | Console()->Register(pName: "jetpack" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeJetpack, pUser: this, pHelp: "Gives jetpack to you" ); |
| 4125 | Console()->Register(pName: "setjumps" , pParams: "i[jumps]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSetJumps, pUser: this, pHelp: "Gives you as many jumps as you specify" ); |
| 4126 | Console()->Register(pName: "weapons" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeWeapons, pUser: this, pHelp: "Gives all weapons to you" ); |
| 4127 | Console()->Register(pName: "unshotgun" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnShotgun, pUser: this, pHelp: "Removes the shotgun from you" ); |
| 4128 | Console()->Register(pName: "ungrenade" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you" ); |
| 4129 | Console()->Register(pName: "unlaser" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you" ); |
| 4130 | Console()->Register(pName: "unrifle" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you" ); |
| 4131 | Console()->Register(pName: "unjetpack" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnJetpack, pUser: this, pHelp: "Removes the jetpack from you" ); |
| 4132 | Console()->Register(pName: "unweapons" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnWeapons, pUser: this, pHelp: "Removes all weapons from you" ); |
| 4133 | Console()->Register(pName: "ninja" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeNinja, pUser: this, pHelp: "Makes you a ninja" ); |
| 4134 | Console()->Register(pName: "unninja" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnNinja, pUser: this, pHelp: "Removes ninja from you" ); |
| 4135 | Console()->Register(pName: "infjump" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessJump, pUser: this, pHelp: "Gives you infinite jump" ); |
| 4136 | Console()->Register(pName: "uninfjump" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you" ); |
| 4137 | Console()->Register(pName: "endless" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessHook, pUser: this, pHelp: "Gives you endless hook" ); |
| 4138 | Console()->Register(pName: "unendless" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you" ); |
| 4139 | Console()->Register(pName: "setswitch" , pParams: "i[switch] ?i['0'|'1'] ?i[seconds]" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSetSwitch, pUser: this, pHelp: "Toggle or set the switch on or off for the specified time (or indefinitely by default)" ); |
| 4140 | Console()->Register(pName: "invincible" , pParams: "?i['0'|'1']" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleInvincible, pUser: this, pHelp: "Toggles invincible mode" ); |
| 4141 | Console()->Register(pName: "collision" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleCollision, pUser: this, pHelp: "Toggles collision" ); |
| 4142 | Console()->Register(pName: "hookcollision" , pParams: "" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHookCollision, pUser: this, pHelp: "Toggles hook collision" ); |
| 4143 | Console()->Register(pName: "hitothers" , pParams: "?s['all'|'hammer'|'shotgun'|'grenade'|'laser']" , Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHitOthers, pUser: this, pHelp: "Toggles hit others" ); |
| 4144 | |
| 4145 | Console()->Register(pName: "kill" , pParams: "" , Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConProtectedKill, pUser: this, pHelp: "Kill yourself when kill-protected during a long game (use f1, kill for regular kill)" ); |
| 4146 | } |
| 4147 | |
| 4148 | void CGameContext::OnInit(const void *pPersistentData) |
| 4149 | { |
| 4150 | const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData; |
| 4151 | |
| 4152 | m_pServer = Kernel()->RequestInterface<IServer>(); |
| 4153 | m_pConfigManager = Kernel()->RequestInterface<IConfigManager>(); |
| 4154 | m_pConfig = m_pConfigManager->Values(); |
| 4155 | m_pConsole = Kernel()->RequestInterface<IConsole>(); |
| 4156 | m_pEngine = Kernel()->RequestInterface<IEngine>(); |
| 4157 | m_pStorage = Kernel()->RequestInterface<IStorage>(); |
| 4158 | m_pAntibot = Kernel()->RequestInterface<IAntibot>(); |
| 4159 | m_World.SetGameServer(this); |
| 4160 | m_Events.SetGameServer(this); |
| 4161 | |
| 4162 | m_GameUuid = RandomUuid(); |
| 4163 | Console()->SetTeeHistorianCommandCallback(pfnCallback: CommandCallback, pUser: this); |
| 4164 | |
| 4165 | uint64_t aSeed[2]; |
| 4166 | secure_random_fill(bytes: aSeed, length: sizeof(aSeed)); |
| 4167 | m_Prng.Seed(aSeed); |
| 4168 | m_World.m_Core.m_pPrng = &m_Prng; |
| 4169 | |
| 4170 | DeleteTempfile(); |
| 4171 | |
| 4172 | for(int i = 0; i < NUM_NETOBJTYPES; i++) |
| 4173 | { |
| 4174 | Server()->SnapSetStaticsize(ItemType: i, Size: m_NetObjHandler.GetObjSize(Type: i)); |
| 4175 | } |
| 4176 | |
| 4177 | // HACK: only set static size for items, which were available in the first 0.7 release |
| 4178 | // so new items don't break the snapshot delta |
| 4179 | static const int OLD_NUM_NETOBJTYPES = 23; |
| 4180 | for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++) |
| 4181 | { |
| 4182 | Server()->SnapSetStaticsize7(ItemType: i, Size: m_NetObjHandler7.GetObjSize(Type: i)); |
| 4183 | } |
| 4184 | |
| 4185 | m_Layers.Init(pMap: Map(), GameOnly: false); |
| 4186 | m_Collision.Init(pLayers: &m_Layers); |
| 4187 | m_World.Init(pCollision: &m_Collision, pTuningList: m_aTuningList); |
| 4188 | m_MapBugs = CMapBugs::Create(pName: Map()->BaseName(), Size: Map()->Size(), Sha256: Map()->Sha256()); |
| 4189 | |
| 4190 | // Reset Tunezones |
| 4191 | for(int i = 0; i < TuneZone::NUM; i++) |
| 4192 | { |
| 4193 | TuningList()[i] = CTuningParams::DEFAULT; |
| 4194 | TuningList()[i].Set(pName: "gun_curvature" , Value: 0); |
| 4195 | TuningList()[i].Set(pName: "gun_speed" , Value: 1400); |
| 4196 | TuningList()[i].Set(pName: "shotgun_curvature" , Value: 0); |
| 4197 | TuningList()[i].Set(pName: "shotgun_speed" , Value: 500); |
| 4198 | TuningList()[i].Set(pName: "shotgun_speeddiff" , Value: 0); |
| 4199 | } |
| 4200 | |
| 4201 | for(int i = 0; i < TuneZone::NUM; i++) |
| 4202 | { |
| 4203 | // Send no text by default when changing tune zones. |
| 4204 | m_aaZoneEnterMsg[i][0] = 0; |
| 4205 | m_aaZoneLeaveMsg[i][0] = 0; |
| 4206 | } |
| 4207 | // Reset Tuning |
| 4208 | if(g_Config.m_SvTuneReset) |
| 4209 | { |
| 4210 | ResetTuning(); |
| 4211 | } |
| 4212 | else |
| 4213 | { |
| 4214 | GlobalTuning()->Set(pName: "gun_speed" , Value: 1400); |
| 4215 | GlobalTuning()->Set(pName: "gun_curvature" , Value: 0); |
| 4216 | GlobalTuning()->Set(pName: "shotgun_speed" , Value: 500); |
| 4217 | GlobalTuning()->Set(pName: "shotgun_speeddiff" , Value: 0); |
| 4218 | GlobalTuning()->Set(pName: "shotgun_curvature" , Value: 0); |
| 4219 | } |
| 4220 | |
| 4221 | if(g_Config.m_SvDDRaceTuneReset) |
| 4222 | { |
| 4223 | g_Config.m_SvHit = 1; |
| 4224 | g_Config.m_SvEndlessDrag = 0; |
| 4225 | g_Config.m_SvOldLaser = 0; |
| 4226 | g_Config.m_SvOldTeleportHook = 0; |
| 4227 | g_Config.m_SvOldTeleportWeapons = 0; |
| 4228 | g_Config.m_SvTeleportHoldHook = 0; |
| 4229 | g_Config.m_SvTeam = SV_TEAM_ALLOWED; |
| 4230 | g_Config.m_SvShowOthersDefault = SHOW_OTHERS_OFF; |
| 4231 | |
| 4232 | for(auto &Switcher : Switchers()) |
| 4233 | Switcher.m_Initial = true; |
| 4234 | } |
| 4235 | |
| 4236 | m_pConfigManager->SetGameSettingsReadOnly(false); |
| 4237 | |
| 4238 | Console()->ExecuteFile(pFilename: g_Config.m_SvResetFile, ClientId: IConsole::CLIENT_ID_UNSPECIFIED); |
| 4239 | |
| 4240 | LoadMapSettings(); |
| 4241 | |
| 4242 | m_pConfigManager->SetGameSettingsReadOnly(true); |
| 4243 | |
| 4244 | m_MapBugs.Dump(); |
| 4245 | |
| 4246 | if(g_Config.m_SvSoloServer) |
| 4247 | { |
| 4248 | g_Config.m_SvTeam = SV_TEAM_FORCED_SOLO; |
| 4249 | g_Config.m_SvShowOthersDefault = SHOW_OTHERS_ON; |
| 4250 | |
| 4251 | GlobalTuning()->Set(pName: "player_collision" , Value: 0); |
| 4252 | GlobalTuning()->Set(pName: "player_hooking" , Value: 0); |
| 4253 | |
| 4254 | for(int i = 0; i < TuneZone::NUM; i++) |
| 4255 | { |
| 4256 | TuningList()[i].Set(pName: "player_collision" , Value: 0); |
| 4257 | TuningList()[i].Set(pName: "player_hooking" , Value: 0); |
| 4258 | } |
| 4259 | } |
| 4260 | |
| 4261 | if(!str_comp(a: Config()->m_SvGametype, b: "mod" )) |
| 4262 | m_pController = new CGameControllerMod(this); |
| 4263 | else |
| 4264 | m_pController = new CGameControllerDDNet(this); |
| 4265 | |
| 4266 | ReadCensorList(); |
| 4267 | |
| 4268 | m_TeeHistorianActive = g_Config.m_SvTeeHistorian; |
| 4269 | if(m_TeeHistorianActive) |
| 4270 | { |
| 4271 | char aGameUuid[UUID_MAXSTRSIZE]; |
| 4272 | FormatUuid(Uuid: m_GameUuid, pBuffer: aGameUuid, BufferLength: sizeof(aGameUuid)); |
| 4273 | |
| 4274 | char aFilename[IO_MAX_PATH_LENGTH]; |
| 4275 | str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "teehistorian/%s.teehistorian" , aGameUuid); |
| 4276 | |
| 4277 | IOHANDLE THFile = Storage()->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE); |
| 4278 | if(!THFile) |
| 4279 | { |
| 4280 | dbg_msg(sys: "teehistorian" , fmt: "failed to open '%s'" , aFilename); |
| 4281 | Server()->SetErrorShutdown("teehistorian open error" ); |
| 4282 | return; |
| 4283 | } |
| 4284 | else |
| 4285 | { |
| 4286 | dbg_msg(sys: "teehistorian" , fmt: "recording to '%s'" , aFilename); |
| 4287 | } |
| 4288 | m_pTeeHistorianFile = aio_new(io: THFile); |
| 4289 | |
| 4290 | char aVersion[128]; |
| 4291 | if(GIT_SHORTREV_HASH) |
| 4292 | { |
| 4293 | str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%s (%s)" , GAME_VERSION, GIT_SHORTREV_HASH); |
| 4294 | } |
| 4295 | else |
| 4296 | { |
| 4297 | str_copy(dst&: aVersion, GAME_VERSION); |
| 4298 | } |
| 4299 | CTeeHistorian::CGameInfo GameInfo; |
| 4300 | GameInfo.m_GameUuid = m_GameUuid; |
| 4301 | GameInfo.m_pServerVersion = aVersion; |
| 4302 | GameInfo.m_StartTime = time(timer: nullptr); |
| 4303 | GameInfo.m_pPrngDescription = m_Prng.Description(); |
| 4304 | |
| 4305 | GameInfo.m_pServerName = g_Config.m_SvName; |
| 4306 | GameInfo.m_ServerPort = Server()->Port(); |
| 4307 | GameInfo.m_pGameType = m_pController->m_pGameType; |
| 4308 | |
| 4309 | GameInfo.m_pConfig = &g_Config; |
| 4310 | GameInfo.m_pTuning = GlobalTuning(); |
| 4311 | GameInfo.m_pUuids = &g_UuidManager; |
| 4312 | |
| 4313 | GameInfo.m_pMapName = Map()->BaseName(); |
| 4314 | GameInfo.m_MapSize = Map()->Size(); |
| 4315 | GameInfo.m_MapSha256 = Map()->Sha256(); |
| 4316 | GameInfo.m_MapCrc = Map()->Crc(); |
| 4317 | |
| 4318 | if(pPersistent) |
| 4319 | { |
| 4320 | GameInfo.m_HavePrevGameUuid = true; |
| 4321 | GameInfo.m_PrevGameUuid = pPersistent->m_PrevGameUuid; |
| 4322 | } |
| 4323 | else |
| 4324 | { |
| 4325 | GameInfo.m_HavePrevGameUuid = false; |
| 4326 | mem_zero(block: &GameInfo.m_PrevGameUuid, size: sizeof(GameInfo.m_PrevGameUuid)); |
| 4327 | } |
| 4328 | |
| 4329 | m_TeeHistorian.Reset(pGameInfo: &GameInfo, pfnWriteCallback: TeeHistorianWrite, pUser: this); |
| 4330 | } |
| 4331 | |
| 4332 | Server()->DemoRecorder_HandleAutoStart(); |
| 4333 | |
| 4334 | if(!m_pScore) |
| 4335 | { |
| 4336 | m_pScore = new CScore(this, ((CServer *)Server())->DbPool()); |
| 4337 | } |
| 4338 | |
| 4339 | // load map info from database |
| 4340 | Score()->LoadMapInfo(); |
| 4341 | |
| 4342 | // create all entities from the game layer |
| 4343 | CreateAllEntities(Initial: true); |
| 4344 | |
| 4345 | m_pAntibot->RoundStart(pGameServer: this); |
| 4346 | } |
| 4347 | |
| 4348 | void CGameContext::CreateAllEntities(bool Initial) |
| 4349 | { |
| 4350 | const CTile *pTiles = m_Collision.GameLayer(); |
| 4351 | const CTile *pFront = m_Collision.FrontLayer(); |
| 4352 | const CSwitchTile *pSwitch = m_Collision.SwitchLayer(); |
| 4353 | |
| 4354 | for(int y = 0; y < m_Collision.GetHeight(); y++) |
| 4355 | { |
| 4356 | for(int x = 0; x < m_Collision.GetWidth(); x++) |
| 4357 | { |
| 4358 | const int Index = y * m_Collision.GetWidth() + x; |
| 4359 | |
| 4360 | // Game layer |
| 4361 | { |
| 4362 | const int GameIndex = pTiles[Index].m_Index; |
| 4363 | if(GameIndex == TILE_OLDLASER) |
| 4364 | { |
| 4365 | g_Config.m_SvOldLaser = 1; |
| 4366 | dbg_msg(sys: "game_layer" , fmt: "found old laser tile" ); |
| 4367 | } |
| 4368 | else if(GameIndex == TILE_NPC) |
| 4369 | { |
| 4370 | GlobalTuning()->Set(pName: "player_collision" , Value: 0); |
| 4371 | dbg_msg(sys: "game_layer" , fmt: "found no collision tile" ); |
| 4372 | } |
| 4373 | else if(GameIndex == TILE_EHOOK) |
| 4374 | { |
| 4375 | g_Config.m_SvEndlessDrag = 1; |
| 4376 | dbg_msg(sys: "game_layer" , fmt: "found unlimited hook time tile" ); |
| 4377 | } |
| 4378 | else if(GameIndex == TILE_NOHIT) |
| 4379 | { |
| 4380 | g_Config.m_SvHit = 0; |
| 4381 | dbg_msg(sys: "game_layer" , fmt: "found no weapons hitting others tile" ); |
| 4382 | } |
| 4383 | else if(GameIndex == TILE_NPH) |
| 4384 | { |
| 4385 | GlobalTuning()->Set(pName: "player_hooking" , Value: 0); |
| 4386 | dbg_msg(sys: "game_layer" , fmt: "found no player hooking tile" ); |
| 4387 | } |
| 4388 | else if(GameIndex >= ENTITY_OFFSET) |
| 4389 | { |
| 4390 | m_pController->OnEntity(Index: GameIndex - ENTITY_OFFSET, x, y, Layer: LAYER_GAME, Flags: pTiles[Index].m_Flags, Initial); |
| 4391 | } |
| 4392 | } |
| 4393 | |
| 4394 | if(pFront) |
| 4395 | { |
| 4396 | const int FrontIndex = pFront[Index].m_Index; |
| 4397 | if(FrontIndex == TILE_OLDLASER) |
| 4398 | { |
| 4399 | g_Config.m_SvOldLaser = 1; |
| 4400 | dbg_msg(sys: "front_layer" , fmt: "found old laser tile" ); |
| 4401 | } |
| 4402 | else if(FrontIndex == TILE_NPC) |
| 4403 | { |
| 4404 | GlobalTuning()->Set(pName: "player_collision" , Value: 0); |
| 4405 | dbg_msg(sys: "front_layer" , fmt: "found no collision tile" ); |
| 4406 | } |
| 4407 | else if(FrontIndex == TILE_EHOOK) |
| 4408 | { |
| 4409 | g_Config.m_SvEndlessDrag = 1; |
| 4410 | dbg_msg(sys: "front_layer" , fmt: "found unlimited hook time tile" ); |
| 4411 | } |
| 4412 | else if(FrontIndex == TILE_NOHIT) |
| 4413 | { |
| 4414 | g_Config.m_SvHit = 0; |
| 4415 | dbg_msg(sys: "front_layer" , fmt: "found no weapons hitting others tile" ); |
| 4416 | } |
| 4417 | else if(FrontIndex == TILE_NPH) |
| 4418 | { |
| 4419 | GlobalTuning()->Set(pName: "player_hooking" , Value: 0); |
| 4420 | dbg_msg(sys: "front_layer" , fmt: "found no player hooking tile" ); |
| 4421 | } |
| 4422 | else if(FrontIndex >= ENTITY_OFFSET) |
| 4423 | { |
| 4424 | m_pController->OnEntity(Index: FrontIndex - ENTITY_OFFSET, x, y, Layer: LAYER_FRONT, Flags: pFront[Index].m_Flags, Initial); |
| 4425 | } |
| 4426 | } |
| 4427 | |
| 4428 | if(pSwitch) |
| 4429 | { |
| 4430 | const int SwitchType = pSwitch[Index].m_Type; |
| 4431 | // TODO: Add off by default door here |
| 4432 | // if(SwitchType == TILE_DOOR_OFF) |
| 4433 | if(SwitchType >= ENTITY_OFFSET) |
| 4434 | { |
| 4435 | m_pController->OnEntity(Index: SwitchType - ENTITY_OFFSET, x, y, Layer: LAYER_SWITCH, Flags: pSwitch[Index].m_Flags, Initial, Number: pSwitch[Index].m_Number); |
| 4436 | } |
| 4437 | } |
| 4438 | } |
| 4439 | } |
| 4440 | } |
| 4441 | |
| 4442 | CPlayer *CGameContext::CreatePlayer(int ClientId, int StartTeam, bool Afk, int LastWhisperTo) |
| 4443 | { |
| 4444 | if(m_apPlayers[ClientId]) |
| 4445 | delete m_apPlayers[ClientId]; |
| 4446 | m_apPlayers[ClientId] = new(ClientId) CPlayer(this, m_NextUniqueClientId, ClientId, StartTeam); |
| 4447 | m_apPlayers[ClientId]->SetInitialAfk(Afk); |
| 4448 | m_apPlayers[ClientId]->m_LastWhisperTo = LastWhisperTo; |
| 4449 | m_NextUniqueClientId += 1; |
| 4450 | return m_apPlayers[ClientId]; |
| 4451 | } |
| 4452 | |
| 4453 | void CGameContext::DeleteTempfile() |
| 4454 | { |
| 4455 | if(m_aDeleteTempfile[0] != 0) |
| 4456 | { |
| 4457 | Storage()->RemoveFile(pFilename: m_aDeleteTempfile, Type: IStorage::TYPE_SAVE); |
| 4458 | m_aDeleteTempfile[0] = 0; |
| 4459 | } |
| 4460 | } |
| 4461 | |
| 4462 | bool CGameContext::OnMapChange(char *pNewMapName, int MapNameSize) |
| 4463 | { |
| 4464 | char aConfig[IO_MAX_PATH_LENGTH]; |
| 4465 | str_format(buffer: aConfig, buffer_size: sizeof(aConfig), format: "maps/%s.cfg" , g_Config.m_SvMap); |
| 4466 | |
| 4467 | CLineReader LineReader; |
| 4468 | if(!LineReader.OpenFile(File: Storage()->OpenFile(pFilename: aConfig, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL))) |
| 4469 | { |
| 4470 | // No map-specific config, just return. |
| 4471 | return true; |
| 4472 | } |
| 4473 | |
| 4474 | CDataFileReader Reader; |
| 4475 | if(!Reader.Open(pFullName: g_Config.m_SvMap, pStorage: Storage(), pPath: pNewMapName, StorageType: IStorage::TYPE_ALL)) |
| 4476 | { |
| 4477 | log_error("mapchange" , "Failed to import settings from '%s': failed to open map '%s' for reading" , aConfig, pNewMapName); |
| 4478 | return false; |
| 4479 | } |
| 4480 | |
| 4481 | std::vector<const char *> vpLines; |
| 4482 | int TotalLength = 0; |
| 4483 | while(const char *pLine = LineReader.Get()) |
| 4484 | { |
| 4485 | vpLines.push_back(x: pLine); |
| 4486 | TotalLength += str_length(str: pLine) + 1; |
| 4487 | } |
| 4488 | |
| 4489 | char *pSettings = (char *)malloc(size: maximum(a: 1, b: TotalLength)); |
| 4490 | int Offset = 0; |
| 4491 | for(const char *pLine : vpLines) |
| 4492 | { |
| 4493 | int Length = str_length(str: pLine) + 1; |
| 4494 | mem_copy(dest: pSettings + Offset, source: pLine, size: Length); |
| 4495 | Offset += Length; |
| 4496 | } |
| 4497 | |
| 4498 | CDataFileWriter Writer; |
| 4499 | |
| 4500 | int SettingsIndex = Reader.NumData(); |
| 4501 | bool FoundInfo = false; |
| 4502 | for(int i = 0; i < Reader.NumItems(); i++) |
| 4503 | { |
| 4504 | int TypeId; |
| 4505 | int ItemId; |
| 4506 | void *pData = Reader.GetItem(Index: i, pType: &TypeId, pId: &ItemId); |
| 4507 | int Size = Reader.GetItemSize(Index: i); |
| 4508 | CMapItemInfoSettings MapInfo; |
| 4509 | if(TypeId == MAPITEMTYPE_INFO && ItemId == 0) |
| 4510 | { |
| 4511 | FoundInfo = true; |
| 4512 | if(Size >= (int)sizeof(CMapItemInfoSettings)) |
| 4513 | { |
| 4514 | CMapItemInfoSettings *pInfo = (CMapItemInfoSettings *)pData; |
| 4515 | if(pInfo->m_Settings > -1) |
| 4516 | { |
| 4517 | SettingsIndex = pInfo->m_Settings; |
| 4518 | char *pMapSettings = (char *)Reader.GetData(Index: SettingsIndex); |
| 4519 | int DataSize = Reader.GetDataSize(Index: SettingsIndex); |
| 4520 | if(DataSize == TotalLength && mem_comp(a: pSettings, b: pMapSettings, size: DataSize) == 0) |
| 4521 | { |
| 4522 | // Configs coincide, no need to update map. |
| 4523 | free(ptr: pSettings); |
| 4524 | return true; |
| 4525 | } |
| 4526 | Reader.UnloadData(Index: pInfo->m_Settings); |
| 4527 | } |
| 4528 | else |
| 4529 | { |
| 4530 | MapInfo = *pInfo; |
| 4531 | MapInfo.m_Settings = SettingsIndex; |
| 4532 | pData = &MapInfo; |
| 4533 | Size = sizeof(MapInfo); |
| 4534 | } |
| 4535 | } |
| 4536 | else |
| 4537 | { |
| 4538 | *(CMapItemInfo *)&MapInfo = *(CMapItemInfo *)pData; |
| 4539 | MapInfo.m_Settings = SettingsIndex; |
| 4540 | pData = &MapInfo; |
| 4541 | Size = sizeof(MapInfo); |
| 4542 | } |
| 4543 | } |
| 4544 | Writer.AddItem(Type: TypeId, Id: ItemId, Size, pData); |
| 4545 | } |
| 4546 | |
| 4547 | if(!FoundInfo) |
| 4548 | { |
| 4549 | CMapItemInfoSettings Info; |
| 4550 | Info.m_Version = 1; |
| 4551 | Info.m_Author = -1; |
| 4552 | Info.m_MapVersion = -1; |
| 4553 | Info.m_Credits = -1; |
| 4554 | Info.m_License = -1; |
| 4555 | Info.m_Settings = SettingsIndex; |
| 4556 | Writer.AddItem(Type: MAPITEMTYPE_INFO, Id: 0, Size: sizeof(Info), pData: &Info); |
| 4557 | } |
| 4558 | |
| 4559 | for(int i = 0; i < Reader.NumData() || i == SettingsIndex; i++) |
| 4560 | { |
| 4561 | if(i == SettingsIndex) |
| 4562 | { |
| 4563 | Writer.AddData(Size: TotalLength, pData: pSettings); |
| 4564 | continue; |
| 4565 | } |
| 4566 | const void *pData = Reader.GetData(Index: i); |
| 4567 | int Size = Reader.GetDataSize(Index: i); |
| 4568 | Writer.AddData(Size, pData); |
| 4569 | Reader.UnloadData(Index: i); |
| 4570 | } |
| 4571 | |
| 4572 | free(ptr: pSettings); |
| 4573 | Reader.Close(); |
| 4574 | |
| 4575 | char aTemp[IO_MAX_PATH_LENGTH]; |
| 4576 | if(!Writer.Open(pStorage: Storage(), pFilename: IStorage::FormatTmpPath(aBuf: aTemp, BufSize: sizeof(aTemp), pPath: pNewMapName))) |
| 4577 | { |
| 4578 | log_error("mapchange" , "Failed to import settings from '%s': failed to open map '%s' for writing" , aConfig, aTemp); |
| 4579 | return false; |
| 4580 | } |
| 4581 | Writer.Finish(); |
| 4582 | log_info("mapchange" , "Imported settings from '%s' into '%s'" , aConfig, aTemp); |
| 4583 | |
| 4584 | str_copy(dst: pNewMapName, src: aTemp, dst_size: MapNameSize); |
| 4585 | str_copy(dst: m_aDeleteTempfile, src: aTemp, dst_size: sizeof(m_aDeleteTempfile)); |
| 4586 | return true; |
| 4587 | } |
| 4588 | |
| 4589 | void CGameContext::OnShutdown(void *pPersistentData) |
| 4590 | { |
| 4591 | CPersistentData *pPersistent = (CPersistentData *)pPersistentData; |
| 4592 | |
| 4593 | if(pPersistent) |
| 4594 | { |
| 4595 | pPersistent->m_PrevGameUuid = m_GameUuid; |
| 4596 | } |
| 4597 | |
| 4598 | Antibot()->RoundEnd(); |
| 4599 | |
| 4600 | if(m_TeeHistorianActive) |
| 4601 | { |
| 4602 | m_TeeHistorian.Finish(); |
| 4603 | aio_close(aio: m_pTeeHistorianFile); |
| 4604 | aio_wait(aio: m_pTeeHistorianFile); |
| 4605 | int Error = aio_error(aio: m_pTeeHistorianFile); |
| 4606 | if(Error) |
| 4607 | { |
| 4608 | dbg_msg(sys: "teehistorian" , fmt: "error closing file, err=%d" , Error); |
| 4609 | Server()->SetErrorShutdown("teehistorian close error" ); |
| 4610 | } |
| 4611 | aio_free(aio: m_pTeeHistorianFile); |
| 4612 | } |
| 4613 | |
| 4614 | // Stop any demos being recorded. |
| 4615 | Server()->StopDemos(); |
| 4616 | |
| 4617 | DeleteTempfile(); |
| 4618 | ConfigManager()->ResetGameSettings(); |
| 4619 | Collision()->Unload(); |
| 4620 | Layers()->Unload(); |
| 4621 | delete m_pController; |
| 4622 | m_pController = nullptr; |
| 4623 | Clear(); |
| 4624 | } |
| 4625 | |
| 4626 | void CGameContext::LoadMapSettings() |
| 4627 | { |
| 4628 | IMap *pMap = Map(); |
| 4629 | int Start, Num; |
| 4630 | pMap->GetType(Type: MAPITEMTYPE_INFO, pStart: &Start, pNum: &Num); |
| 4631 | for(int i = Start; i < Start + Num; i++) |
| 4632 | { |
| 4633 | int ItemId; |
| 4634 | CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(Index: i, pType: nullptr, pId: &ItemId); |
| 4635 | int ItemSize = pMap->GetItemSize(Index: i); |
| 4636 | if(!pItem || ItemId != 0) |
| 4637 | continue; |
| 4638 | |
| 4639 | if(ItemSize < (int)sizeof(CMapItemInfoSettings)) |
| 4640 | break; |
| 4641 | if(!(pItem->m_Settings > -1)) |
| 4642 | break; |
| 4643 | |
| 4644 | int Size = pMap->GetDataSize(Index: pItem->m_Settings); |
| 4645 | char *pSettings = (char *)pMap->GetData(Index: pItem->m_Settings); |
| 4646 | char *pNext = pSettings; |
| 4647 | while(pNext < pSettings + Size) |
| 4648 | { |
| 4649 | int StrSize = str_length(str: pNext) + 1; |
| 4650 | Console()->ExecuteLine(pStr: pNext, ClientId: IConsole::CLIENT_ID_GAME); |
| 4651 | pNext += StrSize; |
| 4652 | } |
| 4653 | pMap->UnloadData(Index: pItem->m_Settings); |
| 4654 | break; |
| 4655 | } |
| 4656 | |
| 4657 | char aBuf[IO_MAX_PATH_LENGTH]; |
| 4658 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "maps/%s.map.cfg" , g_Config.m_SvMap); |
| 4659 | Console()->ExecuteFile(pFilename: aBuf, ClientId: IConsole::CLIENT_ID_NO_GAME); |
| 4660 | } |
| 4661 | |
| 4662 | void CGameContext::OnSnap(int ClientId, bool GlobalSnap, bool RecordingDemo) |
| 4663 | { |
| 4664 | // sixup should only snap during global snap |
| 4665 | dbg_assert(!Server()->IsSixup(ClientId) || GlobalSnap, "sixup should only snap during global snap" ); |
| 4666 | |
| 4667 | // add tuning to demo |
| 4668 | if(RecordingDemo && mem_comp(a: &CTuningParams::DEFAULT, b: &m_aTuningList[0], size: sizeof(CTuningParams)) != 0) |
| 4669 | { |
| 4670 | CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS); |
| 4671 | int *pParams = (int *)&m_aTuningList[0]; |
| 4672 | for(int i = 0; i < CTuningParams::Num(); i++) |
| 4673 | Msg.AddInt(i: pParams[i]); |
| 4674 | Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId); |
| 4675 | } |
| 4676 | |
| 4677 | m_pController->Snap(SnappingClient: ClientId); |
| 4678 | |
| 4679 | for(auto &pPlayer : m_apPlayers) |
| 4680 | { |
| 4681 | if(pPlayer) |
| 4682 | pPlayer->Snap(SnappingClient: ClientId); |
| 4683 | } |
| 4684 | |
| 4685 | if(ClientId > -1) |
| 4686 | m_apPlayers[ClientId]->FakeSnap(); |
| 4687 | |
| 4688 | m_World.Snap(SnappingClient: ClientId); |
| 4689 | |
| 4690 | // events are only sent on global snapshots |
| 4691 | if(GlobalSnap) |
| 4692 | { |
| 4693 | m_Events.Snap(SnappingClient: ClientId); |
| 4694 | } |
| 4695 | } |
| 4696 | |
| 4697 | void CGameContext::OnPostGlobalSnap() |
| 4698 | { |
| 4699 | for(auto &pPlayer : m_apPlayers) |
| 4700 | { |
| 4701 | if(pPlayer && pPlayer->GetCharacter()) |
| 4702 | pPlayer->GetCharacter()->PostGlobalSnap(); |
| 4703 | } |
| 4704 | m_Events.Clear(); |
| 4705 | } |
| 4706 | |
| 4707 | void CGameContext::UpdatePlayerMaps() |
| 4708 | { |
| 4709 | const auto DistCompare = [](std::pair<float, int> a, std::pair<float, int> b) -> bool { |
| 4710 | return (a.first < b.first); |
| 4711 | }; |
| 4712 | |
| 4713 | if(Server()->Tick() % g_Config.m_SvMapUpdateRate != 0) |
| 4714 | return; |
| 4715 | |
| 4716 | std::pair<float, int> Dist[MAX_CLIENTS]; |
| 4717 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 4718 | { |
| 4719 | if(!Server()->ClientIngame(ClientId: i)) |
| 4720 | continue; |
| 4721 | if(Server()->GetClientVersion(ClientId: i) >= VERSION_DDNET_OLD) |
| 4722 | continue; |
| 4723 | int *pMap = Server()->GetIdMap(ClientId: i); |
| 4724 | |
| 4725 | // compute distances |
| 4726 | for(int j = 0; j < MAX_CLIENTS; j++) |
| 4727 | { |
| 4728 | Dist[j].second = j; |
| 4729 | if(j == i) |
| 4730 | continue; |
| 4731 | if(!Server()->ClientIngame(ClientId: j) || !m_apPlayers[j]) |
| 4732 | { |
| 4733 | Dist[j].first = 1e10; |
| 4734 | continue; |
| 4735 | } |
| 4736 | CCharacter *pChr = m_apPlayers[j]->GetCharacter(); |
| 4737 | if(!pChr) |
| 4738 | { |
| 4739 | Dist[j].first = 1e9; |
| 4740 | continue; |
| 4741 | } |
| 4742 | if(!pChr->CanSnapCharacter(SnappingClient: i)) |
| 4743 | Dist[j].first = 1e8; |
| 4744 | else |
| 4745 | Dist[j].first = length_squared(a: m_apPlayers[i]->m_ViewPos - pChr->GetPos()); |
| 4746 | } |
| 4747 | |
| 4748 | // always send the player themselves, even if all in same position |
| 4749 | Dist[i].first = -1; |
| 4750 | |
| 4751 | std::nth_element(first: &Dist[0], nth: &Dist[VANILLA_MAX_CLIENTS - 1], last: &Dist[MAX_CLIENTS], comp: DistCompare); |
| 4752 | |
| 4753 | int Index = 1; // exclude self client id |
| 4754 | for(int j = 0; j < VANILLA_MAX_CLIENTS - 1; j++) |
| 4755 | { |
| 4756 | pMap[j + 1] = -1; // also fill player with empty name to say chat msgs |
| 4757 | if(Dist[j].second == i || Dist[j].first > 5e9f) |
| 4758 | continue; |
| 4759 | pMap[Index++] = Dist[j].second; |
| 4760 | } |
| 4761 | |
| 4762 | // sort by real client ids, guarantee order on distance changes, O(Nlog(N)) worst case |
| 4763 | // sort just clients in game always except first (self client id) and last (fake client id) indexes |
| 4764 | std::sort(first: &pMap[1], last: &pMap[minimum(a: Index, b: VANILLA_MAX_CLIENTS - 1)]); |
| 4765 | } |
| 4766 | } |
| 4767 | |
| 4768 | bool CGameContext::IsClientReady(int ClientId) const |
| 4769 | { |
| 4770 | return m_apPlayers[ClientId] && m_apPlayers[ClientId]->m_IsReady; |
| 4771 | } |
| 4772 | |
| 4773 | bool CGameContext::IsClientPlayer(int ClientId) const |
| 4774 | { |
| 4775 | return m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetTeam() != TEAM_SPECTATORS; |
| 4776 | } |
| 4777 | |
| 4778 | bool CGameContext::IsClientHighBandwidth(int ClientId) const |
| 4779 | { |
| 4780 | // force high bandwidth is not supported for sixup |
| 4781 | return m_apPlayers[ClientId] && !Server()->IsSixup(ClientId) && Server()->IsRconAuthed(ClientId) && |
| 4782 | (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsPaused()); |
| 4783 | } |
| 4784 | |
| 4785 | CUuid CGameContext::GameUuid() const { return m_GameUuid; } |
| 4786 | const char *CGameContext::GameType() const |
| 4787 | { |
| 4788 | dbg_assert(m_pController, "no controller" ); |
| 4789 | dbg_assert(m_pController->m_pGameType, "no gametype" ); |
| 4790 | return m_pController->m_pGameType; |
| 4791 | } |
| 4792 | const char *CGameContext::Version() const { return GAME_VERSION; } |
| 4793 | const char *CGameContext::NetVersion() const { return GAME_NETVERSION; } |
| 4794 | |
| 4795 | IGameServer *CreateGameServer() { return new CGameContext; } |
| 4796 | |
| 4797 | void CGameContext::OnSetAuthed(int ClientId, int Level) |
| 4798 | { |
| 4799 | if(m_apPlayers[ClientId] && m_VoteCloseTime && Level != AUTHED_NO) |
| 4800 | { |
| 4801 | char aBuf[512]; |
| 4802 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ban %s %d Banned by vote" , Server()->ClientAddrString(ClientId, IncludePort: false), g_Config.m_SvVoteKickBantime); |
| 4803 | if(!str_comp_nocase(a: m_aVoteCommand, b: aBuf) && (m_VoteCreator == -1 || Level > Server()->GetAuthedState(ClientId: m_VoteCreator))) |
| 4804 | { |
| 4805 | m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN; |
| 4806 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "game" , pStr: "Vote aborted by authorized login." ); |
| 4807 | } |
| 4808 | } |
| 4809 | |
| 4810 | if(m_TeeHistorianActive) |
| 4811 | { |
| 4812 | if(Level != AUTHED_NO) |
| 4813 | { |
| 4814 | m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName: Server()->GetAuthName(ClientId)); |
| 4815 | } |
| 4816 | else |
| 4817 | { |
| 4818 | m_TeeHistorian.RecordAuthLogout(ClientId); |
| 4819 | } |
| 4820 | } |
| 4821 | } |
| 4822 | |
| 4823 | bool CGameContext::IsRunningVote(int ClientId) const |
| 4824 | { |
| 4825 | return m_VoteCloseTime && m_VoteCreator == ClientId; |
| 4826 | } |
| 4827 | |
| 4828 | bool CGameContext::IsRunningKickOrSpecVote(int ClientId) const |
| 4829 | { |
| 4830 | return IsRunningVote(ClientId) && (IsKickVote() || IsSpecVote()); |
| 4831 | } |
| 4832 | |
| 4833 | void CGameContext::SendRecord(int ClientId) |
| 4834 | { |
| 4835 | if(Server()->IsSixup(ClientId) || GetClientVersion(ClientId) >= VERSION_DDNET_MAP_BESTTIME) |
| 4836 | return; |
| 4837 | |
| 4838 | CNetMsg_Sv_Record Msg; |
| 4839 | CNetMsg_Sv_RecordLegacy MsgLegacy; |
| 4840 | MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = round_to_int(f: Score()->PlayerData(Id: ClientId)->m_BestTime.value_or(u: 0.0f) * 100.0f); |
| 4841 | MsgLegacy.m_ServerTimeBest = Msg.m_ServerTimeBest = m_pController->m_CurrentRecord.has_value() && !g_Config.m_SvHideScore ? round_to_int(f: m_pController->m_CurrentRecord.value() * 100.0f) : 0; |
| 4842 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 4843 | if(GetClientVersion(ClientId) < VERSION_DDNET_MSG_LEGACY) |
| 4844 | { |
| 4845 | Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId); |
| 4846 | } |
| 4847 | } |
| 4848 | |
| 4849 | void CGameContext::SendFinish(int ClientId, float Time, std::optional<float> PreviousBestTime) |
| 4850 | { |
| 4851 | int ClientVersion = m_apPlayers[ClientId]->GetClientVersion(); |
| 4852 | |
| 4853 | if(!Server()->IsSixup(ClientId)) |
| 4854 | { |
| 4855 | CNetMsg_Sv_DDRaceTime Msg; |
| 4856 | CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy; |
| 4857 | MsgLegacy.m_Time = Msg.m_Time = (int)(Time * 100.0f); |
| 4858 | MsgLegacy.m_Check = Msg.m_Check = 0; |
| 4859 | MsgLegacy.m_Finish = Msg.m_Finish = 1; |
| 4860 | |
| 4861 | if(PreviousBestTime.has_value()) |
| 4862 | { |
| 4863 | float Diff100 = (Time - PreviousBestTime.value()) * 100; |
| 4864 | MsgLegacy.m_Check = Msg.m_Check = (int)Diff100; |
| 4865 | } |
| 4866 | if(VERSION_DDRACE <= ClientVersion) |
| 4867 | { |
| 4868 | if(ClientVersion < VERSION_DDNET_MSG_LEGACY) |
| 4869 | { |
| 4870 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 4871 | } |
| 4872 | else |
| 4873 | { |
| 4874 | Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId); |
| 4875 | } |
| 4876 | } |
| 4877 | } |
| 4878 | |
| 4879 | CNetMsg_Sv_RaceFinish RaceFinishMsg; |
| 4880 | RaceFinishMsg.m_ClientId = ClientId; |
| 4881 | RaceFinishMsg.m_Time = Time * 1000; |
| 4882 | RaceFinishMsg.m_Diff = 0; |
| 4883 | if(PreviousBestTime.has_value()) |
| 4884 | { |
| 4885 | float Diff = absolute(a: Time - PreviousBestTime.value()); |
| 4886 | RaceFinishMsg.m_Diff = Diff * 1000 * (Time < PreviousBestTime.value() ? -1 : 1); |
| 4887 | } |
| 4888 | RaceFinishMsg.m_RecordPersonal = (!PreviousBestTime.has_value() || Time < PreviousBestTime.value()); |
| 4889 | RaceFinishMsg.m_RecordServer = Time < m_pController->m_CurrentRecord; |
| 4890 | Server()->SendPackMsg(pMsg: &RaceFinishMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: g_Config.m_SvHideScore ? ClientId : -1); |
| 4891 | } |
| 4892 | |
| 4893 | void CGameContext::SendSaveCode(int Team, int TeamSize, int State, const char *pError, const char *pSaveRequester, const char *pServerName, const char *pGeneratedCode, const char *pCode) |
| 4894 | { |
| 4895 | char aBuf[512]; |
| 4896 | |
| 4897 | CMsgPacker Msg(NETMSGTYPE_SV_SAVECODE); |
| 4898 | Msg.AddInt(i: State); |
| 4899 | Msg.AddString(pStr: pError); |
| 4900 | Msg.AddString(pStr: pSaveRequester); |
| 4901 | Msg.AddString(pStr: pServerName); |
| 4902 | Msg.AddString(pStr: pGeneratedCode); |
| 4903 | Msg.AddString(pStr: pCode); |
| 4904 | char aTeamMembers[1024]; |
| 4905 | aTeamMembers[0] = '\0'; |
| 4906 | int = 0; |
| 4907 | for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++) |
| 4908 | { |
| 4909 | if(!m_apPlayers[MemberId]) |
| 4910 | continue; |
| 4911 | if(GetDDRaceTeam(ClientId: MemberId) != Team) |
| 4912 | continue; |
| 4913 | if(NumMembersSent++ > 10) |
| 4914 | { |
| 4915 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " and %d others" , (TeamSize - NumMembersSent) + 1); |
| 4916 | str_append(dst&: aTeamMembers, src: aBuf); |
| 4917 | break; |
| 4918 | } |
| 4919 | |
| 4920 | if(NumMembersSent > 1) |
| 4921 | str_append(dst&: aTeamMembers, src: ", " ); |
| 4922 | str_append(dst&: aTeamMembers, src: Server()->ClientName(ClientId: MemberId)); |
| 4923 | } |
| 4924 | Msg.AddString(pStr: aTeamMembers); |
| 4925 | |
| 4926 | for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++) |
| 4927 | { |
| 4928 | if(!m_apPlayers[MemberId]) |
| 4929 | continue; |
| 4930 | if(GetDDRaceTeam(ClientId: MemberId) != Team) |
| 4931 | continue; |
| 4932 | |
| 4933 | if(GetClientVersion(ClientId: MemberId) >= VERSION_DDNET_SAVE_CODE) |
| 4934 | { |
| 4935 | Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: MemberId); |
| 4936 | } |
| 4937 | else |
| 4938 | { |
| 4939 | switch(State) |
| 4940 | { |
| 4941 | case SAVESTATE_PENDING: |
| 4942 | if(pCode[0] == '\0') |
| 4943 | { |
| 4944 | str_format(buffer: aBuf, |
| 4945 | buffer_size: sizeof(aBuf), |
| 4946 | format: "Team save in progress. You'll be able to load with '/load %s'" , |
| 4947 | pGeneratedCode); |
| 4948 | } |
| 4949 | else |
| 4950 | { |
| 4951 | str_format(buffer: aBuf, |
| 4952 | buffer_size: sizeof(aBuf), |
| 4953 | format: "Team save in progress. You'll be able to load with '/load %s' if save is successful or with '/load %s' if it fails" , |
| 4954 | pCode, |
| 4955 | pGeneratedCode); |
| 4956 | } |
| 4957 | break; |
| 4958 | case SAVESTATE_DONE: |
| 4959 | if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0) |
| 4960 | { |
| 4961 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), |
| 4962 | format: "Team successfully saved by %s. Use '/load %s' to continue" , |
| 4963 | pSaveRequester, pCode[0] ? pCode : pGeneratedCode); |
| 4964 | } |
| 4965 | else |
| 4966 | { |
| 4967 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), |
| 4968 | format: "Team successfully saved by %s. Use '/load %s' on %s to continue" , |
| 4969 | pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName); |
| 4970 | } |
| 4971 | break; |
| 4972 | case SAVESTATE_FALLBACKFILE: |
| 4973 | SendBroadcast(pText: "Database connection failed, teamsave written to a file instead. On official DDNet servers this will automatically be inserted into the database every full hour." , ClientId: MemberId); |
| 4974 | if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0) |
| 4975 | { |
| 4976 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), |
| 4977 | format: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue" , |
| 4978 | pSaveRequester, pCode[0] ? pCode : pGeneratedCode); |
| 4979 | } |
| 4980 | else |
| 4981 | { |
| 4982 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), |
| 4983 | format: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' on %s to continue" , |
| 4984 | pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName); |
| 4985 | } |
| 4986 | break; |
| 4987 | case SAVESTATE_ERROR: |
| 4988 | case SAVESTATE_WARNING: |
| 4989 | str_copy(dst&: aBuf, src: pError); |
| 4990 | break; |
| 4991 | default: |
| 4992 | dbg_assert_failed("Unexpected save state %d" , State); |
| 4993 | } |
| 4994 | SendChatTarget(To: MemberId, pText: aBuf); |
| 4995 | } |
| 4996 | } |
| 4997 | } |
| 4998 | |
| 4999 | bool CGameContext::ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay) |
| 5000 | { |
| 5001 | if(!m_apPlayers[ClientId]) |
| 5002 | return false; |
| 5003 | if(g_Config.m_SvSpamprotection && m_apPlayers[ClientId]->m_LastChat && m_apPlayers[ClientId]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick()) |
| 5004 | return true; |
| 5005 | else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientId)) |
| 5006 | { |
| 5007 | SendChatTarget(To: ClientId, pText: "Players are not allowed to chat from VPNs at this time" ); |
| 5008 | return true; |
| 5009 | } |
| 5010 | else |
| 5011 | m_apPlayers[ClientId]->m_LastChat = Server()->Tick(); |
| 5012 | |
| 5013 | const std::optional<CMute> Muted = m_Mutes.IsMuted(pAddr: Server()->ClientAddr(ClientId), RespectInitialDelay: RespectChatInitialDelay); |
| 5014 | if(Muted.has_value()) |
| 5015 | { |
| 5016 | char aChatMessage[128]; |
| 5017 | if(Muted->m_InitialDelay) |
| 5018 | { |
| 5019 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "This server has an initial chat delay, you will be able to talk in %d seconds." , Muted->SecondsLeft()); |
| 5020 | } |
| 5021 | else |
| 5022 | { |
| 5023 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to talk for the next %d seconds." , Muted->SecondsLeft()); |
| 5024 | } |
| 5025 | SendChatTarget(To: ClientId, pText: aChatMessage); |
| 5026 | return true; |
| 5027 | } |
| 5028 | |
| 5029 | if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientId]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold) |
| 5030 | { |
| 5031 | MuteWithMessage(pAddr: Server()->ClientAddr(ClientId), Seconds: g_Config.m_SvSpamMuteDuration, pReason: "Spam protection" , pDisplayName: Server()->ClientName(ClientId)); |
| 5032 | m_apPlayers[ClientId]->m_ChatScore = 0; |
| 5033 | return true; |
| 5034 | } |
| 5035 | |
| 5036 | return false; |
| 5037 | } |
| 5038 | |
| 5039 | int CGameContext::GetDDRaceTeam(int ClientId) const |
| 5040 | { |
| 5041 | return m_pController->Teams().m_Core.Team(ClientId); |
| 5042 | } |
| 5043 | |
| 5044 | void CGameContext::ResetTuning() |
| 5045 | { |
| 5046 | *GlobalTuning() = CTuningParams::DEFAULT; |
| 5047 | GlobalTuning()->Set(pName: "gun_speed" , Value: 1400); |
| 5048 | GlobalTuning()->Set(pName: "gun_curvature" , Value: 0); |
| 5049 | GlobalTuning()->Set(pName: "shotgun_speed" , Value: 500); |
| 5050 | GlobalTuning()->Set(pName: "shotgun_speeddiff" , Value: 0); |
| 5051 | GlobalTuning()->Set(pName: "shotgun_curvature" , Value: 0); |
| 5052 | SendTuningParams(ClientId: -1); |
| 5053 | } |
| 5054 | |
| 5055 | void CGameContext::Whisper(int ClientId, char *pStr) |
| 5056 | { |
| 5057 | if(ProcessSpamProtection(ClientId)) |
| 5058 | return; |
| 5059 | |
| 5060 | pStr = str_skip_whitespaces(str: pStr); |
| 5061 | |
| 5062 | const char *pName; |
| 5063 | int Victim; |
| 5064 | bool Error = false; |
| 5065 | |
| 5066 | // add token |
| 5067 | if(*pStr == '"') |
| 5068 | { |
| 5069 | pStr++; |
| 5070 | |
| 5071 | pName = pStr; |
| 5072 | char *pDst = pStr; // we might have to process escape data |
| 5073 | while(true) |
| 5074 | { |
| 5075 | if(pStr[0] == '"') |
| 5076 | { |
| 5077 | break; |
| 5078 | } |
| 5079 | else if(pStr[0] == '\\') |
| 5080 | { |
| 5081 | if(pStr[1] == '\\') |
| 5082 | pStr++; // skip due to escape |
| 5083 | else if(pStr[1] == '"') |
| 5084 | pStr++; // skip due to escape |
| 5085 | } |
| 5086 | else if(pStr[0] == 0) |
| 5087 | { |
| 5088 | Error = true; |
| 5089 | break; |
| 5090 | } |
| 5091 | |
| 5092 | *pDst = *pStr; |
| 5093 | pDst++; |
| 5094 | pStr++; |
| 5095 | } |
| 5096 | |
| 5097 | if(!Error) |
| 5098 | { |
| 5099 | *pDst = '\0'; |
| 5100 | pStr++; |
| 5101 | |
| 5102 | Victim = FindClientIdByName(pName).value_or(u: -1); |
| 5103 | } |
| 5104 | } |
| 5105 | else |
| 5106 | { |
| 5107 | pName = pStr; |
| 5108 | while(true) |
| 5109 | { |
| 5110 | if(pStr[0] == '\0') |
| 5111 | { |
| 5112 | Error = true; |
| 5113 | break; |
| 5114 | } |
| 5115 | if(pStr[0] == ' ') |
| 5116 | { |
| 5117 | pStr[0] = '\0'; |
| 5118 | |
| 5119 | Victim = FindClientIdByName(pName).value_or(u: -1); |
| 5120 | |
| 5121 | pStr[0] = ' '; |
| 5122 | if(Victim != -1) |
| 5123 | break; |
| 5124 | } |
| 5125 | pStr++; |
| 5126 | } |
| 5127 | } |
| 5128 | |
| 5129 | if(pStr[0] != ' ') |
| 5130 | { |
| 5131 | Error = true; |
| 5132 | } |
| 5133 | |
| 5134 | *pStr = '\0'; |
| 5135 | pStr++; |
| 5136 | |
| 5137 | if(Error) |
| 5138 | { |
| 5139 | SendChatTarget(To: ClientId, pText: "Invalid whisper" ); |
| 5140 | return; |
| 5141 | } |
| 5142 | |
| 5143 | if(!CheckClientId(ClientId: Victim)) |
| 5144 | { |
| 5145 | char aBuf[256]; |
| 5146 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No player with name \"%s\" found" , pName); |
| 5147 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5148 | return; |
| 5149 | } |
| 5150 | |
| 5151 | WhisperId(ClientId, VictimId: Victim, pMessage: pStr); |
| 5152 | } |
| 5153 | |
| 5154 | void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage) |
| 5155 | { |
| 5156 | dbg_assert(CheckClientId(ClientId) && m_apPlayers[ClientId] != nullptr, "ClientId invalid" ); |
| 5157 | dbg_assert(CheckClientId(VictimId) && m_apPlayers[VictimId] != nullptr, "VictimId invalid" ); |
| 5158 | |
| 5159 | m_apPlayers[ClientId]->m_LastWhisperTo = VictimId; |
| 5160 | |
| 5161 | char aCensoredMessage[256]; |
| 5162 | CensorMessage(pCensoredMessage: aCensoredMessage, pMessage, Size: sizeof(aCensoredMessage)); |
| 5163 | |
| 5164 | char aBuf[256]; |
| 5165 | |
| 5166 | if(Server()->IsSixup(ClientId)) |
| 5167 | { |
| 5168 | protocol7::CNetMsg_Sv_Chat Msg; |
| 5169 | Msg.m_ClientId = ClientId; |
| 5170 | Msg.m_Mode = protocol7::CHAT_WHISPER; |
| 5171 | Msg.m_pMessage = aCensoredMessage; |
| 5172 | Msg.m_TargetId = VictimId; |
| 5173 | |
| 5174 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 5175 | } |
| 5176 | else if(GetClientVersion(ClientId) >= VERSION_DDNET_WHISPER) |
| 5177 | { |
| 5178 | CNetMsg_Sv_Chat Msg; |
| 5179 | Msg.m_Team = TEAM_WHISPER_SEND; |
| 5180 | Msg.m_ClientId = VictimId; |
| 5181 | Msg.m_pMessage = aCensoredMessage; |
| 5182 | if(g_Config.m_SvDemoChat) |
| 5183 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId); |
| 5184 | else |
| 5185 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); |
| 5186 | } |
| 5187 | else |
| 5188 | { |
| 5189 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[→ %s] %s" , Server()->ClientName(ClientId: VictimId), aCensoredMessage); |
| 5190 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5191 | } |
| 5192 | |
| 5193 | if(!m_apPlayers[VictimId]->m_Whispers) |
| 5194 | { |
| 5195 | SendChatTarget(To: ClientId, pText: "This person has disabled receiving whispers" ); |
| 5196 | return; |
| 5197 | } |
| 5198 | |
| 5199 | if(Server()->IsSixup(ClientId: VictimId)) |
| 5200 | { |
| 5201 | protocol7::CNetMsg_Sv_Chat Msg; |
| 5202 | Msg.m_ClientId = ClientId; |
| 5203 | Msg.m_Mode = protocol7::CHAT_WHISPER; |
| 5204 | Msg.m_pMessage = aCensoredMessage; |
| 5205 | Msg.m_TargetId = VictimId; |
| 5206 | |
| 5207 | Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId); |
| 5208 | } |
| 5209 | else if(GetClientVersion(ClientId: VictimId) >= VERSION_DDNET_WHISPER) |
| 5210 | { |
| 5211 | CNetMsg_Sv_Chat Msg2; |
| 5212 | Msg2.m_Team = TEAM_WHISPER_RECV; |
| 5213 | Msg2.m_ClientId = ClientId; |
| 5214 | Msg2.m_pMessage = aCensoredMessage; |
| 5215 | if(g_Config.m_SvDemoChat) |
| 5216 | Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL, ClientId: VictimId); |
| 5217 | else |
| 5218 | Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId); |
| 5219 | } |
| 5220 | else |
| 5221 | { |
| 5222 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[← %s] %s" , Server()->ClientName(ClientId), aCensoredMessage); |
| 5223 | SendChatTarget(To: VictimId, pText: aBuf); |
| 5224 | } |
| 5225 | } |
| 5226 | |
| 5227 | void CGameContext::Converse(int ClientId, char *pStr) |
| 5228 | { |
| 5229 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 5230 | if(!pPlayer) |
| 5231 | return; |
| 5232 | |
| 5233 | if(ProcessSpamProtection(ClientId)) |
| 5234 | return; |
| 5235 | |
| 5236 | if(pPlayer->m_LastWhisperTo < 0) |
| 5237 | SendChatTarget(To: ClientId, pText: "You do not have an ongoing conversation. Whisper to someone to start one" ); |
| 5238 | else if(!m_apPlayers[pPlayer->m_LastWhisperTo]) |
| 5239 | SendChatTarget(To: ClientId, pText: "The player you were whispering to hasn't reconnected yet or left. Please wait or whisper to someone else" ); |
| 5240 | else |
| 5241 | WhisperId(ClientId, VictimId: pPlayer->m_LastWhisperTo, pMessage: pStr); |
| 5242 | } |
| 5243 | |
| 5244 | bool CGameContext::IsVersionBanned(int Version) |
| 5245 | { |
| 5246 | char aVersion[16]; |
| 5247 | str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%d" , Version); |
| 5248 | |
| 5249 | return str_in_list(list: g_Config.m_SvBannedVersions, delim: "," , needle: aVersion); |
| 5250 | } |
| 5251 | |
| 5252 | void CGameContext::List(int ClientId, const char *pFilter) |
| 5253 | { |
| 5254 | int Total = 0; |
| 5255 | char aBuf[256]; |
| 5256 | int Bufcnt = 0; |
| 5257 | if(pFilter[0]) |
| 5258 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Listing players with \"%s\" in name:" , pFilter); |
| 5259 | else |
| 5260 | str_copy(dst&: aBuf, src: "Listing all players:" ); |
| 5261 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5262 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 5263 | { |
| 5264 | if(m_apPlayers[i]) |
| 5265 | { |
| 5266 | Total++; |
| 5267 | const char *pName = Server()->ClientName(ClientId: i); |
| 5268 | if(str_utf8_find_nocase(haystack: pName, needle: pFilter) == nullptr) |
| 5269 | continue; |
| 5270 | if(Bufcnt + str_length(str: pName) + 4 > 256) |
| 5271 | { |
| 5272 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5273 | Bufcnt = 0; |
| 5274 | } |
| 5275 | if(Bufcnt != 0) |
| 5276 | { |
| 5277 | str_format(buffer: &aBuf[Bufcnt], buffer_size: sizeof(aBuf) - Bufcnt, format: ", %s" , pName); |
| 5278 | Bufcnt += 2 + str_length(str: pName); |
| 5279 | } |
| 5280 | else |
| 5281 | { |
| 5282 | str_copy(dst: &aBuf[Bufcnt], src: pName, dst_size: sizeof(aBuf) - Bufcnt); |
| 5283 | Bufcnt += str_length(str: pName); |
| 5284 | } |
| 5285 | } |
| 5286 | } |
| 5287 | if(Bufcnt != 0) |
| 5288 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5289 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d players online" , Total); |
| 5290 | SendChatTarget(To: ClientId, pText: aBuf); |
| 5291 | } |
| 5292 | |
| 5293 | int CGameContext::GetClientVersion(int ClientId) const |
| 5294 | { |
| 5295 | return Server()->GetClientVersion(ClientId); |
| 5296 | } |
| 5297 | |
| 5298 | CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const |
| 5299 | { |
| 5300 | CClientMask Mask; |
| 5301 | for(int i = 0; i < MAX_CLIENTS; ++i) |
| 5302 | { |
| 5303 | if(GetClientVersion(ClientId: i) >= Version) |
| 5304 | continue; |
| 5305 | Mask.set(pos: i); |
| 5306 | } |
| 5307 | return Mask; |
| 5308 | } |
| 5309 | |
| 5310 | bool CGameContext::PlayerModerating() const |
| 5311 | { |
| 5312 | return std::any_of(first: std::begin(arr: m_apPlayers), last: std::end(arr: m_apPlayers), pred: [](const CPlayer *pPlayer) { return pPlayer && pPlayer->m_Moderating; }); |
| 5313 | } |
| 5314 | |
| 5315 | void CGameContext::ForceVote(bool Success) |
| 5316 | { |
| 5317 | // check if there is a vote running |
| 5318 | if(!m_VoteCloseTime) |
| 5319 | return; |
| 5320 | |
| 5321 | m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN; |
| 5322 | const char *pOption = Success ? "yes" : "no" ; |
| 5323 | |
| 5324 | char aChatMessage[256]; |
| 5325 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "Authorized player forced vote '%s'" , pOption); |
| 5326 | SendChatTarget(To: -1, pText: aChatMessage); |
| 5327 | |
| 5328 | log_info("server" , "Forcing vote '%s'" , pOption); |
| 5329 | } |
| 5330 | |
| 5331 | bool CGameContext::RateLimitPlayerVote(int ClientId) |
| 5332 | { |
| 5333 | int64_t Now = Server()->Tick(); |
| 5334 | int64_t TickSpeed = Server()->TickSpeed(); |
| 5335 | CPlayer *pPlayer = m_apPlayers[ClientId]; |
| 5336 | |
| 5337 | if(g_Config.m_SvRconVote && !Server()->IsRconAuthed(ClientId)) |
| 5338 | { |
| 5339 | SendChatTarget(To: ClientId, pText: "You can only vote after logging in." ); |
| 5340 | return true; |
| 5341 | } |
| 5342 | |
| 5343 | if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1) |
| 5344 | { |
| 5345 | if(m_pServer->DnsblPending(ClientId)) |
| 5346 | { |
| 5347 | SendChatTarget(To: ClientId, pText: "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds." ); |
| 5348 | return true; |
| 5349 | } |
| 5350 | else if(m_pServer->DnsblBlack(ClientId)) |
| 5351 | { |
| 5352 | SendChatTarget(To: ClientId, pText: "You are not allowed to vote because you appear to be using a VPN. Try connecting without a VPN or contacting an admin if you think this is a mistake." ); |
| 5353 | return true; |
| 5354 | } |
| 5355 | } |
| 5356 | |
| 5357 | if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + TickSpeed * 3 > Now) |
| 5358 | return true; |
| 5359 | |
| 5360 | pPlayer->m_LastVoteTry = Now; |
| 5361 | if(m_VoteCloseTime) |
| 5362 | { |
| 5363 | SendChatTarget(To: ClientId, pText: "Wait for current vote to end before calling a new one." ); |
| 5364 | return true; |
| 5365 | } |
| 5366 | |
| 5367 | if(Now < pPlayer->m_FirstVoteTick) |
| 5368 | { |
| 5369 | char aChatMessage[64]; |
| 5370 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You must wait %d seconds before making your first vote." , (int)((pPlayer->m_FirstVoteTick - Now) / TickSpeed) + 1); |
| 5371 | SendChatTarget(To: ClientId, pText: aChatMessage); |
| 5372 | return true; |
| 5373 | } |
| 5374 | |
| 5375 | int TimeLeft = pPlayer->m_LastVoteCall + TickSpeed * g_Config.m_SvVoteDelay - Now; |
| 5376 | if(pPlayer->m_LastVoteCall && TimeLeft > 0) |
| 5377 | { |
| 5378 | char aChatMessage[64]; |
| 5379 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You must wait %d seconds before making another vote." , (int)(TimeLeft / TickSpeed) + 1); |
| 5380 | SendChatTarget(To: ClientId, pText: aChatMessage); |
| 5381 | return true; |
| 5382 | } |
| 5383 | |
| 5384 | const NETADDR *pAddr = Server()->ClientAddr(ClientId); |
| 5385 | std::optional<CMute> Muted = m_VoteMutes.IsMuted(pAddr, RespectInitialDelay: true); |
| 5386 | if(!Muted.has_value()) |
| 5387 | { |
| 5388 | Muted = m_Mutes.IsMuted(pAddr, RespectInitialDelay: true); |
| 5389 | } |
| 5390 | if(Muted.has_value()) |
| 5391 | { |
| 5392 | char aChatMessage[64]; |
| 5393 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to vote for the next %d seconds." , Muted->SecondsLeft()); |
| 5394 | SendChatTarget(To: ClientId, pText: aChatMessage); |
| 5395 | return true; |
| 5396 | } |
| 5397 | return false; |
| 5398 | } |
| 5399 | |
| 5400 | bool CGameContext::RateLimitPlayerMapVote(int ClientId) const |
| 5401 | { |
| 5402 | if(!Server()->IsRconAuthed(ClientId) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay)) |
| 5403 | { |
| 5404 | char aChatMessage[128]; |
| 5405 | str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "There's a %d second delay between map-votes, please wait %d seconds." , |
| 5406 | g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq())); |
| 5407 | SendChatTarget(To: ClientId, pText: aChatMessage); |
| 5408 | return true; |
| 5409 | } |
| 5410 | return false; |
| 5411 | } |
| 5412 | |
| 5413 | void CGameContext::OnUpdatePlayerServerInfo(CJsonWriter *pJsonWriter, int ClientId) |
| 5414 | { |
| 5415 | if(!m_apPlayers[ClientId]) |
| 5416 | return; |
| 5417 | |
| 5418 | CTeeInfo &TeeInfo = m_apPlayers[ClientId]->m_TeeInfos; |
| 5419 | |
| 5420 | pJsonWriter->WriteAttribute(pName: "skin" ); |
| 5421 | pJsonWriter->BeginObject(); |
| 5422 | |
| 5423 | // 0.6 |
| 5424 | if(!Server()->IsSixup(ClientId)) |
| 5425 | { |
| 5426 | pJsonWriter->WriteAttribute(pName: "name" ); |
| 5427 | pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aSkinName); |
| 5428 | |
| 5429 | if(TeeInfo.m_UseCustomColor) |
| 5430 | { |
| 5431 | pJsonWriter->WriteAttribute(pName: "color_body" ); |
| 5432 | pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorBody); |
| 5433 | |
| 5434 | pJsonWriter->WriteAttribute(pName: "color_feet" ); |
| 5435 | pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorFeet); |
| 5436 | } |
| 5437 | } |
| 5438 | // 0.7 |
| 5439 | else |
| 5440 | { |
| 5441 | const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body" , "marking" , "decoration" , "hands" , "feet" , "eyes" }; |
| 5442 | |
| 5443 | for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i) |
| 5444 | { |
| 5445 | pJsonWriter->WriteAttribute(pName: apPartNames[i]); |
| 5446 | pJsonWriter->BeginObject(); |
| 5447 | |
| 5448 | pJsonWriter->WriteAttribute(pName: "name" ); |
| 5449 | pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aaSkinPartNames[i]); |
| 5450 | |
| 5451 | if(TeeInfo.m_aUseCustomColors[i]) |
| 5452 | { |
| 5453 | pJsonWriter->WriteAttribute(pName: "color" ); |
| 5454 | pJsonWriter->WriteIntValue(Value: TeeInfo.m_aSkinPartColors[i]); |
| 5455 | } |
| 5456 | |
| 5457 | pJsonWriter->EndObject(); |
| 5458 | } |
| 5459 | } |
| 5460 | |
| 5461 | pJsonWriter->EndObject(); |
| 5462 | |
| 5463 | pJsonWriter->WriteAttribute(pName: "afk" ); |
| 5464 | pJsonWriter->WriteBoolValue(Value: m_apPlayers[ClientId]->IsAfk()); |
| 5465 | |
| 5466 | const int Team = m_pController->IsTeamPlay() ? m_apPlayers[ClientId]->GetTeam() : (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(ClientId)); |
| 5467 | |
| 5468 | pJsonWriter->WriteAttribute(pName: "team" ); |
| 5469 | pJsonWriter->WriteIntValue(Value: Team); |
| 5470 | } |
| 5471 | |
| 5472 | void CGameContext::ReadCensorList() |
| 5473 | { |
| 5474 | const char *pCensorFilename = "censorlist.txt" ; |
| 5475 | CLineReader LineReader; |
| 5476 | m_vCensorlist.clear(); |
| 5477 | if(LineReader.OpenFile(File: Storage()->OpenFile(pFilename: pCensorFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL))) |
| 5478 | { |
| 5479 | while(const char *pLine = LineReader.Get()) |
| 5480 | { |
| 5481 | m_vCensorlist.emplace_back(args&: pLine); |
| 5482 | } |
| 5483 | } |
| 5484 | else |
| 5485 | { |
| 5486 | dbg_msg(sys: "censorlist" , fmt: "failed to open '%s'" , pCensorFilename); |
| 5487 | } |
| 5488 | } |
| 5489 | |
| 5490 | bool CGameContext::PracticeByDefault() const |
| 5491 | { |
| 5492 | return g_Config.m_SvPracticeByDefault && g_Config.m_SvTestingCommands; |
| 5493 | } |
| 5494 | |