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 while(true)
2207 {
2208 const char *pEndMatch;
2209 pCurLoc = (char *)str_utf8_find_nocase(haystack: pCurLoc, needle: Item.c_str(), end: &pEndMatch);
2210 if(!pCurLoc)
2211 {
2212 break;
2213 }
2214 while(pCurLoc < pEndMatch)
2215 {
2216 *pCurLoc = '*';
2217 pCurLoc++;
2218 }
2219 }
2220 }
2221}
2222
2223void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId)
2224{
2225 if(m_TeeHistorianActive)
2226 {
2227 if(m_NetObjHandler.TeeHistorianRecordMsg(Type: MsgId))
2228 {
2229 m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize());
2230 }
2231 }
2232
2233 void *pRawMsg = PreProcessMsg(pMsgId: &MsgId, pUnpacker, ClientId);
2234
2235 if(!pRawMsg)
2236 return;
2237
2238 if(Server()->ClientIngame(ClientId))
2239 {
2240 switch(MsgId)
2241 {
2242 case NETMSGTYPE_CL_SAY:
2243 OnSayNetMessage(pMsg: static_cast<CNetMsg_Cl_Say *>(pRawMsg), ClientId, pUnpacker);
2244 break;
2245 case NETMSGTYPE_CL_CALLVOTE:
2246 OnCallVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_CallVote *>(pRawMsg), ClientId);
2247 break;
2248 case NETMSGTYPE_CL_VOTE:
2249 OnVoteNetMessage(pMsg: static_cast<CNetMsg_Cl_Vote *>(pRawMsg), ClientId);
2250 break;
2251 case NETMSGTYPE_CL_SETTEAM:
2252 OnSetTeamNetMessage(pMsg: static_cast<CNetMsg_Cl_SetTeam *>(pRawMsg), ClientId);
2253 break;
2254 case NETMSGTYPE_CL_ISDDNETLEGACY:
2255 OnIsDDNetLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_IsDDNetLegacy *>(pRawMsg), ClientId, pUnpacker);
2256 break;
2257 case NETMSGTYPE_CL_SHOWOTHERSLEGACY:
2258 OnShowOthersLegacyNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthersLegacy *>(pRawMsg), ClientId);
2259 break;
2260 case NETMSGTYPE_CL_SHOWOTHERS:
2261 OnShowOthersNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowOthers *>(pRawMsg), ClientId);
2262 break;
2263 case NETMSGTYPE_CL_SHOWDISTANCE:
2264 OnShowDistanceNetMessage(pMsg: static_cast<CNetMsg_Cl_ShowDistance *>(pRawMsg), ClientId);
2265 break;
2266 case NETMSGTYPE_CL_CAMERAINFO:
2267 OnCameraInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_CameraInfo *>(pRawMsg), ClientId);
2268 break;
2269 case NETMSGTYPE_CL_SETSPECTATORMODE:
2270 OnSetSpectatorModeNetMessage(pMsg: static_cast<CNetMsg_Cl_SetSpectatorMode *>(pRawMsg), ClientId);
2271 break;
2272 case NETMSGTYPE_CL_CHANGEINFO:
2273 OnChangeInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_ChangeInfo *>(pRawMsg), ClientId);
2274 break;
2275 case NETMSGTYPE_CL_EMOTICON:
2276 OnEmoticonNetMessage(pMsg: static_cast<CNetMsg_Cl_Emoticon *>(pRawMsg), ClientId);
2277 break;
2278 case NETMSGTYPE_CL_KILL:
2279 OnKillNetMessage(pMsg: static_cast<CNetMsg_Cl_Kill *>(pRawMsg), ClientId);
2280 break;
2281 case NETMSGTYPE_CL_ENABLESPECTATORCOUNT:
2282 OnEnableSpectatorCountNetMessage(pMsg: static_cast<CNetMsg_Cl_EnableSpectatorCount *>(pRawMsg), ClientId);
2283 default:
2284 break;
2285 }
2286 }
2287 if(MsgId == NETMSGTYPE_CL_STARTINFO)
2288 {
2289 OnStartInfoNetMessage(pMsg: static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientId);
2290 }
2291}
2292
2293void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker)
2294{
2295 CPlayer *pPlayer = m_apPlayers[ClientId];
2296 bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get();
2297 if(Check && str_comp(a: pMsg->m_pMessage, b: "xd sure chillerbot.png is lyfe") == 0 && pMsg->m_Team == 0)
2298 {
2299 if(m_TeeHistorianActive)
2300 {
2301 m_TeeHistorian.RecordPlayerMessage(ClientId, pMsg: pUnpacker->CompleteData(), MsgSize: pUnpacker->CompleteSize());
2302 }
2303
2304 pPlayer->m_NotEligibleForFinish = true;
2305 dbg_msg(sys: "hack", fmt: "bot detected, cid=%d", ClientId);
2306 return;
2307 }
2308 int Team = pMsg->m_Team;
2309
2310 // trim right and set maximum length to 256 utf8-characters
2311 int Length = 0;
2312 const char *p = pMsg->m_pMessage;
2313 const char *pEnd = nullptr;
2314 while(*p)
2315 {
2316 const char *pStrOld = p;
2317 int Code = str_utf8_decode(ptr: &p);
2318
2319 // check if unicode is not empty
2320 if(!str_utf8_isspace(code: Code))
2321 {
2322 pEnd = nullptr;
2323 }
2324 else if(pEnd == nullptr)
2325 pEnd = pStrOld;
2326
2327 if(++Length >= 256)
2328 {
2329 *(const_cast<char *>(p)) = 0;
2330 break;
2331 }
2332 }
2333 if(pEnd != nullptr)
2334 *(const_cast<char *>(pEnd)) = 0;
2335
2336 // drop empty and autocreated spam messages (more than 32 characters per second)
2337 if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick())))
2338 return;
2339
2340 int GameTeam = GetDDRaceTeam(ClientId: pPlayer->GetCid());
2341 if(Team)
2342 Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? TEAM_SPECTATORS : GameTeam);
2343 else
2344 Team = TEAM_ALL;
2345
2346 if(pMsg->m_pMessage[0] == '/')
2347 {
2348 const char *pWhisper;
2349 if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "w ")))
2350 {
2351 Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2352 }
2353 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "whisper ")))
2354 {
2355 Whisper(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2356 }
2357 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "c ")))
2358 {
2359 Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2360 }
2361 else if((pWhisper = str_startswith_nocase(str: pMsg->m_pMessage + 1, prefix: "converse ")))
2362 {
2363 Converse(ClientId: pPlayer->GetCid(), pStr: const_cast<char *>(pWhisper));
2364 }
2365 else
2366 {
2367 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())
2368 return;
2369
2370 int64_t Now = Server()->Tick();
2371 pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now;
2372 pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4;
2373
2374 Console()->SetFlagMask(CFGFLAG_CHAT);
2375 {
2376 CClientChatLogger Logger(this, ClientId, log_get_scope_logger());
2377 CLogScope Scope(&Logger);
2378 Console()->ExecuteLine(pStr: pMsg->m_pMessage + 1, ClientId, InterpretSemicolons: false);
2379 }
2380 // m_apPlayers[ClientId] can be NULL, if the player used a
2381 // timeout code and replaced another client.
2382 char aBuf[256];
2383 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d used %s", ClientId, pMsg->m_pMessage);
2384 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "chat-command", pStr: aBuf);
2385
2386 Console()->SetFlagMask(CFGFLAG_SERVER);
2387 }
2388 }
2389 else
2390 {
2391 pPlayer->UpdatePlaytime();
2392 char aCensoredMessage[256];
2393 CensorMessage(pCensoredMessage: aCensoredMessage, pMessage: pMsg->m_pMessage, Size: sizeof(aCensoredMessage));
2394 SendChat(ChatterClientId: ClientId, Team, pText: aCensoredMessage, SpamProtectionClientId: ClientId);
2395 }
2396}
2397
2398void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId)
2399{
2400 if(RateLimitPlayerVote(ClientId) || m_VoteCloseTime)
2401 return;
2402
2403 m_apPlayers[ClientId]->UpdatePlaytime();
2404
2405 m_VoteType = VOTE_TYPE_UNKNOWN;
2406 char aChatmsg[512] = {0};
2407 char aDesc[VOTE_DESC_LENGTH] = {0};
2408 char aSixupDesc[VOTE_DESC_LENGTH] = {0};
2409 char aCmd[VOTE_CMD_LENGTH] = {0};
2410 char aReason[VOTE_REASON_LENGTH] = "No reason given";
2411 if(pMsg->m_pReason[0])
2412 {
2413 str_copy(dst: aReason, src: pMsg->m_pReason, dst_size: sizeof(aReason));
2414 }
2415
2416 if(str_comp_nocase(a: pMsg->m_pType, b: "option") == 0)
2417 {
2418 CVoteOptionServer *pOption = m_pVoteOptionFirst;
2419 while(pOption)
2420 {
2421 if(str_comp_nocase(a: pMsg->m_pValue, b: pOption->m_aDescription) == 0)
2422 {
2423 if(!Console()->LineIsValid(pStr: pOption->m_aCommand))
2424 {
2425 SendChatTarget(To: ClientId, pText: "Invalid option");
2426 return;
2427 }
2428 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))
2429 {
2430 return;
2431 }
2432
2433 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientId),
2434 pOption->m_aDescription, aReason);
2435 str_copy(dst&: aDesc, src: pOption->m_aDescription);
2436
2437 if((str_endswith(str: pOption->m_aCommand, suffix: "random_map") || str_endswith(str: pOption->m_aCommand, suffix: "random_unfinished_map")))
2438 {
2439 if(str_length(str: aReason) == 1 && aReason[0] >= '0' && aReason[0] <= '5')
2440 {
2441 int Stars = aReason[0] - '0';
2442 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d", pOption->m_aCommand, Stars);
2443 }
2444 else if(str_length(str: aReason) == 3 && aReason[1] == '-' && aReason[0] >= '0' && aReason[0] <= '5' && aReason[2] >= '0' && aReason[2] <= '5')
2445 {
2446 int Start = aReason[0] - '0';
2447 int End = aReason[2] - '0';
2448 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "%s %d %d", pOption->m_aCommand, Start, End);
2449 }
2450 else
2451 {
2452 str_copy(dst&: aCmd, src: pOption->m_aCommand);
2453 }
2454 }
2455 else
2456 {
2457 str_copy(dst&: aCmd, src: pOption->m_aCommand);
2458 }
2459
2460 m_LastMapVote = time_get();
2461 break;
2462 }
2463
2464 pOption = pOption->m_pNext;
2465 }
2466
2467 if(!pOption)
2468 {
2469 if(!Server()->IsRconAuthedAdmin(ClientId)) // allow admins to call any vote they want
2470 {
2471 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' isn't an option on this server", pMsg->m_pValue);
2472 SendChatTarget(To: ClientId, pText: aChatmsg);
2473 return;
2474 }
2475 else
2476 {
2477 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s'", Server()->ClientName(ClientId), pMsg->m_pValue);
2478 str_copy(dst&: aDesc, src: pMsg->m_pValue);
2479 str_copy(dst&: aCmd, src: pMsg->m_pValue);
2480 }
2481 }
2482
2483 m_VoteType = VOTE_TYPE_OPTION;
2484 }
2485 else if(str_comp_nocase(a: pMsg->m_pType, b: "kick") == 0)
2486 {
2487 if(!g_Config.m_SvVoteKick && !Server()->IsRconAuthed(ClientId)) // allow admins to call kick votes even if they are forbidden
2488 {
2489 SendChatTarget(To: ClientId, pText: "Server does not allow voting to kick players");
2490 return;
2491 }
2492 if(!Server()->IsRconAuthed(ClientId) && time_get() < m_apPlayers[ClientId]->m_LastKickVote + (time_freq() * g_Config.m_SvVoteKickDelay))
2493 {
2494 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)",
2495 g_Config.m_SvVoteKickDelay,
2496 (int)((m_apPlayers[ClientId]->m_LastKickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq()));
2497 SendChatTarget(To: ClientId, pText: aChatmsg);
2498 return;
2499 }
2500
2501 if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientId))
2502 {
2503 const NETADDR *apAddresses[MAX_CLIENTS];
2504 for(int i = 0; i < MAX_CLIENTS; i++)
2505 {
2506 if(m_apPlayers[i])
2507 {
2508 apAddresses[i] = Server()->ClientAddr(ClientId: i);
2509 }
2510 }
2511 int NumPlayers = 0;
2512 for(int i = 0; i < MAX_CLIENTS; ++i)
2513 {
2514 if(m_apPlayers[i] && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: i))
2515 {
2516 NumPlayers++;
2517 for(int j = 0; j < i; j++)
2518 {
2519 if(m_apPlayers[j] && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(ClientId: j))
2520 {
2521 if(!net_addr_comp_noport(a: apAddresses[i], b: apAddresses[j]))
2522 {
2523 NumPlayers--;
2524 break;
2525 }
2526 }
2527 }
2528 }
2529 }
2530
2531 if(NumPlayers < g_Config.m_SvVoteKickMin)
2532 {
2533 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "Kick voting requires %d players", g_Config.m_SvVoteKickMin);
2534 SendChatTarget(To: ClientId, pText: aChatmsg);
2535 return;
2536 }
2537 }
2538
2539 int KickId = str_toint(str: pMsg->m_pValue);
2540
2541 if(KickId < 0 || KickId >= MAX_CLIENTS || !m_apPlayers[KickId])
2542 {
2543 SendChatTarget(To: ClientId, pText: "Invalid client id to kick");
2544 return;
2545 }
2546 if(KickId == ClientId)
2547 {
2548 SendChatTarget(To: ClientId, pText: "You can't kick yourself");
2549 return;
2550 }
2551 if(!Server()->ReverseTranslate(Target&: KickId, Client: ClientId))
2552 {
2553 return;
2554 }
2555 int Authed = Server()->GetAuthedState(ClientId);
2556 int KickedAuthed = Server()->GetAuthedState(ClientId: KickId);
2557 if(KickedAuthed > Authed)
2558 {
2559 SendChatTarget(To: ClientId, pText: "You can't kick authorized players");
2560 char aBufKick[128];
2561 str_format(buffer: aBufKick, buffer_size: sizeof(aBufKick), format: "'%s' called for vote to kick you", Server()->ClientName(ClientId));
2562 SendChatTarget(To: KickId, pText: aBufKick);
2563 return;
2564 }
2565
2566 // Don't allow kicking if a player has no character
2567 if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: KickId))
2568 {
2569 SendChatTarget(To: ClientId, pText: "You can kick only your team member");
2570 return;
2571 }
2572
2573 if(GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: KickId))
2574 {
2575 if(!g_Config.m_SvVoteKickMuteTime)
2576 {
2577 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);
2578 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", KickId, Server()->ClientName(ClientId: KickId));
2579 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "muteid %d %d Muted by vote", KickId, g_Config.m_SvVoteKickMuteTime);
2580 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Mute '%s'", Server()->ClientName(ClientId: KickId));
2581 }
2582 else
2583 {
2584 SendChatTarget(To: ClientId, pText: "You can kick only your team member");
2585 return;
2586 }
2587 }
2588 else
2589 {
2590 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);
2591 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", KickId, Server()->ClientName(ClientId: KickId));
2592 if(!GetDDRaceTeam(ClientId))
2593 {
2594 if(!g_Config.m_SvVoteKickBantime)
2595 {
2596 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "kick %d Kicked by vote", KickId);
2597 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Kick '%s'", Server()->ClientName(ClientId: KickId));
2598 }
2599 else
2600 {
2601 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);
2602 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Ban '%s'", Server()->ClientName(ClientId: KickId));
2603 }
2604 }
2605 else
2606 {
2607 str_format(buffer: aCmd, buffer_size: sizeof(aCmd), format: "uninvite %d %d; set_team_ddr %d 0", KickId, GetDDRaceTeam(ClientId: KickId), KickId);
2608 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to team 0", Server()->ClientName(ClientId: KickId));
2609 }
2610 }
2611 m_apPlayers[ClientId]->m_LastKickVote = time_get();
2612 m_VoteType = VOTE_TYPE_KICK;
2613 m_VoteVictim = KickId;
2614 }
2615 else if(str_comp_nocase(a: pMsg->m_pType, b: "spectate") == 0)
2616 {
2617 if(!g_Config.m_SvVoteSpectate)
2618 {
2619 SendChatTarget(To: ClientId, pText: "Server does not allow voting to move players to spectators");
2620 return;
2621 }
2622
2623 int SpectateId = str_toint(str: pMsg->m_pValue);
2624
2625 if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !m_apPlayers[SpectateId] || m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
2626 {
2627 SendChatTarget(To: ClientId, pText: "Invalid client id to move to spectators");
2628 return;
2629 }
2630 if(SpectateId == ClientId)
2631 {
2632 SendChatTarget(To: ClientId, pText: "You can't move yourself to spectators");
2633 return;
2634 }
2635 int Authed = Server()->GetAuthedState(ClientId);
2636 int SpectateAuthed = Server()->GetAuthedState(ClientId: SpectateId);
2637 if(SpectateAuthed > Authed)
2638 {
2639 SendChatTarget(To: ClientId, pText: "You can't move authorized players to spectators");
2640 char aBufSpectate[128];
2641 str_format(buffer: aBufSpectate, buffer_size: sizeof(aBufSpectate), format: "'%s' called for vote to move you to spectators", Server()->ClientName(ClientId));
2642 SendChatTarget(To: SpectateId, pText: aBufSpectate);
2643 return;
2644 }
2645 if(!Server()->ReverseTranslate(Target&: SpectateId, Client: ClientId))
2646 {
2647 return;
2648 }
2649
2650 if(!GetPlayerChar(ClientId) || !GetPlayerChar(ClientId: SpectateId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(ClientId: SpectateId))
2651 {
2652 SendChatTarget(To: ClientId, pText: "You can only move your team member to spectators");
2653 return;
2654 }
2655
2656 str_format(buffer: aSixupDesc, buffer_size: sizeof(aSixupDesc), format: "%2d: %s", SpectateId, Server()->ClientName(ClientId: SpectateId));
2657 if(g_Config.m_SvPauseable && g_Config.m_SvVotePause)
2658 {
2659 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);
2660 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Pause '%s' (%ds)", Server()->ClientName(ClientId: SpectateId), g_Config.m_SvVotePauseTime);
2661 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);
2662 }
2663 else
2664 {
2665 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);
2666 str_format(buffer: aDesc, buffer_size: sizeof(aDesc), format: "Move '%s' to spectators", Server()->ClientName(ClientId: SpectateId));
2667 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);
2668 }
2669 m_VoteType = VOTE_TYPE_SPECTATE;
2670 m_VoteVictim = SpectateId;
2671 }
2672
2673 if(aCmd[0] && str_comp_nocase(a: aCmd, b: "info") != 0)
2674 CallVote(ClientId, pDesc: aDesc, pCmd: aCmd, pReason: aReason, pChatmsg: aChatmsg, pSixupDesc: aSixupDesc[0] ? aSixupDesc : nullptr);
2675}
2676
2677void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId)
2678{
2679 if(!m_VoteCloseTime)
2680 return;
2681
2682 CPlayer *pPlayer = m_apPlayers[ClientId];
2683
2684 if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick())
2685 return;
2686
2687 pPlayer->m_LastVoteTry = Server()->Tick();
2688 pPlayer->UpdatePlaytime();
2689
2690 if(!pMsg->m_Vote)
2691 return;
2692
2693 // Allow the vote creator to cancel the vote
2694 if(pPlayer->GetCid() == m_VoteCreator && pMsg->m_Vote == -1)
2695 {
2696 m_VoteEnforce = VOTE_ENFORCE_CANCEL;
2697 return;
2698 }
2699
2700 pPlayer->m_Vote = pMsg->m_Vote;
2701 pPlayer->m_VotePos = ++m_VotePos;
2702 m_VoteUpdate = true;
2703
2704 CNetMsg_Sv_YourVote Msg = {.m_Voted: pMsg->m_Vote};
2705 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
2706}
2707
2708void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId)
2709{
2710 if(m_pController->IsGamePaused())
2711 return;
2712
2713 CPlayer *pPlayer = m_apPlayers[ClientId];
2714
2715 if(pPlayer->GetTeam() == pMsg->m_Team)
2716 return;
2717 if(g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick())
2718 return;
2719
2720 // Kill Protection
2721 CCharacter *pChr = pPlayer->GetCharacter();
2722 if(pChr)
2723 {
2724 int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
2725 if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
2726 {
2727 SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to join the spectators, first type /kill");
2728 return;
2729 }
2730 }
2731
2732 if(pPlayer->m_TeamChangeTick > Server()->Tick())
2733 {
2734 pPlayer->m_LastSetTeam = Server()->Tick();
2735 int TimeLeft = (pPlayer->m_TeamChangeTick - Server()->Tick()) / Server()->TickSpeed();
2736 char aTime[32];
2737 str_time(centisecs: (int64_t)TimeLeft * 100, format: ETimeFormat::HOURS, buffer: aTime, buffer_size: sizeof(aTime));
2738 char aBuf[128];
2739 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Time to wait before changing team: %s", aTime);
2740 SendBroadcast(pText: aBuf, ClientId);
2741 return;
2742 }
2743
2744 // Switch team on given client and kill/respawn them
2745 char aTeamJoinError[512];
2746 if(m_pController->CanJoinTeam(Team: pMsg->m_Team, NotThisId: ClientId, pErrorReason: aTeamJoinError, ErrorReasonSize: sizeof(aTeamJoinError)))
2747 {
2748 if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
2749 m_VoteUpdate = true;
2750 m_pController->DoTeamChange(pPlayer, Team: pMsg->m_Team);
2751 pPlayer->m_TeamChangeTick = Server()->Tick();
2752 }
2753 else
2754 SendBroadcast(pText: aTeamJoinError, ClientId);
2755}
2756
2757void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker)
2758{
2759 IServer::CClientInfo Info;
2760 if(Server()->GetClientInfo(ClientId, pInfo: &Info) && Info.m_GotDDNetVersion)
2761 {
2762 return;
2763 }
2764 int DDNetVersion = pUnpacker->GetInt();
2765 if(pUnpacker->Error() || DDNetVersion < 0)
2766 {
2767 DDNetVersion = VERSION_DDRACE;
2768 }
2769 Server()->SetClientDDNetVersion(ClientId, DDNetVersion);
2770 OnClientDDNetVersionKnown(ClientId);
2771}
2772
2773void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId)
2774{
2775 if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
2776 {
2777 CPlayer *pPlayer = m_apPlayers[ClientId];
2778 pPlayer->m_ShowOthers = pMsg->m_Show;
2779 }
2780}
2781
2782void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId)
2783{
2784 if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
2785 {
2786 CPlayer *pPlayer = m_apPlayers[ClientId];
2787 pPlayer->m_ShowOthers = pMsg->m_Show;
2788 }
2789}
2790
2791void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId)
2792{
2793 CPlayer *pPlayer = m_apPlayers[ClientId];
2794 pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y);
2795}
2796
2797void CGameContext::OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId)
2798{
2799 CPlayer *pPlayer = m_apPlayers[ClientId];
2800 pPlayer->m_CameraInfo.Write(pMsg);
2801}
2802
2803void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId)
2804{
2805 if(m_pController->IsGamePaused())
2806 return;
2807
2808 int SpectatorId = std::clamp(val: pMsg->m_SpectatorId, lo: (int)SPEC_FOLLOW, hi: MAX_CLIENTS - 1);
2809 if(SpectatorId >= 0)
2810 if(!Server()->ReverseTranslate(Target&: SpectatorId, Client: ClientId))
2811 return;
2812
2813 CPlayer *pPlayer = m_apPlayers[ClientId];
2814 if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick()))
2815 return;
2816
2817 pPlayer->m_LastSetSpectatorMode = Server()->Tick();
2818 pPlayer->UpdatePlaytime();
2819 if(SpectatorId >= 0 && (!m_apPlayers[SpectatorId] || m_apPlayers[SpectatorId]->GetTeam() == TEAM_SPECTATORS))
2820 SendChatTarget(To: ClientId, pText: "Invalid spectator id used");
2821 else
2822 pPlayer->SetSpectatorId(SpectatorId);
2823}
2824
2825void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId)
2826{
2827 CPlayer *pPlayer = m_apPlayers[ClientId];
2828 if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
2829 return;
2830
2831 bool SixupNeedsUpdate = false;
2832
2833 pPlayer->m_LastChangeInfo = Server()->Tick();
2834 pPlayer->UpdatePlaytime();
2835
2836 if(g_Config.m_SvSpamprotection)
2837 {
2838 CNetMsg_Sv_ChangeInfoCooldown ChangeInfoCooldownMsg;
2839 ChangeInfoCooldownMsg.m_WaitUntil = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay;
2840 Server()->SendPackMsg(pMsg: &ChangeInfoCooldownMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
2841 }
2842
2843 // set infos
2844 if(Server()->WouldClientNameChange(ClientId, pNameRequest: pMsg->m_pName) && !ProcessSpamProtection(ClientId))
2845 {
2846 char aOldName[MAX_NAME_LENGTH];
2847 str_copy(dst: aOldName, src: Server()->ClientName(ClientId), dst_size: sizeof(aOldName));
2848
2849 Server()->SetClientName(ClientId, pName: pMsg->m_pName);
2850
2851 char aChatText[256];
2852 str_format(buffer: aChatText, buffer_size: sizeof(aChatText), format: "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientId));
2853 SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aChatText);
2854
2855 // reload scores
2856 Score()->PlayerData(Id: ClientId)->Reset();
2857 Server()->SetClientScore(ClientId, Score: std::nullopt);
2858 Score()->LoadPlayerData(ClientId);
2859
2860 SixupNeedsUpdate = true;
2861
2862 LogEvent(Description: "Name change", ClientId);
2863 }
2864
2865 if(Server()->WouldClientClanChange(ClientId, pClanRequest: pMsg->m_pClan))
2866 {
2867 SixupNeedsUpdate = true;
2868 Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan);
2869 }
2870
2871 if(Server()->ClientCountry(ClientId) != pMsg->m_Country)
2872 SixupNeedsUpdate = true;
2873 Server()->SetClientCountry(ClientId, Country: pMsg->m_Country);
2874
2875 str_copy(dst: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin, dst_size: sizeof(pPlayer->m_TeeInfos.m_aSkinName));
2876 pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
2877 pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
2878 pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
2879 if(!Server()->IsSixup(ClientId))
2880 pPlayer->m_TeeInfos.ToSixup();
2881
2882 if(SixupNeedsUpdate)
2883 {
2884 protocol7::CNetMsg_Sv_ClientDrop Drop;
2885 Drop.m_ClientId = ClientId;
2886 Drop.m_pReason = "";
2887 Drop.m_Silent = true;
2888
2889 protocol7::CNetMsg_Sv_ClientInfo Info;
2890 Info.m_ClientId = ClientId;
2891 Info.m_pName = Server()->ClientName(ClientId);
2892 Info.m_Country = pMsg->m_Country;
2893 Info.m_pClan = pMsg->m_pClan;
2894 Info.m_Local = 0;
2895 Info.m_Silent = true;
2896 Info.m_Team = pPlayer->GetTeam();
2897
2898 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
2899 {
2900 Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p];
2901 Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
2902 Info.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
2903 }
2904
2905 for(int i = 0; i < Server()->MaxClients(); i++)
2906 {
2907 if(i != ClientId)
2908 {
2909 Server()->SendPackMsg(pMsg: &Drop, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i);
2910 Server()->SendPackMsg(pMsg: &Info, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: i);
2911 }
2912 }
2913 }
2914 else
2915 {
2916 SendSkinChange7(ClientId);
2917 }
2918
2919 Server()->ExpireServerInfo();
2920}
2921
2922void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId)
2923{
2924 if(m_pController->IsGamePaused())
2925 return;
2926
2927 CPlayer *pPlayer = m_apPlayers[ClientId];
2928
2929 auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) {
2930 return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000);
2931 };
2932
2933 if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmote, (int64_t)g_Config.m_SvEmoticonMsDelay))
2934 return;
2935
2936 CCharacter *pChr = pPlayer->GetCharacter();
2937
2938 // player needs a character to send emotes
2939 if(!pChr)
2940 return;
2941
2942 pPlayer->m_LastEmote = Server()->Tick();
2943 pPlayer->UpdatePlaytime();
2944
2945 // check if the global emoticon is prevented and emotes are only send to nearby players
2946 if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmoteGlobal, (int64_t)g_Config.m_SvGlobalEmoticonMsDelay))
2947 {
2948 for(int i = 0; i < MAX_CLIENTS; ++i)
2949 {
2950 if(m_apPlayers[i] && pChr->CanSnapCharacter(SnappingClient: i) && pChr->IsSnappingCharacterInView(SnappingClientId: i))
2951 {
2952 SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: i);
2953 }
2954 }
2955 }
2956 else
2957 {
2958 // else send emoticons to all players
2959 pPlayer->m_LastEmoteGlobal = Server()->Tick();
2960 SendEmoticon(ClientId, Emoticon: pMsg->m_Emoticon, TargetClientId: -1);
2961 }
2962
2963 if(g_Config.m_SvEmotionalTees == 1 && pPlayer->m_EyeEmoteEnabled)
2964 {
2965 int EmoteType = EMOTE_NORMAL;
2966 switch(pMsg->m_Emoticon)
2967 {
2968 case EMOTICON_EXCLAMATION:
2969 case EMOTICON_GHOST:
2970 case EMOTICON_QUESTION:
2971 case EMOTICON_WTF:
2972 EmoteType = EMOTE_SURPRISE;
2973 break;
2974 case EMOTICON_DOTDOT:
2975 case EMOTICON_DROP:
2976 case EMOTICON_ZZZ:
2977 EmoteType = EMOTE_BLINK;
2978 break;
2979 case EMOTICON_EYES:
2980 case EMOTICON_HEARTS:
2981 case EMOTICON_MUSIC:
2982 EmoteType = EMOTE_HAPPY;
2983 break;
2984 case EMOTICON_OOP:
2985 case EMOTICON_SORRY:
2986 case EMOTICON_SUSHI:
2987 EmoteType = EMOTE_PAIN;
2988 break;
2989 case EMOTICON_DEVILTEE:
2990 case EMOTICON_SPLATTEE:
2991 case EMOTICON_ZOMG:
2992 EmoteType = EMOTE_ANGRY;
2993 break;
2994 default:
2995 break;
2996 }
2997 pChr->SetEmote(Emote: EmoteType, Tick: Server()->Tick() + 2 * Server()->TickSpeed());
2998 }
2999}
3000
3001void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId)
3002{
3003 if(m_pController->IsGamePaused())
3004 return;
3005
3006 if(IsRunningKickOrSpecVote(ClientId) && GetDDRaceTeam(ClientId))
3007 {
3008 SendChatTarget(To: ClientId, pText: "You are running a vote please try again after the vote is done!");
3009 return;
3010 }
3011 CPlayer *pPlayer = m_apPlayers[ClientId];
3012 if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick())
3013 return;
3014 if(pPlayer->IsPaused())
3015 return;
3016
3017 CCharacter *pChr = pPlayer->GetCharacter();
3018 if(!pChr)
3019 return;
3020
3021 // Kill Protection
3022 int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
3023 if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
3024 {
3025 SendChatTarget(To: ClientId, pText: "Kill Protection enabled. If you really want to kill, type /kill");
3026 return;
3027 }
3028
3029 pPlayer->m_LastKill = Server()->Tick();
3030 pPlayer->KillCharacter(Weapon: WEAPON_SELF);
3031 pPlayer->Respawn();
3032}
3033
3034void CGameContext::OnEnableSpectatorCountNetMessage(const CNetMsg_Cl_EnableSpectatorCount *pMsg, int ClientId)
3035{
3036 CPlayer *pPlayer = m_apPlayers[ClientId];
3037 if(!pPlayer)
3038 return;
3039
3040 pPlayer->m_EnableSpectatorCount = pMsg->m_Enable;
3041}
3042
3043void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId)
3044{
3045 CPlayer *pPlayer = m_apPlayers[ClientId];
3046
3047 if(pPlayer->m_IsReady)
3048 return;
3049
3050 pPlayer->m_LastChangeInfo = Server()->Tick();
3051
3052 // set start infos
3053 Server()->SetClientName(ClientId, pName: pMsg->m_pName);
3054 // trying to set client name can delete the player object, check if it still exists
3055 if(!m_apPlayers[ClientId])
3056 {
3057 return;
3058 }
3059 Server()->SetClientClan(ClientId, pClan: pMsg->m_pClan);
3060 // trying to set client clan can delete the player object, check if it still exists
3061 if(!m_apPlayers[ClientId])
3062 {
3063 return;
3064 }
3065 Server()->SetClientCountry(ClientId, Country: pMsg->m_Country);
3066 str_copy(dst: pPlayer->m_TeeInfos.m_aSkinName, src: pMsg->m_pSkin, dst_size: sizeof(pPlayer->m_TeeInfos.m_aSkinName));
3067 pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
3068 pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
3069 pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
3070 if(!Server()->IsSixup(ClientId))
3071 pPlayer->m_TeeInfos.ToSixup();
3072
3073 // send clear vote options
3074 CNetMsg_Sv_VoteClearOptions ClearMsg;
3075 Server()->SendPackMsg(pMsg: &ClearMsg, Flags: MSGFLAG_VITAL, ClientId);
3076
3077 // begin sending vote options
3078 pPlayer->m_SendVoteIndex = 0;
3079
3080 // send tuning parameters to client
3081 SendTuningParams(ClientId, Zone: pPlayer->m_TuneZone);
3082
3083 // client is ready to enter
3084 pPlayer->m_IsReady = true;
3085 CNetMsg_Sv_ReadyToEnter ReadyMsg;
3086 Server()->SendPackMsg(pMsg: &ReadyMsg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId);
3087
3088 Server()->ExpireServerInfo();
3089}
3090
3091void CGameContext::ConTuneParam(IConsole::IResult *pResult, void *pUserData)
3092{
3093 CGameContext *pSelf = (CGameContext *)pUserData;
3094 const char *pParamName = pResult->GetString(Index: 0);
3095
3096 char aBuf[256];
3097 if(pResult->NumArguments() == 2)
3098 {
3099 float NewValue = pResult->GetFloat(Index: 1);
3100 if(pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue))
3101 {
3102 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f", pParamName, NewValue);
3103 pSelf->SendTuningParams(ClientId: -1);
3104 }
3105 else
3106 {
3107 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3108 }
3109 }
3110 else
3111 {
3112 float Value;
3113 if(pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &Value))
3114 {
3115 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f", pParamName, Value);
3116 }
3117 else
3118 {
3119 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3120 }
3121 }
3122 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3123}
3124
3125void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData)
3126{
3127 CGameContext *pSelf = (CGameContext *)pUserData;
3128 const char *pParamName = pResult->GetString(Index: 0);
3129 float OldValue;
3130
3131 char aBuf[256];
3132 if(!pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &OldValue))
3133 {
3134 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3135 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3136 return;
3137 }
3138
3139 float NewValue = absolute(a: OldValue - pResult->GetFloat(Index: 1)) < 0.0001f ? pResult->GetFloat(Index: 2) : pResult->GetFloat(Index: 1);
3140
3141 pSelf->GlobalTuning()->Set(pName: pParamName, Value: NewValue);
3142 pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &NewValue);
3143
3144 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s changed to %.2f", pParamName, NewValue);
3145 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3146 pSelf->SendTuningParams(ClientId: -1);
3147}
3148
3149void CGameContext::ConTuneReset(IConsole::IResult *pResult, void *pUserData)
3150{
3151 CGameContext *pSelf = (CGameContext *)pUserData;
3152 if(pResult->NumArguments())
3153 {
3154 const char *pParamName = pResult->GetString(Index: 0);
3155 float DefaultValue = 0.0f;
3156 char aBuf[256];
3157
3158 if(CTuningParams::DEFAULT.Get(pName: pParamName, pValue: &DefaultValue) && pSelf->GlobalTuning()->Set(pName: pParamName, Value: DefaultValue) && pSelf->GlobalTuning()->Get(pName: pParamName, pValue: &DefaultValue))
3159 {
3160 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s reset to %.2f", pParamName, DefaultValue);
3161 pSelf->SendTuningParams(ClientId: -1);
3162 }
3163 else
3164 {
3165 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3166 }
3167 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3168 }
3169 else
3170 {
3171 pSelf->ResetTuning();
3172 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: "Tuning reset");
3173 }
3174}
3175
3176void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData)
3177{
3178 CGameContext *pSelf = (CGameContext *)pUserData;
3179 char aBuf[256];
3180 for(int i = 0; i < CTuningParams::Num(); i++)
3181 {
3182 float Value;
3183 pSelf->GlobalTuning()->Get(Index: i, pValue: &Value);
3184 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %.2f", CTuningParams::Name(Index: i), Value);
3185 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3186 }
3187}
3188
3189void CGameContext::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
3190{
3191 CGameContext *pSelf = (CGameContext *)pUserData;
3192 int List = pResult->GetInteger(Index: 0);
3193 const char *pParamName = pResult->GetString(Index: 1);
3194 float NewValue = pResult->GetFloat(Index: 2);
3195
3196 if(List >= 0 && List < TuneZone::NUM)
3197 {
3198 char aBuf[256];
3199 if(pSelf->TuningList()[List].Set(pName: pParamName, Value: NewValue) && pSelf->TuningList()[List].Get(pName: pParamName, pValue: &NewValue))
3200 {
3201 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s in zone %d changed to %.2f", pParamName, List, NewValue);
3202 pSelf->SendTuningParams(ClientId: -1, Zone: List);
3203 }
3204 else
3205 {
3206 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such tuning parameter: %s", pParamName);
3207 }
3208 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3209 }
3210}
3211
3212void CGameContext::ConTuneDumpZone(IConsole::IResult *pResult, void *pUserData)
3213{
3214 CGameContext *pSelf = (CGameContext *)pUserData;
3215 int List = pResult->GetInteger(Index: 0);
3216 char aBuf[256];
3217 if(List >= 0 && List < TuneZone::NUM)
3218 {
3219 for(int i = 0; i < CTuningParams::Num(); i++)
3220 {
3221 float Value;
3222 pSelf->TuningList()[List].Get(Index: i, pValue: &Value);
3223 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "zone %d: %s %.2f", List, CTuningParams::Name(Index: i), Value);
3224 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3225 }
3226 }
3227}
3228
3229void CGameContext::ConTuneResetZone(IConsole::IResult *pResult, void *pUserData)
3230{
3231 CGameContext *pSelf = (CGameContext *)pUserData;
3232 if(pResult->NumArguments())
3233 {
3234 int List = pResult->GetInteger(Index: 0);
3235 if(List >= 0 && List < TuneZone::NUM)
3236 {
3237 pSelf->TuningList()[List] = CTuningParams::DEFAULT;
3238 char aBuf[256];
3239 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Tunezone %d reset", List);
3240 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: aBuf);
3241 pSelf->SendTuningParams(ClientId: -1, Zone: List);
3242 }
3243 }
3244 else
3245 {
3246 for(int i = 0; i < TuneZone::NUM; i++)
3247 {
3248 *(pSelf->TuningList() + i) = CTuningParams::DEFAULT;
3249 pSelf->SendTuningParams(ClientId: -1, Zone: i);
3250 }
3251 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "tuning", pStr: "All Tunezones reset");
3252 }
3253}
3254
3255void CGameContext::ConTuneSetZoneMsgEnter(IConsole::IResult *pResult, void *pUserData)
3256{
3257 CGameContext *pSelf = (CGameContext *)pUserData;
3258 if(pResult->NumArguments())
3259 {
3260 int List = pResult->GetInteger(Index: 0);
3261 if(List >= 0 && List < TuneZone::NUM)
3262 {
3263 str_copy(dst: pSelf->m_aaZoneEnterMsg[List], src: pResult->GetString(Index: 1), dst_size: sizeof(pSelf->m_aaZoneEnterMsg[List]));
3264 }
3265 }
3266}
3267
3268void CGameContext::ConTuneSetZoneMsgLeave(IConsole::IResult *pResult, void *pUserData)
3269{
3270 CGameContext *pSelf = (CGameContext *)pUserData;
3271 if(pResult->NumArguments())
3272 {
3273 int List = pResult->GetInteger(Index: 0);
3274 if(List >= 0 && List < TuneZone::NUM)
3275 {
3276 str_copy(dst: pSelf->m_aaZoneLeaveMsg[List], src: pResult->GetString(Index: 1), dst_size: sizeof(pSelf->m_aaZoneLeaveMsg[List]));
3277 }
3278 }
3279}
3280
3281void CGameContext::ConMapbug(IConsole::IResult *pResult, void *pUserData)
3282{
3283 CGameContext *pSelf = (CGameContext *)pUserData;
3284
3285 if(pSelf->m_pController)
3286 {
3287 log_info("mapbugs", "can't add map bugs after the game started");
3288 return;
3289 }
3290
3291 const char *pMapBugName = pResult->GetString(Index: 0);
3292 switch(pSelf->m_MapBugs.Update(pBug: pMapBugName))
3293 {
3294 case EMapBugUpdate::OK:
3295 break;
3296 case EMapBugUpdate::OVERRIDDEN:
3297 log_info("mapbugs", "map-internal setting overridden by database");
3298 break;
3299 case EMapBugUpdate::NOTFOUND:
3300 log_info("mapbugs", "unknown map bug '%s', ignoring", pMapBugName);
3301 break;
3302 default:
3303 dbg_assert_failed("unreachable");
3304 }
3305}
3306
3307void CGameContext::ConSwitchOpen(IConsole::IResult *pResult, void *pUserData)
3308{
3309 CGameContext *pSelf = (CGameContext *)pUserData;
3310 int Switch = pResult->GetInteger(Index: 0);
3311
3312 if(in_range(a: Switch, upper: (int)pSelf->Switchers().size() - 1))
3313 {
3314 pSelf->Switchers()[Switch].m_Initial = false;
3315 char aBuf[256];
3316 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "switch %d opened by default", Switch);
3317 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3318 }
3319}
3320
3321void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData)
3322{
3323 CGameContext *pSelf = (CGameContext *)pUserData;
3324
3325 pSelf->m_pController->SetGamePaused(!pSelf->m_pController->IsGamePaused());
3326}
3327
3328void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData)
3329{
3330 CGameContext *pSelf = (CGameContext *)pUserData;
3331 pSelf->m_pController->ChangeMap(pToMap: pResult->GetString(Index: 0));
3332}
3333
3334void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData)
3335{
3336 CGameContext *pSelf = (CGameContext *)pUserData;
3337
3338 const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
3339 int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1;
3340 int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars;
3341
3342 if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5))
3343 return;
3344
3345 pSelf->m_pScore->RandomMap(ClientId, MinStars, MaxStars);
3346}
3347
3348void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData)
3349{
3350 CGameContext *pSelf = (CGameContext *)pUserData;
3351
3352 const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
3353 int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : -1;
3354 int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : MinStars;
3355
3356 if(!in_range(a: MinStars, lower: -1, upper: 5) || !in_range(a: MaxStars, lower: -1, upper: 5))
3357 return;
3358
3359 pSelf->m_pScore->RandomUnfinishedMap(ClientId, MinStars, MaxStars);
3360}
3361
3362void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData)
3363{
3364 CGameContext *pSelf = (CGameContext *)pUserData;
3365 if(pResult->NumArguments())
3366 pSelf->m_pController->DoWarmup(Seconds: pResult->GetInteger(Index: 0));
3367 else
3368 pSelf->m_pController->StartRound();
3369}
3370
3371static void UnescapeNewlines(char *pBuf)
3372{
3373 int i, j;
3374 for(i = 0, j = 0; pBuf[i]; i++, j++)
3375 {
3376 if(pBuf[i] == '\\' && pBuf[i + 1] == 'n')
3377 {
3378 pBuf[j] = '\n';
3379 i++;
3380 }
3381 else if(i != j)
3382 {
3383 pBuf[j] = pBuf[i];
3384 }
3385 }
3386 pBuf[j] = '\0';
3387}
3388
3389void CGameContext::ConServerAlert(IConsole::IResult *pResult, void *pUserData)
3390{
3391 CGameContext *pSelf = (CGameContext *)pUserData;
3392
3393 char aBuf[1024];
3394 str_copy(dst: aBuf, src: pResult->GetString(Index: 0), dst_size: sizeof(aBuf));
3395 UnescapeNewlines(pBuf: aBuf);
3396
3397 pSelf->SendServerAlert(pMessage: aBuf);
3398}
3399
3400void CGameContext::ConModAlert(IConsole::IResult *pResult, void *pUserData)
3401{
3402 CGameContext *pSelf = (CGameContext *)pUserData;
3403
3404 const int Victim = pResult->GetVictim();
3405 if(!CheckClientId(ClientId: Victim) || !pSelf->m_apPlayers[Victim])
3406 {
3407 log_info("moderator_alert", "Client ID not found: %d", Victim);
3408 return;
3409 }
3410
3411 char aBuf[1024];
3412 str_copy(dst: aBuf, src: pResult->GetString(Index: 1), dst_size: sizeof(aBuf));
3413 UnescapeNewlines(pBuf: aBuf);
3414
3415 pSelf->SendModeratorAlert(pMessage: aBuf, ToClientId: Victim);
3416}
3417
3418void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData)
3419{
3420 CGameContext *pSelf = (CGameContext *)pUserData;
3421
3422 char aBuf[1024];
3423 str_copy(dst: aBuf, src: pResult->GetString(Index: 0), dst_size: sizeof(aBuf));
3424 UnescapeNewlines(pBuf: aBuf);
3425
3426 pSelf->SendBroadcast(pText: aBuf, ClientId: -1);
3427}
3428
3429void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData)
3430{
3431 CGameContext *pSelf = (CGameContext *)pUserData;
3432 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: pResult->GetString(Index: 0));
3433}
3434
3435void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData)
3436{
3437 CGameContext *pSelf = (CGameContext *)pUserData;
3438 int Team = pResult->GetInteger(Index: 1);
3439 if(!pSelf->m_pController->IsValidTeam(Team))
3440 {
3441 log_info("server", "Invalid Team: %d", Team);
3442 return;
3443 }
3444
3445 int ClientId = std::clamp(val: pResult->GetInteger(Index: 0), lo: 0, hi: (int)MAX_CLIENTS - 1);
3446 int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(Index: 2) : 0;
3447 if(!pSelf->m_apPlayers[ClientId])
3448 return;
3449
3450 char aBuf[256];
3451 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "moved client %d to the %s", ClientId, pSelf->m_pController->GetTeamName(Team));
3452 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3453
3454 pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: false); // reset /spec and /pause to allow rejoin
3455 pSelf->m_apPlayers[ClientId]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60;
3456 pSelf->m_pController->DoTeamChange(pPlayer: pSelf->m_apPlayers[ClientId], Team);
3457 if(Team == TEAM_SPECTATORS)
3458 pSelf->m_apPlayers[ClientId]->Pause(State: CPlayer::PAUSE_NONE, Force: true);
3459}
3460
3461void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData)
3462{
3463 CGameContext *pSelf = (CGameContext *)pUserData;
3464 int Team = pResult->GetInteger(Index: 0);
3465 if(!pSelf->m_pController->IsValidTeam(Team))
3466 {
3467 log_info("server", "Invalid Team: %d", Team);
3468 return;
3469 }
3470
3471 char aBuf[256];
3472 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "All players were moved to the %s", pSelf->m_pController->GetTeamName(Team));
3473 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf);
3474
3475 for(auto &pPlayer : pSelf->m_apPlayers)
3476 if(pPlayer)
3477 pSelf->m_pController->DoTeamChange(pPlayer, Team, DoChatMsg: false);
3478}
3479
3480void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData)
3481{
3482 CGameContext *pSelf = (CGameContext *)pUserData;
3483 for(int i = 0; i < MAX_CLIENTS; i++)
3484 {
3485 if(!pSelf->GetPlayerChar(ClientId: i))
3486 continue;
3487
3488 CCharacter *pChar = pSelf->GetPlayerChar(ClientId: i);
3489
3490 // Save the tee individually
3491 pSelf->m_apSavedTees[i] = new CSaveHotReloadTee();
3492 pSelf->m_apSavedTees[i]->Save(pChr: pChar, AddPenalty: false);
3493
3494 // Save the team state
3495 pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(ClientId: i);
3496 if(pSelf->m_aTeamMapping[i] == TEAM_SUPER)
3497 pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper;
3498
3499 if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]])
3500 continue;
3501
3502 pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]] = new CSaveTeam();
3503 pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]->Save(pGameServer: pSelf, Team: pSelf->m_aTeamMapping[i], Dry: true, Force: true);
3504 }
3505 pSelf->Server()->ReloadMap();
3506}
3507
3508void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData)
3509{
3510 CGameContext *pSelf = (CGameContext *)pUserData;
3511 const char *pDescription = pResult->GetString(Index: 0);
3512 const char *pCommand = pResult->GetString(Index: 1);
3513
3514 pSelf->AddVote(pDescription, pCommand);
3515}
3516
3517void CGameContext::AddVote(const char *pDescription, const char *pCommand)
3518{
3519 if(m_NumVoteOptions == MAX_VOTE_OPTIONS)
3520 {
3521 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "maximum number of vote options reached");
3522 return;
3523 }
3524
3525 // check for valid option
3526 if(!Console()->LineIsValid(pStr: pCommand) || str_length(str: pCommand) >= VOTE_CMD_LENGTH)
3527 {
3528 char aBuf[256];
3529 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid command '%s'", pCommand);
3530 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3531 return;
3532 }
3533 while(*pDescription == ' ')
3534 pDescription++;
3535 if(str_length(str: pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0)
3536 {
3537 char aBuf[256];
3538 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "skipped invalid option '%s'", pDescription);
3539 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3540 return;
3541 }
3542
3543 // check for duplicate entry
3544 CVoteOptionServer *pOption = m_pVoteOptionFirst;
3545 while(pOption)
3546 {
3547 if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0)
3548 {
3549 char aBuf[256];
3550 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' already exists", pDescription);
3551 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3552 return;
3553 }
3554 pOption = pOption->m_pNext;
3555 }
3556
3557 // add the option
3558 ++m_NumVoteOptions;
3559 int Len = str_length(str: pCommand);
3560
3561 pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer));
3562 pOption->m_pNext = nullptr;
3563 pOption->m_pPrev = m_pVoteOptionLast;
3564 if(pOption->m_pPrev)
3565 pOption->m_pPrev->m_pNext = pOption;
3566 m_pVoteOptionLast = pOption;
3567 if(!m_pVoteOptionFirst)
3568 m_pVoteOptionFirst = pOption;
3569
3570 str_copy(dst: pOption->m_aDescription, src: pDescription, dst_size: sizeof(pOption->m_aDescription));
3571 str_copy(dst: pOption->m_aCommand, src: pCommand, dst_size: Len + 1);
3572}
3573
3574void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData)
3575{
3576 CGameContext *pSelf = (CGameContext *)pUserData;
3577 const char *pDescription = pResult->GetString(Index: 0);
3578
3579 // check for valid option
3580 CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
3581 while(pOption)
3582 {
3583 if(str_comp_nocase(a: pDescription, b: pOption->m_aDescription) == 0)
3584 break;
3585 pOption = pOption->m_pNext;
3586 }
3587 if(!pOption)
3588 {
3589 char aBuf[256];
3590 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "option '%s' does not exist", pDescription);
3591 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3592 return;
3593 }
3594
3595 // start reloading vote option list
3596 // clear vote options
3597 CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
3598 pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1);
3599
3600 // reset sending of vote options
3601 for(auto &pPlayer : pSelf->m_apPlayers)
3602 {
3603 if(pPlayer)
3604 pPlayer->m_SendVoteIndex = 0;
3605 }
3606
3607 // TODO: improve this
3608 // remove the option
3609 --pSelf->m_NumVoteOptions;
3610
3611 CHeap *pVoteOptionHeap = new CHeap();
3612 CVoteOptionServer *pVoteOptionFirst = nullptr;
3613 CVoteOptionServer *pVoteOptionLast = nullptr;
3614 int NumVoteOptions = pSelf->m_NumVoteOptions;
3615 for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext)
3616 {
3617 if(pSrc == pOption)
3618 continue;
3619
3620 // copy option
3621 int Len = str_length(str: pSrc->m_aCommand);
3622 CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(Size: sizeof(CVoteOptionServer) + Len, Alignment: alignof(CVoteOptionServer));
3623 pDst->m_pNext = nullptr;
3624 pDst->m_pPrev = pVoteOptionLast;
3625 if(pDst->m_pPrev)
3626 pDst->m_pPrev->m_pNext = pDst;
3627 pVoteOptionLast = pDst;
3628 if(!pVoteOptionFirst)
3629 pVoteOptionFirst = pDst;
3630
3631 str_copy(dst: pDst->m_aDescription, src: pSrc->m_aDescription, dst_size: sizeof(pDst->m_aDescription));
3632 str_copy(dst: pDst->m_aCommand, src: pSrc->m_aCommand, dst_size: Len + 1);
3633 }
3634
3635 // clean up
3636 delete pSelf->m_pVoteOptionHeap;
3637 pSelf->m_pVoteOptionHeap = pVoteOptionHeap;
3638 pSelf->m_pVoteOptionFirst = pVoteOptionFirst;
3639 pSelf->m_pVoteOptionLast = pVoteOptionLast;
3640 pSelf->m_NumVoteOptions = NumVoteOptions;
3641}
3642
3643void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData)
3644{
3645 CGameContext *pSelf = (CGameContext *)pUserData;
3646 const char *pType = pResult->GetString(Index: 0);
3647 const char *pValue = pResult->GetString(Index: 1);
3648 const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(Index: 2)[0] ? pResult->GetString(Index: 2) : "No reason given";
3649 char aBuf[128] = {0};
3650
3651 if(str_comp_nocase(a: pType, b: "option") == 0)
3652 {
3653 CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
3654 while(pOption)
3655 {
3656 if(str_comp_nocase(a: pValue, b: pOption->m_aDescription) == 0)
3657 {
3658 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "authorized player forced server option '%s' (%s)", pValue, pReason);
3659 pSelf->SendChatTarget(To: -1, pText: aBuf, VersionFlags: FLAG_SIX);
3660 pSelf->m_VoteCreator = pResult->m_ClientId;
3661 pSelf->Console()->ExecuteLine(pStr: pOption->m_aCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
3662 break;
3663 }
3664
3665 pOption = pOption->m_pNext;
3666 }
3667
3668 if(!pOption)
3669 {
3670 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' isn't an option on this server", pValue);
3671 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: aBuf);
3672 return;
3673 }
3674 }
3675 else if(str_comp_nocase(a: pType, b: "kick") == 0)
3676 {
3677 int KickId = str_toint(str: pValue);
3678 if(KickId < 0 || KickId >= MAX_CLIENTS || !pSelf->m_apPlayers[KickId])
3679 {
3680 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "Invalid client id to kick");
3681 return;
3682 }
3683
3684 if(!g_Config.m_SvVoteKickBantime)
3685 {
3686 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "kick %d %s", KickId, pReason);
3687 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3688 }
3689 else
3690 {
3691 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);
3692 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3693 }
3694 }
3695 else if(str_comp_nocase(a: pType, b: "spectate") == 0)
3696 {
3697 int SpectateId = str_toint(str: pValue);
3698 if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateId] || pSelf->m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
3699 {
3700 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "Invalid client id to move");
3701 return;
3702 }
3703
3704 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(ClientId: SpectateId), pReason);
3705 pSelf->SendChatTarget(To: -1, pText: aBuf);
3706 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "set_team %d -1 %d", SpectateId, g_Config.m_SvVoteSpectateRejoindelay);
3707 pSelf->Console()->ExecuteLine(pStr: aBuf, ClientId: IConsole::CLIENT_ID_UNSPECIFIED, InterpretSemicolons: false);
3708 }
3709}
3710
3711void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData)
3712{
3713 CGameContext *pSelf = (CGameContext *)pUserData;
3714
3715 CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
3716 pSelf->Server()->SendPackMsg(pMsg: &VoteClearOptionsMsg, Flags: MSGFLAG_VITAL, ClientId: -1);
3717 pSelf->m_pVoteOptionHeap->Reset();
3718 pSelf->m_pVoteOptionFirst = nullptr;
3719 pSelf->m_pVoteOptionLast = nullptr;
3720 pSelf->m_NumVoteOptions = 0;
3721
3722 // reset sending of vote options
3723 for(auto &pPlayer : pSelf->m_apPlayers)
3724 {
3725 if(pPlayer)
3726 pPlayer->m_SendVoteIndex = 0;
3727 }
3728}
3729
3730struct CMapNameItem
3731{
3732 char m_aName[IO_MAX_PATH_LENGTH - 4];
3733 bool m_IsDirectory;
3734
3735 static bool CompareFilenameAscending(const CMapNameItem Lhs, const CMapNameItem Rhs)
3736 {
3737 if(str_comp(a: Rhs.m_aName, b: "..") == 0)
3738 return false;
3739 if(str_comp(a: Lhs.m_aName, b: "..") == 0)
3740 return true;
3741 if(Lhs.m_IsDirectory != Rhs.m_IsDirectory)
3742 return Lhs.m_IsDirectory;
3743 return str_comp_filenames(a: Lhs.m_aName, b: Rhs.m_aName) < 0;
3744 }
3745};
3746
3747void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData)
3748{
3749 CGameContext *pSelf = (CGameContext *)pUserData;
3750
3751 std::vector<CMapNameItem> vMapList;
3752 const char *pDirectory = pResult->GetString(Index: 0);
3753
3754 // Don't allow moving to parent directories
3755 if(str_find_nocase(haystack: pDirectory, needle: ".."))
3756 return;
3757
3758 char aPath[IO_MAX_PATH_LENGTH] = "maps/";
3759 str_append(dst: aPath, src: pDirectory, dst_size: sizeof(aPath));
3760 pSelf->Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: aPath, pfnCallback: MapScan, pUser: &vMapList);
3761 std::sort(first: vMapList.begin(), last: vMapList.end(), comp: CMapNameItem::CompareFilenameAscending);
3762
3763 for(auto &Item : vMapList)
3764 {
3765 if(!str_comp(a: Item.m_aName, b: "..") && (!str_comp(a: aPath, b: "maps/")))
3766 continue;
3767
3768 char aDescription[VOTE_DESC_LENGTH];
3769 str_format(buffer: aDescription, buffer_size: sizeof(aDescription), format: "%s: %s%s", Item.m_IsDirectory ? "Directory" : "Map", Item.m_aName, Item.m_IsDirectory ? "/" : "");
3770
3771 char aCommand[VOTE_CMD_LENGTH];
3772 char aOptionEscaped[IO_MAX_PATH_LENGTH * 2];
3773 char *pDst = aOptionEscaped;
3774 str_escape(dst: &pDst, src: Item.m_aName, end: aOptionEscaped + sizeof(aOptionEscaped));
3775
3776 char aDirectory[IO_MAX_PATH_LENGTH] = "";
3777 if(pResult->NumArguments())
3778 str_copy(dst&: aDirectory, src: pDirectory);
3779
3780 if(!str_comp(a: Item.m_aName, b: ".."))
3781 {
3782 fs_parent_dir(path: aDirectory);
3783 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"", aDirectory);
3784 }
3785 else if(Item.m_IsDirectory)
3786 {
3787 str_append(dst: aDirectory, src: "/", dst_size: sizeof(aDirectory));
3788 str_append(dst: aDirectory, src: aOptionEscaped, dst_size: sizeof(aDirectory));
3789
3790 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "clear_votes; add_map_votes \"%s\"", aDirectory);
3791 }
3792 else
3793 str_format(buffer: aCommand, buffer_size: sizeof(aCommand), format: "change_map \"%s%s%s\"", pDirectory, pDirectory[0] == '\0' ? "" : "/", aOptionEscaped);
3794
3795 pSelf->AddVote(pDescription: aDescription, pCommand: aCommand);
3796 }
3797
3798 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "server", pStr: "added maps to votes");
3799}
3800
3801int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData)
3802{
3803 if((!IsDir && !str_endswith(str: pName, suffix: ".map")) || !str_comp(a: pName, b: "."))
3804 return 0;
3805
3806 CMapNameItem Item;
3807 Item.m_IsDirectory = IsDir;
3808 if(!IsDir)
3809 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pName, truncation_len: str_length(str: pName) - str_length(str: ".map"));
3810 else
3811 str_copy(dst&: Item.m_aName, src: pName);
3812 static_cast<std::vector<CMapNameItem> *>(pUserData)->push_back(x: Item);
3813
3814 return 0;
3815}
3816
3817void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData)
3818{
3819 CGameContext *pSelf = (CGameContext *)pUserData;
3820
3821 if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "yes") == 0)
3822 pSelf->ForceVote(Success: true);
3823 else if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "no") == 0)
3824 pSelf->ForceVote(Success: false);
3825}
3826
3827void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData)
3828{
3829 CGameContext *pSelf = (CGameContext *)pUserData;
3830
3831 int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(Index: 0) : 0;
3832 static const int s_EntriesPerPage = 20;
3833 const int Start = Page * s_EntriesPerPage;
3834 const int End = (Page + 1) * s_EntriesPerPage;
3835
3836 char aBuf[512];
3837 int Count = 0;
3838 for(CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; pOption; pOption = pOption->m_pNext, Count++)
3839 {
3840 if(Count < Start || Count >= End)
3841 {
3842 continue;
3843 }
3844
3845 str_copy(dst&: aBuf, src: "add_vote \"");
3846 char *pDst = aBuf + str_length(str: aBuf);
3847 str_escape(dst: &pDst, src: pOption->m_aDescription, end: aBuf + sizeof(aBuf));
3848 str_append(dst&: aBuf, src: "\" \"");
3849 pDst = aBuf + str_length(str: aBuf);
3850 str_escape(dst: &pDst, src: pOption->m_aCommand, end: aBuf + sizeof(aBuf));
3851 str_append(dst&: aBuf, src: "\"");
3852
3853 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes", pStr: aBuf);
3854 }
3855 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d %s, showing entries %d - %d", Count, Count == 1 ? "vote" : "votes", Start, End - 1);
3856 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "votes", pStr: aBuf);
3857}
3858
3859void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3860{
3861 pfnCallback(pResult, pCallbackUserData);
3862 if(pResult->NumArguments())
3863 {
3864 CGameContext *pSelf = (CGameContext *)pUserData;
3865 pSelf->SendMotd(ClientId: -1);
3866 }
3867}
3868
3869void CGameContext::ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3870{
3871 pfnCallback(pResult, pCallbackUserData);
3872 if(pResult->NumArguments())
3873 {
3874 CGameContext *pSelf = (CGameContext *)pUserData;
3875 pSelf->SendSettings(ClientId: -1);
3876 }
3877}
3878
3879void CGameContext::ConchainPracticeByDefaultUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3880{
3881 const int OldValue = g_Config.m_SvPracticeByDefault;
3882 pfnCallback(pResult, pCallbackUserData);
3883
3884 if(pResult->NumArguments() && g_Config.m_SvTestingCommands)
3885 {
3886 CGameContext *pSelf = (CGameContext *)pUserData;
3887
3888 if(pSelf->m_pController == nullptr)
3889 return;
3890
3891 const int Enable = pResult->GetInteger(Index: 0);
3892 if(Enable == OldValue)
3893 return;
3894
3895 char aBuf[256];
3896 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Practice is %s by default.", Enable ? "enabled" : "disabled");
3897 if(Enable)
3898 str_append(dst&: aBuf, src: " Join a team and /unpractice to turn it off for your team.");
3899
3900 pSelf->SendChat(ChatterClientId: -1, Team: TEAM_ALL, pText: aBuf);
3901
3902 for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
3903 {
3904 if(Team == TEAM_FLOCK || pSelf->m_pController->Teams().TeamSize(Team) == 0)
3905 {
3906 pSelf->m_pController->Teams().SetPractice(Team, Enabled: Enable);
3907 }
3908 }
3909 }
3910}
3911
3912void CGameContext::OnConsoleInit()
3913{
3914 m_pServer = Kernel()->RequestInterface<IServer>();
3915 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
3916 m_pConfig = m_pConfigManager->Values();
3917 m_pConsole = Kernel()->RequestInterface<IConsole>();
3918 m_pEngine = Kernel()->RequestInterface<IEngine>();
3919 m_pStorage = Kernel()->RequestInterface<IStorage>();
3920
3921 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");
3922 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");
3923 Console()->Register(pName: "tune_reset", pParams: "?s[tuning]", Flags: CFGFLAG_SERVER, pfnFunc: ConTuneReset, pUser: this, pHelp: "Reset all or one tuning variable to default");
3924 Console()->Register(pName: "tunes", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConTunes, pUser: this, pHelp: "List all tuning variables and their values");
3925 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");
3926 Console()->Register(pName: "tune_zone_dump", pParams: "i[zone]", Flags: CFGFLAG_SERVER, pfnFunc: ConTuneDumpZone, pUser: this, pHelp: "Dump zone tuning in zone x");
3927 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");
3928 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");
3929 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");
3930 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)");
3931 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)");
3932 Console()->Register(pName: "pause_game", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConPause, pUser: this, pHelp: "Pause/unpause game");
3933 Console()->Register(pName: "change_map", pParams: "r[map]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConChangeMap, pUser: this, pHelp: "Change map");
3934 Console()->Register(pName: "random_map", pParams: "?i[stars] ?i[max stars]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRandomMap, pUser: this, pHelp: "Random map");
3935 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");
3936 Console()->Register(pName: "restart", pParams: "?i[seconds]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConRestart, pUser: this, pHelp: "Restart in x seconds (0 = abort)");
3937 Console()->Register(pName: "server_alert", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConServerAlert, pUser: this, pHelp: "Send a server alert message to all players");
3938 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");
3939 Console()->Register(pName: "broadcast", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConBroadcast, pUser: this, pHelp: "Broadcast message");
3940 Console()->Register(pName: "say", pParams: "r[message]", Flags: CFGFLAG_SERVER, pfnFunc: ConSay, pUser: this, pHelp: "Say in chat");
3941 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)");
3942 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)");
3943 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");
3944 Console()->Register(pName: "reload_censorlist", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConReloadCensorlist, pUser: this, pHelp: "Reload the censorlist");
3945
3946 Console()->Register(pName: "add_vote", pParams: "s[name] r[command]", Flags: CFGFLAG_SERVER, pfnFunc: ConAddVote, pUser: this, pHelp: "Add a voting option");
3947 Console()->Register(pName: "remove_vote", pParams: "r[name]", Flags: CFGFLAG_SERVER, pfnFunc: ConRemoveVote, pUser: this, pHelp: "remove a voting option");
3948 Console()->Register(pName: "force_vote", pParams: "s[name] s[command] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConForceVote, pUser: this, pHelp: "Force a voting option");
3949 Console()->Register(pName: "clear_votes", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConClearVotes, pUser: this, pHelp: "Clears the voting options");
3950 Console()->Register(pName: "add_map_votes", pParams: "?s[directory]", Flags: CFGFLAG_SERVER, pfnFunc: ConAddMapVotes, pUser: this, pHelp: "Automatically adds voting options for all maps");
3951 Console()->Register(pName: "vote", pParams: "r['yes'|'no']", Flags: CFGFLAG_SERVER, pfnFunc: ConVote, pUser: this, pHelp: "Force a vote to yes/no");
3952 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)");
3953 Console()->Register(pName: "dump_antibot", pParams: "", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConDumpAntibot, pUser: this, pHelp: "Dumps the antibot status");
3954 Console()->Register(pName: "antibot", pParams: "r[command]", Flags: CFGFLAG_SERVER | CFGFLAG_STORE, pfnFunc: ConAntibot, pUser: this, pHelp: "Sends a command to the antibot");
3955
3956 Console()->Chain(pName: "sv_motd", pfnChainFunc: ConchainSpecialMotdupdate, pUser: this);
3957
3958 Console()->Chain(pName: "sv_vote_kick", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3959 Console()->Chain(pName: "sv_vote_kick_min", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3960 Console()->Chain(pName: "sv_vote_spectate", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3961 Console()->Chain(pName: "sv_spectator_slots", pfnChainFunc: ConchainSettingUpdate, pUser: this);
3962
3963 RegisterDDRaceCommands();
3964 RegisterChatCommands();
3965}
3966
3967void CGameContext::RegisterDDRaceCommands()
3968{
3969 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");
3970 Console()->Register(pName: "totele", pParams: "i[number]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i");
3971 Console()->Register(pName: "totelecp", pParams: "i[number]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i");
3972 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)");
3973 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)");
3974 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)");
3975 Console()->Register(pName: "shotgun", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConShotgun, pUser: this, pHelp: "Gives a shotgun to you");
3976 Console()->Register(pName: "grenade", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGrenade, pUser: this, pHelp: "Gives a grenade launcher to you");
3977 Console()->Register(pName: "laser", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you");
3978 Console()->Register(pName: "rifle", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLaser, pUser: this, pHelp: "Gives a laser to you");
3979 Console()->Register(pName: "jetpack", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConJetpack, pUser: this, pHelp: "Gives jetpack to you");
3980 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");
3981 Console()->Register(pName: "weapons", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConWeapons, pUser: this, pHelp: "Gives all weapons to you");
3982 Console()->Register(pName: "unshotgun", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnShotgun, pUser: this, pHelp: "Removes the shotgun from you");
3983 Console()->Register(pName: "ungrenade", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you");
3984 Console()->Register(pName: "unlaser", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you");
3985 Console()->Register(pName: "unrifle", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLaser, pUser: this, pHelp: "Removes the laser from you");
3986 Console()->Register(pName: "unjetpack", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnJetpack, pUser: this, pHelp: "Removes the jetpack from you");
3987 Console()->Register(pName: "unweapons", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnWeapons, pUser: this, pHelp: "Removes all weapons from you");
3988 Console()->Register(pName: "ninja", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConNinja, pUser: this, pHelp: "Makes you a ninja");
3989 Console()->Register(pName: "unninja", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnNinja, pUser: this, pHelp: "Removes ninja from you");
3990 Console()->Register(pName: "super", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSuper, pUser: this, pHelp: "Makes you super");
3991 Console()->Register(pName: "unsuper", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConUnSuper, pUser: this, pHelp: "Removes super from you");
3992 Console()->Register(pName: "invincible", pParams: "?i['0'|'1']", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConToggleInvincible, pUser: this, pHelp: "Toggles invincible mode");
3993 Console()->Register(pName: "infinite_jump", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessJump, pUser: this, pHelp: "Gives you infinite jump");
3994 Console()->Register(pName: "uninfinite_jump", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you");
3995 Console()->Register(pName: "endless_hook", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConEndlessHook, pUser: this, pHelp: "Gives you endless hook");
3996 Console()->Register(pName: "unendless_hook", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you");
3997 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)");
3998 Console()->Register(pName: "solo", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConSolo, pUser: this, pHelp: "Puts you into solo part");
3999 Console()->Register(pName: "unsolo", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnSolo, pUser: this, pHelp: "Puts you out of solo part");
4000 Console()->Register(pName: "freeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConFreeze, pUser: this, pHelp: "Puts you into freeze");
4001 Console()->Register(pName: "unfreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnfreeze, pUser: this, pHelp: "Puts you out of freeze");
4002 Console()->Register(pName: "deep", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConDeep, pUser: this, pHelp: "Puts you into deep freeze");
4003 Console()->Register(pName: "undeep", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnDeep, pUser: this, pHelp: "Puts you out of deep freeze");
4004 Console()->Register(pName: "livefreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConLiveFreeze, pUser: this, pHelp: "Makes you live frozen");
4005 Console()->Register(pName: "unlivefreeze", pParams: "", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze");
4006 Console()->Register(pName: "left", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoLeft, pUser: this, pHelp: "Makes you move 1 tile left");
4007 Console()->Register(pName: "right", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoRight, pUser: this, pHelp: "Makes you move 1 tile right");
4008 Console()->Register(pName: "up", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoUp, pUser: this, pHelp: "Makes you move 1 tile up");
4009 Console()->Register(pName: "down", pParams: "?i[tiles]", Flags: CFGFLAG_SERVER | CMDFLAG_TEST, pfnFunc: ConGoDown, pUser: this, pHelp: "Makes you move 1 tile down");
4010
4011 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");
4012 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");
4013 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");
4014 Console()->Register(pName: "force_unpause", pParams: "v[id]", Flags: CFGFLAG_SERVER, pfnFunc: ConForcePause, pUser: this, pHelp: "Set force-pause timer of i to 0.");
4015
4016 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");
4017 Console()->Register(pName: "uninvite", pParams: "v[id] i[team]", Flags: CFGFLAG_SERVER, pfnFunc: ConUninvite, pUser: this, pHelp: "Uninvite player from team");
4018
4019 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>'");
4020 Console()->Register(pName: "muteid", pParams: "v[id] i[seconds] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConMuteId, pUser: this, pHelp: "Mute player with client ID");
4021 Console()->Register(pName: "muteip", pParams: "s[ip] i[seconds] ?r[reason]", Flags: CFGFLAG_SERVER, pfnFunc: ConMuteIp, pUser: this, pHelp: "Mute player with IP address");
4022 Console()->Register(pName: "unmute", pParams: "i[index]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmute, pUser: this, pHelp: "Unmute player with list index");
4023 Console()->Register(pName: "unmuteid", pParams: "v[id]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteId, pUser: this, pHelp: "Unmute player with client ID");
4024 Console()->Register(pName: "unmuteip", pParams: "s[ip]", Flags: CFGFLAG_SERVER, pfnFunc: ConUnmuteIp, pUser: this, pHelp: "Unmute player with IP address");
4025 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)");
4026
4027 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>'");
4028 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");
4029 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");
4030 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");
4031 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");
4032 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");
4033 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)");
4034
4035 Console()->Register(pName: "moderate", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConModerate, pUser: this, pHelp: "Enables/disables active moderator mode for the player");
4036 Console()->Register(pName: "vote_no", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConVoteNo, pUser: this, pHelp: "Same as \"vote no\"");
4037 Console()->Register(pName: "save_dry", pParams: "", Flags: CFGFLAG_SERVER, pfnFunc: ConDrySave, pUser: this, pHelp: "Dump the current savestring");
4038 Console()->Register(pName: "dump_log", pParams: "?i[seconds]", Flags: CFGFLAG_SERVER, pfnFunc: ConDumpLog, pUser: this, pHelp: "Show logs of the last i seconds");
4039
4040 Console()->Chain(pName: "sv_practice_by_default", pfnChainFunc: ConchainPracticeByDefaultUpdate, pUser: this);
4041}
4042
4043void CGameContext::RegisterChatCommands()
4044{
4045 Console()->Register(pName: "credits", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCredits, pUser: this, pHelp: "Shows the credits of the DDNet mod");
4046 Console()->Register(pName: "rules", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConRules, pUser: this, pHelp: "Shows the server rules");
4047 Console()->Register(pName: "emote", pParams: "?s[emote name] i[duration in seconds]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConEyeEmote, pUser: this, pHelp: "Sets your tee's eye emote");
4048 Console()->Register(pName: "eyeemote", pParams: "?s['on'|'off'|'toggle']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSetEyeEmote, pUser: this, pHelp: "Toggles use of standard eye-emotes on/off, eyeemote s, where s = on for on, off for off, toggle for toggle and nothing to show current status");
4049 Console()->Register(pName: "settings", pParams: "?s[configname]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSettings, pUser: this, pHelp: "Shows gameplay information for this server");
4050 Console()->Register(pName: "help", pParams: "?r[command]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConHelp, pUser: this, pHelp: "Shows help to command r, general help if left blank");
4051 Console()->Register(pName: "info", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConInfo, pUser: this, pHelp: "Shows info about this server");
4052 Console()->Register(pName: "list", pParams: "?s[filter]", Flags: CFGFLAG_CHAT, pfnFunc: ConList, pUser: this, pHelp: "List connected players with optional case-insensitive substring matching filter");
4053 Console()->Register(pName: "w", pParams: "s[player name] r[message]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhisper, pUser: this, pHelp: "Whisper something to someone (private message)");
4054 Console()->Register(pName: "whisper", pParams: "s[player name] r[message]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhisper, pUser: this, pHelp: "Whisper something to someone (private message)");
4055 Console()->Register(pName: "c", pParams: "r[message]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConConverse, pUser: this, pHelp: "Converse with the last person you whispered to (private message)");
4056 Console()->Register(pName: "converse", pParams: "r[message]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConConverse, pUser: this, pHelp: "Converse with the last person you whispered to (private message)");
4057 Console()->Register(pName: "pause", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePause, pUser: this, pHelp: "Toggles pause");
4058 Console()->Register(pName: "spec", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConToggleSpec, pUser: this, pHelp: "Toggles spec (if not available behaves as /pause)");
4059 Console()->Register(pName: "pausevoted", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTogglePauseVoted, pUser: this, pHelp: "Toggles pause on the currently voted player");
4060 Console()->Register(pName: "specvoted", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConToggleSpecVoted, pUser: this, pHelp: "Toggles spec on the currently voted player");
4061 Console()->Register(pName: "dnd", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConDND, pUser: this, pHelp: "Toggle Do Not Disturb (no chat and server messages)");
4062 Console()->Register(pName: "whispers", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConWhispers, pUser: this, pHelp: "Toggle receiving whispers");
4063 Console()->Register(pName: "mapinfo", pParams: "?r[map]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConMapInfo, pUser: this, pHelp: "Show info about the map with name r gives (current map by default)");
4064 Console()->Register(pName: "timeout", pParams: "?s[code]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimeout, pUser: this, pHelp: "Set timeout protection code s");
4065 Console()->Register(pName: "practice", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPractice, pUser: this, pHelp: "Enable cheats for your current team's run, but you can't earn a rank");
4066 Console()->Register(pName: "unpractice", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConUnPractice, pUser: this, pHelp: "Kills team and disables practice mode");
4067 Console()->Register(pName: "practicecmdlist", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPracticeCmdList, pUser: this, pHelp: "List all commands that are available in practice mode");
4068 Console()->Register(pName: "swap", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSwap, pUser: this, pHelp: "Request to swap your tee with another team member");
4069 Console()->Register(pName: "cancelswap", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConCancelSwap, pUser: this, pHelp: "Cancel your swap request");
4070 Console()->Register(pName: "save", pParams: "?r[code]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSave, pUser: this, pHelp: "Save team with code r.");
4071 Console()->Register(pName: "load", pParams: "?r[code]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConLoad, pUser: this, pHelp: "Load with code r. /load to check your existing saves");
4072 Console()->Register(pName: "map", pParams: "?r[map]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConMap, pUser: this, pHelp: "Vote a map by name");
4073
4074 Console()->Register(pName: "rankteam", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamRank, pUser: this, pHelp: "Shows the team rank of player with name r (your team rank by default)");
4075 Console()->Register(pName: "teamrank", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamRank, pUser: this, pHelp: "Shows the team rank of player with name r (your team rank by default)");
4076
4077 Console()->Register(pName: "rank", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConRank, pUser: this, pHelp: "Shows the rank of player with name r (your rank by default)");
4078 Console()->Register(pName: "top5team", pParams: "?s[player name] ?i[rank to start with]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamTop5, pUser: this, pHelp: "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)");
4079 Console()->Register(pName: "teamtop5", pParams: "?s[player name] ?i[rank to start with]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeamTop5, pUser: this, pHelp: "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)");
4080 Console()->Register(pName: "top", pParams: "?i[rank to start with]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTop, pUser: this, pHelp: "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)");
4081 Console()->Register(pName: "top5", pParams: "?i[rank to start with]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTop, pUser: this, pHelp: "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)");
4082 Console()->Register(pName: "times", pParams: "?s[player name] ?i[number of times to skip]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimes, pUser: this, pHelp: "/times ?s?i shows last 5 times of the server or of a player beginning with name s starting with time i (i = 1 by default, -1 for first)");
4083 Console()->Register(pName: "points", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConPoints, pUser: this, pHelp: "Shows the global points of a player beginning with name r (your rank by default)");
4084 Console()->Register(pName: "top5points", pParams: "?i[number]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTopPoints, pUser: this, pHelp: "Shows five points of the global point ladder beginning with rank i (1 by default)");
4085 Console()->Register(pName: "timecp", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTimeCP, pUser: this, pHelp: "Set your checkpoints based on another player");
4086
4087 Console()->Register(pName: "team", pParams: "?i[id]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeam, pUser: this, pHelp: "Lets you join team i (shows your team if left blank)");
4088 Console()->Register(pName: "lock", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConLock, pUser: this, pHelp: "Toggle team lock so no one else can join and so the team restarts when a player dies. /lock 0 to unlock, /lock 1 to lock");
4089 Console()->Register(pName: "unlock", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConUnlock, pUser: this, pHelp: "Unlock a team");
4090 Console()->Register(pName: "invite", pParams: "r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConInvite, pUser: this, pHelp: "Invite a person to a locked team");
4091 Console()->Register(pName: "join", pParams: "r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConJoin, pUser: this, pHelp: "Join the team of the specified player");
4092 Console()->Register(pName: "team0mode", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTeam0Mode, pUser: this, pHelp: "Toggle team between team 0 and team mode. This mode will make your team behave like team 0.");
4093
4094 Console()->Register(pName: "showothers", pParams: "?i['0'|'1'|'2']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConShowOthers, pUser: this, pHelp: "Whether to show players from other teams or not (off by default), optional i = 0 for off, i = 1 for on, i = 2 for own team only");
4095 Console()->Register(pName: "showall", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConShowAll, pUser: this, pHelp: "Whether to show players at any distance (off by default), optional i = 0 for off else for on");
4096 Console()->Register(pName: "specteam", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSpecTeam, pUser: this, pHelp: "Whether to show players from other teams when spectating (on by default), optional i = 0 for off else for on");
4097 Console()->Register(pName: "ninjajetpack", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConNinjaJetpack, pUser: this, pHelp: "Whether to use ninja jetpack or not. Makes jetpack look more awesome");
4098 Console()->Register(pName: "saytime", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConSayTime, pUser: this, pHelp: "Privately messages someone's current time in this current running race (your time by default)");
4099 Console()->Register(pName: "saytimeall", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, pfnFunc: ConSayTimeAll, pUser: this, pHelp: "Publicly messages everyone your current time in this current running race");
4100 Console()->Register(pName: "time", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConTime, pUser: this, pHelp: "Privately shows you your current time in this current running race in the broadcast message");
4101 Console()->Register(pName: "timer", pParams: "?s['gametimer'|'broadcast'|'both'|'none'|'cycle']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConSetTimerType, pUser: this, pHelp: "Personal Setting of showing time in either broadcast or game/round timer, timer s, where s = broadcast for broadcast, gametimer for game/round timer, cycle for cycle, both for both, none for no timer and nothing to show current status");
4102 Console()->Register(pName: "r", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescue, pUser: this, pHelp: "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)");
4103 Console()->Register(pName: "rescue", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescue, pUser: this, pHelp: "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)");
4104 Console()->Register(pName: "back", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConBack, pUser: this, pHelp: "Teleport yourself to the last auto rescue position before you died (use sv_rescue 1 to enable this feature)");
4105 Console()->Register(pName: "rescuemode", pParams: "?r['auto'|'manual']", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConRescueMode, pUser: this, pHelp: "Sets one of the two rescue modes (auto or manual). Prints current mode if no arguments provided");
4106 Console()->Register(pName: "tp", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleTo, pUser: this, pHelp: "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
4107 Console()->Register(pName: "teleport", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleTo, pUser: this, pHelp: "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
4108 Console()->Register(pName: "tpxy", pParams: "s[x] s[y]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleXY, pUser: this, pHelp: "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right");
4109 Console()->Register(pName: "lasttp", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConLastTele, pUser: this, pHelp: "Teleport yourself to the last location you teleported to");
4110 Console()->Register(pName: "tc", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleCursor, pUser: this, pHelp: "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
4111 Console()->Register(pName: "telecursor", pParams: "?r[player name]", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, pfnFunc: ConTeleCursor, pUser: this, pHelp: "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
4112 Console()->Register(pName: "totele", pParams: "i[number]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToTeleporter, pUser: this, pHelp: "Teleports you to teleporter i");
4113 Console()->Register(pName: "totelecp", pParams: "i[number]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToCheckTeleporter, pUser: this, pHelp: "Teleports you to checkpoint teleporter i");
4114 Console()->Register(pName: "unsolo", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnSolo, pUser: this, pHelp: "Puts you out of solo part");
4115 Console()->Register(pName: "solo", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSolo, pUser: this, pHelp: "Puts you into solo part");
4116 Console()->Register(pName: "undeep", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnDeep, pUser: this, pHelp: "Puts you out of deep freeze");
4117 Console()->Register(pName: "deep", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeDeep, pUser: this, pHelp: "Puts you into deep freeze");
4118 Console()->Register(pName: "unlivefreeze", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLiveFreeze, pUser: this, pHelp: "Puts you out of live freeze");
4119 Console()->Register(pName: "livefreeze", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLiveFreeze, pUser: this, pHelp: "Makes you live frozen");
4120 Console()->Register(pName: "addweapon", pParams: "i[weapon-id]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeAddWeapon, pUser: this, pHelp: "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
4121 Console()->Register(pName: "removeweapon", pParams: "i[weapon-id]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeRemoveWeapon, pUser: this, pHelp: "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
4122 Console()->Register(pName: "shotgun", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeShotgun, pUser: this, pHelp: "Gives a shotgun to you");
4123 Console()->Register(pName: "grenade", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeGrenade, pUser: this, pHelp: "Gives a grenade launcher to you");
4124 Console()->Register(pName: "laser", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you");
4125 Console()->Register(pName: "rifle", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeLaser, pUser: this, pHelp: "Gives a laser to you");
4126 Console()->Register(pName: "jetpack", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeJetpack, pUser: this, pHelp: "Gives jetpack to you");
4127 Console()->Register(pName: "setjumps", pParams: "i[jumps]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSetJumps, pUser: this, pHelp: "Gives you as many jumps as you specify");
4128 Console()->Register(pName: "weapons", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeWeapons, pUser: this, pHelp: "Gives all weapons to you");
4129 Console()->Register(pName: "unshotgun", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnShotgun, pUser: this, pHelp: "Removes the shotgun from you");
4130 Console()->Register(pName: "ungrenade", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnGrenade, pUser: this, pHelp: "Removes the grenade launcher from you");
4131 Console()->Register(pName: "unlaser", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you");
4132 Console()->Register(pName: "unrifle", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnLaser, pUser: this, pHelp: "Removes the laser from you");
4133 Console()->Register(pName: "unjetpack", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnJetpack, pUser: this, pHelp: "Removes the jetpack from you");
4134 Console()->Register(pName: "unweapons", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnWeapons, pUser: this, pHelp: "Removes all weapons from you");
4135 Console()->Register(pName: "ninja", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeNinja, pUser: this, pHelp: "Makes you a ninja");
4136 Console()->Register(pName: "unninja", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnNinja, pUser: this, pHelp: "Removes ninja from you");
4137 Console()->Register(pName: "infjump", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessJump, pUser: this, pHelp: "Gives you infinite jump");
4138 Console()->Register(pName: "uninfjump", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessJump, pUser: this, pHelp: "Removes infinite jump from you");
4139 Console()->Register(pName: "endless", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeEndlessHook, pUser: this, pHelp: "Gives you endless hook");
4140 Console()->Register(pName: "unendless", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeUnEndlessHook, pUser: this, pHelp: "Removes endless hook from you");
4141 Console()->Register(pName: "setswitch", pParams: "i[switch] ?i['0'|'1'] ?i[seconds]", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeSetSwitch, pUser: this, pHelp: "Toggle or set the switch on or off for the specified time (or indefinitely by default)");
4142 Console()->Register(pName: "invincible", pParams: "?i['0'|'1']", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleInvincible, pUser: this, pHelp: "Toggles invincible mode");
4143 Console()->Register(pName: "collision", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleCollision, pUser: this, pHelp: "Toggles collision");
4144 Console()->Register(pName: "hookcollision", pParams: "", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHookCollision, pUser: this, pHelp: "Toggles hook collision");
4145 Console()->Register(pName: "hitothers", pParams: "?s['all'|'hammer'|'shotgun'|'grenade'|'laser']", Flags: CFGFLAG_CHAT | CMDFLAG_PRACTICE, pfnFunc: ConPracticeToggleHitOthers, pUser: this, pHelp: "Toggles hit others");
4146
4147 Console()->Register(pName: "kill", pParams: "", Flags: CFGFLAG_CHAT | CFGFLAG_SERVER, pfnFunc: ConProtectedKill, pUser: this, pHelp: "Kill yourself when kill-protected during a long game (use f1, kill for regular kill)");
4148}
4149
4150void CGameContext::OnInit(const void *pPersistentData)
4151{
4152 const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData;
4153
4154 m_pServer = Kernel()->RequestInterface<IServer>();
4155 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
4156 m_pConfig = m_pConfigManager->Values();
4157 m_pConsole = Kernel()->RequestInterface<IConsole>();
4158 m_pEngine = Kernel()->RequestInterface<IEngine>();
4159 m_pStorage = Kernel()->RequestInterface<IStorage>();
4160 m_pAntibot = Kernel()->RequestInterface<IAntibot>();
4161 m_World.SetGameServer(this);
4162 m_Events.SetGameServer(this);
4163
4164 m_GameUuid = RandomUuid();
4165 Console()->SetTeeHistorianCommandCallback(pfnCallback: CommandCallback, pUser: this);
4166
4167 uint64_t aSeed[2];
4168 secure_random_fill(bytes: aSeed, length: sizeof(aSeed));
4169 m_Prng.Seed(aSeed);
4170 m_World.m_Core.m_pPrng = &m_Prng;
4171
4172 DeleteTempfile();
4173
4174 for(int i = 0; i < NUM_NETOBJTYPES; i++)
4175 {
4176 Server()->SnapSetStaticsize(ItemType: i, Size: m_NetObjHandler.GetObjSize(Type: i));
4177 }
4178
4179 // HACK: only set static size for items, which were available in the first 0.7 release
4180 // so new items don't break the snapshot delta
4181 static const int OLD_NUM_NETOBJTYPES = 23;
4182 for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++)
4183 {
4184 Server()->SnapSetStaticsize7(ItemType: i, Size: m_NetObjHandler7.GetObjSize(Type: i));
4185 }
4186
4187 m_Layers.Init(pMap: Map(), GameOnly: false);
4188 m_Collision.Init(pLayers: &m_Layers);
4189 m_World.Init(pCollision: &m_Collision, pTuningList: m_aTuningList);
4190 m_MapBugs = CMapBugs::Create(pName: Map()->BaseName(), Size: Map()->Size(), Sha256: Map()->Sha256());
4191
4192 // Reset Tunezones
4193 for(int i = 0; i < TuneZone::NUM; i++)
4194 {
4195 TuningList()[i] = CTuningParams::DEFAULT;
4196 TuningList()[i].Set(pName: "gun_curvature", Value: 0);
4197 TuningList()[i].Set(pName: "gun_speed", Value: 1400);
4198 TuningList()[i].Set(pName: "shotgun_curvature", Value: 0);
4199 TuningList()[i].Set(pName: "shotgun_speed", Value: 500);
4200 TuningList()[i].Set(pName: "shotgun_speeddiff", Value: 0);
4201 }
4202
4203 for(int i = 0; i < TuneZone::NUM; i++)
4204 {
4205 // Send no text by default when changing tune zones.
4206 m_aaZoneEnterMsg[i][0] = 0;
4207 m_aaZoneLeaveMsg[i][0] = 0;
4208 }
4209 // Reset Tuning
4210 if(g_Config.m_SvTuneReset)
4211 {
4212 ResetTuning();
4213 }
4214 else
4215 {
4216 GlobalTuning()->Set(pName: "gun_speed", Value: 1400);
4217 GlobalTuning()->Set(pName: "gun_curvature", Value: 0);
4218 GlobalTuning()->Set(pName: "shotgun_speed", Value: 500);
4219 GlobalTuning()->Set(pName: "shotgun_speeddiff", Value: 0);
4220 GlobalTuning()->Set(pName: "shotgun_curvature", Value: 0);
4221 }
4222
4223 if(g_Config.m_SvDDRaceTuneReset)
4224 {
4225 g_Config.m_SvHit = 1;
4226 g_Config.m_SvEndlessDrag = 0;
4227 g_Config.m_SvOldLaser = 0;
4228 g_Config.m_SvOldTeleportHook = 0;
4229 g_Config.m_SvOldTeleportWeapons = 0;
4230 g_Config.m_SvTeleportHoldHook = 0;
4231 g_Config.m_SvTeam = SV_TEAM_ALLOWED;
4232 g_Config.m_SvShowOthersDefault = SHOW_OTHERS_OFF;
4233
4234 for(auto &Switcher : Switchers())
4235 Switcher.m_Initial = true;
4236 }
4237
4238 m_pConfigManager->SetGameSettingsReadOnly(false);
4239
4240 Console()->ExecuteFile(pFilename: g_Config.m_SvResetFile, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
4241
4242 LoadMapSettings();
4243
4244 m_pConfigManager->SetGameSettingsReadOnly(true);
4245
4246 m_MapBugs.Dump();
4247
4248 if(g_Config.m_SvSoloServer)
4249 {
4250 g_Config.m_SvTeam = SV_TEAM_FORCED_SOLO;
4251 g_Config.m_SvShowOthersDefault = SHOW_OTHERS_ON;
4252
4253 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4254 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4255
4256 for(int i = 0; i < TuneZone::NUM; i++)
4257 {
4258 TuningList()[i].Set(pName: "player_collision", Value: 0);
4259 TuningList()[i].Set(pName: "player_hooking", Value: 0);
4260 }
4261 }
4262
4263 if(!str_comp(a: Config()->m_SvGametype, b: "mod"))
4264 m_pController = new CGameControllerMod(this);
4265 else
4266 m_pController = new CGameControllerDDNet(this);
4267
4268 ReadCensorList();
4269
4270 m_TeeHistorianActive = g_Config.m_SvTeeHistorian;
4271 if(m_TeeHistorianActive)
4272 {
4273 char aGameUuid[UUID_MAXSTRSIZE];
4274 FormatUuid(Uuid: m_GameUuid, pBuffer: aGameUuid, BufferLength: sizeof(aGameUuid));
4275
4276 char aFilename[IO_MAX_PATH_LENGTH];
4277 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "teehistorian/%s.teehistorian", aGameUuid);
4278
4279 IOHANDLE THFile = Storage()->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
4280 if(!THFile)
4281 {
4282 dbg_msg(sys: "teehistorian", fmt: "failed to open '%s'", aFilename);
4283 Server()->SetErrorShutdown("teehistorian open error");
4284 return;
4285 }
4286 else
4287 {
4288 dbg_msg(sys: "teehistorian", fmt: "recording to '%s'", aFilename);
4289 }
4290 m_pTeeHistorianFile = aio_new(io: THFile);
4291
4292 char aVersion[128];
4293 if(GIT_SHORTREV_HASH)
4294 {
4295 str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%s (%s)", GAME_VERSION, GIT_SHORTREV_HASH);
4296 }
4297 else
4298 {
4299 str_copy(dst&: aVersion, GAME_VERSION);
4300 }
4301 CTeeHistorian::CGameInfo GameInfo;
4302 GameInfo.m_GameUuid = m_GameUuid;
4303 GameInfo.m_pServerVersion = aVersion;
4304 GameInfo.m_StartTime = time(timer: nullptr);
4305 GameInfo.m_pPrngDescription = m_Prng.Description();
4306
4307 GameInfo.m_pServerName = g_Config.m_SvName;
4308 GameInfo.m_ServerPort = Server()->Port();
4309 GameInfo.m_pGameType = m_pController->m_pGameType;
4310
4311 GameInfo.m_pConfig = &g_Config;
4312 GameInfo.m_pTuning = GlobalTuning();
4313 GameInfo.m_pUuids = &g_UuidManager;
4314
4315 GameInfo.m_pMapName = Map()->BaseName();
4316 GameInfo.m_MapSize = Map()->Size();
4317 GameInfo.m_MapSha256 = Map()->Sha256();
4318 GameInfo.m_MapCrc = Map()->Crc();
4319
4320 if(pPersistent)
4321 {
4322 GameInfo.m_HavePrevGameUuid = true;
4323 GameInfo.m_PrevGameUuid = pPersistent->m_PrevGameUuid;
4324 }
4325 else
4326 {
4327 GameInfo.m_HavePrevGameUuid = false;
4328 mem_zero(block: &GameInfo.m_PrevGameUuid, size: sizeof(GameInfo.m_PrevGameUuid));
4329 }
4330
4331 m_TeeHistorian.Reset(pGameInfo: &GameInfo, pfnWriteCallback: TeeHistorianWrite, pUser: this);
4332 }
4333
4334 Server()->DemoRecorder_HandleAutoStart();
4335
4336 if(!m_pScore)
4337 {
4338 m_pScore = new CScore(this, ((CServer *)Server())->DbPool());
4339 }
4340
4341 // load map info from database
4342 Score()->LoadMapInfo();
4343
4344 // create all entities from the game layer
4345 CreateAllEntities(Initial: true);
4346
4347 m_pAntibot->RoundStart(pGameServer: this);
4348}
4349
4350void CGameContext::CreateAllEntities(bool Initial)
4351{
4352 const CTile *pTiles = m_Collision.GameLayer();
4353 const CTile *pFront = m_Collision.FrontLayer();
4354 const CSwitchTile *pSwitch = m_Collision.SwitchLayer();
4355
4356 for(int y = 0; y < m_Collision.GetHeight(); y++)
4357 {
4358 for(int x = 0; x < m_Collision.GetWidth(); x++)
4359 {
4360 const int Index = y * m_Collision.GetWidth() + x;
4361
4362 // Game layer
4363 {
4364 const int GameIndex = pTiles[Index].m_Index;
4365 if(GameIndex == TILE_OLDLASER)
4366 {
4367 g_Config.m_SvOldLaser = 1;
4368 dbg_msg(sys: "game_layer", fmt: "found old laser tile");
4369 }
4370 else if(GameIndex == TILE_NPC)
4371 {
4372 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4373 dbg_msg(sys: "game_layer", fmt: "found no collision tile");
4374 }
4375 else if(GameIndex == TILE_EHOOK)
4376 {
4377 g_Config.m_SvEndlessDrag = 1;
4378 dbg_msg(sys: "game_layer", fmt: "found unlimited hook time tile");
4379 }
4380 else if(GameIndex == TILE_NOHIT)
4381 {
4382 g_Config.m_SvHit = 0;
4383 dbg_msg(sys: "game_layer", fmt: "found no weapons hitting others tile");
4384 }
4385 else if(GameIndex == TILE_NPH)
4386 {
4387 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4388 dbg_msg(sys: "game_layer", fmt: "found no player hooking tile");
4389 }
4390 else if(GameIndex >= ENTITY_OFFSET)
4391 {
4392 m_pController->OnEntity(Index: GameIndex - ENTITY_OFFSET, x, y, Layer: LAYER_GAME, Flags: pTiles[Index].m_Flags, Initial);
4393 }
4394 }
4395
4396 if(pFront)
4397 {
4398 const int FrontIndex = pFront[Index].m_Index;
4399 if(FrontIndex == TILE_OLDLASER)
4400 {
4401 g_Config.m_SvOldLaser = 1;
4402 dbg_msg(sys: "front_layer", fmt: "found old laser tile");
4403 }
4404 else if(FrontIndex == TILE_NPC)
4405 {
4406 GlobalTuning()->Set(pName: "player_collision", Value: 0);
4407 dbg_msg(sys: "front_layer", fmt: "found no collision tile");
4408 }
4409 else if(FrontIndex == TILE_EHOOK)
4410 {
4411 g_Config.m_SvEndlessDrag = 1;
4412 dbg_msg(sys: "front_layer", fmt: "found unlimited hook time tile");
4413 }
4414 else if(FrontIndex == TILE_NOHIT)
4415 {
4416 g_Config.m_SvHit = 0;
4417 dbg_msg(sys: "front_layer", fmt: "found no weapons hitting others tile");
4418 }
4419 else if(FrontIndex == TILE_NPH)
4420 {
4421 GlobalTuning()->Set(pName: "player_hooking", Value: 0);
4422 dbg_msg(sys: "front_layer", fmt: "found no player hooking tile");
4423 }
4424 else if(FrontIndex >= ENTITY_OFFSET)
4425 {
4426 m_pController->OnEntity(Index: FrontIndex - ENTITY_OFFSET, x, y, Layer: LAYER_FRONT, Flags: pFront[Index].m_Flags, Initial);
4427 }
4428 }
4429
4430 if(pSwitch)
4431 {
4432 const int SwitchType = pSwitch[Index].m_Type;
4433 // TODO: Add off by default door here
4434 // if(SwitchType == TILE_DOOR_OFF)
4435 if(SwitchType >= ENTITY_OFFSET)
4436 {
4437 m_pController->OnEntity(Index: SwitchType - ENTITY_OFFSET, x, y, Layer: LAYER_SWITCH, Flags: pSwitch[Index].m_Flags, Initial, Number: pSwitch[Index].m_Number);
4438 }
4439 }
4440 }
4441 }
4442}
4443
4444CPlayer *CGameContext::CreatePlayer(int ClientId, int StartTeam, bool Afk, int LastWhisperTo)
4445{
4446 if(m_apPlayers[ClientId])
4447 delete m_apPlayers[ClientId];
4448 m_apPlayers[ClientId] = new(ClientId) CPlayer(this, m_NextUniqueClientId, ClientId, StartTeam);
4449 m_apPlayers[ClientId]->SetInitialAfk(Afk);
4450 m_apPlayers[ClientId]->m_LastWhisperTo = LastWhisperTo;
4451 m_NextUniqueClientId += 1;
4452 return m_apPlayers[ClientId];
4453}
4454
4455void CGameContext::DeleteTempfile()
4456{
4457 if(m_aDeleteTempfile[0] != 0)
4458 {
4459 Storage()->RemoveFile(pFilename: m_aDeleteTempfile, Type: IStorage::TYPE_SAVE);
4460 m_aDeleteTempfile[0] = 0;
4461 }
4462}
4463
4464bool CGameContext::OnMapChange(char *pNewMapName, int MapNameSize)
4465{
4466 char aConfig[IO_MAX_PATH_LENGTH];
4467 str_format(buffer: aConfig, buffer_size: sizeof(aConfig), format: "maps/%s.cfg", g_Config.m_SvMap);
4468
4469 CLineReader LineReader;
4470 if(!LineReader.OpenFile(File: Storage()->OpenFile(pFilename: aConfig, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
4471 {
4472 // No map-specific config, just return.
4473 return true;
4474 }
4475
4476 CDataFileReader Reader;
4477 if(!Reader.Open(pFullName: g_Config.m_SvMap, pStorage: Storage(), pPath: pNewMapName, StorageType: IStorage::TYPE_ALL))
4478 {
4479 log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for reading", aConfig, pNewMapName);
4480 return false;
4481 }
4482
4483 std::vector<const char *> vpLines;
4484 int TotalLength = 0;
4485 while(const char *pLine = LineReader.Get())
4486 {
4487 vpLines.push_back(x: pLine);
4488 TotalLength += str_length(str: pLine) + 1;
4489 }
4490
4491 char *pSettings = (char *)malloc(size: maximum(a: 1, b: TotalLength));
4492 int Offset = 0;
4493 for(const char *pLine : vpLines)
4494 {
4495 int Length = str_length(str: pLine) + 1;
4496 mem_copy(dest: pSettings + Offset, source: pLine, size: Length);
4497 Offset += Length;
4498 }
4499
4500 CDataFileWriter Writer;
4501
4502 int SettingsIndex = Reader.NumData();
4503 bool FoundInfo = false;
4504 for(int i = 0; i < Reader.NumItems(); i++)
4505 {
4506 int TypeId;
4507 int ItemId;
4508 void *pData = Reader.GetItem(Index: i, pType: &TypeId, pId: &ItemId);
4509 int Size = Reader.GetItemSize(Index: i);
4510 CMapItemInfoSettings MapInfo;
4511 if(TypeId == MAPITEMTYPE_INFO && ItemId == 0)
4512 {
4513 FoundInfo = true;
4514 if(Size >= (int)sizeof(CMapItemInfoSettings))
4515 {
4516 CMapItemInfoSettings *pInfo = (CMapItemInfoSettings *)pData;
4517 if(pInfo->m_Settings > -1)
4518 {
4519 SettingsIndex = pInfo->m_Settings;
4520 char *pMapSettings = (char *)Reader.GetData(Index: SettingsIndex);
4521 int DataSize = Reader.GetDataSize(Index: SettingsIndex);
4522 if(DataSize == TotalLength && mem_comp(a: pSettings, b: pMapSettings, size: DataSize) == 0)
4523 {
4524 // Configs coincide, no need to update map.
4525 free(ptr: pSettings);
4526 return true;
4527 }
4528 Reader.UnloadData(Index: pInfo->m_Settings);
4529 }
4530 else
4531 {
4532 MapInfo = *pInfo;
4533 MapInfo.m_Settings = SettingsIndex;
4534 pData = &MapInfo;
4535 Size = sizeof(MapInfo);
4536 }
4537 }
4538 else
4539 {
4540 *(CMapItemInfo *)&MapInfo = *(CMapItemInfo *)pData;
4541 MapInfo.m_Settings = SettingsIndex;
4542 pData = &MapInfo;
4543 Size = sizeof(MapInfo);
4544 }
4545 }
4546 Writer.AddItem(Type: TypeId, Id: ItemId, Size, pData);
4547 }
4548
4549 if(!FoundInfo)
4550 {
4551 CMapItemInfoSettings Info;
4552 Info.m_Version = 1;
4553 Info.m_Author = -1;
4554 Info.m_MapVersion = -1;
4555 Info.m_Credits = -1;
4556 Info.m_License = -1;
4557 Info.m_Settings = SettingsIndex;
4558 Writer.AddItem(Type: MAPITEMTYPE_INFO, Id: 0, Size: sizeof(Info), pData: &Info);
4559 }
4560
4561 for(int i = 0; i < Reader.NumData() || i == SettingsIndex; i++)
4562 {
4563 if(i == SettingsIndex)
4564 {
4565 Writer.AddData(Size: TotalLength, pData: pSettings);
4566 continue;
4567 }
4568 const void *pData = Reader.GetData(Index: i);
4569 int Size = Reader.GetDataSize(Index: i);
4570 Writer.AddData(Size, pData);
4571 Reader.UnloadData(Index: i);
4572 }
4573
4574 free(ptr: pSettings);
4575 Reader.Close();
4576
4577 char aTemp[IO_MAX_PATH_LENGTH];
4578 if(!Writer.Open(pStorage: Storage(), pFilename: IStorage::FormatTmpPath(aBuf: aTemp, BufSize: sizeof(aTemp), pPath: pNewMapName)))
4579 {
4580 log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for writing", aConfig, aTemp);
4581 return false;
4582 }
4583 Writer.Finish();
4584 log_info("mapchange", "Imported settings from '%s' into '%s'", aConfig, aTemp);
4585
4586 str_copy(dst: pNewMapName, src: aTemp, dst_size: MapNameSize);
4587 str_copy(dst: m_aDeleteTempfile, src: aTemp, dst_size: sizeof(m_aDeleteTempfile));
4588 return true;
4589}
4590
4591void CGameContext::OnShutdown(void *pPersistentData)
4592{
4593 CPersistentData *pPersistent = (CPersistentData *)pPersistentData;
4594
4595 if(pPersistent)
4596 {
4597 pPersistent->m_PrevGameUuid = m_GameUuid;
4598 }
4599
4600 Antibot()->RoundEnd();
4601
4602 if(m_TeeHistorianActive)
4603 {
4604 m_TeeHistorian.Finish();
4605 aio_close(aio: m_pTeeHistorianFile);
4606 aio_wait(aio: m_pTeeHistorianFile);
4607 int Error = aio_error(aio: m_pTeeHistorianFile);
4608 if(Error)
4609 {
4610 dbg_msg(sys: "teehistorian", fmt: "error closing file, err=%d", Error);
4611 Server()->SetErrorShutdown("teehistorian close error");
4612 }
4613 aio_free(aio: m_pTeeHistorianFile);
4614 }
4615
4616 // Stop any demos being recorded.
4617 Server()->StopDemos();
4618
4619 DeleteTempfile();
4620 ConfigManager()->ResetGameSettings();
4621 Collision()->Unload();
4622 Layers()->Unload();
4623 delete m_pController;
4624 m_pController = nullptr;
4625 Clear();
4626}
4627
4628void CGameContext::LoadMapSettings()
4629{
4630 IMap *pMap = Map();
4631 int Start, Num;
4632 pMap->GetType(Type: MAPITEMTYPE_INFO, pStart: &Start, pNum: &Num);
4633 for(int i = Start; i < Start + Num; i++)
4634 {
4635 int ItemId;
4636 CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(Index: i, pType: nullptr, pId: &ItemId);
4637 int ItemSize = pMap->GetItemSize(Index: i);
4638 if(!pItem || ItemId != 0)
4639 continue;
4640
4641 if(ItemSize < (int)sizeof(CMapItemInfoSettings))
4642 break;
4643 if(!(pItem->m_Settings > -1))
4644 break;
4645
4646 int Size = pMap->GetDataSize(Index: pItem->m_Settings);
4647 char *pSettings = (char *)pMap->GetData(Index: pItem->m_Settings);
4648 char *pNext = pSettings;
4649 while(pNext < pSettings + Size)
4650 {
4651 int StrSize = str_length(str: pNext) + 1;
4652 Console()->ExecuteLine(pStr: pNext, ClientId: IConsole::CLIENT_ID_GAME);
4653 pNext += StrSize;
4654 }
4655 pMap->UnloadData(Index: pItem->m_Settings);
4656 break;
4657 }
4658
4659 char aBuf[IO_MAX_PATH_LENGTH];
4660 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "maps/%s.map.cfg", g_Config.m_SvMap);
4661 Console()->ExecuteFile(pFilename: aBuf, ClientId: IConsole::CLIENT_ID_NO_GAME);
4662}
4663
4664void CGameContext::OnSnap(int ClientId, bool GlobalSnap, bool RecordingDemo)
4665{
4666 // sixup should only snap during global snap
4667 dbg_assert(!Server()->IsSixup(ClientId) || GlobalSnap, "sixup should only snap during global snap");
4668
4669 // add tuning to demo
4670 if(RecordingDemo && mem_comp(a: &CTuningParams::DEFAULT, b: &m_aTuningList[0], size: sizeof(CTuningParams)) != 0)
4671 {
4672 CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
4673 int *pParams = (int *)&m_aTuningList[0];
4674 for(int i = 0; i < CTuningParams::Num(); i++)
4675 Msg.AddInt(i: pParams[i]);
4676 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_NOSEND, ClientId);
4677 }
4678
4679 m_pController->Snap(SnappingClient: ClientId);
4680
4681 for(auto &pPlayer : m_apPlayers)
4682 {
4683 if(pPlayer)
4684 pPlayer->Snap(SnappingClient: ClientId);
4685 }
4686
4687 if(ClientId > -1)
4688 m_apPlayers[ClientId]->FakeSnap();
4689
4690 m_World.Snap(SnappingClient: ClientId);
4691
4692 // events are only sent on global snapshots
4693 if(GlobalSnap)
4694 {
4695 m_Events.Snap(SnappingClient: ClientId);
4696 }
4697}
4698
4699void CGameContext::OnPostGlobalSnap()
4700{
4701 for(auto &pPlayer : m_apPlayers)
4702 {
4703 if(pPlayer && pPlayer->GetCharacter())
4704 pPlayer->GetCharacter()->PostGlobalSnap();
4705 }
4706 m_Events.Clear();
4707}
4708
4709void CGameContext::UpdatePlayerMaps()
4710{
4711 const auto DistCompare = [](std::pair<float, int> a, std::pair<float, int> b) -> bool {
4712 return (a.first < b.first);
4713 };
4714
4715 if(Server()->Tick() % g_Config.m_SvMapUpdateRate != 0)
4716 return;
4717
4718 std::pair<float, int> Dist[MAX_CLIENTS];
4719 for(int i = 0; i < MAX_CLIENTS; i++)
4720 {
4721 if(!Server()->ClientIngame(ClientId: i))
4722 continue;
4723 if(Server()->GetClientVersion(ClientId: i) >= VERSION_DDNET_OLD)
4724 continue;
4725 int *pMap = Server()->GetIdMap(ClientId: i);
4726
4727 // compute distances
4728 for(int j = 0; j < MAX_CLIENTS; j++)
4729 {
4730 Dist[j].second = j;
4731 if(j == i)
4732 continue;
4733 if(!Server()->ClientIngame(ClientId: j) || !m_apPlayers[j])
4734 {
4735 Dist[j].first = 1e10;
4736 continue;
4737 }
4738 CCharacter *pChr = m_apPlayers[j]->GetCharacter();
4739 if(!pChr)
4740 {
4741 Dist[j].first = 1e9;
4742 continue;
4743 }
4744 if(!pChr->CanSnapCharacter(SnappingClient: i))
4745 Dist[j].first = 1e8;
4746 else
4747 Dist[j].first = length_squared(a: m_apPlayers[i]->m_ViewPos - pChr->GetPos());
4748 }
4749
4750 // always send the player themselves, even if all in same position
4751 Dist[i].first = -1;
4752
4753 std::nth_element(first: &Dist[0], nth: &Dist[VANILLA_MAX_CLIENTS - 1], last: &Dist[MAX_CLIENTS], comp: DistCompare);
4754
4755 int Index = 1; // exclude self client id
4756 for(int j = 0; j < VANILLA_MAX_CLIENTS - 1; j++)
4757 {
4758 pMap[j + 1] = -1; // also fill player with empty name to say chat msgs
4759 if(Dist[j].second == i || Dist[j].first > 5e9f)
4760 continue;
4761 pMap[Index++] = Dist[j].second;
4762 }
4763
4764 // sort by real client ids, guarantee order on distance changes, O(Nlog(N)) worst case
4765 // sort just clients in game always except first (self client id) and last (fake client id) indexes
4766 std::sort(first: &pMap[1], last: &pMap[minimum(a: Index, b: VANILLA_MAX_CLIENTS - 1)]);
4767 }
4768}
4769
4770bool CGameContext::IsClientReady(int ClientId) const
4771{
4772 return m_apPlayers[ClientId] && m_apPlayers[ClientId]->m_IsReady;
4773}
4774
4775bool CGameContext::IsClientPlayer(int ClientId) const
4776{
4777 return m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetTeam() != TEAM_SPECTATORS;
4778}
4779
4780bool CGameContext::IsClientHighBandwidth(int ClientId) const
4781{
4782 // force high bandwidth is not supported for sixup
4783 return m_apPlayers[ClientId] && !Server()->IsSixup(ClientId) && Server()->IsRconAuthed(ClientId) &&
4784 (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsPaused());
4785}
4786
4787CUuid CGameContext::GameUuid() const { return m_GameUuid; }
4788const char *CGameContext::GameType() const
4789{
4790 dbg_assert(m_pController, "no controller");
4791 dbg_assert(m_pController->m_pGameType, "no gametype");
4792 return m_pController->m_pGameType;
4793}
4794const char *CGameContext::Version() const { return GAME_VERSION; }
4795const char *CGameContext::NetVersion() const { return GAME_NETVERSION; }
4796
4797IGameServer *CreateGameServer() { return new CGameContext; }
4798
4799void CGameContext::OnSetAuthed(int ClientId, int Level)
4800{
4801 if(m_apPlayers[ClientId] && m_VoteCloseTime && Level != AUTHED_NO)
4802 {
4803 char aBuf[512];
4804 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ban %s %d Banned by vote", Server()->ClientAddrString(ClientId, IncludePort: false), g_Config.m_SvVoteKickBantime);
4805 if(!str_comp_nocase(a: m_aVoteCommand, b: aBuf) && (m_VoteCreator == -1 || Level > Server()->GetAuthedState(ClientId: m_VoteCreator)))
4806 {
4807 m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN;
4808 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "game", pStr: "Vote aborted by authorized login.");
4809 }
4810 }
4811
4812 if(m_TeeHistorianActive)
4813 {
4814 if(Level != AUTHED_NO)
4815 {
4816 m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName: Server()->GetAuthName(ClientId));
4817 }
4818 else
4819 {
4820 m_TeeHistorian.RecordAuthLogout(ClientId);
4821 }
4822 }
4823}
4824
4825bool CGameContext::IsRunningVote(int ClientId) const
4826{
4827 return m_VoteCloseTime && m_VoteCreator == ClientId;
4828}
4829
4830bool CGameContext::IsRunningKickOrSpecVote(int ClientId) const
4831{
4832 return IsRunningVote(ClientId) && (IsKickVote() || IsSpecVote());
4833}
4834
4835void CGameContext::SendRecord(int ClientId)
4836{
4837 if(Server()->IsSixup(ClientId) || GetClientVersion(ClientId) >= VERSION_DDNET_MAP_BESTTIME)
4838 return;
4839
4840 CNetMsg_Sv_Record Msg;
4841 CNetMsg_Sv_RecordLegacy MsgLegacy;
4842 MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = round_to_int(f: Score()->PlayerData(Id: ClientId)->m_BestTime.value_or(u: 0.0f) * 100.0f);
4843 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;
4844 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
4845 if(GetClientVersion(ClientId) < VERSION_DDNET_MSG_LEGACY)
4846 {
4847 Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
4848 }
4849}
4850
4851void CGameContext::SendFinish(int ClientId, float Time, std::optional<float> PreviousBestTime)
4852{
4853 int ClientVersion = m_apPlayers[ClientId]->GetClientVersion();
4854
4855 if(!Server()->IsSixup(ClientId))
4856 {
4857 CNetMsg_Sv_DDRaceTime Msg;
4858 CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy;
4859 MsgLegacy.m_Time = Msg.m_Time = (int)(Time * 100.0f);
4860 MsgLegacy.m_Check = Msg.m_Check = 0;
4861 MsgLegacy.m_Finish = Msg.m_Finish = 1;
4862
4863 if(PreviousBestTime.has_value())
4864 {
4865 float Diff100 = (Time - PreviousBestTime.value()) * 100;
4866 MsgLegacy.m_Check = Msg.m_Check = (int)Diff100;
4867 }
4868 if(VERSION_DDRACE <= ClientVersion)
4869 {
4870 if(ClientVersion < VERSION_DDNET_MSG_LEGACY)
4871 {
4872 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
4873 }
4874 else
4875 {
4876 Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
4877 }
4878 }
4879 }
4880
4881 CNetMsg_Sv_RaceFinish RaceFinishMsg;
4882 RaceFinishMsg.m_ClientId = ClientId;
4883 RaceFinishMsg.m_Time = Time * 1000;
4884 RaceFinishMsg.m_Diff = 0;
4885 if(PreviousBestTime.has_value())
4886 {
4887 float Diff = absolute(a: Time - PreviousBestTime.value());
4888 RaceFinishMsg.m_Diff = Diff * 1000 * (Time < PreviousBestTime.value() ? -1 : 1);
4889 }
4890 RaceFinishMsg.m_RecordPersonal = (!PreviousBestTime.has_value() || Time < PreviousBestTime.value());
4891 RaceFinishMsg.m_RecordServer = Time < m_pController->m_CurrentRecord;
4892 Server()->SendPackMsg(pMsg: &RaceFinishMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: g_Config.m_SvHideScore ? ClientId : -1);
4893}
4894
4895void CGameContext::SendSaveCode(int Team, int TeamSize, int State, const char *pError, const char *pSaveRequester, const char *pServerName, const char *pGeneratedCode, const char *pCode)
4896{
4897 char aBuf[512];
4898
4899 CMsgPacker Msg(NETMSGTYPE_SV_SAVECODE);
4900 Msg.AddInt(i: State);
4901 Msg.AddString(pStr: pError);
4902 Msg.AddString(pStr: pSaveRequester);
4903 Msg.AddString(pStr: pServerName);
4904 Msg.AddString(pStr: pGeneratedCode);
4905 Msg.AddString(pStr: pCode);
4906 char aTeamMembers[1024];
4907 aTeamMembers[0] = '\0';
4908 int NumMembersSent = 0;
4909 for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
4910 {
4911 if(!m_apPlayers[MemberId])
4912 continue;
4913 if(GetDDRaceTeam(ClientId: MemberId) != Team)
4914 continue;
4915 if(NumMembersSent++ > 10)
4916 {
4917 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " and %d others", (TeamSize - NumMembersSent) + 1);
4918 str_append(dst&: aTeamMembers, src: aBuf);
4919 break;
4920 }
4921
4922 if(NumMembersSent > 1)
4923 str_append(dst&: aTeamMembers, src: ", ");
4924 str_append(dst&: aTeamMembers, src: Server()->ClientName(ClientId: MemberId));
4925 }
4926 Msg.AddString(pStr: aTeamMembers);
4927
4928 for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
4929 {
4930 if(!m_apPlayers[MemberId])
4931 continue;
4932 if(GetDDRaceTeam(ClientId: MemberId) != Team)
4933 continue;
4934
4935 if(GetClientVersion(ClientId: MemberId) >= VERSION_DDNET_SAVE_CODE)
4936 {
4937 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: MemberId);
4938 }
4939 else
4940 {
4941 switch(State)
4942 {
4943 case SAVESTATE_PENDING:
4944 if(pCode[0] == '\0')
4945 {
4946 str_format(buffer: aBuf,
4947 buffer_size: sizeof(aBuf),
4948 format: "Team save in progress. You'll be able to load with '/load %s'",
4949 pGeneratedCode);
4950 }
4951 else
4952 {
4953 str_format(buffer: aBuf,
4954 buffer_size: sizeof(aBuf),
4955 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",
4956 pCode,
4957 pGeneratedCode);
4958 }
4959 break;
4960 case SAVESTATE_DONE:
4961 if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0)
4962 {
4963 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4964 format: "Team successfully saved by %s. Use '/load %s' to continue",
4965 pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
4966 }
4967 else
4968 {
4969 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4970 format: "Team successfully saved by %s. Use '/load %s' on %s to continue",
4971 pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
4972 }
4973 break;
4974 case SAVESTATE_FALLBACKFILE:
4975 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);
4976 if(str_comp(a: pServerName, b: g_Config.m_SvSqlServerName) == 0)
4977 {
4978 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4979 format: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue",
4980 pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
4981 }
4982 else
4983 {
4984 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
4985 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",
4986 pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
4987 }
4988 break;
4989 case SAVESTATE_ERROR:
4990 case SAVESTATE_WARNING:
4991 str_copy(dst&: aBuf, src: pError);
4992 break;
4993 default:
4994 dbg_assert_failed("Unexpected save state %d", State);
4995 }
4996 SendChatTarget(To: MemberId, pText: aBuf);
4997 }
4998 }
4999}
5000
5001bool CGameContext::ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay)
5002{
5003 if(!m_apPlayers[ClientId])
5004 return false;
5005 if(g_Config.m_SvSpamprotection && m_apPlayers[ClientId]->m_LastChat && m_apPlayers[ClientId]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick())
5006 return true;
5007 else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientId))
5008 {
5009 SendChatTarget(To: ClientId, pText: "Players are not allowed to chat from VPNs at this time");
5010 return true;
5011 }
5012 else
5013 m_apPlayers[ClientId]->m_LastChat = Server()->Tick();
5014
5015 const std::optional<CMute> Muted = m_Mutes.IsMuted(pAddr: Server()->ClientAddr(ClientId), RespectInitialDelay: RespectChatInitialDelay);
5016 if(Muted.has_value())
5017 {
5018 char aChatMessage[128];
5019 if(Muted->m_InitialDelay)
5020 {
5021 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());
5022 }
5023 else
5024 {
5025 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to talk for the next %d seconds.", Muted->SecondsLeft());
5026 }
5027 SendChatTarget(To: ClientId, pText: aChatMessage);
5028 return true;
5029 }
5030
5031 if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientId]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold)
5032 {
5033 MuteWithMessage(pAddr: Server()->ClientAddr(ClientId), Seconds: g_Config.m_SvSpamMuteDuration, pReason: "Spam protection", pDisplayName: Server()->ClientName(ClientId));
5034 m_apPlayers[ClientId]->m_ChatScore = 0;
5035 return true;
5036 }
5037
5038 return false;
5039}
5040
5041int CGameContext::GetDDRaceTeam(int ClientId) const
5042{
5043 return m_pController->Teams().m_Core.Team(ClientId);
5044}
5045
5046void CGameContext::ResetTuning()
5047{
5048 *GlobalTuning() = CTuningParams::DEFAULT;
5049 GlobalTuning()->Set(pName: "gun_speed", Value: 1400);
5050 GlobalTuning()->Set(pName: "gun_curvature", Value: 0);
5051 GlobalTuning()->Set(pName: "shotgun_speed", Value: 500);
5052 GlobalTuning()->Set(pName: "shotgun_speeddiff", Value: 0);
5053 GlobalTuning()->Set(pName: "shotgun_curvature", Value: 0);
5054 SendTuningParams(ClientId: -1);
5055}
5056
5057void CGameContext::Whisper(int ClientId, char *pStr)
5058{
5059 if(ProcessSpamProtection(ClientId))
5060 return;
5061
5062 pStr = str_skip_whitespaces(str: pStr);
5063
5064 const char *pName;
5065 int Victim;
5066 bool Error = false;
5067
5068 // add token
5069 if(*pStr == '"')
5070 {
5071 pStr++;
5072
5073 pName = pStr;
5074 char *pDst = pStr; // we might have to process escape data
5075 while(true)
5076 {
5077 if(pStr[0] == '"')
5078 {
5079 break;
5080 }
5081 else if(pStr[0] == '\\')
5082 {
5083 if(pStr[1] == '\\')
5084 pStr++; // skip due to escape
5085 else if(pStr[1] == '"')
5086 pStr++; // skip due to escape
5087 }
5088 else if(pStr[0] == 0)
5089 {
5090 Error = true;
5091 break;
5092 }
5093
5094 *pDst = *pStr;
5095 pDst++;
5096 pStr++;
5097 }
5098
5099 if(!Error)
5100 {
5101 *pDst = '\0';
5102 pStr++;
5103
5104 Victim = FindClientIdByName(pName).value_or(u: -1);
5105 }
5106 }
5107 else
5108 {
5109 pName = pStr;
5110 while(true)
5111 {
5112 if(pStr[0] == '\0')
5113 {
5114 Error = true;
5115 break;
5116 }
5117 if(pStr[0] == ' ')
5118 {
5119 pStr[0] = '\0';
5120
5121 Victim = FindClientIdByName(pName).value_or(u: -1);
5122
5123 pStr[0] = ' ';
5124 if(Victim != -1)
5125 break;
5126 }
5127 pStr++;
5128 }
5129 }
5130
5131 if(pStr[0] != ' ')
5132 {
5133 Error = true;
5134 }
5135
5136 *pStr = '\0';
5137 pStr++;
5138
5139 if(Error)
5140 {
5141 SendChatTarget(To: ClientId, pText: "Invalid whisper");
5142 return;
5143 }
5144
5145 if(!CheckClientId(ClientId: Victim))
5146 {
5147 char aBuf[256];
5148 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No player with name \"%s\" found", pName);
5149 SendChatTarget(To: ClientId, pText: aBuf);
5150 return;
5151 }
5152
5153 WhisperId(ClientId, VictimId: Victim, pMessage: pStr);
5154}
5155
5156void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage)
5157{
5158 dbg_assert(CheckClientId(ClientId) && m_apPlayers[ClientId] != nullptr, "ClientId invalid");
5159 dbg_assert(CheckClientId(VictimId) && m_apPlayers[VictimId] != nullptr, "VictimId invalid");
5160
5161 m_apPlayers[ClientId]->m_LastWhisperTo = VictimId;
5162
5163 char aCensoredMessage[256];
5164 CensorMessage(pCensoredMessage: aCensoredMessage, pMessage, Size: sizeof(aCensoredMessage));
5165
5166 char aBuf[256];
5167
5168 if(Server()->IsSixup(ClientId))
5169 {
5170 protocol7::CNetMsg_Sv_Chat Msg;
5171 Msg.m_ClientId = ClientId;
5172 Msg.m_Mode = protocol7::CHAT_WHISPER;
5173 Msg.m_pMessage = aCensoredMessage;
5174 Msg.m_TargetId = VictimId;
5175
5176 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
5177 }
5178 else if(GetClientVersion(ClientId) >= VERSION_DDNET_WHISPER)
5179 {
5180 CNetMsg_Sv_Chat Msg;
5181 Msg.m_Team = TEAM_WHISPER_SEND;
5182 Msg.m_ClientId = VictimId;
5183 Msg.m_pMessage = aCensoredMessage;
5184 if(g_Config.m_SvDemoChat)
5185 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
5186 else
5187 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
5188 }
5189 else
5190 {
5191 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[→ %s] %s", Server()->ClientName(ClientId: VictimId), aCensoredMessage);
5192 SendChatTarget(To: ClientId, pText: aBuf);
5193 }
5194
5195 if(!m_apPlayers[VictimId]->m_Whispers)
5196 {
5197 SendChatTarget(To: ClientId, pText: "This person has disabled receiving whispers");
5198 return;
5199 }
5200
5201 if(Server()->IsSixup(ClientId: VictimId))
5202 {
5203 protocol7::CNetMsg_Sv_Chat Msg;
5204 Msg.m_ClientId = ClientId;
5205 Msg.m_Mode = protocol7::CHAT_WHISPER;
5206 Msg.m_pMessage = aCensoredMessage;
5207 Msg.m_TargetId = VictimId;
5208
5209 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId);
5210 }
5211 else if(GetClientVersion(ClientId: VictimId) >= VERSION_DDNET_WHISPER)
5212 {
5213 CNetMsg_Sv_Chat Msg2;
5214 Msg2.m_Team = TEAM_WHISPER_RECV;
5215 Msg2.m_ClientId = ClientId;
5216 Msg2.m_pMessage = aCensoredMessage;
5217 if(g_Config.m_SvDemoChat)
5218 Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL, ClientId: VictimId);
5219 else
5220 Server()->SendPackMsg(pMsg: &Msg2, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: VictimId);
5221 }
5222 else
5223 {
5224 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "[← %s] %s", Server()->ClientName(ClientId), aCensoredMessage);
5225 SendChatTarget(To: VictimId, pText: aBuf);
5226 }
5227}
5228
5229void CGameContext::Converse(int ClientId, char *pStr)
5230{
5231 CPlayer *pPlayer = m_apPlayers[ClientId];
5232 if(!pPlayer)
5233 return;
5234
5235 if(ProcessSpamProtection(ClientId))
5236 return;
5237
5238 if(pPlayer->m_LastWhisperTo < 0)
5239 SendChatTarget(To: ClientId, pText: "You do not have an ongoing conversation. Whisper to someone to start one");
5240 else if(!m_apPlayers[pPlayer->m_LastWhisperTo])
5241 SendChatTarget(To: ClientId, pText: "The player you were whispering to hasn't reconnected yet or left. Please wait or whisper to someone else");
5242 else
5243 WhisperId(ClientId, VictimId: pPlayer->m_LastWhisperTo, pMessage: pStr);
5244}
5245
5246bool CGameContext::IsVersionBanned(int Version)
5247{
5248 char aVersion[16];
5249 str_format(buffer: aVersion, buffer_size: sizeof(aVersion), format: "%d", Version);
5250
5251 return str_in_list(list: g_Config.m_SvBannedVersions, delim: ",", needle: aVersion);
5252}
5253
5254void CGameContext::List(int ClientId, const char *pFilter)
5255{
5256 int Total = 0;
5257 char aBuf[256];
5258 int Bufcnt = 0;
5259 if(pFilter[0])
5260 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Listing players with \"%s\" in name:", pFilter);
5261 else
5262 str_copy(dst&: aBuf, src: "Listing all players:");
5263 SendChatTarget(To: ClientId, pText: aBuf);
5264 for(int i = 0; i < MAX_CLIENTS; i++)
5265 {
5266 if(m_apPlayers[i])
5267 {
5268 Total++;
5269 const char *pName = Server()->ClientName(ClientId: i);
5270 if(str_utf8_find_nocase(haystack: pName, needle: pFilter) == nullptr)
5271 continue;
5272 if(Bufcnt + str_length(str: pName) + 4 > 256)
5273 {
5274 SendChatTarget(To: ClientId, pText: aBuf);
5275 Bufcnt = 0;
5276 }
5277 if(Bufcnt != 0)
5278 {
5279 str_format(buffer: &aBuf[Bufcnt], buffer_size: sizeof(aBuf) - Bufcnt, format: ", %s", pName);
5280 Bufcnt += 2 + str_length(str: pName);
5281 }
5282 else
5283 {
5284 str_copy(dst: &aBuf[Bufcnt], src: pName, dst_size: sizeof(aBuf) - Bufcnt);
5285 Bufcnt += str_length(str: pName);
5286 }
5287 }
5288 }
5289 if(Bufcnt != 0)
5290 SendChatTarget(To: ClientId, pText: aBuf);
5291 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d players online", Total);
5292 SendChatTarget(To: ClientId, pText: aBuf);
5293}
5294
5295int CGameContext::GetClientVersion(int ClientId) const
5296{
5297 return Server()->GetClientVersion(ClientId);
5298}
5299
5300CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const
5301{
5302 CClientMask Mask;
5303 for(int i = 0; i < MAX_CLIENTS; ++i)
5304 {
5305 if(GetClientVersion(ClientId: i) >= Version)
5306 continue;
5307 Mask.set(pos: i);
5308 }
5309 return Mask;
5310}
5311
5312bool CGameContext::PlayerModerating() const
5313{
5314 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; });
5315}
5316
5317void CGameContext::ForceVote(bool Success)
5318{
5319 // check if there is a vote running
5320 if(!m_VoteCloseTime)
5321 return;
5322
5323 m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
5324 const char *pOption = Success ? "yes" : "no";
5325
5326 char aChatMessage[256];
5327 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "Authorized player forced vote '%s'", pOption);
5328 SendChatTarget(To: -1, pText: aChatMessage);
5329
5330 log_info("server", "Forcing vote '%s'", pOption);
5331}
5332
5333bool CGameContext::RateLimitPlayerVote(int ClientId)
5334{
5335 int64_t Now = Server()->Tick();
5336 int64_t TickSpeed = Server()->TickSpeed();
5337 CPlayer *pPlayer = m_apPlayers[ClientId];
5338
5339 if(g_Config.m_SvRconVote && !Server()->IsRconAuthed(ClientId))
5340 {
5341 SendChatTarget(To: ClientId, pText: "You can only vote after logging in.");
5342 return true;
5343 }
5344
5345 if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1)
5346 {
5347 if(m_pServer->DnsblPending(ClientId))
5348 {
5349 SendChatTarget(To: ClientId, pText: "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds.");
5350 return true;
5351 }
5352 else if(m_pServer->DnsblBlack(ClientId))
5353 {
5354 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.");
5355 return true;
5356 }
5357 }
5358
5359 if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + TickSpeed * 3 > Now)
5360 return true;
5361
5362 pPlayer->m_LastVoteTry = Now;
5363 if(m_VoteCloseTime)
5364 {
5365 SendChatTarget(To: ClientId, pText: "Wait for current vote to end before calling a new one.");
5366 return true;
5367 }
5368
5369 if(Now < pPlayer->m_FirstVoteTick)
5370 {
5371 char aChatMessage[64];
5372 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);
5373 SendChatTarget(To: ClientId, pText: aChatMessage);
5374 return true;
5375 }
5376
5377 int TimeLeft = pPlayer->m_LastVoteCall + TickSpeed * g_Config.m_SvVoteDelay - Now;
5378 if(pPlayer->m_LastVoteCall && TimeLeft > 0)
5379 {
5380 char aChatMessage[64];
5381 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You must wait %d seconds before making another vote.", (int)(TimeLeft / TickSpeed) + 1);
5382 SendChatTarget(To: ClientId, pText: aChatMessage);
5383 return true;
5384 }
5385
5386 const NETADDR *pAddr = Server()->ClientAddr(ClientId);
5387 std::optional<CMute> Muted = m_VoteMutes.IsMuted(pAddr, RespectInitialDelay: true);
5388 if(!Muted.has_value())
5389 {
5390 Muted = m_Mutes.IsMuted(pAddr, RespectInitialDelay: true);
5391 }
5392 if(Muted.has_value())
5393 {
5394 char aChatMessage[64];
5395 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "You are not permitted to vote for the next %d seconds.", Muted->SecondsLeft());
5396 SendChatTarget(To: ClientId, pText: aChatMessage);
5397 return true;
5398 }
5399 return false;
5400}
5401
5402bool CGameContext::RateLimitPlayerMapVote(int ClientId) const
5403{
5404 if(!Server()->IsRconAuthed(ClientId) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay))
5405 {
5406 char aChatMessage[128];
5407 str_format(buffer: aChatMessage, buffer_size: sizeof(aChatMessage), format: "There's a %d second delay between map-votes, please wait %d seconds.",
5408 g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq()));
5409 SendChatTarget(To: ClientId, pText: aChatMessage);
5410 return true;
5411 }
5412 return false;
5413}
5414
5415void CGameContext::OnUpdatePlayerServerInfo(CJsonWriter *pJsonWriter, int ClientId)
5416{
5417 if(!m_apPlayers[ClientId])
5418 return;
5419
5420 CTeeInfo &TeeInfo = m_apPlayers[ClientId]->m_TeeInfos;
5421
5422 pJsonWriter->WriteAttribute(pName: "skin");
5423 pJsonWriter->BeginObject();
5424
5425 // 0.6
5426 if(!Server()->IsSixup(ClientId))
5427 {
5428 pJsonWriter->WriteAttribute(pName: "name");
5429 pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aSkinName);
5430
5431 if(TeeInfo.m_UseCustomColor)
5432 {
5433 pJsonWriter->WriteAttribute(pName: "color_body");
5434 pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorBody);
5435
5436 pJsonWriter->WriteAttribute(pName: "color_feet");
5437 pJsonWriter->WriteIntValue(Value: TeeInfo.m_ColorFeet);
5438 }
5439 }
5440 // 0.7
5441 else
5442 {
5443 const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};
5444
5445 for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i)
5446 {
5447 pJsonWriter->WriteAttribute(pName: apPartNames[i]);
5448 pJsonWriter->BeginObject();
5449
5450 pJsonWriter->WriteAttribute(pName: "name");
5451 pJsonWriter->WriteStrValue(pValue: TeeInfo.m_aaSkinPartNames[i]);
5452
5453 if(TeeInfo.m_aUseCustomColors[i])
5454 {
5455 pJsonWriter->WriteAttribute(pName: "color");
5456 pJsonWriter->WriteIntValue(Value: TeeInfo.m_aSkinPartColors[i]);
5457 }
5458
5459 pJsonWriter->EndObject();
5460 }
5461 }
5462
5463 pJsonWriter->EndObject();
5464
5465 pJsonWriter->WriteAttribute(pName: "afk");
5466 pJsonWriter->WriteBoolValue(Value: m_apPlayers[ClientId]->IsAfk());
5467
5468 const int Team = m_pController->IsTeamPlay() ? m_apPlayers[ClientId]->GetTeam() : (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(ClientId));
5469
5470 pJsonWriter->WriteAttribute(pName: "team");
5471 pJsonWriter->WriteIntValue(Value: Team);
5472}
5473
5474void CGameContext::ReadCensorList()
5475{
5476 const char *pCensorFilename = "censorlist.txt";
5477 CLineReader LineReader;
5478 m_vCensorlist.clear();
5479 if(LineReader.OpenFile(File: Storage()->OpenFile(pFilename: pCensorFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
5480 {
5481 while(const char *pLine = LineReader.Get())
5482 {
5483 m_vCensorlist.emplace_back(args&: pLine);
5484 }
5485 }
5486 else
5487 {
5488 dbg_msg(sys: "censorlist", fmt: "failed to open '%s'", pCensorFilename);
5489 }
5490}
5491
5492bool CGameContext::PracticeByDefault() const
5493{
5494 return g_Config.m_SvPracticeByDefault && g_Config.m_SvTestingCommands;
5495}
5496