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