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