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 "player.h"
4#include "entities/character.h"
5#include "gamecontext.h"
6#include "gamecontroller.h"
7#include "score.h"
8
9#include <base/system.h>
10
11#include <engine/antibot.h>
12#include <engine/server.h>
13#include <engine/shared/config.h>
14
15#include <game/gamecore.h>
16#include <game/teamscore.h>
17
18MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS)
19
20IServer *CPlayer::Server() const { return m_pGameServer->Server(); }
21
22CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientId, int ClientId, int Team) :
23 m_UniqueClientId(UniqueClientId)
24{
25 m_pGameServer = pGameServer;
26 m_ClientId = ClientId;
27 m_Team = GameServer()->m_pController->ClampTeam(Team);
28 m_NumInputs = 0;
29 Reset();
30 GameServer()->Antibot()->OnPlayerInit(ClientId: m_ClientId);
31}
32
33CPlayer::~CPlayer()
34{
35 GameServer()->Antibot()->OnPlayerDestroy(ClientId: m_ClientId);
36 delete m_pLastTarget;
37 delete m_pCharacter;
38 m_pCharacter = 0;
39}
40
41void CPlayer::Reset()
42{
43 m_DieTick = Server()->Tick();
44 m_PreviousDieTick = m_DieTick;
45 m_JoinTick = Server()->Tick();
46 delete m_pCharacter;
47 m_pCharacter = 0;
48 m_SpectatorId = SPEC_FREEVIEW;
49 m_LastActionTick = Server()->Tick();
50 m_TeamChangeTick = Server()->Tick();
51 m_LastInvited = 0;
52 m_WeakHookSpawn = false;
53
54 int *pIdMap = Server()->GetIdMap(ClientId: m_ClientId);
55 for(int i = 1; i < VANILLA_MAX_CLIENTS; i++)
56 {
57 pIdMap[i] = -1;
58 }
59 pIdMap[0] = m_ClientId;
60
61 // DDRace
62
63 m_LastCommandPos = 0;
64 m_LastPlaytime = 0;
65 m_ChatScore = 0;
66 m_Moderating = false;
67 m_EyeEmoteEnabled = true;
68 if(Server()->IsSixup(ClientId: m_ClientId))
69 m_TimerType = TIMERTYPE_SIXUP;
70 else
71 m_TimerType = (g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER || g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST) ? TIMERTYPE_BROADCAST : g_Config.m_SvDefaultTimerType;
72
73 m_DefEmote = EMOTE_NORMAL;
74 m_Afk = true;
75 m_LastWhisperTo = -1;
76 m_LastSetSpectatorMode = 0;
77 m_aTimeoutCode[0] = '\0';
78 delete m_pLastTarget;
79 m_pLastTarget = new CNetObj_PlayerInput({.m_Direction: 0});
80 m_LastTargetInit = false;
81 m_TuneZone = 0;
82 m_TuneZoneOld = m_TuneZone;
83 m_Halloween = false;
84 m_FirstPacket = true;
85
86 m_SendVoteIndex = -1;
87
88 if(g_Config.m_Events)
89 {
90 const ETimeSeason Season = time_season();
91 if(Season == SEASON_NEWYEAR)
92 {
93 m_DefEmote = EMOTE_HAPPY;
94 }
95 else if(Season == SEASON_HALLOWEEN)
96 {
97 m_DefEmote = EMOTE_ANGRY;
98 m_Halloween = true;
99 }
100 else
101 {
102 m_DefEmote = EMOTE_NORMAL;
103 }
104 }
105 m_OverrideEmoteReset = -1;
106
107 GameServer()->Score()->PlayerData(Id: m_ClientId)->Reset();
108
109 m_Last_KickVote = 0;
110 m_Last_Team = 0;
111 m_ShowOthers = g_Config.m_SvShowOthersDefault;
112 m_ShowAll = g_Config.m_SvShowAllDefault;
113 m_ShowDistance = vec2(1200, 800);
114 m_SpecTeam = false;
115 m_NinjaJetpack = false;
116
117 m_Paused = PAUSE_NONE;
118 m_DND = false;
119
120 m_LastPause = 0;
121 m_Score.reset();
122
123 // Variable initialized:
124 m_Last_Team = 0;
125 m_LastSqlQuery = 0;
126 m_ScoreQueryResult = nullptr;
127 m_ScoreFinishResult = nullptr;
128
129 int64_t Now = Server()->Tick();
130 int64_t TickSpeed = Server()->TickSpeed();
131 // If the player joins within ten seconds of the server becoming
132 // non-empty, allow them to vote immediately. This allows players to
133 // vote after map changes or when they join an empty server.
134 //
135 // Otherwise, block voting in the beginning after joining.
136 if(Now > GameServer()->m_NonEmptySince + 10 * TickSpeed)
137 m_FirstVoteTick = Now + g_Config.m_SvJoinVoteDelay * TickSpeed;
138 else
139 m_FirstVoteTick = Now;
140
141 m_NotEligibleForFinish = false;
142 m_EligibleForFinishCheck = 0;
143 m_VotedForPractice = false;
144 m_SwapTargetsClientId = -1;
145 m_BirthdayAnnounced = false;
146 m_RescueMode = RESCUEMODE_AUTO;
147}
148
149static int PlayerFlags_SixToSeven(int Flags)
150{
151 int Seven = 0;
152 if(Flags & PLAYERFLAG_CHATTING)
153 Seven |= protocol7::PLAYERFLAG_CHATTING;
154 if(Flags & PLAYERFLAG_SCOREBOARD)
155 Seven |= protocol7::PLAYERFLAG_SCOREBOARD;
156
157 return Seven;
158}
159
160void CPlayer::Tick()
161{
162 if(m_ScoreQueryResult != nullptr && m_ScoreQueryResult->m_Completed && m_SentSnaps >= 3)
163 {
164 ProcessScoreResult(Result&: *m_ScoreQueryResult);
165 m_ScoreQueryResult = nullptr;
166 }
167 if(m_ScoreFinishResult != nullptr && m_ScoreFinishResult->m_Completed)
168 {
169 ProcessScoreResult(Result&: *m_ScoreFinishResult);
170 m_ScoreFinishResult = nullptr;
171 }
172
173 if(!Server()->ClientIngame(ClientId: m_ClientId))
174 return;
175
176 if(m_ChatScore > 0)
177 m_ChatScore--;
178
179 Server()->SetClientScore(ClientId: m_ClientId, Score: m_Score);
180
181 if(m_Moderating && m_Afk)
182 {
183 m_Moderating = false;
184 GameServer()->SendChatTarget(To: m_ClientId, pText: "Active moderator mode disabled because you are afk.");
185
186 if(!GameServer()->PlayerModerating())
187 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: "Server kick/spec votes are no longer actively moderated.");
188 }
189
190 // do latency stuff
191 {
192 IServer::CClientInfo Info;
193 if(Server()->GetClientInfo(ClientId: m_ClientId, pInfo: &Info))
194 {
195 m_Latency.m_Accum += Info.m_Latency;
196 m_Latency.m_AccumMax = maximum(a: m_Latency.m_AccumMax, b: Info.m_Latency);
197 m_Latency.m_AccumMin = minimum(a: m_Latency.m_AccumMin, b: Info.m_Latency);
198 }
199 // each second
200 if(Server()->Tick() % Server()->TickSpeed() == 0)
201 {
202 m_Latency.m_Avg = m_Latency.m_Accum / Server()->TickSpeed();
203 m_Latency.m_Max = m_Latency.m_AccumMax;
204 m_Latency.m_Min = m_Latency.m_AccumMin;
205 m_Latency.m_Accum = 0;
206 m_Latency.m_AccumMin = 1000;
207 m_Latency.m_AccumMax = 0;
208 }
209 }
210
211 if(Server()->GetNetErrorString(ClientId: m_ClientId)[0])
212 {
213 SetInitialAfk(true);
214
215 char aBuf[512];
216 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' would have timed out, but can use timeout protection now", Server()->ClientName(ClientId: m_ClientId));
217 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
218 Server()->ResetNetErrorString(ClientId: m_ClientId);
219 }
220
221 if(!GameServer()->m_World.m_Paused)
222 {
223 int EarliestRespawnTick = m_PreviousDieTick + Server()->TickSpeed() * 3;
224 int RespawnTick = maximum(a: m_DieTick, b: EarliestRespawnTick) + 2;
225 if(!m_pCharacter && RespawnTick <= Server()->Tick())
226 m_Spawning = true;
227
228 if(m_pCharacter)
229 {
230 if(m_pCharacter->IsAlive())
231 {
232 ProcessPause();
233 if(!m_Paused)
234 m_ViewPos = m_pCharacter->m_Pos;
235 }
236 else if(!m_pCharacter->IsPaused())
237 {
238 delete m_pCharacter;
239 m_pCharacter = 0;
240 }
241 }
242 else if(m_Spawning && !m_WeakHookSpawn)
243 TryRespawn();
244 }
245 else
246 {
247 ++m_DieTick;
248 ++m_PreviousDieTick;
249 ++m_JoinTick;
250 ++m_LastActionTick;
251 ++m_TeamChangeTick;
252 }
253
254 m_TuneZoneOld = m_TuneZone; // determine needed tunings with viewpos
255 int CurrentIndex = GameServer()->Collision()->GetMapIndex(Pos: m_ViewPos);
256 m_TuneZone = GameServer()->Collision()->IsTune(Index: CurrentIndex);
257
258 if(m_TuneZone != m_TuneZoneOld) // don't send tunings all the time
259 {
260 GameServer()->SendTuningParams(ClientId: m_ClientId, Zone: m_TuneZone);
261 }
262
263 if(m_OverrideEmoteReset >= 0 && m_OverrideEmoteReset <= Server()->Tick())
264 {
265 m_OverrideEmoteReset = -1;
266 }
267
268 if(m_Halloween && m_pCharacter && !m_pCharacter->IsPaused())
269 {
270 if(1200 - ((Server()->Tick() - m_pCharacter->GetLastAction()) % (1200)) < 5)
271 {
272 GameServer()->SendEmoticon(ClientId: GetCid(), Emoticon: EMOTICON_GHOST, TargetClientId: -1);
273 }
274 }
275}
276
277void CPlayer::PostTick()
278{
279 // update latency value
280 if(m_PlayerFlags & PLAYERFLAG_IN_MENU)
281 m_aCurLatency[m_ClientId] = GameServer()->m_apPlayers[m_ClientId]->m_Latency.m_Min;
282
283 if(m_PlayerFlags & PLAYERFLAG_SCOREBOARD)
284 {
285 for(int i = 0; i < MAX_CLIENTS; ++i)
286 {
287 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS)
288 m_aCurLatency[i] = GameServer()->m_apPlayers[i]->m_Latency.m_Min;
289 }
290 }
291
292 // update view pos for spectators
293 if((m_Team == TEAM_SPECTATORS || m_Paused) && m_SpectatorId != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorId] && GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter())
294 m_ViewPos = GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter()->m_Pos;
295}
296
297void CPlayer::PostPostTick()
298{
299 if(!Server()->ClientIngame(ClientId: m_ClientId))
300 return;
301
302 if(!GameServer()->m_World.m_Paused && !m_pCharacter && m_Spawning && m_WeakHookSpawn)
303 TryRespawn();
304}
305
306void CPlayer::Snap(int SnappingClient)
307{
308 if(!Server()->ClientIngame(ClientId: m_ClientId))
309 return;
310
311 int id = m_ClientId;
312 if(!Server()->Translate(Target&: id, Client: SnappingClient))
313 return;
314
315 CNetObj_ClientInfo *pClientInfo = Server()->SnapNewItem<CNetObj_ClientInfo>(Id: id);
316 if(!pClientInfo)
317 return;
318
319 StrToInts(pInts: &pClientInfo->m_Name0, NumInts: 4, pStr: Server()->ClientName(ClientId: m_ClientId));
320 StrToInts(pInts: &pClientInfo->m_Clan0, NumInts: 3, pStr: Server()->ClientClan(ClientId: m_ClientId));
321 pClientInfo->m_Country = Server()->ClientCountry(ClientId: m_ClientId);
322 StrToInts(pInts: &pClientInfo->m_Skin0, NumInts: 6, pStr: m_TeeInfos.m_aSkinName);
323 pClientInfo->m_UseCustomColor = m_TeeInfos.m_UseCustomColor;
324 pClientInfo->m_ColorBody = m_TeeInfos.m_ColorBody;
325 pClientInfo->m_ColorFeet = m_TeeInfos.m_ColorFeet;
326
327 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
328 int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientId];
329
330 int Score;
331 // This is the time sent to the player while ingame (do not confuse to the one reported to the master server).
332 // Due to clients expecting this as a negative value, we have to make sure it's negative.
333 // Special numbers:
334 // -9999: means no time and isn't displayed in the scoreboard.
335 if(m_Score.has_value())
336 {
337 // shift the time by a second if the player actually took 9999
338 // seconds to finish the map.
339 if(m_Score.value() == 9999)
340 Score = -10000;
341 else
342 Score = -m_Score.value();
343 }
344 else
345 {
346 Score = -9999;
347 }
348
349 // send 0 if times of others are not shown
350 if(SnappingClient != m_ClientId && g_Config.m_SvHideScore)
351 Score = -9999;
352
353 if(!Server()->IsSixup(ClientId: SnappingClient))
354 {
355 CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem<CNetObj_PlayerInfo>(Id: id);
356 if(!pPlayerInfo)
357 return;
358
359 pPlayerInfo->m_Latency = Latency;
360 pPlayerInfo->m_Score = Score;
361 pPlayerInfo->m_Local = (int)(m_ClientId == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD));
362 pPlayerInfo->m_ClientId = id;
363 pPlayerInfo->m_Team = m_Team;
364 if(SnappingClientVersion < VERSION_DDNET_INDEPENDENT_SPECTATORS_TEAM)
365 {
366 // In older versions the SPECTATORS TEAM was also used if the own player is in PAUSE_PAUSED or if any player is in PAUSE_SPEC.
367 pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientId != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS;
368 }
369 }
370 else
371 {
372 protocol7::CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem<protocol7::CNetObj_PlayerInfo>(Id: id);
373 if(!pPlayerInfo)
374 return;
375
376 pPlayerInfo->m_PlayerFlags = PlayerFlags_SixToSeven(Flags: m_PlayerFlags);
377 if(SnappingClientVersion >= VERSION_DDRACE && (m_PlayerFlags & PLAYERFLAG_AIM))
378 pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_AIM;
379 if(Server()->ClientAuthed(ClientId: m_ClientId))
380 pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_ADMIN;
381
382 // Times are in milliseconds for 0.7
383 pPlayerInfo->m_Score = m_Score.has_value() ? GameServer()->Score()->PlayerData(Id: m_ClientId)->m_BestTime * 1000 : -1;
384 pPlayerInfo->m_Latency = Latency;
385 }
386
387 if(m_ClientId == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused))
388 {
389 if(!Server()->IsSixup(ClientId: SnappingClient))
390 {
391 CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem<CNetObj_SpectatorInfo>(Id: m_ClientId);
392 if(!pSpectatorInfo)
393 return;
394
395 pSpectatorInfo->m_SpectatorId = m_SpectatorId;
396 pSpectatorInfo->m_X = m_ViewPos.x;
397 pSpectatorInfo->m_Y = m_ViewPos.y;
398 }
399 else
400 {
401 protocol7::CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem<protocol7::CNetObj_SpectatorInfo>(Id: m_ClientId);
402 if(!pSpectatorInfo)
403 return;
404
405 pSpectatorInfo->m_SpecMode = m_SpectatorId == SPEC_FREEVIEW ? protocol7::SPEC_FREEVIEW : protocol7::SPEC_PLAYER;
406 pSpectatorInfo->m_SpectatorId = m_SpectatorId;
407 pSpectatorInfo->m_X = m_ViewPos.x;
408 pSpectatorInfo->m_Y = m_ViewPos.y;
409 }
410 }
411
412 CNetObj_DDNetPlayer *pDDNetPlayer = Server()->SnapNewItem<CNetObj_DDNetPlayer>(Id: id);
413 if(!pDDNetPlayer)
414 return;
415
416 pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(ClientId: m_ClientId);
417 pDDNetPlayer->m_Flags = 0;
418 if(m_Afk)
419 pDDNetPlayer->m_Flags |= EXPLAYERFLAG_AFK;
420 if(m_Paused == PAUSE_SPEC)
421 pDDNetPlayer->m_Flags |= EXPLAYERFLAG_SPEC;
422 if(m_Paused == PAUSE_PAUSED)
423 pDDNetPlayer->m_Flags |= EXPLAYERFLAG_PAUSED;
424
425 if(Server()->IsSixup(ClientId: SnappingClient) && m_pCharacter && m_pCharacter->m_DDRaceState == DDRACE_STARTED &&
426 GameServer()->m_apPlayers[SnappingClient]->m_TimerType == TIMERTYPE_SIXUP)
427 {
428 protocol7::CNetObj_PlayerInfoRace *pRaceInfo = Server()->SnapNewItem<protocol7::CNetObj_PlayerInfoRace>(Id: id);
429 if(!pRaceInfo)
430 return;
431 pRaceInfo->m_RaceStartTick = m_pCharacter->m_StartTime;
432 }
433
434 bool ShowSpec = m_pCharacter && m_pCharacter->IsPaused() && m_pCharacter->CanSnapCharacter(SnappingClient);
435
436 if(SnappingClient != SERVER_DEMO_CLIENT)
437 {
438 CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient];
439 ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(ClientId: m_ClientId) == GameServer()->GetDDRaceTeam(ClientId: SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()));
440 }
441
442 if(ShowSpec)
443 {
444 CNetObj_SpecChar *pSpecChar = Server()->SnapNewItem<CNetObj_SpecChar>(Id: id);
445 if(!pSpecChar)
446 return;
447
448 pSpecChar->m_X = m_pCharacter->Core()->m_Pos.x;
449 pSpecChar->m_Y = m_pCharacter->Core()->m_Pos.y;
450 }
451}
452
453void CPlayer::FakeSnap()
454{
455 m_SentSnaps++;
456 if(GetClientVersion() >= VERSION_DDNET_OLD)
457 return;
458
459 if(Server()->IsSixup(ClientId: m_ClientId))
460 return;
461
462 int FakeId = VANILLA_MAX_CLIENTS - 1;
463
464 CNetObj_ClientInfo *pClientInfo = Server()->SnapNewItem<CNetObj_ClientInfo>(Id: FakeId);
465
466 if(!pClientInfo)
467 return;
468
469 StrToInts(pInts: &pClientInfo->m_Name0, NumInts: 4, pStr: " ");
470 StrToInts(pInts: &pClientInfo->m_Clan0, NumInts: 3, pStr: "");
471 StrToInts(pInts: &pClientInfo->m_Skin0, NumInts: 6, pStr: "default");
472
473 if(m_Paused != PAUSE_PAUSED)
474 return;
475
476 CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem<CNetObj_PlayerInfo>(Id: FakeId);
477 if(!pPlayerInfo)
478 return;
479
480 pPlayerInfo->m_Latency = m_Latency.m_Min;
481 pPlayerInfo->m_Local = 1;
482 pPlayerInfo->m_ClientId = FakeId;
483 pPlayerInfo->m_Score = -9999;
484 pPlayerInfo->m_Team = TEAM_SPECTATORS;
485
486 CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem<CNetObj_SpectatorInfo>(Id: FakeId);
487 if(!pSpectatorInfo)
488 return;
489
490 pSpectatorInfo->m_SpectatorId = m_SpectatorId;
491 pSpectatorInfo->m_X = m_ViewPos.x;
492 pSpectatorInfo->m_Y = m_ViewPos.y;
493}
494
495void CPlayer::OnDisconnect()
496{
497 KillCharacter();
498
499 m_Moderating = false;
500}
501
502void CPlayer::OnPredictedInput(CNetObj_PlayerInput *pNewInput)
503{
504 // skip the input if chat is active
505 if((m_PlayerFlags & PLAYERFLAG_CHATTING) && (pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING))
506 return;
507
508 AfkTimer();
509
510 m_NumInputs++;
511
512 if(m_pCharacter && !m_Paused)
513 m_pCharacter->OnPredictedInput(pNewInput);
514
515 // Magic number when we can hope that client has successfully identified itself
516 if(m_NumInputs == 20 && g_Config.m_SvClientSuggestion[0] != '\0' && GetClientVersion() <= VERSION_DDNET_OLD)
517 GameServer()->SendBroadcast(pText: g_Config.m_SvClientSuggestion, ClientId: m_ClientId);
518 else if(m_NumInputs == 200 && Server()->IsSixup(ClientId: m_ClientId))
519 GameServer()->SendBroadcast(pText: "This server uses an experimental translation from Teeworlds 0.7 to 0.6. Please report bugs on ddnet.org/discord", ClientId: m_ClientId);
520}
521
522void CPlayer::OnDirectInput(CNetObj_PlayerInput *pNewInput)
523{
524 Server()->SetClientFlags(ClientId: m_ClientId, Flags: pNewInput->m_PlayerFlags);
525
526 AfkTimer();
527
528 if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW)
529 m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY);
530
531 // check for activity
532 if(mem_comp(a: pNewInput, b: m_pLastTarget, size: sizeof(CNetObj_PlayerInput)))
533 {
534 mem_copy(dest: m_pLastTarget, source: pNewInput, size: sizeof(CNetObj_PlayerInput));
535 // Ignore the first direct input and keep the player afk as it is sent automatically
536 if(m_LastTargetInit)
537 UpdatePlaytime();
538 m_LastActionTick = Server()->Tick();
539 m_LastTargetInit = true;
540 }
541}
542
543void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput)
544{
545 m_PlayerFlags = pNewInput->m_PlayerFlags;
546
547 if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (pNewInput->m_Fire & 1))
548 m_Spawning = true;
549
550 // skip the input if chat is active
551 if(m_PlayerFlags & PLAYERFLAG_CHATTING)
552 return;
553
554 if(m_pCharacter && !m_Paused)
555 m_pCharacter->OnDirectInput(pNewInput);
556}
557
558int CPlayer::GetClientVersion() const
559{
560 return m_pGameServer->GetClientVersion(ClientId: m_ClientId);
561}
562
563CCharacter *CPlayer::GetCharacter()
564{
565 if(m_pCharacter && m_pCharacter->IsAlive())
566 return m_pCharacter;
567 return 0;
568}
569
570const CCharacter *CPlayer::GetCharacter() const
571{
572 if(m_pCharacter && m_pCharacter->IsAlive())
573 return m_pCharacter;
574 return 0;
575}
576
577void CPlayer::KillCharacter(int Weapon, bool SendKillMsg)
578{
579 if(m_pCharacter)
580 {
581 m_pCharacter->Die(Killer: m_ClientId, Weapon, SendKillMsg);
582
583 delete m_pCharacter;
584 m_pCharacter = 0;
585 }
586}
587
588void CPlayer::Respawn(bool WeakHook)
589{
590 if(m_Team != TEAM_SPECTATORS)
591 {
592 m_WeakHookSpawn = WeakHook;
593 m_Spawning = true;
594 }
595}
596
597CCharacter *CPlayer::ForceSpawn(vec2 Pos)
598{
599 m_Spawning = false;
600 m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(ClientId: m_ClientId));
601 m_pCharacter->Spawn(pPlayer: this, Pos);
602 m_Team = 0;
603 return m_pCharacter;
604}
605
606void CPlayer::SetTeam(int Team, bool DoChatMsg)
607{
608 KillCharacter();
609
610 m_Team = Team;
611 m_LastSetTeam = Server()->Tick();
612 m_LastActionTick = Server()->Tick();
613 m_SpectatorId = SPEC_FREEVIEW;
614
615 protocol7::CNetMsg_Sv_Team Msg;
616 Msg.m_ClientId = m_ClientId;
617 Msg.m_Team = m_Team;
618 Msg.m_Silent = !DoChatMsg;
619 Msg.m_CooldownTick = m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay;
620 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1);
621
622 if(Team == TEAM_SPECTATORS)
623 {
624 // update spectator modes
625 for(auto &pPlayer : GameServer()->m_apPlayers)
626 {
627 if(pPlayer && pPlayer->m_SpectatorId == m_ClientId)
628 pPlayer->m_SpectatorId = SPEC_FREEVIEW;
629 }
630 }
631
632 Server()->ExpireServerInfo();
633}
634
635bool CPlayer::SetTimerType(int TimerType)
636{
637 if(TimerType == TIMERTYPE_DEFAULT)
638 {
639 if(Server()->IsSixup(ClientId: m_ClientId))
640 m_TimerType = TIMERTYPE_SIXUP;
641 else
642 SetTimerType(g_Config.m_SvDefaultTimerType);
643
644 return true;
645 }
646
647 if(Server()->IsSixup(ClientId: m_ClientId))
648 {
649 if(TimerType == TIMERTYPE_SIXUP || TimerType == TIMERTYPE_NONE)
650 {
651 m_TimerType = TimerType;
652 return true;
653 }
654 else
655 return false;
656 }
657
658 if(TimerType == TIMERTYPE_GAMETIMER)
659 {
660 if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
661 m_TimerType = TimerType;
662 else
663 return false;
664 }
665 else if(TimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST)
666 {
667 if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
668 m_TimerType = TimerType;
669 else
670 {
671 m_TimerType = TIMERTYPE_BROADCAST;
672 return false;
673 }
674 }
675 else
676 m_TimerType = TimerType;
677
678 return true;
679}
680
681void CPlayer::TryRespawn()
682{
683 vec2 SpawnPos;
684
685 if(!GameServer()->m_pController->CanSpawn(Team: m_Team, pOutPos: &SpawnPos, DDTeam: GameServer()->GetDDRaceTeam(ClientId: m_ClientId)))
686 return;
687
688 m_WeakHookSpawn = false;
689 m_Spawning = false;
690 m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(ClientId: m_ClientId));
691 m_ViewPos = SpawnPos;
692 m_pCharacter->Spawn(pPlayer: this, Pos: SpawnPos);
693 GameServer()->CreatePlayerSpawn(Pos: SpawnPos, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
694
695 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
696 m_pCharacter->SetSolo(true);
697}
698
699void CPlayer::UpdatePlaytime()
700{
701 m_LastPlaytime = time_get();
702}
703
704void CPlayer::AfkTimer()
705{
706 SetAfk(g_Config.m_SvMaxAfkTime != 0 && m_LastPlaytime < time_get() - time_freq() * g_Config.m_SvMaxAfkTime);
707}
708
709void CPlayer::SetAfk(bool Afk)
710{
711 if(m_Afk != Afk)
712 {
713 Server()->ExpireServerInfo();
714 m_Afk = Afk;
715 }
716}
717
718void CPlayer::SetInitialAfk(bool Afk)
719{
720 if(g_Config.m_SvMaxAfkTime == 0)
721 {
722 SetAfk(false);
723 return;
724 }
725
726 SetAfk(Afk);
727
728 // Ensure that the AFK state is not reset again automatically
729 if(Afk)
730 m_LastPlaytime = time_get() - time_freq() * g_Config.m_SvMaxAfkTime - 1;
731 else
732 m_LastPlaytime = time_get();
733}
734
735int CPlayer::GetDefaultEmote() const
736{
737 if(m_OverrideEmoteReset >= 0)
738 return m_OverrideEmote;
739
740 return m_DefEmote;
741}
742
743void CPlayer::OverrideDefaultEmote(int Emote, int Tick)
744{
745 m_OverrideEmote = Emote;
746 m_OverrideEmoteReset = Tick;
747 m_LastEyeEmote = Server()->Tick();
748}
749
750bool CPlayer::CanOverrideDefaultEmote() const
751{
752 return m_LastEyeEmote == 0 || m_LastEyeEmote + (int64_t)g_Config.m_SvEyeEmoteChangeDelay * Server()->TickSpeed() < Server()->Tick();
753}
754
755void CPlayer::ProcessPause()
756{
757 if(m_ForcePauseTime && m_ForcePauseTime < Server()->Tick())
758 {
759 m_ForcePauseTime = 0;
760 Pause(State: PAUSE_NONE, Force: true);
761 }
762
763 if(m_Paused == PAUSE_SPEC && !m_pCharacter->IsPaused() && m_pCharacter->IsGrounded() && m_pCharacter->m_Pos == m_pCharacter->m_PrevPos)
764 {
765 m_pCharacter->Pause(Pause: true);
766 GameServer()->CreateDeath(Pos: m_pCharacter->m_Pos, ClientId: m_ClientId, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
767 GameServer()->CreateSound(Pos: m_pCharacter->m_Pos, Sound: SOUND_PLAYER_DIE, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
768 }
769}
770
771int CPlayer::Pause(int State, bool Force)
772{
773 if(State < PAUSE_NONE || State > PAUSE_SPEC) // Invalid pause state passed
774 return 0;
775
776 if(!m_pCharacter)
777 return 0;
778
779 char aBuf[128];
780 if(State != m_Paused)
781 {
782 // Get to wanted state
783 switch(State)
784 {
785 case PAUSE_PAUSED:
786 case PAUSE_NONE:
787 if(m_pCharacter->IsPaused()) // First condition might be unnecessary
788 {
789 if(!Force && m_LastPause && m_LastPause + (int64_t)g_Config.m_SvSpecFrequency * Server()->TickSpeed() > Server()->Tick())
790 {
791 GameServer()->SendChatTarget(To: m_ClientId, pText: "Can't /spec that quickly.");
792 return m_Paused; // Do not update state. Do not collect $200
793 }
794 m_pCharacter->Pause(Pause: false);
795 m_ViewPos = m_pCharacter->m_Pos;
796 GameServer()->CreatePlayerSpawn(Pos: m_pCharacter->m_Pos, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
797 }
798 [[fallthrough]];
799 case PAUSE_SPEC:
800 if(g_Config.m_SvPauseMessages)
801 {
802 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: (State > PAUSE_NONE) ? "'%s' speced" : "'%s' resumed", Server()->ClientName(ClientId: m_ClientId));
803 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
804 }
805 break;
806 }
807
808 // Update state
809 m_Paused = State;
810 m_LastPause = Server()->Tick();
811
812 // Sixup needs a teamchange
813 protocol7::CNetMsg_Sv_Team Msg;
814 Msg.m_ClientId = m_ClientId;
815 Msg.m_CooldownTick = Server()->Tick();
816 Msg.m_Silent = true;
817 Msg.m_Team = m_Paused ? protocol7::TEAM_SPECTATORS : m_Team;
818
819 GameServer()->Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: m_ClientId);
820 }
821
822 return m_Paused;
823}
824
825int CPlayer::ForcePause(int Time)
826{
827 m_ForcePauseTime = Server()->Tick() + Server()->TickSpeed() * Time;
828
829 if(g_Config.m_SvPauseMessages)
830 {
831 char aBuf[128];
832 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' was force-paused for %ds", Server()->ClientName(ClientId: m_ClientId), Time);
833 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
834 }
835
836 return Pause(State: PAUSE_SPEC, Force: true);
837}
838
839int CPlayer::IsPaused() const
840{
841 return m_ForcePauseTime ? m_ForcePauseTime : -1 * m_Paused;
842}
843
844bool CPlayer::IsPlaying() const
845{
846 return m_pCharacter && m_pCharacter->IsAlive();
847}
848
849void CPlayer::SpectatePlayerName(const char *pName)
850{
851 if(!pName)
852 return;
853
854 for(int i = 0; i < MAX_CLIENTS; ++i)
855 {
856 if(i != m_ClientId && Server()->ClientIngame(ClientId: i) && !str_comp(a: pName, b: Server()->ClientName(ClientId: i)))
857 {
858 m_SpectatorId = i;
859 return;
860 }
861 }
862}
863
864void CPlayer::ProcessScoreResult(CScorePlayerResult &Result)
865{
866 if(Result.m_Success) // SQL request was successful
867 {
868 switch(Result.m_MessageKind)
869 {
870 case CScorePlayerResult::DIRECT:
871 for(auto &aMessage : Result.m_Data.m_aaMessages)
872 {
873 if(aMessage[0] == 0)
874 break;
875 GameServer()->SendChatTarget(To: m_ClientId, pText: aMessage);
876 }
877 break;
878 case CScorePlayerResult::ALL:
879 {
880 bool PrimaryMessage = true;
881 for(auto &aMessage : Result.m_Data.m_aaMessages)
882 {
883 if(aMessage[0] == 0)
884 break;
885
886 if(GameServer()->ProcessSpamProtection(ClientId: m_ClientId) && PrimaryMessage)
887 break;
888
889 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aMessage, SpamProtectionClientId: -1);
890 PrimaryMessage = false;
891 }
892 break;
893 }
894 case CScorePlayerResult::BROADCAST:
895 if(Result.m_Data.m_aBroadcast[0] != 0)
896 GameServer()->SendBroadcast(pText: Result.m_Data.m_aBroadcast, ClientId: -1);
897 break;
898 case CScorePlayerResult::MAP_VOTE:
899 GameServer()->m_VoteType = CGameContext::VOTE_TYPE_OPTION;
900 GameServer()->m_LastMapVote = time_get();
901
902 char aCmd[256];
903 str_format(buffer: aCmd, buffer_size: sizeof(aCmd),
904 format: "sv_reset_file types/%s/flexreset.cfg; change_map \"%s\"",
905 Result.m_Data.m_MapVote.m_aServer, Result.m_Data.m_MapVote.m_aMap);
906
907 char aChatmsg[512];
908 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s' (%s)",
909 Server()->ClientName(ClientId: m_ClientId), Result.m_Data.m_MapVote.m_aMap, "/map");
910
911 GameServer()->CallVote(ClientId: m_ClientId, pDesc: Result.m_Data.m_MapVote.m_aMap, pCmd: aCmd, pReason: "/map", pChatmsg: aChatmsg);
912 break;
913 case CScorePlayerResult::PLAYER_INFO:
914 {
915 if(Result.m_Data.m_Info.m_Time.has_value())
916 {
917 GameServer()->Score()->PlayerData(Id: m_ClientId)->Set(Time: Result.m_Data.m_Info.m_Time.value(), aTimeCp: Result.m_Data.m_Info.m_aTimeCp);
918 m_Score = Result.m_Data.m_Info.m_Time;
919 }
920 Server()->ExpireServerInfo();
921 int Birthday = Result.m_Data.m_Info.m_Birthday;
922 if(Birthday != 0 && !m_BirthdayAnnounced)
923 {
924 char aBuf[512];
925 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
926 format: "Happy DDNet birthday to %s for finishing their first map %d year%s ago!",
927 Server()->ClientName(ClientId: m_ClientId), Birthday, Birthday > 1 ? "s" : "");
928 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: m_ClientId);
929 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
930 format: "Happy DDNet birthday, %s!\nYou have finished your first map exactly %d year%s ago!",
931 Server()->ClientName(ClientId: m_ClientId), Birthday, Birthday > 1 ? "s" : "");
932 GameServer()->SendBroadcast(pText: aBuf, ClientId: m_ClientId);
933 m_BirthdayAnnounced = true;
934 }
935 GameServer()->SendRecord(ClientId: m_ClientId);
936 break;
937 }
938 case CScorePlayerResult::PLAYER_TIMECP:
939 GameServer()->Score()->PlayerData(Id: m_ClientId)->SetBestTimeCp(Result.m_Data.m_Info.m_aTimeCp);
940 char aBuf[128], aTime[32];
941 str_time_float(secs: Result.m_Data.m_Info.m_Time.value(), format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
942 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Showing the checkpoint times for '%s' with a race time of %s", Result.m_Data.m_Info.m_aRequestedPlayer, aTime);
943 GameServer()->SendChatTarget(To: m_ClientId, pText: aBuf);
944 break;
945 }
946 }
947}
948