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