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 for(int Id = 0; Id < MAX_CLIENTS; Id++)
1468 {
1469 if(ClientId == Id)
1470 continue;
1471
1472 CPlayer *pPlayer = m_apPlayers[Id];
1473 if(!pPlayer)
1474 continue;
1475
1476 if(Server()->GetClientVersion(ClientId: Id) < VERSION_DDNET_PREINPUT)
1477 continue;
1478
1479 if(pPlayer->GetTeam() == TEAM_SPECTATORS || GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: Id) || pPlayer->IsAfk())
1480 continue;
1481
1482 if(!pInputChr->CanSnapCharacter(SnappingClient: Id) || pInputChr->NetworkClipped(SnappingClient: Id))
1483 continue;
1484
1485 pClients[Id] = true;
1486 }
1487}
1488
1489// Server hooks
1490void CGameContext::OnClientPrepareInput(int ClientId, void *pInput)
1491{
1492 CNetObj_PlayerInput *pPlayerInput = static_cast<CNetObj_PlayerInput *>(pInput);
1493
1494 if(Server()->IsSixup(ClientId))
1495 pPlayerInput->m_PlayerFlags = PlayerFlags_SevenToSix(Flags: pPlayerInput->m_PlayerFlags);
1496}
1497
1498void CGameContext::OnClientDirectInput(int ClientId, const void *pInput)
1499{
1500 const CNetObj_PlayerInput *pPlayerInput = static_cast<const CNetObj_PlayerInput *>(pInput);
1501
1502 if(!m_pController->IsGamePaused())
1503 m_apPlayers[ClientId]->OnDirectInput(pNewInput: pPlayerInput);
1504
1505 int Flags = pPlayerInput->m_PlayerFlags;
1506 if((Flags & 256) || (Flags & 512))
1507 {
1508 Server()->Kick(ClientId, pReason: "please update your client or use DDNet client");
1509 }
1510}
1511
1512void CGameContext::OnClientPredictedInput(int ClientId, const void *pInput)
1513{
1514 const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput);
1515
1516 if(pApplyInput == nullptr)
1517 {
1518 // early return if no input at all has been sent by a player
1519 if(!m_aPlayerHasInput[ClientId])
1520 {
1521 return;
1522 }
1523 // set to last sent input when no new input has been sent
1524 pApplyInput = &m_aLastPlayerInput[ClientId];
1525 }
1526
1527 if(!m_pController->IsGamePaused())
1528 m_apPlayers[ClientId]->OnPredictedInput(pNewInput: pApplyInput);
1529}
1530
1531void CGameContext::OnClientPredictedEarlyInput(int ClientId, const void *pInput)
1532{
1533 const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput);
1534
1535 if(pApplyInput == nullptr)
1536 {
1537 // early return if no input at all has been sent by a player
1538 if(!m_aPlayerHasInput[ClientId])
1539 {
1540 return;
1541 }
1542 // set to last sent input when no new input has been sent
1543 pApplyInput = &m_aLastPlayerInput[ClientId];
1544 }
1545 else
1546 {
1547 // Store input in this function and not in `OnClientPredictedInput`,
1548 // because this function is called on all inputs, while
1549 // `OnClientPredictedInput` is only called on the first input of each
1550 // tick.
1551 mem_copy(dest: &m_aLastPlayerInput[ClientId], source: pApplyInput, size: sizeof(m_aLastPlayerInput[ClientId]));
1552 m_aPlayerHasInput[ClientId] = true;
1553 }
1554
1555 if(!m_pController->IsGamePaused())
1556 m_apPlayers[ClientId]->OnPredictedEarlyInput(pNewInput: pApplyInput);
1557
1558 if(m_TeeHistorianActive)
1559 {
1560 m_TeeHistorian.RecordPlayerInput(ClientId, UniqueClientId: m_apPlayers[ClientId]->GetUniqueCid(), pInput: pApplyInput);
1561 }
1562}
1563
1564const CVoteOptionServer *CGameContext::GetVoteOption(int Index) const
1565{
1566 const CVoteOptionServer *pCurrent;
1567 for(pCurrent = m_pVoteOptionFirst;
1568 Index > 0 && pCurrent;
1569 Index--, pCurrent = pCurrent->m_pNext)
1570 ;
1571
1572 if(Index > 0)
1573 return nullptr;
1574 return pCurrent;
1575}
1576
1577void CGameContext::ProgressVoteOptions(int ClientId)
1578{
1579 CPlayer *pPl = m_apPlayers[ClientId];
1580
1581 if(pPl->m_SendVoteIndex == -1)
1582 return; // we didn't start sending options yet
1583
1584 if(pPl->m_SendVoteIndex > m_NumVoteOptions)
1585 return; // shouldn't happen / fail silently
1586
1587 int VotesLeft = m_NumVoteOptions - pPl->m_SendVoteIndex;
1588 int NumVotesToSend = std::min(a: g_Config.m_SvSendVotesPerTick, b: VotesLeft);
1589
1590 if(!VotesLeft)
1591 {
1592 // player has up to date vote option list
1593 return;
1594 }
1595
1596 // build vote option list msg
1597 int CurIndex = 0;
1598
1599 CNetMsg_Sv_VoteOptionListAdd OptionMsg;
1600 OptionMsg.m_pDescription0 = "";
1601 OptionMsg.m_pDescription1 = "";
1602 OptionMsg.m_pDescription2 = "";
1603 OptionMsg.m_pDescription3 = "";
1604 OptionMsg.m_pDescription4 = "";
1605 OptionMsg.m_pDescription5 = "";
1606 OptionMsg.m_pDescription6 = "";
1607 OptionMsg.m_pDescription7 = "";
1608 OptionMsg.m_pDescription8 = "";
1609 OptionMsg.m_pDescription9 = "";
1610 OptionMsg.m_pDescription10 = "";
1611 OptionMsg.m_pDescription11 = "";
1612 OptionMsg.m_pDescription12 = "";
1613 OptionMsg.m_pDescription13 = "";
1614 OptionMsg.m_pDescription14 = "";
1615
1616 // get current vote option by index
1617 const CVoteOptionServer *pCurrent = GetVoteOption(Index: pPl->m_SendVoteIndex);
1618
1619 while(CurIndex < NumVotesToSend && pCurrent != nullptr)
1620 {
1621 switch(CurIndex)
1622 {
1623 case 0: OptionMsg.m_pDescription0 = pCurrent->m_aDescription; break;
1624 case 1: OptionMsg.m_pDescription1 = pCurrent->m_aDescription; break;
1625 case 2: OptionMsg.m_pDescription2 = pCurrent->m_aDescription; break;
1626 case 3: OptionMsg.m_pDescription3 = pCurrent->m_aDescription; break;
1627 case 4: OptionMsg.m_pDescription4 = pCurrent->m_aDescription; break;
1628 case 5: OptionMsg.m_pDescription5 = pCurrent->m_aDescription; break;
1629 case 6: OptionMsg.m_pDescription6 = pCurrent->m_aDescription; break;
1630 case 7: OptionMsg.m_pDescription7 = pCurrent->m_aDescription; break;
1631 case 8: OptionMsg.m_pDescription8 = pCurrent->m_aDescription; break;
1632 case 9: OptionMsg.m_pDescription9 = pCurrent->m_aDescription; break;
1633 case 10: OptionMsg.m_pDescription10 = pCurrent->m_aDescription; break;
1634 case 11: OptionMsg.m_pDescription11 = pCurrent->m_aDescription; break;
1635 case 12: OptionMsg.m_pDescription12 = pCurrent->m_aDescription; break;
1636 case 13: OptionMsg.m_pDescription13 = pCurrent->m_aDescription; break;
1637 case 14: OptionMsg.m_pDescription14 = pCurrent->m_aDescription; break;
1638 }
1639
1640 CurIndex++;
1641 pCurrent = pCurrent->m_pNext;
1642 }
1643
1644 // send msg
1645 if(pPl->m_SendVoteIndex == 0)
1646 {
1647 CNetMsg_Sv_VoteOptionGroupStart StartMsg;
1648 Server()->SendPackMsg(pMsg: &StartMsg, Flags: MSGFLAG_VITAL, ClientId);
1649 }
1650
1651 OptionMsg.m_NumOptions = NumVotesToSend;
1652 Server()->SendPackMsg(pMsg: &OptionMsg, Flags: MSGFLAG_VITAL, ClientId);
1653
1654 pPl->m_SendVoteIndex += NumVotesToSend;
1655
1656 if(pPl->m_SendVoteIndex == m_NumVoteOptions)
1657 {
1658 CNetMsg_Sv_VoteOptionGroupEnd EndMsg;
1659 Server()->SendPackMsg(pMsg: &EndMsg, Flags: MSGFLAG_VITAL, ClientId);
1660 }
1661}
1662
1663void CGameContext::OnClientEnter(int ClientId)
1664{
1665 if(m_TeeHistorianActive)
1666 {
1667 m_TeeHistorian.RecordPlayerReady(ClientId);
1668 }
1669 m_pController->OnPlayerConnect(pPlayer: m_apPlayers[ClientId]);
1670
1671 {
1672 CNetMsg_Sv_CommandInfoGroupStart Msg;
1673 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1674 }
1675 for(const IConsole::ICommandInfo *pCmd = Console()->FirstCommandInfo(ClientId, FlagMask: CFGFLAG_CHAT);
1676 pCmd; pCmd = Console()->NextCommandInfo(pInfo: pCmd, ClientId, FlagMask: CFGFLAG_CHAT))
1677 {
1678 const char *pName = pCmd->Name();
1679
1680 if(Server()->IsSixup(ClientId))
1681 {
1682 if(!str_comp_nocase(a: pName, b: "w") || !str_comp_nocase(a: pName, b: "whisper"))
1683 continue;
1684
1685 if(!str_comp_nocase(a: pName, b: "r"))
1686 pName = "rescue";
1687
1688 protocol7::CNetMsg_Sv_CommandInfo Msg;
1689 Msg.m_pName = pName;
1690 Msg.m_pArgsFormat = pCmd->Params();
1691 Msg.m_pHelpText = pCmd->Help();
1692 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1693 }
1694 else
1695 {
1696 CNetMsg_Sv_CommandInfo Msg;
1697 Msg.m_pName = pName;
1698 Msg.m_pArgsFormat = pCmd->Params();
1699 Msg.m_pHelpText = pCmd->Help();
1700 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1701 }
1702 }
1703 {
1704 CNetMsg_Sv_CommandInfoGroupEnd Msg;
1705 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1706 }
1707
1708 {
1709 int Empty = -1;
1710 for(int i = 0; i < MAX_CLIENTS; i++)
1711 {
1712 if(Server()->ClientSlotEmpty(ClientId: i))
1713 {
1714 Empty = i;
1715 break;
1716 }
1717 }
1718 CNetMsg_Sv_Chat Msg;
1719 Msg.m_Team = 0;
1720 Msg.m_ClientId = Empty;
1721 Msg.m_pMessage = "Do you know someone who uses a bot? Please report them to the moderators.";
1722 m_apPlayers[ClientId]->m_EligibleForFinishCheck = time_get();
1723 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1724 }
1725
1726 IServer::CClientInfo Info;
1727 if(Server()->GetClientInfo(ClientId, pInfo: &Info) && Info.m_GotDDNetVersion)
1728 {
1729 if(OnClientDDNetVersionKnown(ClientId))
1730 return; // kicked
1731 }
1732
1733 if(!Server()->ClientPrevIngame(ClientId))
1734 {
1735 if(g_Config.m_SvWelcome[0] != 0)
1736 SendChatTarget(To: ClientId, pText: g_Config.m_SvWelcome);
1737
1738 if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF)
1739 {
1740 if(g_Config.m_SvShowOthers)
1741 SendChatTarget(To: ClientId, pText: "You can see other players. To disable this use DDNet client and type /showothers");
1742
1743 m_apPlayers[ClientId]->m_ShowOthers = g_Config.m_SvShowOthersDefault;
1744 }
1745 }
1746 m_VoteUpdate = true;
1747
1748 // send active vote
1749 if(m_VoteCloseTime)
1750 SendVoteSet(ClientId);
1751
1752 Server()->ExpireServerInfo();
1753
1754 // send map info if loaded from database
1755 if(m_aMapInfoMessage[0] != '\0')
1756 {
1757 CNetMsg_Sv_MapInfo MapInfoMsg;
1758 MapInfoMsg.m_pDescription = m_aMapInfoMessage;
1759 Server()->SendPackMsg(pMsg: &MapInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1760 }
1761
1762 CPlayer *pNewPlayer = m_apPlayers[ClientId];
1763 mem_zero(block: &m_aLastPlayerInput[ClientId], size: sizeof(m_aLastPlayerInput[ClientId]));
1764 m_aPlayerHasInput[ClientId] = false;
1765
1766 // new info for others
1767 protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg;
1768 NewClientInfoMsg.m_ClientId = ClientId;
1769 NewClientInfoMsg.m_Local = 0;
1770 NewClientInfoMsg.m_Team = pNewPlayer->GetTeam();
1771 NewClientInfoMsg.m_pName = Server()->ClientName(ClientId);
1772 NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientId);
1773 NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientId);
1774 NewClientInfoMsg.m_Silent = false;
1775
1776 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
1777 {
1778 NewClientInfoMsg.m_apSkinPartNames[p] = pNewPlayer->m_TeeInfos.m_aaSkinPartNames[p];
1779 NewClientInfoMsg.m_aUseCustomColors[p] = pNewPlayer->m_TeeInfos.m_aUseCustomColors[p];
1780 NewClientInfoMsg.m_aSkinPartColors[p] = pNewPlayer->m_TeeInfos.m_aSkinPartColors[p];
1781 }
1782
1783 // update client infos (others before local)
1784 for(int i = 0; i < Server()->MaxClients(); ++i)
1785 {
1786 if(i == ClientId || !m_apPlayers[i] || !Server()->ClientIngame(ClientId: i))
1787 continue;
1788
1789 CPlayer *pPlayer = m_apPlayers[i];
1790
1791 if(Server()->IsSixup(ClientId: i))
1792 Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i);
1793
1794 if(Server()->IsSixup(ClientId))
1795 {
1796 // existing infos for new player
1797 protocol7::CNetMsg_Sv_ClientInfo ClientInfoMsg;
1798 ClientInfoMsg.m_ClientId = i;
1799 ClientInfoMsg.m_Local = 0;
1800 ClientInfoMsg.m_Team = pPlayer->GetTeam();
1801 ClientInfoMsg.m_pName = Server()->ClientName(ClientId: i);
1802 ClientInfoMsg.m_pClan = Server()->ClientClan(ClientId: i);
1803 ClientInfoMsg.m_Country = Server()->ClientCountry(ClientId: i);
1804 ClientInfoMsg.m_Silent = 0;
1805
1806 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
1807 {
1808 ClientInfoMsg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p];
1809 ClientInfoMsg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
1810 ClientInfoMsg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
1811 }
1812
1813 Server()->SendPackMsg(pMsg: &ClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1814 }
1815 }
1816
1817 // local info
1818 if(Server()->IsSixup(ClientId))
1819 {
1820 NewClientInfoMsg.m_Local = 1;
1821 Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
1822 }
1823
1824 // initial chat delay
1825 if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientId]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed())
1826 {
1827 char aBuf[128];
1828 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);
1829 SendChatTarget(To: ClientId, pText: aBuf);
1830 m_Mutes.Mute(pAddr: Server()->ClientAddr(ClientId), Seconds: g_Config.m_SvChatInitialDelay, pReason: "Initial chat delay", pClientName: Server()->ClientName(ClientId), InitialDelay: true);
1831 }
1832
1833 LogEvent(Description: "Connect", ClientId);
1834}
1835
1836bool CGameContext::OnClientDataPersist(int ClientId, void *pData)
1837{
1838 CPersistentClientData *pPersistent = (CPersistentClientData *)pData;
1839 if(!m_apPlayers[ClientId])
1840 {
1841 return false;
1842 }
1843 new(pPersistent) CPersistentClientData();
1844 pPersistent->m_IsSpectator = m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS;
1845 pPersistent->m_IsAfk = m_apPlayers[ClientId]->IsAfk();
1846 pPersistent->m_LastWhisperTo = m_apPlayers[ClientId]->m_LastWhisperTo;
1847 return true;
1848}
1849
1850void CGameContext::OnClientConnected(int ClientId, void *pData)
1851{
1852 CPersistentClientData *pPersistentData = (CPersistentClientData *)pData;
1853 bool Spec = false;
1854 bool Afk = true;
1855 int LastWhisperTo = -1;
1856 if(pPersistentData)
1857 {
1858 Spec = pPersistentData->m_IsSpectator;
1859 Afk = pPersistentData->m_IsAfk;
1860 LastWhisperTo = pPersistentData->m_LastWhisperTo;
1861 }
1862 else
1863 {
1864 // new player connected, clear whispers waiting for the old player with this id
1865 for(auto &pPlayer : m_apPlayers)
1866 {
1867 if(pPlayer && pPlayer->m_LastWhisperTo == ClientId)
1868 pPlayer->m_LastWhisperTo = -1;
1869 }
1870 }
1871
1872 {
1873 bool Empty = true;
1874 for(auto &pPlayer : m_apPlayers)
1875 {
1876 // connecting clients with spoofed ips can clog slots without being ingame
1877 if(pPlayer && Server()->ClientIngame(ClientId: pPlayer->GetCid()))
1878 {
1879 Empty = false;
1880 break;
1881 }
1882 }
1883 if(Empty)
1884 {
1885 m_NonEmptySince = Server()->Tick();
1886 }
1887 }
1888
1889 // Check which team the player should be on
1890 const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(NotThisId: ClientId);
1891 CreatePlayer(ClientId, StartTeam, Afk, LastWhisperTo);
1892
1893 SendMotd(ClientId);
1894 SendSettings(ClientId);
1895
1896 Server()->ExpireServerInfo();
1897}
1898
1899void CGameContext::OnClientDrop(int ClientId, const char *pReason)
1900{
1901 LogEvent(Description: "Disconnect", ClientId);
1902
1903 AbortVoteKickOnDisconnect(ClientId);
1904 m_pController->OnPlayerDisconnect(pPlayer: m_apPlayers[ClientId], pReason);
1905 delete m_apPlayers[ClientId];
1906 m_apPlayers[ClientId] = nullptr;
1907
1908 delete m_apSavedTeams[ClientId];
1909 m_apSavedTeams[ClientId] = nullptr;
1910
1911 delete m_apSavedTees[ClientId];
1912 m_apSavedTees[ClientId] = nullptr;
1913
1914 m_aTeamMapping[ClientId] = -1;
1915
1916 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && PracticeByDefault())
1917 m_pController->Teams().SetPractice(Team: GetDDRaceTeam(ClientId), Enabled: true);
1918
1919 m_VoteUpdate = true;
1920 if(m_VoteCreator == ClientId)
1921 {
1922 m_VoteCreator = -1;
1923 }
1924
1925 // update spectator modes
1926 for(auto &pPlayer : m_apPlayers)
1927 {
1928 if(pPlayer && pPlayer->SpectatorId() == ClientId)
1929 pPlayer->SetSpectatorId(SPEC_FREEVIEW);
1930 }
1931
1932 // update conversation targets
1933 for(auto &pPlayer : m_apPlayers)
1934 {
1935 if(pPlayer && pPlayer->m_LastWhisperTo == ClientId)
1936 pPlayer->m_LastWhisperTo = -1;
1937 }
1938
1939 protocol7::CNetMsg_Sv_ClientDrop Msg;
1940 Msg.m_ClientId = ClientId;
1941 Msg.m_pReason = pReason;
1942 Msg.m_Silent = false;
1943 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1);
1944
1945 Server()->ExpireServerInfo();
1946}
1947
1948void CGameContext::TeehistorianRecordAntibot(const void *pData, int DataSize)
1949{
1950 if(m_TeeHistorianActive)
1951 {
1952 m_TeeHistorian.RecordAntibot(pData, DataSize);
1953 }
1954}
1955
1956void CGameContext::TeehistorianRecordPlayerJoin(int ClientId, bool Sixup)
1957{
1958 if(m_TeeHistorianActive)
1959 {
1960 m_TeeHistorian.RecordPlayerJoin(ClientId, Protocol: !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7);
1961 }
1962}
1963
1964void CGameContext::TeehistorianRecordPlayerDrop(int ClientId, const char *pReason)
1965{
1966 if(m_TeeHistorianActive)
1967 {
1968 m_TeeHistorian.RecordPlayerDrop(ClientId, pReason);
1969 }
1970}
1971
1972void CGameContext::TeehistorianRecordPlayerRejoin(int ClientId)
1973{
1974 if(m_TeeHistorianActive)
1975 {
1976 m_TeeHistorian.RecordPlayerRejoin(ClientId);
1977 }
1978}
1979
1980void CGameContext::TeehistorianRecordPlayerName(int ClientId, const char *pName)
1981{
1982 if(m_TeeHistorianActive)
1983 {
1984 m_TeeHistorian.RecordPlayerName(ClientId, pName);
1985 }
1986}
1987
1988void CGameContext::TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks)
1989{
1990 if(m_TeeHistorianActive)
1991 {
1992 m_TeeHistorian.RecordPlayerFinish(ClientId, TimeTicks);
1993 }
1994}
1995
1996void CGameContext::TeehistorianRecordTeamFinish(int TeamId, int TimeTicks)
1997{
1998 if(m_TeeHistorianActive)
1999 {
2000 m_TeeHistorian.RecordTeamFinish(TeamId, TimeTicks);
2001 }
2002}
2003
2004void CGameContext::TeehistorianRecordAuthLogin(int ClientId, int Level, const char *pAuthName)
2005{
2006 if(m_TeeHistorianActive)
2007 {
2008 m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName);
2009 }
2010}
2011
2012bool CGameContext::OnClientDDNetVersionKnown(int ClientId)
2013{
2014 IServer::CClientInfo Info;
2015 dbg_assert(Server()->GetClientInfo(ClientId, &Info), "failed to get client info");
2016 int ClientVersion = Info.m_DDNetVersion;
2017 dbg_msg(sys: "ddnet", fmt: "cid=%d version=%d", ClientId, ClientVersion);
2018
2019 if(m_TeeHistorianActive)
2020 {
2021 if(Info.m_pConnectionId && Info.m_pDDNetVersionStr)
2022 {
2023 m_TeeHistorian.RecordDDNetVersion(ClientId, ConnectionId: *Info.m_pConnectionId, DDNetVersion: ClientVersion, pDDNetVersionStr: Info.m_pDDNetVersionStr);
2024 }
2025 else
2026 {
2027 m_TeeHistorian.RecordDDNetVersionOld(ClientId, DDNetVersion: ClientVersion);
2028 }
2029 }
2030
2031 // Autoban known bot versions.
2032 if(g_Config.m_SvBannedVersions[0] != '\0' && IsVersionBanned(Version: ClientVersion))
2033 {
2034 Server()->Kick(ClientId, pReason: "unsupported client");
2035 return true;
2036 }
2037
2038 CPlayer *pPlayer = m_apPlayers[ClientId];
2039 if(ClientVersion >= VERSION_DDNET_GAMETICK)
2040 pPlayer->m_TimerType = g_Config.m_SvDefaultTimerType;
2041
2042 // First update the teams state.
2043 m_pController->Teams().SendTeamsState(ClientId);
2044
2045 // Then send records.
2046 SendRecord(ClientId);
2047
2048 // And report correct tunings.
2049 if(ClientVersion < VERSION_DDNET_EARLY_VERSION)
2050 SendTuningParams(ClientId, Zone: pPlayer->m_TuneZone);
2051
2052 // Tell old clients to update.
2053 if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0')
2054 SendBroadcast(pText: g_Config.m_SvClientSuggestionOld, ClientId);
2055 // Tell known bot clients that they're botting and we know it.
2056 if(((ClientVersion >= 15 && ClientVersion < 100) || ClientVersion == 502) && g_Config.m_SvClientSuggestionBot[0] != '\0')
2057 SendBroadcast(pText: g_Config.m_SvClientSuggestionBot, ClientId);
2058
2059 return false;
2060}
2061
2062void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId)
2063{
2064 if(Server()->IsSixup(ClientId) && *pMsgId < OFFSET_UUID)
2065 {
2066 void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(Type: *pMsgId, pUnpacker);
2067 if(!pRawMsg)
2068 return nullptr;
2069
2070 CPlayer *pPlayer = m_apPlayers[ClientId];
2071 static char s_aRawMsg[1024];
2072
2073 if(*pMsgId == protocol7::NETMSGTYPE_CL_SAY)
2074 {
2075 protocol7::CNetMsg_Cl_Say *pMsg7 = (protocol7::CNetMsg_Cl_Say *)pRawMsg;
2076 // Should probably use a placement new to start the lifetime of the object to avoid future weirdness
2077 ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;
2078
2079 if(pMsg7->m_Mode == protocol7::CHAT_WHISPER)
2080 {
2081 if(!CheckClientId(ClientId: pMsg7->m_Target) || !Server()->ClientIngame(ClientId: pMsg7->m_Target))
2082 return nullptr;
2083 if(ProcessSpamProtection(ClientId))
2084 return nullptr;
2085
2086 WhisperId(ClientId, VictimId: pMsg7->m_Target, pMessage: pMsg7->m_pMessage);
2087 return nullptr;
2088 }
2089 else
2090 {
2091 pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM;
2092 pMsg->m_pMessage = pMsg7->m_pMessage;
2093 }
2094 }
2095 else if(*pMsgId == protocol7::NETMSGTYPE_CL_STARTINFO)
2096 {
2097 protocol7::CNetMsg_Cl_StartInfo *pMsg7 = (protocol7::CNetMsg_Cl_StartInfo *)pRawMsg;
2098 ::CNetMsg_Cl_StartInfo *pMsg = (::CNetMsg_Cl_StartInfo *)s_aRawMsg;
2099
2100 pMsg->m_pName = pMsg7->m_pName;
2101 pMsg->m_pClan = pMsg7->m_pClan;
2102 pMsg->m_Country = pMsg7->m_Country;
2103
2104 pPlayer->m_TeeInfos = CTeeInfo(pMsg7->m_apSkinPartNames, pMsg7->m_aUseCustomColors, pMsg7->m_aSkinPartColors);
2105 pPlayer->m_TeeInfos.FromSixup();
2106
2107 str_copy(dst: s_aRawMsg + sizeof(*pMsg), src: pPlayer->m_TeeInfos.m_aSkinName, dst_size: sizeof(s_aRawMsg) - sizeof(*pMsg));
2108
2109 pMsg->m_pSkin = s_aRawMsg + sizeof(*pMsg);
2110 pMsg->m_UseCustomColor = pPlayer->m_TeeInfos.m_UseCustomColor;
2111 pMsg->m_ColorBody = pPlayer->m_TeeInfos.m_ColorBody;
2112 pMsg->m_ColorFeet = pPlayer->m_TeeInfos.m_ColorFeet;
2113 }
2114 else if(*pMsgId == protocol7::NETMSGTYPE_CL_SKINCHANGE)
2115 {
2116 protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg;
2117 if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo &&
2118 pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
2119 return nullptr;
2120
2121 pPlayer->m_LastChangeInfo = Server()->Tick();
2122
2123 CTeeInfo Info(pMsg->m_apSkinPartNames, pMsg->m_aUseCustomColors, pMsg->m_aSkinPartColors);
2124 Info.FromSixup();
2125 pPlayer->m_TeeInfos = Info;
2126 SendSkinChange7(ClientId);
2127
2128 return nullptr;
2129 }
2130 else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE)
2131 {
2132 protocol7::CNetMsg_Cl_SetSpectatorMode *pMsg7 = (protocol7::CNetMsg_Cl_SetSpectatorMode *)pRawMsg;
2133 ::CNetMsg_Cl_SetSpectatorMode *pMsg = (::CNetMsg_Cl_SetSpectatorMode *)s_aRawMsg;
2134
2135 if(pMsg7->m_SpecMode == protocol7::SPEC_FREEVIEW)
2136 pMsg->m_SpectatorId = SPEC_FREEVIEW;
2137 else if(pMsg7->m_SpecMode == protocol7::SPEC_PLAYER)
2138 pMsg->m_SpectatorId = pMsg7->m_SpectatorId;
2139 else
2140 pMsg->m_SpectatorId = SPEC_FREEVIEW; // Probably not needed
2141 }
2142 else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM)
2143 {
2144 protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg;
2145 ::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg;
2146
2147 pMsg->m_Team = pMsg7->m_Team;
2148 }
2149 else if(*pMsgId == protocol7::NETMSGTYPE_CL_COMMAND)
2150 {
2151 protocol7::CNetMsg_Cl_Command *pMsg7 = (protocol7::CNetMsg_Cl_Command *)pRawMsg;
2152 ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;
2153
2154 str_format(buffer: s_aRawMsg + sizeof(*pMsg), buffer_size: sizeof(s_aRawMsg) - sizeof(*pMsg), format: "/%s %s", pMsg7->m_pName, pMsg7->m_pArguments);
2155 pMsg->m_pMessage = s_aRawMsg + sizeof(*pMsg);
2156 pMsg->m_Team = 0;
2157
2158 *pMsgId = NETMSGTYPE_CL_SAY;
2159 return s_aRawMsg;
2160 }
2161 else if(*pMsgId == protocol7::NETMSGTYPE_CL_CALLVOTE)
2162 {
2163 protocol7::CNetMsg_Cl_CallVote *pMsg7 = (protocol7::CNetMsg_Cl_CallVote *)pRawMsg;
2164
2165 if(pMsg7->m_Force)
2166 {
2167 if(!Server()->IsRconAuthed(ClientId))
2168 {
2169 return nullptr;
2170 }
2171 char aCommand[IConsole::CMDLINE_LENGTH];
2172 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "force_vote \"%s\" \"%s\" \"%s\"", pMsg7->m_pType, pMsg7->m_pValue, pMsg7->m_pReason);
2173 Console()->ExecuteLine(pStr: aCommand, ClientId, InterpretSemicolons: false);
2174 return nullptr;
2175 }
2176
2177 ::CNetMsg_Cl_CallVote *pMsg = (::CNetMsg_Cl_CallVote *)s_aRawMsg;
2178 pMsg->m_pValue = pMsg7->m_pValue;
2179 pMsg->m_pReason = pMsg7->m_pReason;
2180 pMsg->m_pType = pMsg7->m_pType;
2181 }
2182 else if(*pMsgId == protocol7::NETMSGTYPE_CL_EMOTICON)
2183 {
2184 protocol7::CNetMsg_Cl_Emoticon *pMsg7 = (protocol7::CNetMsg_Cl_Emoticon *)pRawMsg;
2185 ::CNetMsg_Cl_Emoticon *pMsg = (::CNetMsg_Cl_Emoticon *)s_aRawMsg;
2186
2187 pMsg->m_Emoticon = pMsg7->m_Emoticon;
2188 }
2189 else if(*pMsgId == protocol7::NETMSGTYPE_CL_VOTE)
2190 {
2191 protocol7::CNetMsg_Cl_Vote *pMsg7 = (protocol7::CNetMsg_Cl_Vote *)pRawMsg;
2192 ::CNetMsg_Cl_Vote *pMsg = (::CNetMsg_Cl_Vote *)s_aRawMsg;
2193
2194 pMsg->m_Vote = pMsg7->m_Vote;
2195 }
2196
2197 *pMsgId = Msg_SevenToSix(a: *pMsgId);
2198
2199 return s_aRawMsg;
2200 }
2201 else
2202 return m_NetObjHandler.SecureUnpackMsg(Type: *pMsgId, pUnpacker);
2203}
2204
2205void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size)
2206{
2207 str_copy(dst: pCensoredMessage, src: pMessage, dst_size: Size);
2208
2209 for(auto &Item : m_vCensorlist)
2210 {
2211 char *pCurLoc = pCensoredMessage;
2212 while(true)
2213 {
2214 const char *pEndMatch;
2215 pCurLoc = (char *)str_utf8_find_nocase(haystack: pCurLoc, needle: Item.c_str(), end: &pEndMatch);
2216 if(!pCurLoc)
2217 {
2218 break;
2219 }
2220 while(pCurLoc < pEndMatch)
2221 {
2222 *pCurLoc = '*';
2223 pCurLoc++;
2224 }
2225 }
2226 }
2227}
2228
2229void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId)
2230{
2231 if(m_TeeHistorianActive)
2232 {
2233 if(m_NetObjHandler.TeeHistorianRecordMsg(Type: MsgId))
2234 {
2235 m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize());
2236 }
2237 }
2238
2239 void *pRawMsg = PreProcessMsg(pMsgId: &MsgId, pUnpacker, ClientId);
2240
2241 if(!pRawMsg)
2242 return;
2243
2244 if(Server()->ClientIngame(ClientId))
2245 {
2246 switch(MsgId)
2247 {
2248 case NETMSGTYPE_CL_SAY:
2249 OnSayNetMessage(pMsg: static_cast<CNetMsg_Cl_Say *>(pRawMsg), ClientId, pUnpacker);
2250 break;
2251 case NETMSGTYPE_CL_CALLVOTE:
2252 OnCallVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_CallVote *>(pRawMsg), ClientId);
2253 break;
2254 case NETMSGTYPE_CL_VOTE:
2255 OnVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_Vote *>(pRawMsg), ClientId);
2256 break;
2257 case NETMSGTYPE_CL_SETTEAM:
2258 OnSetTeamNetMessage(pMsg: static_cast<CNetMsg_Cl_SetTeam *>(pRawMsg), ClientId);
2259 break;
2260 case NETMSGTYPE_CL_ISDDNETLEGACY:
2261 OnIsDDNetLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_IsDDNetLegacy *>(pRawMsg), ClientId, pUnpacker);
2262 break;
2263 case NETMSGTYPE_CL_SHOWOTHERSLEGACY:
2264 OnShowOthersLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthersLegacy *>(pRawMsg), ClientId);
2265 break;
2266 case NETMSGTYPE_CL_SHOWOTHERS:
2267 OnShowOthersNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthers *>(pRawMsg), ClientId);
2268 break;
2269 case NETMSGTYPE_CL_SHOWDISTANCE:
2270 OnShowDistanceNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowDistance *>(pRawMsg), ClientId);
2271 break;
2272 case NETMSGTYPE_CL_CAMERAINFO:
2273 OnCameraInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_CameraInfo *>(pRawMsg), ClientId);
2274 break;
2275 case NETMSGTYPE_CL_SETSPECTATORMODE:
2276 OnSetSpectatorModeNetMessage(pMsg: static_cast<CNetMsg_Cl_SetSpectatorMode *>(pRawMsg), ClientId);
2277 break;
2278 case NETMSGTYPE_CL_CHANGEINFO:
2279 OnChangeInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_ChangeInfo *>(pRawMsg), ClientId);
2280 break;
2281 case NETMSGTYPE_CL_EMOTICON:
2282 OnEmoticonNetMessage(pMsg: static_cast<CNetMsg_Cl_Emoticon *>(pRawMsg), ClientId);
2283 break;
2284 case NETMSGTYPE_CL_KILL:
2285 OnKillNetMessage(pMsg: static_cast<CNetMsg_Cl_Kill *>(pRawMsg), ClientId);
2286 break;
2287 case NETMSGTYPE_CL_ENABLESPECTATORCOUNT:
2288 OnEnableSpectatorCountNetMessage(pMsg: static_cast<CNetMsg_Cl_EnableSpectatorCount *>(pRawMsg), ClientId);
2289 default:
2290 break;
2291 }
2292 }
2293 if(MsgId == NETMSGTYPE_CL_STARTINFO)
2294 {
2295 OnStartInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientId);
2296 }
2297}
2298
2299void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker)
2300{
2301 CPlayer *pPlayer = m_apPlayers[ClientId];
2302 bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get();
2303 if(Check && str_comp(a: pMsg->m_pMessage, b: "xd sure chillerbot.png is lyfe") == 0 && pMsg->m_Team == 0)
2304 {
2305 if(m_TeeHistorianActive)
2306 {
2307 m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize());
2308 }
2309
2310 pPlayer->m_NotEligibleForFinish = true;
2311 dbg_msg(sys: "hack", fmt: "bot detected, cid=%d", ClientId);
2312 return;
2313 }
2314 int Team = pMsg->m_Team;
2315
2316 // trim right and set maximum length to 256 utf8-characters
2317 int Length = 0;
2318 const char *p = pMsg->m_pMessage;
2319 const char *pEnd = nullptr;
2320 while(*p)
2321 {
2322 const char *pStrOld = p;
2323 int Code = str_utf8_decode(ptr: &p);
2324
2325 // check if unicode is not empty
2326 if(!str_utf8_isspace(code: Code))
2327 {
2328 pEnd = nullptr;
2329 }
2330 else if(pEnd == nullptr)
2331 pEnd = pStrOld;
2332
2333 if(++Length >= 256)
2334 {
2335 *(const_cast<char *>(p)) = 0;
2336 break;
2337 }
2338 }
2339 if(pEnd != nullptr)
2340 *(const_cast<char *>(pEnd)) = 0;
2341
2342 // drop empty and autocreated spam messages (more than 32 characters per second)
2343 if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick())))
2344 return;
2345
2346 int GameTeam = GetDDRaceTeam(ClientId: pPlayer->GetCid());
2347 if(Team)
2348 Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? TEAM_SPECTATORS : GameTeam);
2349 else
2350 Team = TEAM_ALL;
2351
2352 if(pMsg->m_pMessage[0] == '/')
2353 {
2354 const char *pWhisper;
2355 if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "w ")))
2356 {
2357 Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2358 }
2359 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "whisper ")))
2360 {
2361 Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2362 }
2363 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "c ")))
2364 {
2365 Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2366 }
2367 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "converse ")))
2368 {
2369 Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2370 }
2371 else
2372 {
2373 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())
2374 return;
2375
2376 int64_t Now = Server()->Tick();
2377 pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now;
2378 pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4;
2379
2380 Console()->SetFlagMask(CFGFLAG_CHAT);
2381 {
2382 CClientChatLogger Logger(this, ClientId, log_get_scope_logger());
2383 CLogScope Scope(&Logger);
2384 Console()->ExecuteLine(pStr: pMsg->m_pMessage + 1, ClientId, InterpretSemicolons: false);
2385 }
2386 // m_apPlayers[ClientId] can be nullptr, if the player used a
2387 // timeout code and replaced another client.
2388 char aBuf[256];
2389 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d used %s", ClientId, pMsg->m_pMessage);
2390 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "chat-command", pStr: aBuf);
2391
2392 Console()->SetFlagMask(CFGFLAG_SERVER);
2393 }
2394 }
2395 else
2396 {
2397 pPlayer->UpdatePlaytime();
2398 char aCensoredMessage[256];
2399 CensorMessage(pCensoredMessage: aCensoredMessage, pMessage: pMsg->m_pMessage, Size: sizeof(aCensoredMessage));
2400 SendChat(ChatterClientId: ClientId, Team, pText: aCensoredMessage, SpamProtectionClientId: ClientId);
2401 }
2402}
2403
2404void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId)
2405{
2406 if(RateLimitPlayerVote(ClientId) || m_VoteCloseTime)
2407 return;
2408
2409 m_apPlayers[ClientId]->UpdatePlaytime();
2410
2411 m_VoteType = VOTE_TYPE_UNKNOWN;
2412 char aChatmsg[512] = {0};
2413 char aDesc[VOTE_DESC_LENGTH] = {0};
2414 char aSixupDesc[VOTE_DESC_LENGTH] = {0};
2415 char aCmd[VOTE_CMD_LENGTH] = {0};
2416 char aReason[VOTE_REASON_LENGTH] = "No reason given";
2417 if(pMsg->m_pReason[0])
2418 {
2419 str_copy(dst&: aReason, src: pMsg->m_pReason);
2420 }
2421
2422 if(str_comp_nocase(a: pMsg->m_pType, b: "option") == 0)
2423 {
2424 CVoteOptionServer *pOption = m_pVoteOptionFirst;
2425 while(pOption)
2426 {
2427 if(str_comp_nocase(a: pMsg->m_pValue, b: pOption->m_aDescription) == 0)
2428 {
2429 if(!Console()->LineIsValid(pStr: pOption->m_aCommand))
2430 {
2431 SendChatTarget(To: ClientId, pText: "Invalid option");
2432 return;
2433 }
2434 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))
2435 {
2436 return;
2437 }
2438
2439 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientId),
2440 pOption->m_aDescription, aReason);
2441 str_copy(dst&: aDesc, src: pOption->m_aDescription);
2442
2443 if((str_endswith(str: pOption->m_aCommand, suffix: "random_map") || str_endswith(str: pOption->m_aCommand, suffix: "random_unfinished_map")))
2444 {
2445 if(str_length(str: aReason) == 1 && aReason[0] >= '0' && aReason[0] <= '5')
2446 {
2447 int Stars = aReason[0] - '0';
2448 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d", pOption->m_aCommand, Stars);
2449 }
2450 else if(str_length(str: aReason) == 3 && aReason[1] == '-' && aReason[0] >= '0' && aReason[0] <= '5' && aReason[2] >= '0' && aReason[2] <= '5')
2451 {
2452 int Start = aReason[0] - '0';
2453 int End = aReason[2] - '0';
2454 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d %d", pOption->m_aCommand, Start, End);
2455 }
2456 else
2457 {
2458 str_copy(dst&: aCmd, src: pOption->m_aCommand);
2459 }
2460 }
2461 else
2462 {
2463 str_copy(dst&: aCmd, src: pOption->m_aCommand);
2464 }
2465
2466 m_LastMapVote = time_get();
2467 break;
2468 }
2469
2470 pOption = pOption->m_pNext;
2471 }
2472
2473 if(!pOption)
2474 {
2475 if(!Server()->IsRconAuthedAdmin(ClientId)) // allow admins to call any vote they want
2476 {
2477 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' isn't an option on this server", pMsg->m_pValue);
2478 SendChatTarget(To: ClientId, pText: aChatmsg);
2479 return;
2480 }
2481 else
2482 {
2483 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s'", Server()->ClientName(ClientId), pMsg->m_pValue);
2484 str_copy(dst&: aDesc, src: pMsg->m_pValue);
2485 str_copy(dst&: aCmd, src: pMsg->m_pValue);
2486 }
2487 }
2488
2489 m_VoteType = VOTE_TYPE_OPTION;
2490 }
2491 else if(str_comp_nocase(a: pMsg->m_pType, b: "kick") == 0)
2492 {
2493 if(!g_Config.m_SvVoteKick && !Server()->IsRconAuthed(ClientId)) // allow admins to call kick votes even if they are forbidden
2494 {
2495 SendChatTarget(To: ClientId, pText: "Server does not allow voting to kick players");
2496 return;
2497 }
2498 if(!Server()->IsRconAuthed(ClientId) && time_get() < m_apPlayers[ClientId]->m_LastKickVote + (time_freq() * g_Config.m_SvVoteKickDelay))
2499 {
2500 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)",
2501 g_Config.m_SvVoteKickDelay,
2502 (int)((m_apPlayers[ClientId]->m_LastKickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq()));
2503 SendChatTarget(To: ClientId, pText: aChatmsg);
2504 return;
2505 }
2506
2507 if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientId))
2508 {
2509 const NETADDR *apAddresses[MAX_CLIENTS];
2510 for(int i = 0; i < MAX_CLIENTS; i++)
2511 {
2512 if(m_apPlayers[i])
2513 {
2514 apAddresses[i] = Server()->ClientAddr(ClientId: i);
2515 }
2516 }
2517 int NumPlayers = 0;
2518 for(int i = 0; i < MAX_CLIENTS; ++i)
2519 {
2520 if(m_apPlayers[i] && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: i))
2521 {
2522 NumPlayers++;
2523 for(int j = 0; j < i; j++)
2524 {
2525 if(m_apPlayers[j] && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: j))
2526 {
2527 if(!net_addr_comp_noport(a: apAddresses[i], b: apAddresses[j]))
2528 {
2529 NumPlayers--;
2530 break;
2531 }
2532 }
2533 }
2534 }
2535 }
2536
2537 if(NumPlayers < g_Config.m_SvVoteKickMin)
2538 {
2539 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "Kick voting requires %d players", g_Config.m_SvVoteKickMin);
2540 SendChatTarget(To: ClientId, pText: aChatmsg);
2541 return;
2542 }
2543 }
2544
2545 int KickId = str_toint(str: pMsg->m_pValue);
2546
2547 if(KickId < 0 || KickId >= MAX_CLIENTS || !m_apPlayers[KickId])
2548 {
2549 SendChatTarget(To: ClientId, pText: "Invalid client id to kick");
2550 return;
2551 }
2552 if(KickId == ClientId)
2553 {
2554 SendChatTarget(To: ClientId, pText: "You can't kick yourself");
2555 return;
2556 }
2557 if(!Server()->ReverseTranslate(Target&: KickId, Client: ClientId))
2558 {
2559 return;
2560 }
2561 int Authed = Server()->GetAuthedState(ClientId);
2562 int KickedAuthed = Server()->GetAuthedState(ClientId: KickId);
2563 if(KickedAuthed > Authed)
2564 {
2565 SendChatTarget(To: ClientId, pText: "You can't kick authorized players");
2566 char aBufKick[128];
2567 str_format(buffer: aBufKick, buffer_size: sizeof(aBufKick), format: "'%s' called for vote to kick you", Server()->ClientName(ClientId));
2568 SendChatTarget(To: KickId, pText: aBufKick);
2569 return;
2570 }
2571
2572 // Don't allow kicking if a player has no character
2573 if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: KickId))
2574 {
2575 SendChatTarget(To: ClientId, pText: "You can kick only your team member");
2576 return;
2577 }
2578
2579 if(GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: KickId))
2580 {
2581 if(!g_Config.m_SvVoteKickMuteTime)
2582 {
2583 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);
2584 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", KickId, Server()->ClientName(ClientId: KickId));
2585 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "muteid %d %d Muted by vote", KickId, g_Config.m_SvVoteKickMuteTime);
2586 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Mute '%s'", Server()->ClientName(ClientId: KickId));
2587 }
2588 else
2589 {
2590 SendChatTarget(To: ClientId, pText: "You can kick only your team member");
2591 return;
2592 }
2593 }
2594 else
2595 {
2596 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);
2597 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", KickId, Server()->ClientName(ClientId: KickId));
2598 if(!GetDDRaceTeam(ClientId))
2599 {
2600 if(!g_Config.m_SvVoteKickBantime)
2601 {
2602 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "kick %d Kicked by vote", KickId);
2603 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Kick '%s'", Server()->ClientName(ClientId: KickId));
2604 }
2605 else
2606 {
2607 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);
2608 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Ban '%s'", Server()->ClientName(ClientId: KickId));
2609 }
2610 }
2611 else
2612 {
2613 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "uninvite %d %d; set_team_ddr %d 0", KickId, GetDDRaceTeam(ClientId: KickId), KickId);
2614 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to team 0", Server()->ClientName(ClientId: KickId));
2615 }
2616 }
2617 m_apPlayers[ClientId]->m_LastKickVote = time_get();
2618 m_VoteType = VOTE_TYPE_KICK;
2619 m_VoteVictim = KickId;
2620 }
2621 else if(str_comp_nocase(a: pMsg->m_pType, b: "spectate") == 0)
2622 {
2623 if(!g_Config.m_SvVoteSpectate)
2624 {
2625 SendChatTarget(To: ClientId, pText: "Server does not allow voting to move players to spectators");
2626 return;
2627 }
2628
2629 int SpectateId = str_toint(str: pMsg->m_pValue);
2630
2631 if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !m_apPlayers[SpectateId] || m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
2632 {
2633 SendChatTarget(To: ClientId, pText: "Invalid client id to move to spectators");
2634 return;
2635 }
2636 if(SpectateId == ClientId)
2637 {
2638 SendChatTarget(To: ClientId, pText: "You can't move yourself to spectators");
2639 return;
2640 }
2641 int Authed = Server()->GetAuthedState(ClientId);
2642 int SpectateAuthed = Server()->GetAuthedState(ClientId: SpectateId);
2643 if(SpectateAuthed > Authed)
2644 {
2645 SendChatTarget(To: ClientId, pText: "You can't move authorized players to spectators");
2646 char aBufSpectate[128];
2647 str_format(buffer: aBufSpectate, buffer_size: sizeof(aBufSpectate), format: "'%s' called for vote to move you to spectators", Server()->ClientName(ClientId));
2648 SendChatTarget(To: SpectateId, pText: aBufSpectate);
2649 return;
2650 }
2651 if(!Server()->ReverseTranslate(Target&: SpectateId, Client: ClientId))
2652 {
2653 return;
2654 }
2655
2656 if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: SpectateId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: SpectateId))
2657 {
2658 SendChatTarget(To: ClientId, pText: "You can only move your team member to spectators");
2659 return;
2660 }
2661
2662 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", SpectateId, Server()->ClientName(ClientId: SpectateId));
2663 if(g_Config.m_SvPauseable && g_Config.m_SvVotePause)
2664 {
2665 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);
2666 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Pause '%s' (%ds)", Server()->ClientName(ClientId: SpectateId), g_Config.m_SvVotePauseTime);
2667 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);
2668 }
2669 else
2670 {
2671 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);
2672 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to spectators", Server()->ClientName(ClientId: SpectateId));
2673 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);
2674 }
2675 m_VoteType = VOTE_TYPE_SPECTATE;
2676 m_VoteVictim = SpectateId;
2677 }
2678
2679 if(aCmd[0] && str_comp_nocase(a: aCmd, b: "info") != 0)
2680 CallVote(ClientId, pDesc: aDesc, pCmd: aCmd, pReason: aReason, pChatmsg: aChatmsg, pSixupDesc: aSixupDesc[0] ? aSixupDesc : nullptr);
2681}
2682
2683void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId)
2684{
2685 if(!m_VoteCloseTime)
2686 return;
2687
2688 CPlayer *pPlayer = m_apPlayers[ClientId];
2689
2690 if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick())
2691 return;
2692
2693 pPlayer->m_LastVoteTry = Server()->Tick();
2694 pPlayer->UpdatePlaytime();
2695
2696 if(!pMsg->m_Vote)
2697 return;
2698
2699 // Allow the vote creator to cancel the vote
2700 if(pPlayer->GetCid() == m_VoteCreator && pMsg->m_Vote == -1)
2701 {
2702 m_VoteEnforce = VOTE_ENFORCE_CANCEL;
2703 return;
2704 }
2705
2706 pPlayer->m_Vote = pMsg->m_Vote;
2707 pPlayer->m_VotePos = ++m_VotePos;
2708 m_VoteUpdate = true;
2709
2710 CNetMsg_Sv_YourVote Msg = {.m_Voted: pMsg->m_Vote};
2711 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
2712}
2713
2714void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId)
2715{
2716 if(m_pController->IsGamePaused())
2717 return;
2718
2719 CPlayer *pPlayer = m_apPlayers[ClientId];
2720
2721 if(pPlayer->GetTeam() == pMsg->m_Team)
2722 return;
2723 if(g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick())
2724 return;
2725
2726 // Kill Protection
2727 CCharacter *pChr = pPlayer->GetCharacter();
2728 if(pChr)
2729 {
2730 int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
2731 if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
2732 {
2733 SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to join the spectators, first type /kill");
2734 return;
2735 }
2736 }
2737
2738 if(pPlayer->m_TeamChangeTick > Server()->Tick())
2739 {
2740 pPlayer->m_LastSetTeam = Server()->Tick();
2741 int TimeLeft = (pPlayer->m_TeamChangeTick - Server()->Tick()) / Server()->TickSpeed();
2742 char aTime[32];
2743 str_time(centisecs: (int64_t)TimeLeft * 100, format: ETimeFormat::HOURS, buffer: aTime, buffer_size: sizeof(aTime));
2744 char aBuf[128];
2745 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Time to wait before changing team: %s", aTime);
2746 SendBroadcast(pText: aBuf, ClientId);
2747 return;
2748 }
2749
2750 // Switch team on given client and kill/respawn them
2751 char aTeamJoinError[512];
2752 if(m_pController->CanJoinTeam(Team: pMsg->m_Team, NotThisId: ClientId, pErrorReason: aTeamJoinError, ErrorReasonSize: sizeof(aTeamJoinError)))
2753 {
2754 if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
2755 m_VoteUpdate = true;
2756 m_pController->DoTeamChange(pPlayer, Team: pMsg->m_Team, DoChatMsg: true);
2757 pPlayer->m_TeamChangeTick = Server()->Tick();
2758 }
2759 else
2760 SendBroadcast(pText: aTeamJoinError, ClientId);
2761}
2762
2763void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker)
2764{
2765 IServer::CClientInfo Info;
2766 if(Server()->GetClientInfo(ClientId, pInfo: &Info) && Info.m_GotDDNetVersion)
2767 {
2768 return;
2769 }
2770 int DDNetVersion = pUnpacker->GetInt();
2771 if(pUnpacker->Error() || DDNetVersion < 0)
2772 {
2773 DDNetVersion = VERSION_DDRACE;
2774 }
2775 Server()->SetClientDDNetVersion(ClientId, DDNetVersion);
2776 OnClientDDNetVersionKnown(ClientId);
2777}
2778
2779void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId)
2780{
2781 if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
2782 {
2783 CPlayer *pPlayer = m_apPlayers[ClientId];
2784 pPlayer->m_ShowOthers = pMsg->m_Show;
2785 }
2786}
2787
2788void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId)
2789{
2790 if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
2791 {
2792 CPlayer *pPlayer = m_apPlayers[ClientId];
2793 pPlayer->m_ShowOthers = pMsg->m_Show;
2794 }
2795}
2796
2797void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId)
2798{
2799 CPlayer *pPlayer = m_apPlayers[ClientId];
2800 pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y);
2801}
2802
2803void CGameContext::OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId)
2804{
2805 CPlayer *pPlayer = m_apPlayers[ClientId];
2806 pPlayer->m_CameraInfo.Write(pMsg);
2807}
2808
2809void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId)
2810{
2811 if(m_pController->IsGamePaused())
2812 return;
2813
2814 int SpectatorId = std::clamp(val: pMsg->m_SpectatorId, lo: (int)SPEC_FOLLOW, hi: MAX_CLIENTS - 1);
2815 if(SpectatorId >= 0)
2816 if(!Server()->ReverseTranslate(Target&: SpectatorId, Client: ClientId))
2817 return;
2818
2819 CPlayer *pPlayer = m_apPlayers[ClientId];
2820 if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick()))
2821 return;
2822
2823 pPlayer->m_LastSetSpectatorMode = Server()->Tick();
2824 pPlayer->UpdatePlaytime();
2825 if(SpectatorId >= 0 && (!m_apPlayers[SpectatorId] || m_apPlayers[SpectatorId]->GetTeam() == TEAM_SPECTATORS))
2826 SendChatTarget(To: ClientId, pText: "Invalid spectator id used");
2827 else
2828 pPlayer->SetSpectatorId(SpectatorId);
2829}
2830
2831void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId)
2832{
2833 CPlayer *pPlayer = m_apPlayers[ClientId];
2834 if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
2835 return;
2836
2837 bool SixupNeedsUpdate = false;
2838
2839 pPlayer->m_LastChangeInfo = Server()->Tick();
2840 pPlayer->UpdatePlaytime();
2841
2842 if(g_Config.m_SvSpamprotection)
2843 {
2844 CNetMsg_Sv_ChangeInfoCooldown ChangeInfoCooldownMsg;
2845 ChangeInfoCooldownMsg.m_WaitUntil = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay;
2846 Server()->SendPackMsg(pMsg: &ChangeInfoCooldownMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
2847 }
2848
2849 // set infos
2850 if(Server()->WouldClientNameChange(ClientId, pNameRequest: pMsg->m_pName) && !ProcessSpamProtection(ClientId))
2851 {
2852 char aOldName[MAX_NAME_LENGTH];
2853 str_copy(dst&: aOldName, src: Server()->ClientName(ClientId));
2854
2855 Server()->SetClientName(ClientId, pName: pMsg->m_pName);
2856
2857 char aChatText[256];
2858 str_format(buffer: aChatText, buffer_size: sizeof(aChatText), format: "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientId));
2859 SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aChatText);
2860
2861 // reload scores
2862 Score()->PlayerData(Id: ClientId)->Reset();
2863 Server()->SetClientScore(ClientId, Score: std::nullopt);
2864 Score()->LoadPlayerData(ClientId);
2865
2866 SixupNeedsUpdate = true;
2867
2868 LogEvent(Description: "Name change", ClientId);
2869 }
2870
2871 if(Server()->WouldClientClanChange(ClientId, pClanRequest: pMsg->m_pClan))
2872 {
2873 SixupNeedsUpdate = true;
2874 Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan);
2875 }
2876
2877 if(Server()->ClientCountry(ClientId) != pMsg->m_Country)
2878 SixupNeedsUpdate = true;
2879 Server()->SetClientCountry(ClientId, Country: pMsg->m_Country);
2880
2881 str_copy(dst&: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin);
2882 pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
2883 pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
2884 pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
2885 if(!Server()->IsSixup(ClientId))
2886 pPlayer->m_TeeInfos.ToSixup();
2887
2888 if(SixupNeedsUpdate)
2889 {
2890 protocol7::CNetMsg_Sv_ClientDrop Drop;
2891 Drop.m_ClientId = ClientId;
2892 Drop.m_pReason = "";
2893 Drop.m_Silent = true;
2894
2895 protocol7::CNetMsg_Sv_ClientInfo Info;
2896 Info.m_ClientId = ClientId;
2897 Info.m_pName = Server()->ClientName(ClientId);
2898 Info.m_Country = pMsg->m_Country;
2899 Info.m_pClan = pMsg->m_pClan;
2900 Info.m_Local = 0;
2901 Info.m_Silent = true;
2902 Info.m_Team = pPlayer->GetTeam();
2903
2904 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
2905 {
2906 Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p];
2907 Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
2908 Info.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
2909 }
2910
2911 for(int i = 0; i < Server()->MaxClients(); i++)
2912 {
2913 if(i != ClientId)
2914 {
2915 Server()->SendPackMsg(pMsg: &Drop, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i);
2916 Server()->SendPackMsg(pMsg: &Info, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i);
2917 }
2918 }
2919 }
2920 else
2921 {
2922 SendSkinChange7(ClientId);
2923 }
2924
2925 Server()->ExpireServerInfo();
2926}
2927
2928void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId)
2929{
2930 if(m_pController->IsGamePaused())
2931 return;
2932
2933 CPlayer *pPlayer = m_apPlayers[ClientId];
2934
2935 auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) {
2936 return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000);
2937 };
2938
2939 if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmote, (int64_t)g_Config.m_SvEmoticonMsDelay))
2940 return;
2941
2942 CCharacter *pChr = pPlayer->GetCharacter();
2943
2944 // player needs a character to send emotes
2945 if(!pChr)
2946 return;
2947
2948 pPlayer->m_LastEmote = Server()->Tick();
2949 pPlayer->UpdatePlaytime();
2950
2951 // check if the global emoticon is prevented and emotes are only send to nearby players
2952 if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmoteGlobal, (int64_t)g_Config.m_SvGlobalEmoticonMsDelay))
2953 {
2954 for(int i = 0; i < MAX_CLIENTS; ++i)
2955 {
2956 if(m_apPlayers[i] && pChr->CanSnapCharacter(SnappingClient: i) && pChr->IsSnappingCharacterInView(SnappingClientId: i))
2957 {
2958 SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: i);
2959 }
2960 }
2961 }
2962 else
2963 {
2964 // else send emoticons to all players
2965 pPlayer->m_LastEmoteGlobal = Server()->Tick();
2966 SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: -1);
2967 }
2968
2969 if(g_Config.m_SvEmotionalTees == 1 && pPlayer->m_EyeEmoteEnabled)
2970 {
2971 int EmoteType = EMOTE_NORMAL;
2972 switch(pMsg->m_Emoticon)
2973 {
2974 case EMOTICON_EXCLAMATION:
2975 case EMOTICON_GHOST:
2976 case EMOTICON_QUESTION:
2977 case EMOTICON_WTF:
2978 EmoteType = EMOTE_SURPRISE;
2979 break;
2980 case EMOTICON_DOTDOT:
2981 case EMOTICON_DROP:
2982 case EMOTICON_ZZZ:
2983 EmoteType = EMOTE_BLINK;
2984 break;
2985 case EMOTICON_EYES:
2986 case EMOTICON_HEARTS:
2987 case EMOTICON_MUSIC:
2988 EmoteType = EMOTE_HAPPY;
2989 break;
2990 case EMOTICON_OOP:
2991 case EMOTICON_SORRY:
2992 case EMOTICON_SUSHI:
2993 EmoteType = EMOTE_PAIN;
2994 break;
2995 case EMOTICON_DEVILTEE:
2996 case EMOTICON_SPLATTEE:
2997 case EMOTICON_ZOMG:
2998 EmoteType = EMOTE_ANGRY;
2999 break;
3000 default:
3001 break;
3002 }
3003 pChr->SetEmote(Emote: EmoteType, Tick: Server()->Tick() + 2 * Server()->TickSpeed());
3004 }
3005}
3006
3007void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId)
3008{
3009 if(m_pController->IsGamePaused())
3010 return;
3011
3012 if(IsRunningKickOrSpecVote(ClientId) && GetDDRaceTeam(ClientId))
3013 {
3014 SendChatTarget(To: ClientId, pText: "You are running a vote please try again after the vote is done!");
3015 return;
3016 }
3017 CPlayer *pPlayer = m_apPlayers[ClientId];
3018 if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick())
3019 return;
3020 if(pPlayer->IsPaused())
3021 return;
3022
3023 CCharacter *pChr = pPlayer->GetCharacter();
3024 if(!pChr)
3025 return;
3026
3027 // Kill Protection
3028 int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
3029 if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
3030 {
3031 SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to kill, type /kill");
3032 return;
3033 }
3034
3035 pPlayer->m_LastKill = Server()->Tick();
3036 pPlayer->KillCharacter(Weapon: WEAPON_SELF);
3037 pPlayer->Respawn();
3038}
3039
3040void CGameContext::OnEnableSpectatorCountNetMessage(const CNetMsg_Cl_EnableSpectatorCount *pMsg, int ClientId)
3041{
3042 CPlayer *pPlayer = m_apPlayers[ClientId];
3043 if(!pPlayer)
3044 return;
3045
3046 pPlayer->m_EnableSpectatorCount = pMsg->m_Enable;
3047}
3048
3049void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId)
3050{
3051 CPlayer *pPlayer = m_apPlayers[ClientId];
3052
3053 if(pPlayer->m_IsReady)
3054 return;
3055
3056 pPlayer->m_LastChangeInfo = Server()->Tick();
3057
3058 // set start infos
3059 Server()->SetClientName(ClientId, pName: pMsg->m_pName);
3060 // trying to set client name can delete the player object, check if it still exists
3061 if(!m_apPlayers[ClientId])
3062 {
3063 return;
3064 }
3065 Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan);
3066 // trying to set client clan can delete the player object, check if it still exists
3067 if(!m_apPlayers[ClientId])
3068 {
3069 return;
3070 }
3071 Server()->SetClientCountry(ClientId, Country: pMsg->m_Country);
3072 str_copy(dst&: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin);
3073 pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
3074 pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
3075 pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
3076 if(!Server()->IsSixup(ClientId))
3077 pPlayer->m_TeeInfos.ToSixup();
3078
3079 // send clear vote options
3080 CNetMsg_Sv_VoteClearOptions ClearMsg;
3081 Server()->SendPackMsg(pMsg: &ClearMsg, Flags: MSGFLAG_VITAL, ClientId);
3082
3083 // begin sending vote options
3084 pPlayer->m_SendVoteIndex = 0;
3085
3086 // send tuning parameters to client
3087 SendTuningParams(ClientId, Zone: pPlayer->m_TuneZone);
3088
3089 // client is ready to enter
3090 pPlayer->m_IsReady = true;
3091 CNetMsg_Sv_ReadyToEnter ReadyMsg;
3092 Server()->SendPackMsg(pMsg: &ReadyMsg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId);
3093
3094 Server()->ExpireServerInfo();
3095}
3096
3097void CGameContext::ConTuneParam(IConsole::IResult *pResult, void *pUserData)
3098{
3099 CGameContext *pSelf = (CGameContext *)pUserData;
3100 const char *pParamName = pResult->GetString(Index: 0);
3101
3102 char aBuf[256];
3103 if(pResult->NumArguments() == 2)
3104 {
3105 float NewValue = pResult->GetFloat(Index: 1);
3106 if(pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue))
3107 {
3108 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f", pParamName, NewValue);
3109 pSelf->SendTuningParams(ClientId: -1);
3110 }
3111 else
3112 {
3113 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3114 }
3115 }
3116 else
3117 {
3118 float Value;
3119 if(pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &Value))
3120 {
3121 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f", pParamName, Value);
3122 }
3123 else
3124 {
3125 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3126 }
3127 }
3128 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3129}
3130
3131void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData)
3132{
3133 CGameContext *pSelf = (CGameContext *)pUserData;
3134 const char *pParamName = pResult->GetString(Index: 0);
3135 float OldValue;
3136
3137 char aBuf[256];
3138 if(!pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &OldValue))
3139 {
3140 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3141 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3142 return;
3143 }
3144
3145 float NewValue = absolute(a: OldValue - pResult->GetFloat(Index: 1)) < 0.0001f ? pResult->GetFloat(Index: 2) : pResult->GetFloat(Index: 1);
3146
3147 pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue);
3148 pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue);
3149
3150 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f", pParamName, NewValue);
3151 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3152 pSelf->SendTuningParams(ClientId: -1);
3153}
3154
3155void CGameContext::ConTuneReset(IConsole::IResult *pResult, void *pUserData)
3156{
3157 CGameContext *pSelf = (CGameContext *)pUserData;
3158 if(pResult->NumArguments())
3159 {
3160 const char *pParamName = pResult->GetString(Index: 0);
3161 float DefaultValue = 0.0f;
3162 char aBuf[256];
3163
3164 if(CTuningParams::DEFAULT.Get(pName: pParamName, pValue: &DefaultValue) && pSelf->GlobalTuning()->Set(pName: pParamName, Value: DefaultValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &DefaultValue))
3165 {
3166 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s reset to %.2f", pParamName, DefaultValue);
3167 pSelf->SendTuningParams(ClientId: -1);
3168 }
3169 else
3170 {
3171 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3172 }
3173 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3174 }
3175 else
3176 {
3177 pSelf->ResetTuning();
3178 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: "Tuning reset");
3179 }
3180}
3181
3182void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData)
3183{
3184 CGameContext *pSelf = (CGameContext *)pUserData;
3185 char aBuf[256];
3186 for(int i = 0; i < CTuningParams::Num(); i++)
3187 {
3188 float Value;
3189 pSelf->GlobalTuning()->Get(Index: i, pValue: &Value);
3190 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f", CTuningParams::Name(Index: i), Value);
3191 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3192 }
3193}
3194
3195void CGameContext::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
3196{
3197 CGameContext *pSelf = (CGameContext *)pUserData;
3198 int List = pResult->GetInteger(Index: 0);
3199 const char *pParamName = pResult->GetString(Index: 1);
3200 float NewValue = pResult->GetFloat(Index: 2);
3201
3202 if(List >= 0 && List < TuneZone::NUM)
3203 {
3204 char aBuf[256];
3205 if(pSelf->TuningList()[List].Set(pName: pParamName, Value: NewValue) && pSelf->TuningList()[List].Get(pName: pParamName, pValue: &NewValue))
3206 {
3207 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s in zone %d changed to %.2f", pParamName, List, NewValue);
3208 pSelf->SendTuningParams(ClientId: -1, Zone: List);
3209 }
3210 else
3211 {
3212 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3213 }
3214 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3215 }
3216}
3217
3218void CGameContext::ConTuneDumpZone(IConsole::IResult *pResult, void *pUserData)
3219{
3220 CGameContext *pSelf = (CGameContext *)pUserData;
3221 int List = pResult->GetInteger(Index: 0);
3222 char aBuf[256];
3223 if(List >= 0 && List < TuneZone::NUM)
3224 {
3225 for(int i = 0; i < CTuningParams::Num(); i++)
3226 {
3227 float Value;
3228 pSelf->TuningList()[List].Get(Index: i, pValue: &Value);
3229 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "zone %d: %s %.2f", List, CTuningParams::Name(Index: i), Value);
3230 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3231 }
3232 }
3233}
3234
3235void CGameContext::ConTuneResetZone(IConsole::IResult *pResult, void *pUserData)
3236{
3237 CGameContext *pSelf = (CGameContext *)pUserData;
3238 if(pResult->NumArguments())
3239 {
3240 int List = pResult->GetInteger(Index: 0);
3241 if(List >= 0 && List < TuneZone::NUM)
3242 {
3243 pSelf->TuningList()[List] = CTuningParams::DEFAULT;
3244 char aBuf[256];
3245 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Tunezone %d reset", List);
3246 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3247 pSelf->SendTuningParams(ClientId: -1, Zone: List);
3248 }
3249 }
3250 else
3251 {
3252 for(int i = 0; i < TuneZone::NUM; i++)
3253 {
3254 *(pSelf->TuningList() + i) = CTuningParams::DEFAULT;
3255 pSelf->SendTuningParams(ClientId: -1, Zone: i);
3256 }
3257 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: "All Tunezones reset");
3258 }
3259}
3260
3261void CGameContext::ConTuneSetZoneMsgEnter(IConsole::IResult *pResult, void *pUserData)
3262{
3263 CGameContext *pSelf = (CGameContext *)pUserData;
3264 if(pResult->NumArguments())
3265 {
3266 int List = pResult->GetInteger(Index: 0);
3267 if(List >= 0 && List < TuneZone::NUM)
3268 {
3269 str_copy(dst&: pSelf->m_aaZoneEnterMsg[List], src: pResult->GetString(Index: 1));
3270 }
3271 }
3272}
3273
3274void CGameContext::ConTuneSetZoneMsgLeave(IConsole::IResult *pResult, void *pUserData)
3275{
3276 CGameContext *pSelf = (CGameContext *)pUserData;
3277 if(pResult->NumArguments())
3278 {
3279 int List = pResult->GetInteger(Index: 0);
3280 if(List >= 0 && List < TuneZone::NUM)
3281 {
3282 str_copy(dst&: pSelf->m_aaZoneLeaveMsg[List], src: pResult->GetString(Index: 1));
3283 }
3284 }
3285}
3286
3287void CGameContext::ConMapbug(IConsole::IResult *pResult, void *pUserData)
3288{
3289 CGameContext *pSelf = (CGameContext *)pUserData;
3290
3291 if(pSelf->m_pController)
3292 {
3293 log_info("mapbugs", "can't add map bugs after the game started");
3294 return;
3295 }
3296
3297 const char *pMapBugName = pResult->GetString(Index: 0);
3298 switch(pSelf->m_MapBugs.Update(pBug: pMapBugName))
3299 {
3300 case EMapBugUpdate::OK:
3301 break;
3302 case EMapBugUpdate::OVERRIDDEN:
3303 log_info("mapbugs", "map-internal setting overridden by database");
3304 break;
3305 case EMapBugUpdate::NOTFOUND:
3306 log_info("mapbugs", "unknown map bug '%s', ignoring", pMapBugName);
3307 break;
3308 default:
3309 dbg_assert_failed("unreachable");
3310 }
3311}
3312
3313void CGameContext::ConSwitchOpen(IConsole::IResult *pResult, void *pUserData)
3314{
3315 CGameContext *pSelf = (CGameContext *)pUserData;
3316 int Switch = pResult->GetInteger(Index: 0);
3317
3318 if(in_range(a: Switch, upper: (int)pSelf->Switchers().size() - 1))
3319 {
3320 pSelf->Switchers()[Switch].m_Initial = false;
3321 char aBuf[256];
3322 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "switch %d opened by default", Switch);
3323 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3324 }
3325}
3326
3327void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData)
3328{
3329 CGameContext *pSelf = (CGameContext *)pUserData;
3330
3331 pSelf->m_pController->SetGamePaused(!pSelf->m_pController->IsGamePaused());
3332}
3333
3334void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData)
3335{
3336 CGameContext *pSelf = (CGameContext *)pUserData;
3337 pSelf->m_pController->ChangeMap(pToMap: pResult->GetString(Index: 0));
3338}
3339
3340void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData)
3341{
3342 CGameContext *pSelf = (CGameContext *)pUserData;
3343
3344 const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
3345 int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1;
3346 int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars;
3347
3348 if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5))
3349 return;
3350
3351 pSelf->m_pScore->RandomMap(ClientId, MinStars, MaxStars);
3352}
3353
3354void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData)
3355{
3356 CGameContext *pSelf = (CGameContext *)pUserData;
3357
3358 const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
3359 int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1;
3360 int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars;
3361
3362 if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5))
3363 return;
3364
3365 pSelf->m_pScore->RandomUnfinishedMap(ClientId, MinStars, MaxStars);
3366}
3367
3368void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData)
3369{
3370 CGameContext *pSelf = (CGameContext *)pUserData;
3371 if(pResult->NumArguments())
3372 pSelf->m_pController->DoWarmup(Seconds: pResult->GetInteger(Index: 0));
3373 else
3374 pSelf->m_pController->StartRound();
3375}
3376
3377static void UnescapeNewlines(char *pBuf)
3378{
3379 int i, j;
3380 for(i = 0, j = 0; pBuf[i]; i++, j++)
3381 {
3382 if(pBuf[i] == '\\' && pBuf[i + 1] == 'n')
3383 {
3384 pBuf[j] = '\n';
3385 i++;
3386 }
3387 else if(i != j)
3388 {
3389 pBuf[j] = pBuf[i];
3390 }
3391 }
3392 pBuf[j] = '\0';
3393}
3394
3395void CGameContext::ConServerAlert(IConsole::IResult *pResult, void *pUserData)
3396{
3397 CGameContext *pSelf = (CGameContext *)pUserData;
3398
3399 char aBuf[1024];
3400 str_copy(dst&: aBuf, src: pResult->GetString(Index: 0));
3401 UnescapeNewlines(pBuf: aBuf);
3402
3403 pSelf->SendServerAlert(pMessage: aBuf);
3404}
3405
3406void CGameContext::ConModAlert(IConsole::IResult *pResult, void *pUserData)
3407{
3408 CGameContext *pSelf = (CGameContext *)pUserData;
3409
3410 const int Victim = pResult->GetVictim();
3411 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
3412 {
3413 log_info("moderator_alert", "Client ID not found: %d", Victim);
3414 return;
3415 }
3416
3417 char aBuf[1024];
3418 str_copy(dst&: aBuf, src: pResult->GetString(Index: 1));
3419 UnescapeNewlines(pBuf: aBuf);
3420
3421 pSelf->SendModeratorAlert(ToClientId: Victim, pMessage: aBuf);
3422}
3423
3424void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData)
3425{
3426 CGameContext *pSelf = (CGameContext *)pUserData;
3427
3428 char aBuf[1024];
3429 str_copy(dst&: aBuf, src: pResult->GetString(Index: 0));
3430 UnescapeNewlines(pBuf: aBuf);
3431
3432 pSelf->SendBroadcast(pText: aBuf, ClientId: -1);
3433}
3434
3435void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData)
3436{
3437 CGameContext *pSelf = (CGameContext *)pUserData;
3438 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: pResult->GetString(Index: 0));
3439}
3440
3441void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData)
3442{
3443 CGameContext *pSelf = (CGameContext *)pUserData;
3444 int Team = pResult->GetInteger(Index: 1);
3445 if(!pSelf->m_pController->IsValidTeam(Team))
3446 {
3447 log_info("server", "Invalid Team: %d", Team);
3448 return;
3449 }
3450
3451 int ClientId = std::clamp(val: pResult->GetInteger(Index: 0), lo: 0, hi: (int)MAX_CLIENTS - 1);
3452 int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(Index: 2) : 0;
3453 if(!pSelf->m_apPlayers[ClientId])
3454 return;
3455
3456 char aBuf[256];
3457 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "moved client %d to the %s", ClientId, pSelf->m_pController->GetTeamName(Team));
3458 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3459
3460 pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: false); // reset /spec and /pause to allow rejoin
3461 pSelf->m_apPlayers[ClientId]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60;
3462 pSelf->m_pController->DoTeamChange(pPlayer: pSelf->m_apPlayers[ClientId], Team, DoChatMsg: true);
3463 if(Team == TEAM_SPECTATORS)
3464 pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: true);
3465}
3466
3467void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData)
3468{
3469 CGameContext *pSelf = (CGameContext *)pUserData;
3470 int Team = pResult->GetInteger(Index: 0);
3471 if(!pSelf->m_pController->IsValidTeam(Team))
3472 {
3473 log_info("server", "Invalid Team: %d", Team);
3474 return;
3475 }
3476
3477 char aBuf[256];
3478 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "All players were moved to the %s", pSelf->m_pController->GetTeamName(Team));
3479 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf);
3480
3481 for(auto &pPlayer : pSelf->m_apPlayers)
3482 if(pPlayer)
3483 pSelf->m_pController->DoTeamChange(pPlayer, Team, DoChatMsg: false);
3484}
3485
3486void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData)
3487{
3488 CGameContext *pSelf = (CGameContext *)pUserData;
3489 for(int i = 0; i < MAX_CLIENTS; i++)
3490 {
3491 if(!pSelf->GetPlayerChar(ClientId: i))
3492 continue;
3493
3494 CCharacter *pChar = pSelf->GetPlayerChar(ClientId: i);
3495
3496 // Save the tee individually
3497 pSelf->m_apSavedTees[i] = new CSaveHotReloadTee();
3498 pSelf->m_apSavedTees[i]->Save(pChr: pChar, AddPenalty: false);
3499
3500 // Save the team state
3501 pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(ClientId: i);
3502 if(pSelf->m_aTeamMapping[i] == TEAM_SUPER)
3503 pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper;
3504
3505 if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]])
3506 continue;
3507
3508 pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]] = new CSaveTeam();
3509 pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]->Save(pGameServer: pSelf, Team: pSelf->m_aTeamMapping[i], Dry: true, Force: true);
3510 }
3511 pSelf->Server()->ReloadMap();
3512}
3513
3514void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData)
3515{
3516 CGameContext *pSelf = (CGameContext *)pUserData;
3517 const char *pDescription = pResult->GetString(Index: 0);
3518 const char *pCommand = pResult->GetString(Index: 1);
3519
3520 pSelf->AddVote(pDescription, pCommand);
3521}
3522
3523void CGameContext::AddVote(const char *pDescription, const char *pCommand)
3524{
3525 if(m_NumVoteOptions == MAX_VOTE_OPTIONS)
3526 {
3527 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "maximum number of vote options reached");
3528 return;
3529 }
3530
3531 // check for valid option
3532 if(!Console()->LineIsValid(pStr: pCommand) || str_length(str: pCommand) >= VOTE_CMD_LENGTH)
3533 {
3534 char aBuf[256];
3535 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid command '%s'", pCommand);
3536 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3537 return;
3538 }
3539 while(*pDescription == ' ')
3540 pDescription++;
3541 if(str_length(str: pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0)
3542 {
3543 char aBuf[256];
3544 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid option '%s'", pDescription);
3545 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3546 return;
3547 }
3548
3549 // check for duplicate entry
3550 CVoteOptionServer *pOption = m_pVoteOptionFirst;
3551 while(pOption)
3552 {
3553 if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0)
3554 {
3555 char aBuf[256];
3556 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' already exists", pDescription);
3557 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3558 return;
3559 }
3560 pOption = pOption->m_pNext;
3561 }
3562
3563 // add the option
3564 ++m_NumVoteOptions;
3565 int Len = str_length(str: pCommand);
3566
3567 pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer));
3568 pOption->m_pNext = nullptr;
3569 pOption->m_pPrev = m_pVoteOptionLast;
3570 if(pOption->m_pPrev)
3571 pOption->m_pPrev->m_pNext = pOption;
3572 m_pVoteOptionLast = pOption;
3573 if(!m_pVoteOptionFirst)
3574 m_pVoteOptionFirst = pOption;
3575
3576 str_copy(dst&: pOption->m_aDescription, src: pDescription);
3577 str_copy(dst: pOption->m_aCommand, src: pCommand, dst_size: Len + 1);
3578}
3579
3580void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData)
3581{
3582 CGameContext *pSelf = (CGameContext *)pUserData;
3583 const char *pDescription = pResult->GetString(Index: 0);
3584
3585 // check for valid option
3586 CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
3587 while(pOption)
3588 {
3589 if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0)
3590 break;
3591 pOption = pOption->m_pNext;
3592 }
3593 if(!pOption)
3594 {
3595 char aBuf[256];
3596 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' does not exist", pDescription);
3597 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3598 return;
3599 }
3600
3601 // start reloading vote option list
3602 // clear vote options
3603 CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
3604 pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1);
3605
3606 // reset sending of vote options
3607 for(auto &pPlayer : pSelf->m_apPlayers)
3608 {
3609 if(pPlayer)
3610 pPlayer->m_SendVoteIndex = 0;
3611 }
3612
3613 // TODO: improve this
3614 // remove the option
3615 --pSelf->m_NumVoteOptions;
3616
3617 CHeap *pVoteOptionHeap = new CHeap();
3618 CVoteOptionServer *pVoteOptionFirst = nullptr;
3619 CVoteOptionServer *pVoteOptionLast = nullptr;
3620 int NumVoteOptions = pSelf->m_NumVoteOptions;
3621 for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext)
3622 {
3623 if(pSrc == pOption)
3624 continue;
3625
3626 // copy option
3627 int Len = str_length(str: pSrc->m_aCommand);
3628 CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer));
3629 pDst->m_pNext = nullptr;
3630 pDst->m_pPrev = pVoteOptionLast;
3631 if(pDst->m_pPrev)
3632 pDst->m_pPrev->m_pNext = pDst;
3633 pVoteOptionLast = pDst;
3634 if(!pVoteOptionFirst)
3635 pVoteOptionFirst = pDst;
3636
3637 str_copy(dst&: pDst->m_aDescription, src: pSrc->m_aDescription);
3638 str_copy(dst: pDst->m_aCommand, src: pSrc->m_aCommand, dst_size: Len + 1);
3639 }
3640
3641 // clean up
3642 delete pSelf->m_pVoteOptionHeap;
3643 pSelf->m_pVoteOptionHeap = pVoteOptionHeap;
3644 pSelf->m_pVoteOptionFirst = pVoteOptionFirst;
3645 pSelf->m_pVoteOptionLast = pVoteOptionLast;
3646 pSelf->m_NumVoteOptions = NumVoteOptions;
3647}
3648
3649void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData)
3650{
3651 CGameContext *pSelf = (CGameContext *)pUserData;
3652 const char *pType = pResult->GetString(Index: 0);
3653 const char *pValue = pResult->GetString(Index: 1);
3654 const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(Index: 2)[0] ? pResult->GetString(Index: 2) : "No reason given";
3655 char aBuf[128] = {0};
3656
3657 if(str_comp_nocase(a: pType, b: "option") == 0)
3658 {
3659 CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
3660 while(pOption)
3661 {
3662 if(str_comp_nocase(a: pValue, b: pOption->m_aDescription) == 0)
3663 {
3664 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "authorized player forced server option '%s' (%s)", pValue, pReason);
3665 pSelf->SendChatTarget(To: -1, pText: aBuf, VersionFlags: FLAG_SIX);
3666 pSelf->m_VoteCreator = pResult->m_ClientId;
3667 pSelf->Console()->ExecuteLine(pStr: pOption->m_aCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
3668 break;
3669 }
3670
3671 pOption = pOption->m_pNext;
3672 }
3673
3674 if(!pOption)
3675 {
3676 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' isn't an option on this server", pValue);
3677 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3678 return;
3679 }
3680 }
3681 else if(str_comp_nocase(a: pType, b: "kick") == 0)
3682 {
3683 int KickId = str_toint(str: pValue);
3684 if(KickId < 0 || KickId >= MAX_CLIENTS || !pSelf->m_apPlayers[KickId])
3685 {
3686 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "Invalid client id to kick");
3687 return;
3688 }
3689
3690 if(!g_Config.m_SvVoteKickBantime)
3691 {
3692 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "kick %d %s", KickId, pReason);
3693 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3694 }
3695 else
3696 {
3697 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);
3698 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3699 }
3700 }
3701 else if(str_comp_nocase(a: pType, b: "spectate") == 0)
3702 {
3703 int SpectateId = str_toint(str: pValue);
3704 if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateId] || pSelf->m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
3705 {
3706 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "Invalid client id to move");
3707 return;
3708 }
3709
3710 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(ClientId: SpectateId), pReason);
3711 pSelf->SendChatTarget(To: -1, pText: aBuf);
3712 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "set_team %d -1 %d", SpectateId, g_Config.m_SvVoteSpectateRejoindelay);
3713 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3714 }
3715}
3716
3717void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData)
3718{
3719 CGameContext *pSelf = (CGameContext *)pUserData;
3720
3721 CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
3722 pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1);
3723 pSelf->m_pVoteOptionHeap->Reset();
3724 pSelf->m_pVoteOptionFirst = nullptr;
3725 pSelf->m_pVoteOptionLast = nullptr;
3726 pSelf->m_NumVoteOptions = 0;
3727
3728 // reset sending of vote options
3729 for(auto &pPlayer : pSelf->m_apPlayers)
3730 {
3731 if(pPlayer)
3732 pPlayer->m_SendVoteIndex = 0;
3733 }
3734}
3735
3736struct CMapNameItem
3737{
3738 char m_aName[IO_MAX_PATH_LENGTH - 4];
3739 bool m_IsDirectory;
3740
3741 static bool CompareFilenameAscending(const CMapNameItem Lhs, const CMapNameItem Rhs)
3742 {
3743 if(str_comp(a: Rhs.m_aName, b: "..") == 0)
3744 return false;
3745 if(str_comp(a: Lhs.m_aName, b: "..") == 0)
3746 return true;
3747 if(Lhs.m_IsDirectory != Rhs.m_IsDirectory)
3748 return Lhs.m_IsDirectory;
3749 return str_comp_filenames(a: Lhs.m_aName, b: Rhs.m_aName) < 0;
3750 }
3751};
3752
3753void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData)
3754{
3755 CGameContext *pSelf = (CGameContext *)pUserData;
3756
3757 std::vector<CMapNameItem> vMapList;
3758 const char *pDirectory = pResult->GetString(Index: 0);
3759
3760 // Don't allow moving to parent directories
3761 if(str_find_nocase(haystack: pDirectory, needle: ".."))
3762 return;
3763
3764 char aPath[IO_MAX_PATH_LENGTH] = "maps/";
3765 str_append(dst&: aPath, src: pDirectory);
3766 pSelf->Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: aPath, pfnCallback: MapScan, pUser: &vMapList);
3767 std::sort(first: vMapList.begin(), last: vMapList.end(), comp: CMapNameItem::CompareFilenameAscending);
3768
3769 for(auto &Item : vMapList)
3770 {
3771 if(!str_comp(a: Item.m_aName, b: "..") && (!str_comp(a: aPath, b: "maps/")))
3772 continue;
3773
3774 char aDescription[VOTE_DESC_LENGTH];
3775 str_format(buffer: aDescription, buffer_size: sizeof(aDescription), format: "%s: %s%s", Item.m_IsDirectory ? "Directory" : "Map", Item.m_aName, Item.m_IsDirectory ? "/" : "");
3776
3777 char aCommand[VOTE_CMD_LENGTH];
3778 char aOptionEscaped[IO_MAX_PATH_LENGTH * 2];
3779 char *pDst = aOptionEscaped;
3780 str_escape(dst: &pDst, src: Item.m_aName, end: aOptionEscaped + sizeof(aOptionEscaped));
3781
3782 char aDirectory[IO_MAX_PATH_LENGTH] = "";
3783 if(pResult->NumArguments())
3784 str_copy(dst&: aDirectory, src: pDirectory);
3785
3786 if(!str_comp(a: Item.m_aName, b: ".."))
3787 {
3788 dbg_assert(fs_parent_dir(aDirectory) == 0, "Parent folder vote selected but there is no parent folder");
3789 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"", aDirectory);
3790 }
3791 else if(Item.m_IsDirectory)
3792 {
3793 str_append(dst&: aDirectory, src: "/");
3794 str_append(dst&: aDirectory, src: aOptionEscaped);
3795
3796 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"", aDirectory);
3797 }
3798 else
3799 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "change_map \"%s%s%s\"", pDirectory, pDirectory[0] == '\0' ? "" : "/", aOptionEscaped);
3800
3801 pSelf->AddVote(pDescription: aDescription, pCommand: aCommand);
3802 }
3803
3804 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "added maps to votes");
3805}
3806
3807int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData)
3808{
3809 if((!IsDir && !str_endswith(str: pName, suffix: ".map")) || !str_comp(a: pName, b: "."))
3810 return 0;
3811
3812 CMapNameItem Item;
3813 Item.m_IsDirectory = IsDir;
3814 if(!IsDir)
3815 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pName, truncation_len: str_length(str: pName) - str_length(str: ".map"));
3816 else
3817 str_copy(dst&: Item.m_aName, src: pName);
3818 static_cast<std::vector<CMapNameItem> *>(pUserData)->push_back(x: Item);
3819
3820 return 0;
3821}
3822
3823void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData)
3824{
3825 CGameContext *pSelf = (CGameContext *)pUserData;
3826
3827 if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "yes") == 0)
3828 pSelf->ForceVote(Success: true);
3829 else if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "no") == 0)
3830 pSelf->ForceVote(Success: false);
3831}
3832
3833void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData)
3834{
3835 CGameContext *pSelf = (CGameContext *)pUserData;
3836
3837 int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : 0;
3838 static const int s_EntriesPerPage = 20;
3839 const int Start = Page * s_EntriesPerPage;
3840 const int End = (Page + 1) * s_EntriesPerPage;
3841
3842 char aBuf[512];
3843 int Count = 0;
3844 for(CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; pOption; pOption = pOption->m_pNext, Count++)
3845 {
3846 if(Count < Start || Count >= End)
3847 {
3848 continue;
3849 }
3850
3851 str_copy(dst&: aBuf, src: "add_vote \"");
3852 char *pDst = aBuf + str_length(str: aBuf);
3853 str_escape(dst: &pDst, src: pOption->m_aDescription, end: aBuf + sizeof(aBuf));
3854 str_append(dst&: aBuf, src: "\" \"");
3855 pDst = aBuf + str_length(str: aBuf);
3856 str_escape(dst: &pDst, src: pOption->m_aCommand, end: aBuf + sizeof(aBuf));
3857 str_append(dst&: aBuf, src: "\"");
3858
3859 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes", pStr: aBuf);
3860 }
3861 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d %s, showing entries %d - %d", Count, Count == 1 ? "vote" : "votes", Start, End - 1);
3862 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes", pStr: aBuf);
3863}
3864
3865void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3866{
3867 pfnCallback(pResult, pCallbackUserData);
3868 if(pResult->NumArguments())
3869 {
3870 CGameContext *pSelf = (CGameContext *)pUserData;
3871 pSelf->SendMotd(ClientId: -1);
3872 }
3873}
3874
3875void CGameContext::ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3876{
3877 pfnCallback(pResult, pCallbackUserData);
3878 if(pResult->NumArguments())
3879 {
3880 CGameContext *pSelf = (CGameContext *)pUserData;
3881 pSelf->SendSettings(ClientId: -1);
3882 }
3883}
3884
3885void CGameContext::ConchainPracticeByDefaultUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3886{
3887 const int OldValue = g_Config.m_SvPracticeByDefault;
3888 pfnCallback(pResult, pCallbackUserData);
3889
3890 if(pResult->NumArguments() && g_Config.m_SvTestingCommands)
3891 {
3892 CGameContext *pSelf = (CGameContext *)pUserData;
3893
3894 if(pSelf->m_pController == nullptr)
3895 return;
3896
3897 const int Enable = pResult->GetInteger(Index: 0);
3898 if(Enable == OldValue)
3899 return;
3900
3901 char aBuf[256];
3902 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Practice is %s by default.", Enable ? "enabled" : "disabled");
3903 if(Enable)
3904 str_append(dst&: aBuf, src: " Join a team and /unpractice to turn it off for your team.");
3905
3906 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf);
3907
3908 for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
3909 {
3910 if(Team == TEAM_FLOCK || pSelf->m_pController->Teams().TeamSize(Team) == 0)
3911 {
3912 pSelf->m_pController->Teams().SetPractice(Team, Enabled: Enable);
3913 }
3914 }
3915 }
3916}
3917
3918void CGameContext::OnConsoleInit()
3919{
3920 m_pServer = Kernel()->RequestInterface<IServer>();
3921 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
3922 m_pConfig = m_pConfigManager->Values();
3923 m_pConsole = Kernel()->RequestInterface<IConsole>();
3924 m_pEngine = Kernel()->RequestInterface<IEngine>();
3925 m_pStorage = Kernel()->RequestInterface<IStorage>();
3926
3927 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");
3928 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");
3929 Console()->Register(pName: "tune_reset", pParams: "?s[tuning]", Flags: CFGFLAG_SERVER, pfnFunc: ConTuneReset, pUser: this, pHelp: "Reset all or one tuning variable to default");
3930 Console()->Register(pName: "tunes", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConTunes, pUser: this, pHelp: "List all tuning variables and their values");
3931 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");
3932 Console()->Register(pName: "tune_zone_dump", pParams: "i[zone]", Flags: CFGFLAG_SERVER, pfnFunc: ConTuneDumpZone, pUser: this, pHelp: "Dump zone tuning in zone x");
3933 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");
3934 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");
3935 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");
3936 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)");
3937 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)");
3938 Console()->Register(pName: "pause_game", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConPause, pUser: this, pHelp: "Pause/unpause game");
3939 Console()->Register(pName: "change_map", pParams: "r[map]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConChangeMap, pUser: this, pHelp: "Change map");
3940 Console()->Register(pName: "random_map", pParams: "?i[stars] ?i[max stars]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRandomMap, pUser: this, pHelp: "Random map");
3941 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");
3942 Console()->Register(pName: "restart", pParams: "?i[seconds]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRestart, pUser: this, pHelp: "Restart in x seconds (0 = abort)");
3943 Console()->Register(pName: "server_alert", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConServerAlert, pUser: this, pHelp: "Send a server alert message to all players");
3944 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");
3945 Console()->Register(pName: "broadcast", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConBroadcast, pUser: this, pHelp: "Broadcast message");
3946 Console()->Register(pName: "say", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConSay, pUser: this, pHelp: "Say in chat");
3947 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)");
3948 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)");
3949 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");
3950 Console()->Register(pName: "reload_censorlist", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConReloadCensorlist, pUser: this, pHelp: "Reload the censorlist");
3951
3952 Console()->Register(pName: "add_vote", pParams: "s[name] r[command]", Flags: CFGFLAG_SERVER, pfnFunc: ConAddVote, pUser: this, pHelp: "Add a voting option");
3953 Console()->Register(pName: "remove_vote", pParams: "r[name]", Flags: CFGFLAG_SERVER, pfnFunc: ConRemoveVote, pUser: this, pHelp: "remove a voting option");
3954 Console()->Register(pName: "force_vote", pParams: "s[name] s[command] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConForceVote, pUser: this, pHelp: "Force a voting option");
3955 Console()->Register(pName: "clear_votes", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConClearVotes, pUser: this, pHelp: "Clears the voting options");
3956 Console()->Register(pName: "add_map_votes", pParams: "?s[directory]", Flags: CFGFLAG_SERVER, pfnFunc: ConAddMapVotes, pUser: this, pHelp: "Automatically adds voting options for all maps");
3957 Console()->Register(pName: "vote", pParams: "r['yes'|'no']", Flags: CFGFLAG_SERVER, pfnFunc: ConVote, pUser: this, pHelp: "Force a vote to yes/no");
3958 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)");
3959 Console()->Register(pName: "dump_antibot", pParams: "", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConDumpAntibot, pUser: this, pHelp: "Dumps the antibot status");
3960 Console()->Register(pName: "antibot", pParams: "r[command]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConAntibot, pUser: this, pHelp: "Sends a command to the antibot");
3961
3962 Console()->Chain(pName: "sv_motd", pfnChainFunc: ConchainSpecialMotdupdate, pUser: this);
3963
3964 Console()->Chain(pName: "sv_vote_kick", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3965 Console()->Chain(pName: "sv_vote_kick_min", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3966 Console()->Chain(pName: "sv_vote_spectate", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3967 Console()->Chain(pName: "sv_spectator_slots", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3968
3969 RegisterDDRaceCommands();
3970 RegisterChatCommands();
3971}
3972
3973void CGameContext::RegisterDDRaceCommands()
3974{
3975 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");
3976 Console()->Register(pName: "totele", pParams: "i[number]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i");
3977 Console()->Register(pName: "totelecp", pParams: "i[number]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i");
3978 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)");
3979 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)");
3980 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)");
3981 Console()->Register(pName: "shotgun", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConShotgun, pUser: this, pHelp: "Gives a shotgun to you");
3982 Console()->Register(pName: "grenade", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGrenade, pUser: this, pHelp: "Gives a grenade launcher to you");
3983 Console()->Register(pName: "laser", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you");
3984 Console()->Register(pName: "rifle", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you");
3985 Console()->Register(pName: "jetpack", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConJetpack, pUser: this, pHelp: "Gives jetpack to you");
3986 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");
3987 Console()->Register(pName: "weapons", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConWeapons, pUser: this, pHelp: "Gives all weapons to you");
3988 Console()->Register(pName: "unshotgun", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnShotgun, pUser: this, pHelp: "Removes the shotgun from you");
3989 Console()->Register(pName: "ungrenade", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you");
3990 Console()->Register(pName: "unlaser", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you");
3991 Console()->Register(pName: "unrifle", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you");
3992 Console()->Register(pName: "unjetpack", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnJetpack, pUser: this, pHelp: "Removes the jetpack from you");
3993 Console()->Register(pName: "unweapons", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnWeapons, pUser: this, pHelp: "Removes all weapons from you");
3994 Console()->Register(pName: "ninja", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConNinja, pUser: this, pHelp: "Makes you a ninja");
3995 Console()->Register(pName: "unninja", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnNinja, pUser: this, pHelp: "Removes ninja from you");
3996 Console()->Register(pName: "super", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSuper, pUser: this, pHelp: "Makes you super");
3997 Console()->Register(pName: "unsuper", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConUnSuper, pUser: this, pHelp: "Removes super from you");
3998 Console()->Register(pName: "invincible", pParams: "?i['0'|'1']", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToggleInvincible, pUser: this, pHelp: "Toggles invincible mode");
3999 Console()->Register(pName: "infinite_jump", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessJump, pUser: this, pHelp: "Gives you infinite jump");
4000 Console()->Register(pName: "uninfinite_jump", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you");
4001 Console()->Register(pName: "endless_hook", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessHook, pUser: this, pHelp: "Gives you endless hook");
4002 Console()->Register(pName: "unendless_hook", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you");
4003 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)");
4004 Console()->Register(pName: "solo", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSolo, pUser: this, pHelp: "Puts you into solo part");
4005 Console()->Register(pName: "unsolo", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnSolo, pUser: this, pHelp: "Puts you out of solo part");
4006 Console()->Register(pName: "freeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConFreeze, pUser: this, pHelp: "Puts you into freeze");
4007 Console()->Register(pName: "unfreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnfreeze, pUser: this, pHelp: "Puts you out of freeze");
4008 Console()->Register(pName: "deep", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConDeep, pUser: this, pHelp: "Puts you into deep freeze");
4009 Console()->Register(pName: "undeep", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnDeep, pUser: this, pHelp: "Puts you out of deep freeze");
4010 Console()->Register(pName: "livefreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLiveFreeze, pUser: this, pHelp: "Makes you live frozen");
4011 Console()->Register(pName: "unlivefreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze");
4012 Console()->Register(pName: "left", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoLeft, pUser: this, pHelp: "Makes you move 1 tile left");
4013 Console()->Register(pName: "right", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoRight, pUser: this, pHelp: "Makes you move 1 tile right");
4014 Console()->Register(pName: "up", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoUp, pUser: this, pHelp: "Makes you move 1 tile up");
4015 Console()->Register(pName: "down", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoDown, pUser: this, pHelp: "Makes you move 1 tile down");
4016
4017 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");
4018 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");
4019 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");
4020 Console()->Register(pName: "force_unpause", pParams: "v[id]", Flags: CFGFLAG_SERVER, pfnFunc: ConForcePause, pUser: this, pHelp: "Set force-pause timer of i to 0.");
4021
4022 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");
4023 Console()->Register(pName: "uninvite", pParams: "v[id] i[team]", Flags: CFGFLAG_SERVER, pfnFunc: ConUninvite, pUser: this, pHelp: "Uninvite player from team");
4024
4025 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>'");
4026 Console()->Register(pName: "muteid", pParams: "v[id] i[seconds] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConMuteId, pUser: this, pHelp: "Mute player with client ID");
4027 Console()->Register(pName: "muteip", pParams: "s[ip] i[seconds] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConMuteIp, pUser: this, pHelp: "Mute player with IP address");
4028 Console()->Register(pName: "unmute", pParams: "i[index]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmute, pUser: this, pHelp: "Unmute player with list index");
4029 Console()->Register(pName: "unmuteid", pParams: "v[id]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteId, pUser: this, pHelp: "Unmute player with client ID");
4030 Console()->Register(pName: "unmuteip", pParams: "s[ip]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteIp, pUser: this, pHelp: "Unmute player with IP address");
4031 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)");
4032
4033 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>'");
4034 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");
4035 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");
4036 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");
4037 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");
4038 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");
4039 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)");
4040
4041 Console()->Register(pName: "moderate", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConModerate, pUser: this, pHelp: "Enables/disables active moderator mode for the player");
4042 Console()->Register(pName: "vote_no", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConVoteNo, pUser: this, pHelp: "Same as \"vote no\"");
4043 Console()->Register(pName: "save_dry", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConDrySave, pUser: this, pHelp: "Dump the current savestring");
4044 Console()->Register(pName: "dump_log", pParams: "?i[seconds]", Flags: CFGFLAG_SERVER, pfnFunc: ConDumpLog, pUser: this, pHelp: "Show logs of the last i seconds");
4045
4046 Console()->Chain(pName: "sv_practice_by_default", pfnChainFunc: ConchainPracticeByDefaultUpdate, pUser: this);
4047}
4048
4049void CGameContext::RegisterChatCommands()
4050{
4051 Console()->Register(pName: "credits", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCredits, pUser: this, pHelp: "Shows the credits of the DDNet mod");
4052 Console()->Register(pName: "rules", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConRules, pUser: this, pHelp: "Shows the server rules");
4053 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");
4054 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");
4055 Console()->Register(pName: "settings", pParams: "?s[configname]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSettings, pUser: this, pHelp: "Shows gameplay information for this server");
4056 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");
4057 Console()->Register(pName: "info", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConInfo, pUser: this, pHelp: "Shows info about this server");
4058 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");
4059 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)");
4060 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)");
4061 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)");
4062 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)");
4063 Console()->Register(pName: "pause", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePause, pUser: this, pHelp: "Toggles pause");
4064 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)");
4065 Console()->Register(pName: "pausevoted", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePauseVoted, pUser: this, pHelp: "Toggles pause on the currently voted player");
4066 Console()->Register(pName: "specvoted", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConToggleSpecVoted, pUser: this, pHelp: "Toggles spec on the currently voted player");
4067 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)");
4068 Console()->Register(pName: "whispers", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhispers, pUser: this, pHelp: "Toggle receiving whispers");
4069 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)");
4070 Console()->Register(pName: "timeout", pParams: "?s[code]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimeout, pUser: this, pHelp: "Set timeout protection code s");
4071 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");
4072 Console()->Register(pName: "unpractice", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConUnPractice, pUser: this, pHelp: "Kills team and disables practice mode");
4073 Console()->Register(pName: "practicecmdlist", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPracticeCmdList, pUser: this, pHelp: "List all commands that are available in practice mode");
4074 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");
4075 Console()->Register(pName: "cancelswap", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCancelSwap, pUser: this, pHelp: "Cancel your swap request");
4076 Console()->Register(pName: "save", pParams: "?r[code]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSave, pUser: this, pHelp: "Save team with code r.");
4077 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");
4078 Console()->Register(pName: "map", pParams: "?r[map]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConMap, pUser: this, pHelp: "Vote a map by name");
4079
4080 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)");
4081 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)");
4082
4083 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)");
4084 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)");
4085 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)");
4086 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)");
4087 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)");
4088 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)");
4089 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)");
4090 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)");
4091 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");
4092
4093 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)");
4094 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");
4095 Console()->Register(pName: "unlock", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConUnlock, pUser: this, pHelp: "Unlock a team");
4096 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");
4097 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");
4098 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.");
4099
4100 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");
4101 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");
4102 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");
4103 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");
4104 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)");
4105 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");
4106 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");
4107 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");
4108 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)");
4109 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)");
4110 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)");
4111 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");
4112 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");
4113 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");
4114 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");
4115 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");
4116 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");
4117 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");
4118 Console()->Register(pName: "totele", pParams: "i[number]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i");
4119 Console()->Register(pName: "totelecp", pParams: "i[number]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i");
4120 Console()->Register(pName: "unsolo", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnSolo, pUser: this, pHelp: "Puts you out of solo part");
4121 Console()->Register(pName: "solo", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSolo, pUser: this, pHelp: "Puts you into solo part");
4122 Console()->Register(pName: "undeep", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnDeep, pUser: this, pHelp: "Puts you out of deep freeze");
4123 Console()->Register(pName: "deep", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeDeep, pUser: this, pHelp: "Puts you into deep freeze");
4124 Console()->Register(pName: "unlivefreeze", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze");
4125 Console()->Register(pName: "livefreeze", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLiveFreeze, pUser: this, pHelp: "Makes you live frozen");
4126 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)");
4127 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)");
4128 Console()->Register(pName: "shotgun", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeShotgun, pUser: this, pHelp: "Gives a shotgun to you");
4129 Console()->Register(pName: "grenade", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeGrenade, pUser: this, pHelp: "Gives a grenade launcher to you");
4130 Console()->Register(pName: "laser", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you");
4131 Console()->Register(pName: "rifle", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you");
4132 Console()->Register(pName: "jetpack", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeJetpack, pUser: this, pHelp: "Gives jetpack to you");
4133 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");
4134 Console()->Register(pName: "weapons", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeWeapons, pUser: this, pHelp: "Gives all weapons to you");
4135 Console()->Register(pName: "unshotgun", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnShotgun, pUser: this, pHelp: "Removes the shotgun from you");
4136 Console()->Register(pName: "ungrenade", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you");
4137 Console()->Register(pName: "unlaser", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you");
4138 Console()->Register(pName: "unrifle", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you");
4139 Console()->Register(pName: "unjetpack", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnJetpack, pUser: this, pHelp: "Removes the jetpack from you");
4140 Console()->Register(pName: "unweapons", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnWeapons, pUser: this, pHelp: "Removes all weapons from you");
4141 Console()->Register(pName: "ninja", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeNinja, pUser: this, pHelp: "Makes you a ninja");
4142 Console()->Register(pName: "unninja", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnNinja, pUser: this, pHelp: "Removes ninja from you");
4143 Console()->Register(pName: "infjump", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessJump, pUser: this, pHelp: "Gives you infinite jump");
4144 Console()->Register(pName: "uninfjump", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you");
4145 Console()->Register(pName: "endless", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessHook, pUser: this, pHelp: "Gives you endless hook");
4146 Console()->Register(pName: "unendless", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you");
4147 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)");
4148 Console()->Register(pName: "invincible", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleInvincible, pUser: this, pHelp: "Toggles invincible mode");
4149 Console()->Register(pName: "collision", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleCollision, pUser: this, pHelp: "Toggles collision");
4150 Console()->Register(pName: "hookcollision", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHookCollision, pUser: this, pHelp: "Toggles hook collision");
4151 Console()->Register(pName: "hitothers", pParams: "?s['all'|'hammer'|'shotgun'|'grenade'|'laser']", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHitOthers, pUser: this, pHelp: "Toggles hit others");
4152
4153 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)");
4154}
4155
4156void CGameContext::OnInit(const void *pPersistentData)
4157{
4158 const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData;
4159
4160 m_pServer = Kernel()->RequestInterface<IServer>();
4161 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
4162 m_pConfig = m_pConfigManager->Values();
4163 m_pConsole = Kernel()->RequestInterface<IConsole>();
4164 m_pEngine = Kernel()->RequestInterface<IEngine>();
4165 m_pStorage = Kernel()->RequestInterface<IStorage>();
4166 m_pAntibot = Kernel()->RequestInterface<IAntibot>();
4167 m_World.SetGameServer(this);
4168 m_Events.SetGameServer(this);
4169
4170 m_GameUuid = RandomUuid();
4171 Console()->SetGetVictimsCommandCallback(pfnCallback: ClientsForVictim, pUser: this);
4172 Console()->SetTeeHistorianCommandCallback(pfnCallback: CommandCallback, pUser: this);
4173
4174 uint64_t aSeed[2];
4175 secure_random_fill(bytes: aSeed, length: sizeof(aSeed));
4176 m_Prng.Seed(aSeed);
4177 m_World.m_Core.m_pPrng = &m_Prng;
4178
4179 DeleteTempfile();
4180
4181 for(int i = 0; i < NUM_NETOBJTYPES; i++)
4182 {
4183 Server()->SnapSetStaticsize(ItemType: i, Size: m_NetObjHandler.GetObjSize(Type: i));
4184 }
4185
4186 // HACK: only set static size for items, which were available in the first 0.7 release
4187 // so new items don't break the snapshot delta
4188 static const int OLD_NUM_NETOBJTYPES = 23;
4189 for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++)
4190 {
4191 Server()->SnapSetStaticsize7(ItemType: i, Size: m_NetObjHandler7.GetObjSize(Type: i));
4192 }
4193
4194 m_Layers.Init(pMap: Map(), GameOnly: false, InitializeTilemapSkip: false);
4195 m_Collision.Init(pLayers: &m_Layers);
4196 m_World.Init(pCollision: &m_Collision, pTuningList: m_aTuningList);
4197 m_MapBugs = CMapBugs::Create(pName: Map()->BaseName(), Size: Map()->Size(), Sha256: Map()->Sha256());
4198
4199 // Reset Tunezones
4200 for(int i = 0; i < TuneZone::NUM; i++)
4201 {
4202 TuningList()[i] = CTuningParams::DEFAULT;
4203 TuningList()[i].Set(pName: "gun_curvature", Value: 0);
4204 TuningList()[i].Set(pName: "gun_speed", Value: 1400);
4205 TuningList()[i].Set(pName: "shotgun_curvature", Value: 0);
4206 TuningList()[i].Set(pName: "shotgun_speed", Value: 500);
4207 TuningList()[i].Set(pName: "shotgun_speeddiff", Value: 0);
4208 }
4209
4210 for(int i = 0; i < TuneZone::NUM; i++)
4211 {
4212 // Send no text by default when changing tune zones.
4213 m_aaZoneEnterMsg[i][0] = 0;
4214 m_aaZoneLeaveMsg[i][0] = 0;
4215 }
4216 // Reset Tuning
4217 if(g_Config.m_SvTuneReset)
4218 {
4219 ResetTuning();
4220 }
4221 else
4222 {
4223 GlobalTuning()->Set(pName: "gun_speed", Value: 1400);
4224 GlobalTuning()->Set(pName: "gun_curvature", Value: 0);
4225 GlobalTuning()->Set(pName: "shotgun_speed", Value: 500);
4226 GlobalTuning()->Set(pName: "shotgun_speeddiff", Value: 0);
4227 GlobalTuning()->Set(pName: "shotgun_curvature", Value: 0);
4228 }
4229
4230 if(g_Config.m_SvDDRaceTuneReset)
4231 {
4232 g_Config.m_SvHit = 1;
4233 g_Config.m_SvEndlessDrag = 0;
4234 g_Config.m_SvOldLaser = 0;
4235 g_Config.m_SvOldTeleportHook = 0;
4236 g_Config.m_SvOldTeleportWeapons = 0;
4237 g_Config.m_SvTeleportHoldHook = 0;
4238 g_Config.m_SvTeam = SV_TEAM_ALLOWED;
4239 g_Config.m_SvShowOthersDefault = SHOW_OTHERS_OFF;
4240
4241 for(auto &Switcher : Switchers())
4242 Switcher.m_Initial = true;
4243 }
4244
4245 m_pConfigManager->SetGameSettingsReadOnly(false);
4246
4247 Console()->ExecuteFile(pFilename: g_Config.m_SvResetFile, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
4248
4249 LoadMapSettings();
4250
4251 m_pConfigManager->SetGameSettingsReadOnly(true);
4252
4253 m_MapBugs.Dump();
4254
4255 if(g_Config.m_SvSoloServer)
4256 {
4257 g_Config.m_SvTeam = SV_TEAM_FORCED_SOLO;
4258 g_Config.m_SvShowOthersDefault = SHOW_OTHERS_ON;
4259
4260 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4261 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4262
4263 for(int i = 0; i < TuneZone::NUM; i++)
4264 {
4265 TuningList()[i].Set(pName: "player_collision", Value: 0);
4266 TuningList()[i].Set(pName: "player_hooking", Value: 0);
4267 }
4268 }
4269
4270 if(!str_comp(a: Config()->m_SvGametype, b: "mod"))
4271 m_pController = new CGameControllerMod(this);
4272 else
4273 m_pController = new CGameControllerDDNet(this);
4274
4275 for(const char *pReservedGameType : {"DM", "TDM", "CTF", "LMS", "LTS"})
4276 {
4277 dbg_assert(str_comp(m_pController->m_pGameType, pReservedGameType) != 0, "Using reserved gametype '%s' is not allowed", m_pController->m_pGameType);
4278 }
4279
4280 ReadCensorList();
4281
4282 m_TeeHistorianActive = g_Config.m_SvTeeHistorian;
4283 if(m_TeeHistorianActive)
4284 {
4285 char aGameUuid[UUID_MAXSTRSIZE];
4286 FormatUuid(Uuid: m_GameUuid, pBuffer: aGameUuid, BufferLength: sizeof(aGameUuid));
4287
4288 char aFilename[IO_MAX_PATH_LENGTH];
4289 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "teehistorian/%s.teehistorian", aGameUuid);
4290
4291 IOHANDLE THFile = Storage()->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
4292 if(!THFile)
4293 {
4294 dbg_msg(sys: "teehistorian", fmt: "failed to open '%s'", aFilename);
4295 Server()->SetErrorShutdown("teehistorian open error");
4296 return;
4297 }
4298 else
4299 {
4300 dbg_msg(sys: "teehistorian", fmt: "recording to '%s'", aFilename);
4301 }
4302 m_pTeeHistorianFile = aio_new(io: THFile);
4303
4304 char aVersion[128];
4305 if(GIT_SHORTREV_HASH)
4306 {
4307 str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%s (%s)", GAME_VERSION, GIT_SHORTREV_HASH);
4308 }
4309 else
4310 {
4311 str_copy(dst&: aVersion, GAME_VERSION);
4312 }
4313 CTeeHistorian::CGameInfo GameInfo;
4314 GameInfo.m_GameUuid = m_GameUuid;
4315 GameInfo.m_pServerVersion = aVersion;
4316 GameInfo.m_StartTime = time(timer: nullptr);
4317 GameInfo.m_pPrngDescription = m_Prng.Description();
4318
4319 GameInfo.m_pServerName = g_Config.m_SvName;
4320 GameInfo.m_ServerPort = Server()->Port();
4321 GameInfo.m_pGameType = m_pController->m_pGameType;
4322
4323 GameInfo.m_pConfig = &g_Config;
4324 GameInfo.m_pTuning = GlobalTuning();
4325 GameInfo.m_pUuids = &g_UuidManager;
4326
4327 GameInfo.m_pMapName = Map()->BaseName();
4328 GameInfo.m_MapSize = Map()->Size();
4329 GameInfo.m_MapSha256 = Map()->Sha256();
4330 GameInfo.m_MapCrc = Map()->Crc();
4331
4332 if(pPersistent)
4333 {
4334 GameInfo.m_HavePrevGameUuid = true;
4335 GameInfo.m_PrevGameUuid = pPersistent->m_PrevGameUuid;
4336 }
4337 else
4338 {
4339 GameInfo.m_HavePrevGameUuid = false;
4340 mem_zero(block: &GameInfo.m_PrevGameUuid, size: sizeof(GameInfo.m_PrevGameUuid));
4341 }
4342
4343 m_TeeHistorian.Reset(pGameInfo: &GameInfo, pfnWriteCallback: TeeHistorianWrite, pUser: this);
4344 }
4345
4346 Server()->DemoRecorder_HandleAutoStart();
4347
4348 if(!m_pScore)
4349 {
4350 m_pScore = new CScore(this, ((CServer *)Server())->DbPool());
4351 }
4352
4353 // load map info from database
4354 Score()->LoadMapInfo();
4355
4356 // create all entities from the game layer
4357 CreateAllEntities(Initial: true);
4358
4359 m_pAntibot->RoundStart(pGameServer: this);
4360}
4361
4362void CGameContext::CreateAllEntities(bool Initial)
4363{
4364 const CTile *pTiles = m_Collision.GameLayer();
4365 const CTile *pFront = m_Collision.FrontLayer();
4366 const CSwitchTile *pSwitch = m_Collision.SwitchLayer();
4367
4368 for(int y = 0; y < m_Collision.GetHeight(); y++)
4369 {
4370 for(int x = 0; x < m_Collision.GetWidth(); x++)
4371 {
4372 const int Index = y * m_Collision.GetWidth() + x;
4373
4374 // Game layer
4375 {
4376 const int GameIndex = pTiles[Index].m_Index;
4377 if(GameIndex == TILE_OLDLASER)
4378 {
4379 g_Config.m_SvOldLaser = 1;
4380 dbg_msg(sys: "game_layer", fmt: "found old laser tile");
4381 }
4382 else if(GameIndex == TILE_NPC)
4383 {
4384 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4385 dbg_msg(sys: "game_layer", fmt: "found no collision tile");
4386 }
4387 else if(GameIndex == TILE_EHOOK)
4388 {
4389 g_Config.m_SvEndlessDrag = 1;
4390 dbg_msg(sys: "game_layer", fmt: "found unlimited hook time tile");
4391 }
4392 else if(GameIndex == TILE_NOHIT)
4393 {
4394 g_Config.m_SvHit = 0;
4395 dbg_msg(sys: "game_layer", fmt: "found no weapons hitting others tile");
4396 }
4397 else if(GameIndex == TILE_NPH)
4398 {
4399 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4400 dbg_msg(sys: "game_layer", fmt: "found no player hooking tile");
4401 }
4402 else if(GameIndex >= ENTITY_OFFSET)
4403 {
4404 m_pController->OnEntity(Index: GameIndex - ENTITY_OFFSET, x, y, Layer: LAYER_GAME, Flags: pTiles[Index].m_Flags, Initial);
4405 }
4406 }
4407
4408 if(pFront)
4409 {
4410 const int FrontIndex = pFront[Index].m_Index;
4411 if(FrontIndex == TILE_OLDLASER)
4412 {
4413 g_Config.m_SvOldLaser = 1;
4414 dbg_msg(sys: "front_layer", fmt: "found old laser tile");
4415 }
4416 else if(FrontIndex == TILE_NPC)
4417 {
4418 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4419 dbg_msg(sys: "front_layer", fmt: "found no collision tile");
4420 }
4421 else if(FrontIndex == TILE_EHOOK)
4422 {
4423 g_Config.m_SvEndlessDrag = 1;
4424 dbg_msg(sys: "front_layer", fmt: "found unlimited hook time tile");
4425 }
4426 else if(FrontIndex == TILE_NOHIT)
4427 {
4428 g_Config.m_SvHit = 0;
4429 dbg_msg(sys: "front_layer", fmt: "found no weapons hitting others tile");
4430 }
4431 else if(FrontIndex == TILE_NPH)
4432 {
4433 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4434 dbg_msg(sys: "front_layer", fmt: "found no player hooking tile");
4435 }
4436 else if(FrontIndex >= ENTITY_OFFSET)
4437 {
4438 m_pController->OnEntity(Index: FrontIndex - ENTITY_OFFSET, x, y, Layer: LAYER_FRONT, Flags: pFront[Index].m_Flags, Initial);
4439 }
4440 }
4441
4442 if(pSwitch)
4443 {
4444 const int SwitchType = pSwitch[Index].m_Type;
4445 // TODO: Add off by default door here
4446 // if(SwitchType == TILE_DOOR_OFF)
4447 if(SwitchType >= ENTITY_OFFSET)
4448 {
4449 m_pController->OnEntity(Index: SwitchType - ENTITY_OFFSET, x, y, Layer: LAYER_SWITCH, Flags: pSwitch[Index].m_Flags, Initial, Number: pSwitch[Index].m_Number);
4450 }
4451 }
4452 }
4453 }
4454}
4455
4456CPlayer *CGameContext::CreatePlayer(int ClientId, int StartTeam, bool Afk, int LastWhisperTo)
4457{
4458 if(m_apPlayers[ClientId])
4459 delete m_apPlayers[ClientId];
4460 m_apPlayers[ClientId] = new(ClientId) CPlayer(this, m_NextUniqueClientId, ClientId, StartTeam);
4461 m_apPlayers[ClientId]->SetInitialAfk(Afk);
4462 m_apPlayers[ClientId]->m_LastWhisperTo = LastWhisperTo;
4463 m_NextUniqueClientId += 1;
4464 return m_apPlayers[ClientId];
4465}
4466
4467void CGameContext::DeleteTempfile()
4468{
4469 if(m_aDeleteTempfile[0] != 0)
4470 {
4471 Storage()->RemoveFile(pFilename: m_aDeleteTempfile, Type: IStorage::TYPE_SAVE);
4472 m_aDeleteTempfile[0] = 0;
4473 }
4474}
4475
4476bool CGameContext::OnMapChange(char *pNewMapName, int MapNameSize)
4477{
4478 char aConfig[IO_MAX_PATH_LENGTH];
4479 str_format(buffer: aConfig, buffer_size: sizeof(aConfig), format: "maps/%s.cfg", g_Config.m_SvMap);
4480
4481 CLineReader LineReader;
4482 if(!LineReader.OpenFile(File: Storage()->OpenFile(pFilename: aConfig, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
4483 {
4484 // No map-specific config, just return.
4485 return true;
4486 }
4487
4488 CDataFileReader Reader;
4489 if(!Reader.Open(pFullName: g_Config.m_SvMap, pStorage: Storage(), pPath: pNewMapName, StorageType: IStorage::TYPE_ALL))
4490 {
4491 log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for reading", aConfig, pNewMapName);
4492 return false;
4493 }
4494
4495 std::vector<const char *> vpLines;
4496 int TotalLength = 0;
4497 while(const char *pLine = LineReader.Get())
4498 {
4499 vpLines.push_back(x: pLine);
4500 TotalLength += str_length(str: pLine) + 1;
4501 }
4502
4503 char *pSettings = (char *)malloc(size: std::max(a: 1, b: TotalLength));
4504 int Offset = 0;
4505 for(const char *pLine : vpLines)
4506 {
4507 int Length = str_length(str: pLine) + 1;
4508 mem_copy(dest: pSettings + Offset, source: pLine, size: Length);
4509 Offset += Length;
4510 }
4511
4512 CDataFileWriter Writer;
4513
4514 int SettingsIndex = Reader.NumData();
4515 bool FoundInfo = false;
4516 for(int i = 0; i < Reader.NumItems(); i++)
4517 {
4518 int TypeId;
4519 int ItemId;
4520 void *pData = Reader.GetItem(Index: i, pType: &TypeId, pId: &ItemId);
4521 int Size = Reader.GetItemSize(Index: i);
4522 CMapItemInfoSettings MapInfo;
4523 if(TypeId == MAPITEMTYPE_INFO && ItemId == 0)
4524 {
4525 FoundInfo = true;
4526 if(Size >= (int)sizeof(CMapItemInfoSettings))
4527 {
4528 CMapItemInfoSettings *pInfo = (CMapItemInfoSettings *)pData;
4529 if(pInfo->m_Settings > -1)
4530 {
4531 SettingsIndex = pInfo->m_Settings;
4532 char *pMapSettings = (char *)Reader.GetData(Index: SettingsIndex);
4533 int DataSize = Reader.GetDataSize(Index: SettingsIndex);
4534 if(DataSize == TotalLength && mem_comp(a: pSettings, b: pMapSettings, size: DataSize) == 0)
4535 {
4536 // Configs coincide, no need to update map.
4537 free(ptr: pSettings);
4538 return true;
4539 }
4540 Reader.UnloadData(Index: pInfo->m_Settings);
4541 }
4542 else
4543 {
4544 MapInfo = *pInfo;
4545 MapInfo.m_Settings = SettingsIndex;
4546 pData = &MapInfo;
4547 Size = sizeof(MapInfo);
4548 }
4549 }
4550 else
4551 {
4552 *(CMapItemInfo *)&MapInfo = *(CMapItemInfo *)pData;
4553 MapInfo.m_Settings = SettingsIndex;
4554 pData = &MapInfo;
4555 Size = sizeof(MapInfo);
4556 }
4557 }
4558 Writer.AddItem(Type: TypeId, Id: ItemId, Size, pData);
4559 }
4560
4561 if(!FoundInfo)
4562 {
4563 CMapItemInfoSettings Info;
4564 Info.m_Version = 1;
4565 Info.m_Author = -1;
4566 Info.m_MapVersion = -1;
4567 Info.m_Credits = -1;
4568 Info.m_License = -1;
4569 Info.m_Settings = SettingsIndex;
4570 Writer.AddItem(Type: MAPITEMTYPE_INFO, Id: 0, Size: sizeof(Info), pData: &Info);
4571 }
4572
4573 for(int i = 0; i < Reader.NumData() || i == SettingsIndex; i++)
4574 {
4575 if(i == SettingsIndex)
4576 {
4577 Writer.AddData(Size: TotalLength, pData: pSettings);
4578 continue;
4579 }
4580 const void *pData = Reader.GetData(Index: i);
4581 int Size = Reader.GetDataSize(Index: i);
4582 Writer.AddData(Size, pData);
4583 Reader.UnloadData(Index: i);
4584 }
4585
4586 free(ptr: pSettings);
4587 Reader.Close();
4588
4589 char aTemp[IO_MAX_PATH_LENGTH];
4590 if(!Writer.Open(pStorage: Storage(), pFilename: IStorage::FormatTmpPath(aBuf: aTemp, BufSize: sizeof(aTemp), pPath: pNewMapName)))
4591 {
4592 log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for writing", aConfig, aTemp);
4593 return false;
4594 }
4595 Writer.Finish();
4596 log_info("mapchange", "Imported settings from '%s' into '%s'", aConfig, aTemp);
4597
4598 str_copy(dst: pNewMapName, src: aTemp, dst_size: MapNameSize);
4599 str_copy(dst&: m_aDeleteTempfile, src: aTemp);
4600 return true;
4601}
4602
4603void CGameContext::OnShutdown(void *pPersistentData)
4604{
4605 CPersistentData *pPersistent = (CPersistentData *)pPersistentData;
4606
4607 if(pPersistent)
4608 {
4609 new(pPersistent) CPersistentData();
4610 pPersistent->m_PrevGameUuid = m_GameUuid;
4611 }
4612
4613 Antibot()->RoundEnd();
4614
4615 if(m_TeeHistorianActive)
4616 {
4617 m_TeeHistorian.Finish();
4618 aio_close(aio: m_pTeeHistorianFile);
4619 aio_wait(aio: m_pTeeHistorianFile);
4620 int Error = aio_error(aio: m_pTeeHistorianFile);
4621 if(Error)
4622 {
4623 dbg_msg(sys: "teehistorian", fmt: "error closing file, err=%d", Error);
4624 Server()->SetErrorShutdown("teehistorian close error");
4625 }
4626 aio_free(aio: m_pTeeHistorianFile);
4627 }
4628
4629 // Stop any demos being recorded.
4630 Server()->StopDemos();
4631
4632 DeleteTempfile();
4633 ConfigManager()->ResetGameSettings();
4634 Collision()->Unload();
4635 Layers()->Unload();
4636 delete m_pController;
4637 m_pController = nullptr;
4638 Clear();
4639}
4640
4641void CGameContext::LoadMapSettings()
4642{
4643 IMap *pMap = Map();
4644 int Start, Num;
4645 pMap->GetType(Type: MAPITEMTYPE_INFO, pStart: &Start, pNum: &Num);
4646 for(int i = Start; i < Start + Num; i++)
4647 {
4648 int ItemId;
4649 CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(Index: i, pType: nullptr, pId: &ItemId);
4650 int ItemSize = pMap->GetItemSize(Index: i);
4651 if(!pItem || ItemId != 0)
4652 continue;
4653
4654 if(ItemSize < (int)sizeof(CMapItemInfoSettings))
4655 break;
4656 if(!(pItem->m_Settings > -1))
4657 break;
4658
4659 int Size = pMap->GetDataSize(Index: pItem->m_Settings);
4660 char *pSettings = (char *)pMap->GetData(Index: pItem->m_Settings);
4661 char *pNext = pSettings;
4662 while(pNext < pSettings + Size)
4663 {
4664 int StrSize = str_length(str: pNext) + 1;
4665 Console()->ExecuteLine(pStr: pNext, ClientId: IConsole::CLIENT_ID_GAME);
4666 pNext += StrSize;
4667 }
4668 pMap->UnloadData(Index: pItem->m_Settings);
4669 break;
4670 }
4671
4672 char aBuf[IO_MAX_PATH_LENGTH];
4673 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "maps/%s.map.cfg", g_Config.m_SvMap);
4674 Console()->ExecuteFile(pFilename: aBuf, ClientId: IConsole::CLIENT_ID_NO_GAME);
4675}
4676
4677void CGameContext::OnSnap(int ClientId, bool GlobalSnap, bool RecordingDemo)
4678{
4679 // sixup should only snap during global snap
4680 dbg_assert(!Server()->IsSixup(ClientId) || GlobalSnap, "sixup should only snap during global snap");
4681
4682 // add tuning to demo
4683 if(RecordingDemo && mem_comp(a: &CTuningParams::DEFAULT, b: &m_aTuningList[0], size: sizeof(CTuningParams)) != 0)
4684 {
4685 CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
4686 int *pParams = (int *)&m_aTuningList[0];
4687 for(int i = 0; i < CTuningParams::Num(); i++)
4688 Msg.AddInt(i: pParams[i]);
4689 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId);
4690 }
4691
4692 m_pController->Snap(SnappingClient: ClientId);
4693
4694 for(auto &pPlayer : m_apPlayers)
4695 {
4696 if(pPlayer)
4697 pPlayer->Snap(SnappingClient: ClientId);
4698 }
4699
4700 if(ClientId > -1)
4701 m_apPlayers[ClientId]->FakeSnap();
4702
4703 m_World.Snap(SnappingClient: ClientId);
4704
4705 // events are only sent on global snapshots
4706 if(GlobalSnap)
4707 {
4708 m_Events.Snap(SnappingClient: ClientId);
4709 }
4710}
4711
4712void CGameContext::OnPostGlobalSnap()
4713{
4714 for(auto &pPlayer : m_apPlayers)
4715 {
4716 if(pPlayer && pPlayer->GetCharacter())
4717 pPlayer->GetCharacter()->PostGlobalSnap();
4718 }
4719 m_Events.Clear();
4720}
4721
4722void CGameContext::UpdatePlayerMaps()
4723{
4724 const auto DistCompare = [](std::pair<float, int> a, std::pair<float, int> b) -> bool {
4725 return (a.first < b.first);
4726 };
4727
4728 if(Server()->Tick() % g_Config.m_SvMapUpdateRate != 0)
4729 return;
4730
4731 std::pair<float, int> Dist[MAX_CLIENTS];
4732 for(int i = 0; i < MAX_CLIENTS; i++)
4733 {
4734 if(!Server()->ClientIngame(ClientId: i))
4735 continue;
4736 if(Server()->GetClientVersion(ClientId: i) >= VERSION_DDNET_OLD)
4737 continue;
4738 int *pMap = Server()->GetIdMap(ClientId: i);
4739
4740 // compute distances
4741 for(int j = 0; j < MAX_CLIENTS; j++)
4742 {
4743 Dist[j].second = j;
4744 if(j == i)
4745 continue;
4746 if(!Server()->ClientIngame(ClientId: j) || !m_apPlayers[j])
4747 {
4748 Dist[j].first = 1e10;
4749 continue;
4750 }
4751 CCharacter *pChr = m_apPlayers[j]->GetCharacter();
4752 if(!pChr)
4753 {
4754 Dist[j].first = 1e9;
4755 continue;
4756 }
4757 if(!pChr->CanSnapCharacter(SnappingClient: i))
4758 Dist[j].first = 1e8;
4759 else
4760 Dist[j].first = length_squared(a: m_apPlayers[i]->m_ViewPos - pChr->GetPos());
4761 }
4762
4763 // always send the player themselves, even if all in same position
4764 Dist[i].first = -1;
4765
4766 std::nth_element(first: &Dist[0], nth: &Dist[VANILLA_MAX_CLIENTS - 1], last: &Dist[MAX_CLIENTS], comp: DistCompare);
4767
4768 int Index = 1; // exclude self client id
4769 for(int j = 0; j < VANILLA_MAX_CLIENTS - 1; j++)
4770 {
4771 pMap[j + 1] = -1; // also fill player with empty name to say chat msgs
4772 if(Dist[j].second == i || Dist[j].first > 5e9f)
4773 continue;
4774 pMap[Index++] = Dist[j].second;
4775 }
4776
4777 // sort by real client ids, guarantee order on distance changes, O(Nlog(N)) worst case
4778 // sort just clients in game always except first (self client id) and last (fake client id) indexes
4779 std::sort(first: &pMap[1], last: &pMap[std::min(a: Index, b: VANILLA_MAX_CLIENTS - 1)]);
4780 }
4781}
4782
4783bool CGameContext::IsClientReady(int ClientId) const
4784{
4785 return m_apPlayers[ClientId] && m_apPlayers[ClientId]->m_IsReady;
4786}
4787
4788bool CGameContext::IsClientPlayer(int ClientId) const
4789{
4790 return m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetTeam() != TEAM_SPECTATORS;
4791}
4792
4793bool CGameContext::IsClientHighBandwidth(int ClientId) const
4794{
4795 // force high bandwidth is not supported for sixup
4796 return m_apPlayers[ClientId] && !Server()->IsSixup(ClientId) && Server()->IsRconAuthed(ClientId) &&
4797 (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsPaused());
4798}
4799
4800CUuid CGameContext::GameUuid() const { return m_GameUuid; }
4801const char *CGameContext::GameType() const
4802{
4803 dbg_assert(m_pController, "no controller");
4804 dbg_assert(m_pController->m_pGameType, "no gametype");
4805 return m_pController->m_pGameType;
4806}
4807const char *CGameContext::Version() const { return GAME_VERSION; }
4808const char *CGameContext::NetVersion() const { return GAME_NETVERSION; }
4809
4810IGameServer *CreateGameServer() { return new CGameContext; }
4811
4812void CGameContext::OnSetAuthed(int ClientId, int Level)
4813{
4814 if(m_apPlayers[ClientId] && m_VoteCloseTime && Level != AUTHED_NO)
4815 {
4816 char aBuf[512];
4817 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ban %s %d Banned by vote", Server()->ClientAddrString(ClientId, IncludePort: false), g_Config.m_SvVoteKickBantime);
4818 if(!str_comp_nocase(a: m_aVoteCommand, b: aBuf) && (m_VoteCreator == -1 || Level > Server()->GetAuthedState(ClientId: m_VoteCreator)))
4819 {
4820 m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN;
4821 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "game", pStr: "Vote aborted by authorized login.");
4822 }
4823 }
4824
4825 if(m_TeeHistorianActive)
4826 {
4827 if(Level != AUTHED_NO)
4828 {
4829 m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName: Server()->GetAuthName(ClientId));
4830 }
4831 else
4832 {
4833 m_TeeHistorian.RecordAuthLogout(ClientId);
4834 }
4835 }
4836}
4837
4838bool CGameContext::IsRunningVote(int ClientId) const
4839{
4840 return m_VoteCloseTime && m_VoteCreator == ClientId;
4841}
4842
4843bool CGameContext::IsRunningKickOrSpecVote(int ClientId) const
4844{
4845 return IsRunningVote(ClientId) && (IsKickVote() || IsSpecVote());
4846}
4847
4848void CGameContext::SendRecord(int ClientId)
4849{
4850 if(Server()->IsSixup(ClientId) || GetClientVersion(ClientId) >= VERSION_DDNET_MAP_BESTTIME)
4851 return;
4852
4853 CNetMsg_Sv_Record Msg;
4854 CNetMsg_Sv_RecordLegacy MsgLegacy;
4855 MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = round_to_int(f: Score()->PlayerData(Id: ClientId)->m_BestTime.value_or(u: 0.0f) * 100.0f);
4856 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;
4857 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
4858 if(GetClientVersion(ClientId) < VERSION_DDNET_MSG_LEGACY)
4859 {
4860 Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
4861 }
4862}
4863
4864void CGameContext::SendFinish(int ClientId, float Time, std::optional<float> PreviousBestTime)
4865{
4866 int ClientVersion = m_apPlayers[ClientId]->GetClientVersion();
4867
4868 if(!Server()->IsSixup(ClientId))
4869 {
4870 CNetMsg_Sv_DDRaceTime Msg;
4871 CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy;
4872 MsgLegacy.m_Time = Msg.m_Time = (int)(Time * 100.0f);
4873 MsgLegacy.m_Check = Msg.m_Check = 0;
4874 MsgLegacy.m_Finish = Msg.m_Finish = 1;
4875
4876 if(PreviousBestTime.has_value())
4877 {
4878 float Diff100 = (Time - PreviousBestTime.value()) * 100;
4879 MsgLegacy.m_Check = Msg.m_Check = (int)Diff100;
4880 }
4881 if(VERSION_DDRACE <= ClientVersion)
4882 {
4883 if(ClientVersion < VERSION_DDNET_MSG_LEGACY)
4884 {
4885 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
4886 }
4887 else
4888 {
4889 Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
4890 }
4891 }
4892 }
4893
4894 CNetMsg_Sv_RaceFinish RaceFinishMsg;
4895 RaceFinishMsg.m_ClientId = ClientId;
4896 RaceFinishMsg.m_Time = Time * 1000;
4897 RaceFinishMsg.m_Diff = 0;
4898 if(PreviousBestTime.has_value())
4899 {
4900 float Diff = absolute(a: Time - PreviousBestTime.value());
4901 RaceFinishMsg.m_Diff = Diff * 1000 * (Time < PreviousBestTime.value() ? -1 : 1);
4902 }
4903 RaceFinishMsg.m_RecordPersonal = (!PreviousBestTime.has_value() || Time < PreviousBestTime.value());
4904 RaceFinishMsg.m_RecordServer = Time < m_pController->m_CurrentRecord;
4905 Server()->SendPackMsg(pMsg: &RaceFinishMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: g_Config.m_SvHideScore ? ClientId : -1);
4906}
4907
4908void CGameContext::SendSaveCode(int Team, int TeamSize, int State, const char *pError, const char *pSaveRequester, const char *pServerName, const char *pGeneratedCode, const char *pCode)
4909{
4910 char aBuf[512];
4911
4912 CMsgPacker Msg(NETMSGTYPE_SV_SAVECODE);
4913 Msg.AddInt(i: State);
4914 Msg.AddString(pStr: pError);
4915 Msg.AddString(pStr: pSaveRequester);
4916 Msg.AddString(pStr: pServerName);
4917 Msg.AddString(pStr: pGeneratedCode);
4918 Msg.AddString(pStr: pCode);
4919 char aTeamMembers[1024];
4920 aTeamMembers[0] = '\0';
4921 int NumMembersSent = 0;
4922 for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
4923 {
4924 if(!m_apPlayers[MemberId])
4925 continue;
4926 if(GetDDRaceTeam(ClientId: MemberId) != Team)
4927 continue;
4928 if(NumMembersSent++ > 10)
4929 {
4930 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " and %d others", (TeamSize - NumMembersSent) + 1);
4931 str_append(dst&: aTeamMembers, src: aBuf);
4932 break;
4933 }
4934
4935 if(NumMembersSent > 1)
4936 str_append(dst&: aTeamMembers, src: ", ");
4937 str_append(dst&: aTeamMembers, src: Server()->ClientName(ClientId: MemberId));
4938 }
4939 Msg.AddString(pStr: aTeamMembers);
4940
4941 for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
4942 {
4943 if(!m_apPlayers[MemberId])
4944 continue;
4945 if(GetDDRaceTeam(ClientId: MemberId) != Team)
4946 continue;
4947
4948 if(GetClientVersion(ClientId: MemberId) >= VERSION_DDNET_SAVE_CODE)
4949 {
4950 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: MemberId);
4951 }
4952 else
4953 {
4954 switch(State)
4955 {
4956 case SAVESTATE_PENDING:
4957 if(pCode[0] == '\0')
4958 {
4959 str_format(buffer: aBuf,
4960 buffer_size: sizeof(aBuf),
4961 format: "Team save in progress. You'll be able to load with '/load %s'",
4962 pGeneratedCode);
4963 }
4964 else
4965 {
4966 str_format(buffer: aBuf,
4967 buffer_size: sizeof(aBuf),
4968 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",
4969 pCode,
4970 pGeneratedCode);
4971 }
4972 break;
4973 case SAVESTATE_DONE:
4974 if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0)
4975 {
4976 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4977 format: "Team successfully saved by %s. Use '/load %s' to continue",
4978 pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
4979 }
4980 else
4981 {
4982 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4983 format: "Team successfully saved by %s. Use '/load %s' on %s to continue",
4984 pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
4985 }
4986 break;
4987 case SAVESTATE_FALLBACKFILE:
4988 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);
4989 if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0)
4990 {
4991 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4992 format: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue",
4993 pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
4994 }
4995 else
4996 {
4997 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4998 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",
4999 pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
5000 }
5001 break;
5002 case SAVESTATE_ERROR:
5003 case SAVESTATE_WARNING:
5004 str_copy(dst&: aBuf, src: pError);
5005 break;
5006 default:
5007 dbg_assert_failed("Unexpected save state %d", State);
5008 }
5009 SendChatTarget(To: MemberId, pText: aBuf);
5010 }
5011 }
5012}
5013
5014bool CGameContext::ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay)
5015{
5016 if(!m_apPlayers[ClientId])
5017 return false;
5018 if(g_Config.m_SvSpamprotection && m_apPlayers[ClientId]->m_LastChat && m_apPlayers[ClientId]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick())
5019 return true;
5020 else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientId))
5021 {
5022 SendChatTarget(To: ClientId, pText: "Players are not allowed to chat from VPNs at this time");
5023 return true;
5024 }
5025 else
5026 m_apPlayers[ClientId]->m_LastChat = Server()->Tick();
5027
5028 const std::optional<CMute> Muted = m_Mutes.IsMuted(pAddr: Server()->ClientAddr(ClientId), RespectInitialDelay: RespectChatInitialDelay);
5029 if(Muted.has_value())
5030 {
5031 char aChatMessage[128];
5032 if(Muted->m_InitialDelay)
5033 {
5034 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());
5035 }
5036 else
5037 {
5038 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to talk for the next %d seconds.", Muted->SecondsLeft());
5039 }
5040 SendChatTarget(To: ClientId, pText: aChatMessage);
5041 return true;
5042 }
5043
5044 if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientId]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold)
5045 {
5046 MuteWithMessage(pAddr: Server()->ClientAddr(ClientId), Seconds: g_Config.m_SvSpamMuteDuration, pReason: "Spam protection", pDisplayName: Server()->ClientName(ClientId));
5047 m_apPlayers[ClientId]->m_ChatScore = 0;
5048 return true;
5049 }
5050
5051 return false;
5052}
5053
5054int CGameContext::GetDDRaceTeam(int ClientId) const
5055{
5056 return m_pController->Teams().m_Core.Team(ClientId);
5057}
5058
5059void CGameContext::ResetTuning()
5060{
5061 *GlobalTuning() = CTuningParams::DEFAULT;
5062 GlobalTuning()->Set(pName: "gun_speed", Value: 1400);
5063 GlobalTuning()->Set(pName: "gun_curvature", Value: 0);
5064 GlobalTuning()->Set(pName: "shotgun_speed", Value: 500);
5065 GlobalTuning()->Set(pName: "shotgun_speeddiff", Value: 0);
5066 GlobalTuning()->Set(pName: "shotgun_curvature", Value: 0);
5067 SendTuningParams(ClientId: -1);
5068}
5069
5070void CGameContext::Whisper(int ClientId, char *pStr)
5071{
5072 if(ProcessSpamProtection(ClientId))
5073 return;
5074
5075 pStr = str_skip_whitespaces(str: pStr);
5076
5077 const char *pName;
5078 int Victim;
5079 bool Error = false;
5080
5081 // add token
5082 if(*pStr == '"')
5083 {
5084 pStr++;
5085
5086 pName = pStr;
5087 char *pDst = pStr; // we might have to process escape data
5088 while(true)
5089 {
5090 if(pStr[0] == '"')
5091 {
5092 break;
5093 }
5094 else if(pStr[0] == '\\')
5095 {
5096 if(pStr[1] == '\\')
5097 pStr++; // skip due to escape
5098 else if(pStr[1] == '"')
5099 pStr++; // skip due to escape
5100 }
5101 else if(pStr[0] == 0)
5102 {
5103 Error = true;
5104 break;
5105 }
5106
5107 *pDst = *pStr;
5108 pDst++;
5109 pStr++;
5110 }
5111
5112 if(!Error)
5113 {
5114 *pDst = '\0';
5115 pStr++;
5116
5117 Victim = FindClientIdByName(pName).value_or(u: -1);
5118 }
5119 }
5120 else
5121 {
5122 pName = pStr;
5123 while(true)
5124 {
5125 if(pStr[0] == '\0')
5126 {
5127 Error = true;
5128 break;
5129 }
5130 if(pStr[0] == ' ')
5131 {
5132 pStr[0] = '\0';
5133
5134 Victim = FindClientIdByName(pName).value_or(u: -1);
5135
5136 pStr[0] = ' ';
5137 if(Victim != -1)
5138 break;
5139 }
5140 pStr++;
5141 }
5142 }
5143
5144 if(pStr[0] != ' ')
5145 {
5146 Error = true;
5147 }
5148
5149 *pStr = '\0';
5150 pStr++;
5151
5152 if(Error)
5153 {
5154 SendChatTarget(To: ClientId, pText: "Invalid whisper");
5155 return;
5156 }
5157
5158 if(!CheckClientId(ClientId: Victim))
5159 {
5160 char aBuf[256];
5161 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No player with name \"%s\" found", pName);
5162 SendChatTarget(To: ClientId, pText: aBuf);
5163 return;
5164 }
5165
5166 WhisperId(ClientId, VictimId: Victim, pMessage: pStr);
5167}
5168
5169void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage)
5170{
5171 dbg_assert(CheckClientId(ClientId) && m_apPlayers[ClientId] != nullptr, "ClientId invalid");
5172 dbg_assert(CheckClientId(VictimId) && m_apPlayers[VictimId] != nullptr, "VictimId invalid");
5173
5174 m_apPlayers[ClientId]->m_LastWhisperTo = VictimId;
5175
5176 char aCensoredMessage[256];
5177 CensorMessage(pCensoredMessage: aCensoredMessage, pMessage, Size: sizeof(aCensoredMessage));
5178
5179 char aBuf[256];
5180
5181 if(Server()->IsSixup(ClientId))
5182 {
5183 protocol7::CNetMsg_Sv_Chat Msg;
5184 Msg.m_ClientId = ClientId;
5185 Msg.m_Mode = protocol7::CHAT_WHISPER;
5186 Msg.m_pMessage = aCensoredMessage;
5187 Msg.m_TargetId = VictimId;
5188
5189 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
5190 }
5191 else if(GetClientVersion(ClientId) >= VERSION_DDNET_WHISPER)
5192 {
5193 CNetMsg_Sv_Chat Msg;
5194 Msg.m_Team = TEAM_WHISPER_SEND;
5195 Msg.m_ClientId = VictimId;
5196 Msg.m_pMessage = aCensoredMessage;
5197 if(g_Config.m_SvDemoChat)
5198 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
5199 else
5200 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
5201 }
5202 else
5203 {
5204 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[→ %s] %s", Server()->ClientName(ClientId: VictimId), aCensoredMessage);
5205 SendChatTarget(To: ClientId, pText: aBuf);
5206 }
5207
5208 if(!m_apPlayers[VictimId]->m_Whispers)
5209 {
5210 SendChatTarget(To: ClientId, pText: "This person has disabled receiving whispers");
5211 return;
5212 }
5213
5214 if(Server()->IsSixup(ClientId: VictimId))
5215 {
5216 protocol7::CNetMsg_Sv_Chat Msg;
5217 Msg.m_ClientId = ClientId;
5218 Msg.m_Mode = protocol7::CHAT_WHISPER;
5219 Msg.m_pMessage = aCensoredMessage;
5220 Msg.m_TargetId = VictimId;
5221
5222 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId);
5223 }
5224 else if(GetClientVersion(ClientId: VictimId) >= VERSION_DDNET_WHISPER)
5225 {
5226 CNetMsg_Sv_Chat Msg2;
5227 Msg2.m_Team = TEAM_WHISPER_RECV;
5228 Msg2.m_ClientId = ClientId;
5229 Msg2.m_pMessage = aCensoredMessage;
5230 if(g_Config.m_SvDemoChat)
5231 Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL, ClientId: VictimId);
5232 else
5233 Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId);
5234 }
5235 else
5236 {
5237 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[← %s] %s", Server()->ClientName(ClientId), aCensoredMessage);
5238 SendChatTarget(To: VictimId, pText: aBuf);
5239 }
5240}
5241
5242void CGameContext::Converse(int ClientId, char *pStr)
5243{
5244 CPlayer *pPlayer = m_apPlayers[ClientId];
5245 if(!pPlayer)
5246 return;
5247
5248 if(ProcessSpamProtection(ClientId))
5249 return;
5250
5251 if(pPlayer->m_LastWhisperTo < 0)
5252 SendChatTarget(To: ClientId, pText: "You do not have an ongoing conversation. Whisper to someone to start one");
5253 else if(!m_apPlayers[pPlayer->m_LastWhisperTo])
5254 SendChatTarget(To: ClientId, pText: "The player you were whispering to hasn't reconnected yet or left. Please wait or whisper to someone else");
5255 else
5256 WhisperId(ClientId, VictimId: pPlayer->m_LastWhisperTo, pMessage: pStr);
5257}
5258
5259bool CGameContext::IsVersionBanned(int Version)
5260{
5261 char aVersion[16];
5262 str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%d", Version);
5263
5264 return str_in_list(list: g_Config.m_SvBannedVersions, delim: ",", needle: aVersion);
5265}
5266
5267void CGameContext::List(int ClientId, const char *pFilter)
5268{
5269 int Total = 0;
5270 char aBuf[256];
5271 int Bufcnt = 0;
5272 if(pFilter[0])
5273 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Listing players with \"%s\" in name:", pFilter);
5274 else
5275 str_copy(dst&: aBuf, src: "Listing all players:");
5276 SendChatTarget(To: ClientId, pText: aBuf);
5277 for(int i = 0; i < MAX_CLIENTS; i++)
5278 {
5279 if(m_apPlayers[i])
5280 {
5281 Total++;
5282 const char *pName = Server()->ClientName(ClientId: i);
5283 if(str_utf8_find_nocase(haystack: pName, needle: pFilter) == nullptr)
5284 continue;
5285 if(Bufcnt + str_length(str: pName) + 4 > 256)
5286 {
5287 SendChatTarget(To: ClientId, pText: aBuf);
5288 Bufcnt = 0;
5289 }
5290 if(Bufcnt != 0)
5291 {
5292 str_format(buffer: &aBuf[Bufcnt], buffer_size: sizeof(aBuf) - Bufcnt, format: ", %s", pName);
5293 Bufcnt += 2 + str_length(str: pName);
5294 }
5295 else
5296 {
5297 str_copy(dst: &aBuf[Bufcnt], src: pName, dst_size: sizeof(aBuf) - Bufcnt);
5298 Bufcnt += str_length(str: pName);
5299 }
5300 }
5301 }
5302 if(Bufcnt != 0)
5303 SendChatTarget(To: ClientId, pText: aBuf);
5304 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d players online", Total);
5305 SendChatTarget(To: ClientId, pText: aBuf);
5306}
5307
5308int CGameContext::GetClientVersion(int ClientId) const
5309{
5310 return Server()->GetClientVersion(ClientId);
5311}
5312
5313CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const
5314{
5315 CClientMask Mask;
5316 for(int i = 0; i < MAX_CLIENTS; ++i)
5317 {
5318 if(GetClientVersion(ClientId: i) >= Version)
5319 continue;
5320 Mask.set(pos: i);
5321 }
5322 return Mask;
5323}
5324
5325bool CGameContext::PlayerModerating() const
5326{
5327 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; });
5328}
5329
5330void CGameContext::ForceVote(bool Success)
5331{
5332 // check if there is a vote running
5333 if(!m_VoteCloseTime)
5334 return;
5335
5336 m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
5337 const char *pOption = Success ? "yes" : "no";
5338
5339 char aChatMessage[256];
5340 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "Authorized player forced vote '%s'", pOption);
5341 SendChatTarget(To: -1, pText: aChatMessage);
5342
5343 log_info("server", "Forcing vote '%s'", pOption);
5344}
5345
5346bool CGameContext::RateLimitPlayerVote(int ClientId)
5347{
5348 int64_t Now = Server()->Tick();
5349 int64_t TickSpeed = Server()->TickSpeed();
5350 CPlayer *pPlayer = m_apPlayers[ClientId];
5351
5352 if(g_Config.m_SvRconVote && !Server()->IsRconAuthed(ClientId))
5353 {
5354 SendChatTarget(To: ClientId, pText: "You can only vote after logging in.");
5355 return true;
5356 }
5357
5358 if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1)
5359 {
5360 if(m_pServer->DnsblPending(ClientId))
5361 {
5362 SendChatTarget(To: ClientId, pText: "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds.");
5363 return true;
5364 }
5365 else if(m_pServer->DnsblBlack(ClientId))
5366 {
5367 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.");
5368 return true;
5369 }
5370 }
5371
5372 if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + TickSpeed * 3 > Now)
5373 return true;
5374
5375 pPlayer->m_LastVoteTry = Now;
5376 if(m_VoteCloseTime)
5377 {
5378 SendChatTarget(To: ClientId, pText: "Wait for current vote to end before calling a new one.");
5379 return true;
5380 }
5381
5382 if(Now < pPlayer->m_FirstVoteTick)
5383 {
5384 char aChatMessage[64];
5385 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);
5386 SendChatTarget(To: ClientId, pText: aChatMessage);
5387 return true;
5388 }
5389
5390 int TimeLeft = pPlayer->m_LastVoteCall + TickSpeed * g_Config.m_SvVoteDelay - Now;
5391 if(pPlayer->m_LastVoteCall && TimeLeft > 0)
5392 {
5393 char aChatMessage[64];
5394 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You must wait %d seconds before making another vote.", (int)(TimeLeft / TickSpeed) + 1);
5395 SendChatTarget(To: ClientId, pText: aChatMessage);
5396 return true;
5397 }
5398
5399 const NETADDR *pAddr = Server()->ClientAddr(ClientId);
5400 std::optional<CMute> Muted = m_VoteMutes.IsMuted(pAddr, RespectInitialDelay: true);
5401 if(!Muted.has_value())
5402 {
5403 Muted = m_Mutes.IsMuted(pAddr, RespectInitialDelay: true);
5404 }
5405 if(Muted.has_value())
5406 {
5407 char aChatMessage[64];
5408 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to vote for the next %d seconds.", Muted->SecondsLeft());
5409 SendChatTarget(To: ClientId, pText: aChatMessage);
5410 return true;
5411 }
5412 return false;
5413}
5414
5415bool CGameContext::RateLimitPlayerMapVote(int ClientId) const
5416{
5417 if(!Server()->IsRconAuthed(ClientId) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay))
5418 {
5419 char aChatMessage[128];
5420 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "There's a %d second delay between map-votes, please wait %d seconds.",
5421 g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq()));
5422 SendChatTarget(To: ClientId, pText: aChatMessage);
5423 return true;
5424 }
5425 return false;
5426}
5427
5428void CGameContext::OnUpdatePlayerServerInfo(CJsonWriter *pJsonWriter, int ClientId)
5429{
5430 if(!m_apPlayers[ClientId])
5431 return;
5432
5433 CTeeInfo &TeeInfo = m_apPlayers[ClientId]->m_TeeInfos;
5434
5435 pJsonWriter->WriteAttribute(pName: "skin");
5436 pJsonWriter->BeginObject();
5437
5438 // 0.6
5439 if(!Server()->IsSixup(ClientId))
5440 {
5441 pJsonWriter->WriteAttribute(pName: "name");
5442 pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aSkinName);
5443
5444 if(TeeInfo.m_UseCustomColor)
5445 {
5446 pJsonWriter->WriteAttribute(pName: "color_body");
5447 pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorBody);
5448
5449 pJsonWriter->WriteAttribute(pName: "color_feet");
5450 pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorFeet);
5451 }
5452 }
5453 // 0.7
5454 else
5455 {
5456 const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};
5457
5458 for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i)
5459 {
5460 pJsonWriter->WriteAttribute(pName: apPartNames[i]);
5461 pJsonWriter->BeginObject();
5462
5463 pJsonWriter->WriteAttribute(pName: "name");
5464 pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aaSkinPartNames[i]);
5465
5466 if(TeeInfo.m_aUseCustomColors[i])
5467 {
5468 pJsonWriter->WriteAttribute(pName: "color");
5469 pJsonWriter->WriteIntValue(Value: TeeInfo.m_aSkinPartColors[i]);
5470 }
5471
5472 pJsonWriter->EndObject();
5473 }
5474 }
5475
5476 pJsonWriter->EndObject();
5477
5478 pJsonWriter->WriteAttribute(pName: "afk");
5479 pJsonWriter->WriteBoolValue(Value: m_apPlayers[ClientId]->IsAfk());
5480
5481 const int Team = m_pController->IsTeamPlay() ? m_apPlayers[ClientId]->GetTeam() : (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(ClientId));
5482
5483 pJsonWriter->WriteAttribute(pName: "team");
5484 pJsonWriter->WriteIntValue(Value: Team);
5485}
5486
5487void CGameContext::ReadCensorList()
5488{
5489 const char *pCensorFilename = "censorlist.txt";
5490 CLineReader LineReader;
5491 m_vCensorlist.clear();
5492 if(LineReader.OpenFile(File: Storage()->OpenFile(pFilename: pCensorFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
5493 {
5494 while(const char *pLine = LineReader.Get())
5495 {
5496 m_vCensorlist.emplace_back(args&: pLine);
5497 }
5498 }
5499 else
5500 {
5501 dbg_msg(sys: "censorlist", fmt: "failed to open '%s'", pCensorFilename);
5502 }
5503}
5504
5505bool CGameContext::PracticeByDefault() const
5506{
5507 return g_Config.m_SvPracticeByDefault && g_Config.m_SvTestingCommands;
5508}
5509