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