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