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