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