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