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