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