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