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