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