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