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#include <engine/shared/protocol.h>
19
20#include <game/gamecore.h>
21#include <game/teamscore.h>
22
23#include <algorithm>
24
25MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS)
26
27IServer *CPlayer::Server() const { return m_pGameServer->Server(); }
28
29CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientId, int ClientId, int Team) :
30 m_UniqueClientId(UniqueClientId)
31{
32 m_pGameServer = pGameServer;
33 m_ClientId = ClientId;
34 dbg_assert(GameServer()->m_pController->IsValidTeam(Team), "Invalid Team: %d", Team);
35 m_Team = Team;
36 m_NumInputs = 0;
37 Reset();
38 GameServer()->Antibot()->OnPlayerInit(ClientId: m_ClientId);
39}
40
41CPlayer::~CPlayer()
42{
43 GameServer()->Antibot()->OnPlayerDestroy(ClientId: m_ClientId);
44 delete m_pLastTarget;
45 delete m_pCharacter;
46 m_pCharacter = nullptr;
47}
48
49void CPlayer::Reset()
50{
51 m_DieTick = Server()->Tick();
52 m_PreviousDieTick = m_DieTick;
53 m_JoinTick = Server()->Tick();
54 m_DDNetVersionKickTick = Server()->Tick() + 3 * Server()->TickSpeed();
55 delete m_pCharacter;
56 m_pCharacter = nullptr;
57 SetSpectatorId(SPEC_FREEVIEW);
58 m_LastActionTick = Server()->Tick();
59 m_TeamChangeTick = Server()->Tick();
60 m_LastSetTeam = 0;
61 m_LastInvited = 0;
62 m_WeakHookSpawn = false;
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 == ETimeSeason::NEWYEAR)
95 {
96 m_DefEmote = EMOTE_HAPPY;
97 }
98 else if(Season == ETimeSeason::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.reset();
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 UpdateNetworkClipRadius();
153 std::fill(first: std::begin(arr&: m_aStrongWeakId), last: std::end(arr&: m_aStrongWeakId), value: 0);
154}
155
156static int PlayerFlags_SixToSeven(int Flags)
157{
158 int Seven = 0;
159 if(Flags & PLAYERFLAG_CHATTING)
160 Seven |= protocol7::PLAYERFLAG_CHATTING;
161 if(Flags & PLAYERFLAG_SCOREBOARD)
162 Seven |= protocol7::PLAYERFLAG_SCOREBOARD;
163
164 return Seven;
165}
166
167void CPlayer::Tick()
168{
169 if(m_ScoreQueryResult != nullptr && m_ScoreQueryResult->m_Completed && m_SentSnaps >= 3)
170 {
171 ProcessScoreResult(Result&: *m_ScoreQueryResult);
172 m_ScoreQueryResult = nullptr;
173 }
174 if(m_ScoreFinishResult != nullptr && m_ScoreFinishResult->m_Completed)
175 {
176 ProcessScoreResult(Result&: *m_ScoreFinishResult);
177 m_ScoreFinishResult = nullptr;
178 }
179
180 if(!Server()->ClientIngame(ClientId: m_ClientId))
181 return;
182
183 if(m_ChatScore > 0)
184 m_ChatScore--;
185
186 if(m_Moderating && m_Afk)
187 {
188 m_Moderating = false;
189 GameServer()->SendChatTarget(To: m_ClientId, pText: "Active moderator mode disabled because you are afk.");
190
191 if(!GameServer()->PlayerModerating())
192 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: "Server kick/spec votes are no longer actively moderated.");
193 }
194
195 // do latency stuff
196 {
197 IServer::CClientInfo Info;
198 if(Server()->GetClientInfo(ClientId: m_ClientId, pInfo: &Info))
199 {
200 m_Latency.m_Accum += Info.m_Latency;
201 m_Latency.m_AccumMax = std::max(a: m_Latency.m_AccumMax, b: Info.m_Latency);
202 m_Latency.m_AccumMin = std::min(a: m_Latency.m_AccumMin, b: Info.m_Latency);
203 }
204 // each second
205 if(Server()->Tick() % Server()->TickSpeed() == 0)
206 {
207 m_Latency.m_Avg = m_Latency.m_Accum / Server()->TickSpeed();
208 m_Latency.m_Max = m_Latency.m_AccumMax;
209 m_Latency.m_Min = m_Latency.m_AccumMin;
210 m_Latency.m_Accum = 0;
211 m_Latency.m_AccumMin = 1000;
212 m_Latency.m_AccumMax = 0;
213 }
214 }
215
216 if(Server()->GetNetErrorString(ClientId: m_ClientId)[0])
217 {
218 SetInitialAfk(true);
219
220 char aBuf[512];
221 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));
222 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
223 Server()->ResetNetErrorString(ClientId: m_ClientId);
224 }
225
226 if(!GameServer()->m_pController->IsGamePaused())
227 {
228 int EarliestRespawnTick = m_PreviousDieTick + Server()->TickSpeed() * 3;
229 int RespawnTick = std::max(a: m_DieTick, b: EarliestRespawnTick) + 2;
230 if(!m_pCharacter && RespawnTick <= Server()->Tick())
231 m_Spawning = true;
232
233 if(m_pCharacter)
234 {
235 if(m_pCharacter->IsAlive())
236 {
237 ProcessPause();
238 if(!m_Paused)
239 m_ViewPos = m_pCharacter->m_Pos;
240 }
241 else if(!m_pCharacter->IsPaused())
242 {
243 delete m_pCharacter;
244 m_pCharacter = nullptr;
245 }
246 }
247 else if(m_Spawning && !m_WeakHookSpawn)
248 TryRespawn();
249 }
250 else
251 {
252 ++m_DieTick;
253 ++m_PreviousDieTick;
254 ++m_JoinTick;
255 ++m_LastActionTick;
256 ++m_TeamChangeTick;
257 }
258
259 m_TuneZoneOld = m_TuneZone; // determine needed tunings with viewpos
260 int CurrentIndex = GameServer()->Collision()->GetMapIndex(Pos: m_ViewPos);
261 m_TuneZone = GameServer()->Collision()->IsTune(Index: CurrentIndex);
262
263 if(m_TuneZone != m_TuneZoneOld) // don't send tunings all the time
264 {
265 GameServer()->SendTuningParams(ClientId: m_ClientId, Zone: m_TuneZone);
266 }
267
268 if(m_OverrideEmoteReset >= 0 && m_OverrideEmoteReset <= Server()->Tick())
269 {
270 m_OverrideEmoteReset = -1;
271 }
272
273 if(m_Halloween && m_pCharacter && !m_pCharacter->IsPaused())
274 {
275 if(1200 - ((Server()->Tick() - m_pCharacter->GetLastAction()) % (1200)) < 5)
276 {
277 GameServer()->SendEmoticon(ClientId: GetCid(), Emoticon: EMOTICON_GHOST, TargetClientId: -1);
278 }
279 }
280}
281
282void CPlayer::PostTick()
283{
284 // update latency value
285 if(m_PlayerFlags & PLAYERFLAG_IN_MENU)
286 m_aCurLatency[m_ClientId] = GameServer()->m_apPlayers[m_ClientId]->m_Latency.m_Min;
287
288 if(m_PlayerFlags & PLAYERFLAG_SCOREBOARD)
289 {
290 for(int i = 0; i < MAX_CLIENTS; ++i)
291 {
292 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS)
293 m_aCurLatency[i] = GameServer()->m_apPlayers[i]->m_Latency.m_Min;
294 }
295 }
296
297 // update view pos for spectators
298 if((m_Team == TEAM_SPECTATORS || m_Paused) && m_SpectatorId != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorId] && GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter())
299 m_ViewPos = GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter()->m_Pos;
300
301 UpdateNetworkClipRadius();
302}
303
304void CPlayer::PostPostTick()
305{
306 if(!Server()->ClientIngame(ClientId: m_ClientId))
307 return;
308
309 if(!GameServer()->m_pController->IsGamePaused() && !m_pCharacter && m_Spawning && m_WeakHookSpawn)
310 TryRespawn();
311}
312
313void CPlayer::Snap(int SnappingClient)
314{
315 if(!Server()->ClientIngame(ClientId: m_ClientId))
316 return;
317
318 int TranslatedId = m_ClientId;
319 if(!Server()->Translate(Target&: TranslatedId, ClientId: SnappingClient))
320 return;
321
322 CNetObj_ClientInfo ClientInfo = {};
323 StrToInts(pInts: ClientInfo.m_aName, NumInts: std::size(ClientInfo.m_aName), pStr: Server()->ClientName(ClientId: m_ClientId));
324 StrToInts(pInts: ClientInfo.m_aClan, NumInts: std::size(ClientInfo.m_aClan), pStr: Server()->ClientClan(ClientId: m_ClientId));
325 ClientInfo.m_Country = Server()->ClientCountry(ClientId: m_ClientId);
326 StrToInts(pInts: ClientInfo.m_aSkin, NumInts: std::size(ClientInfo.m_aSkin), pStr: m_TeeInfos.m_aSkinName);
327 ClientInfo.m_UseCustomColor = m_TeeInfos.m_UseCustomColor;
328 ClientInfo.m_ColorBody = m_TeeInfos.m_ColorBody;
329 ClientInfo.m_ColorFeet = m_TeeInfos.m_ColorFeet;
330 Server()->SnapNewItem(Id: TranslatedId, Data: ClientInfo);
331
332 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
333 int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientId];
334 int Score = GameServer()->m_pController->SnapPlayerScore(SnappingClient, pPlayer: this);
335
336 if(!Server()->IsSixup(ClientId: SnappingClient))
337 {
338 CNetObj_PlayerInfo PlayerInfo = {};
339 PlayerInfo.m_Latency = Latency;
340 PlayerInfo.m_Score = Score;
341 PlayerInfo.m_Local = (int)(m_ClientId == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD));
342 PlayerInfo.m_ClientId = TranslatedId;
343 PlayerInfo.m_Team = m_Team;
344 if(SnappingClientVersion < VERSION_DDNET_INDEPENDENT_SPECTATORS_TEAM)
345 {
346 // 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.
347 PlayerInfo.m_Team = (m_Paused != PAUSE_PAUSED || m_ClientId != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS;
348 }
349 Server()->SnapNewItem(Id: TranslatedId, Data: PlayerInfo);
350 }
351 else
352 {
353 protocol7::CNetObj_PlayerInfo PlayerInfo = {};
354 PlayerInfo.m_PlayerFlags = PlayerFlags_SixToSeven(Flags: m_PlayerFlags);
355 if(SnappingClientVersion >= VERSION_DDRACE && (m_PlayerFlags & PLAYERFLAG_AIM))
356 PlayerInfo.m_PlayerFlags |= protocol7::PLAYERFLAG_AIM;
357 if(Server()->IsRconAuthed(ClientId: m_ClientId) && ((SnappingClient >= 0 && Server()->IsRconAuthed(ClientId: SnappingClient)) || !Server()->HasAuthHidden(ClientId: m_ClientId)))
358 PlayerInfo.m_PlayerFlags |= protocol7::PLAYERFLAG_ADMIN;
359 PlayerInfo.m_Score = Score;
360 PlayerInfo.m_Latency = Latency;
361 Server()->SnapNewItem(Id: TranslatedId, Data: PlayerInfo);
362 }
363
364 if(m_ClientId == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused))
365 {
366 int SpectatorId = m_SpectatorId;
367 if(SpectatorId >= 0 && !Server()->Translate(Target&: SpectatorId, ClientId: m_ClientId))
368 {
369 SpectatorId = TranslatedId;
370 }
371
372 if(!Server()->IsSixup(ClientId: SnappingClient))
373 {
374 CNetObj_SpectatorInfo SpectatorInfo = {};
375 SpectatorInfo.m_SpectatorId = SpectatorId;
376 SpectatorInfo.m_X = m_ViewPos.x;
377 SpectatorInfo.m_Y = m_ViewPos.y;
378 Server()->SnapNewItem(Id: m_ClientId, Data: SpectatorInfo);
379 }
380 else
381 {
382 protocol7::CNetObj_SpectatorInfo SpectatorInfo = {};
383 SpectatorInfo.m_SpecMode = m_SpectatorId == SPEC_FREEVIEW ? protocol7::SPEC_FREEVIEW : protocol7::SPEC_PLAYER;
384 SpectatorInfo.m_SpectatorId = SpectatorId;
385 SpectatorInfo.m_X = m_ViewPos.x;
386 SpectatorInfo.m_Y = m_ViewPos.y;
387 Server()->SnapNewItem(Id: m_ClientId, Data: SpectatorInfo);
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) ? m_ClientId : m_SpectatorId;
395 const CPlayer *pSpecPlayer = GameServer()->m_apPlayers[SpectatingClient];
396
397 if(pSpecPlayer)
398 {
399 CNetObj_DDNetSpectatorInfo DDNetSpectatorInfo = {};
400 DDNetSpectatorInfo.m_HasCameraInfo = pSpecPlayer->m_CameraInfo.m_HasCameraInfo;
401 DDNetSpectatorInfo.m_Zoom = pSpecPlayer->m_CameraInfo.m_Zoom * 1000.0f;
402 DDNetSpectatorInfo.m_Deadzone = pSpecPlayer->m_CameraInfo.m_Deadzone;
403 DDNetSpectatorInfo.m_FollowFactor = pSpecPlayer->m_CameraInfo.m_FollowFactor;
404
405 if(pSpecPlayer->m_EnableSpectatorCount && SpectatingClient == m_ClientId && SnappingClient != SERVER_DEMO_CLIENT && m_Team != TEAM_SPECTATORS && !m_Paused)
406 {
407 CNetObj_SpectatorCount SpectatorCountObj = {};
408 int SpectatorCount = 0;
409 for(auto &pPlayer : GameServer()->m_apPlayers)
410 {
411 if(!pPlayer || !pPlayer->m_EnableSpectatorCount || pPlayer->m_ClientId == m_ClientId || pPlayer->m_Afk ||
412 (Server()->IsRconAuthed(ClientId: pPlayer->m_ClientId) && Server()->HasAuthHidden(ClientId: pPlayer->m_ClientId)) ||
413 !(pPlayer->m_Paused || pPlayer->m_Team == TEAM_SPECTATORS))
414 {
415 continue;
416 }
417
418 if(pPlayer->m_SpectatorId == m_ClientId)
419 {
420 SpectatorCount++;
421 }
422 else if(GameServer()->m_apPlayers[m_ClientId]->GetCharacter())
423 {
424 vec2 CheckPos = GameServer()->m_apPlayers[m_ClientId]->GetCharacter()->GetPos();
425 float dx = pPlayer->m_ViewPos.x - CheckPos.x;
426 float dy = pPlayer->m_ViewPos.y - CheckPos.y;
427 if(absolute(a: dx) < (pPlayer->m_ShowDistance.x / 2.5f) && absolute(a: dy) < (pPlayer->m_ShowDistance.y / 2.3f))
428 SpectatorCount++;
429 }
430 }
431 DDNetSpectatorInfo.m_SpectatorCount = SpectatorCount;
432 SpectatorCountObj.m_NumSpectators = SpectatorCount;
433 Server()->SnapNewItem(Id: 0, Data: SpectatorCountObj);
434 }
435 Server()->SnapNewItem(Id: m_ClientId, Data: DDNetSpectatorInfo);
436 }
437 }
438
439 CNetObj_DDNetPlayer DDNetPlayer = {};
440 if((SnappingClient >= 0 && Server()->IsRconAuthed(ClientId: SnappingClient)) || !Server()->HasAuthHidden(ClientId: m_ClientId))
441 DDNetPlayer.m_AuthLevel = Server()->GetAuthedState(ClientId: m_ClientId);
442 else
443 DDNetPlayer.m_AuthLevel = AUTHED_NO;
444
445 DDNetPlayer.m_Flags = 0;
446 if(m_Afk)
447 DDNetPlayer.m_Flags |= EXPLAYERFLAG_AFK;
448 if(m_Paused == PAUSE_SPEC)
449 DDNetPlayer.m_Flags |= EXPLAYERFLAG_SPEC;
450 if(m_Paused == PAUSE_PAUSED)
451 DDNetPlayer.m_Flags |= EXPLAYERFLAG_PAUSED;
452
453 IGameController::CFinishTime PlayerTime = GameServer()->m_pController->SnapPlayerTime(SnappingClient, pPlayer: this);
454 DDNetPlayer.m_FinishTimeSeconds = PlayerTime.m_Seconds;
455 DDNetPlayer.m_FinishTimeMillis = PlayerTime.m_Milliseconds;
456 Server()->SnapNewItem(Id: TranslatedId, Data: DDNetPlayer);
457
458 if(Server()->IsSixup(ClientId: SnappingClient) && m_pCharacter && m_pCharacter->m_DDRaceState == ERaceState::STARTED &&
459 GameServer()->m_apPlayers[SnappingClient]->m_TimerType == TIMERTYPE_SIXUP)
460 {
461 protocol7::CNetObj_PlayerInfoRace RaceInfo = {};
462 RaceInfo.m_RaceStartTick = m_pCharacter->m_StartTime;
463 Server()->SnapNewItem(Id: TranslatedId, Data: RaceInfo);
464 }
465
466 bool ShowSpec = m_pCharacter && m_pCharacter->IsPaused() && m_pCharacter->CanSnapCharacter(SnappingClient);
467
468 if(SnappingClient != SERVER_DEMO_CLIENT)
469 {
470 CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient];
471 ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(ClientId: m_ClientId) == GameServer()->GetDDRaceTeam(ClientId: SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()));
472 }
473
474 if(ShowSpec)
475 {
476 CNetObj_SpecChar SpecChar = {};
477 SpecChar.m_X = m_pCharacter->Core()->m_Pos.x;
478 SpecChar.m_Y = m_pCharacter->Core()->m_Pos.y;
479 Server()->SnapNewItem(Id: TranslatedId, Data: SpecChar);
480 }
481}
482
483void CPlayer::FakeSnap()
484{
485 m_SentSnaps++;
486 if(GetClientVersion() >= VERSION_DDNET_128_PLAYERS)
487 return;
488
489 // see others in spec
490 int SeeOthersId = GameServer()->m_PlayerMapping.SeeOthersId();
491
492 if(Server()->IsSixup(ClientId: m_ClientId))
493 {
494 if(GameServer()->m_PlayerMapping.TotalOverhang(ClientId: m_ClientId))
495 {
496 protocol7::CNetObj_PlayerInfo PlayerInfo = {};
497 PlayerInfo.m_PlayerFlags = 0;
498 PlayerInfo.m_Score = FinishTime::NOT_FINISHED_TIMESCORE;
499 PlayerInfo.m_Latency = 0;
500 Server()->SnapNewItem(Id: SeeOthersId, Data: PlayerInfo);
501 }
502
503 // nothing more to process in fake snap for 0.7
504 return;
505 }
506
507 // see others
508 if(GameServer()->m_PlayerMapping.TotalOverhang(ClientId: m_ClientId))
509 {
510 CNetObj_ClientInfo ClientInfo = {};
511 StrToInts(pInts: ClientInfo.m_aName, NumInts: std::size(ClientInfo.m_aName), pStr: GameServer()->m_PlayerMapping.SeeOthersName(ClientId: m_ClientId));
512 StrToInts(pInts: ClientInfo.m_aClan, NumInts: std::size(ClientInfo.m_aClan), pStr: "");
513 StrToInts(pInts: ClientInfo.m_aSkin, NumInts: std::size(ClientInfo.m_aSkin), pStr: "default");
514 ClientInfo.m_UseCustomColor = 1;
515 ClientInfo.m_ColorBody = ClientInfo.m_ColorFeet = 6618880;
516 Server()->SnapNewItem(Id: SeeOthersId, Data: ClientInfo);
517
518 CNetObj_PlayerInfo PlayerInfo = {};
519 PlayerInfo.m_Latency = 0;
520 PlayerInfo.m_Local = 0;
521 PlayerInfo.m_ClientId = SeeOthersId;
522 PlayerInfo.m_Score = FinishTime::NOT_FINISHED_TIMESCORE;
523 PlayerInfo.m_Team = TEAM_BLUE;
524 Server()->SnapNewItem(Id: SeeOthersId, Data: PlayerInfo);
525 }
526
527 int FakeId = LEGACY_MAX_CLIENTS - 1;
528 CNetObj_ClientInfo ClientInfo = {};
529 StrToInts(pInts: ClientInfo.m_aName, NumInts: std::size(ClientInfo.m_aName), pStr: " ");
530 StrToInts(pInts: ClientInfo.m_aClan, NumInts: std::size(ClientInfo.m_aClan), pStr: "");
531 StrToInts(pInts: ClientInfo.m_aSkin, NumInts: std::size(ClientInfo.m_aSkin), pStr: "default");
532 Server()->SnapNewItem(Id: FakeId, Data: ClientInfo);
533}
534
535void CPlayer::SendConnect(int FakeId, int ClientId)
536{
537 if(!Server()->IsSixup(ClientId: m_ClientId))
538 return;
539
540 CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId];
541 if(!pPlayer)
542 return;
543
544 protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg;
545 NewClientInfoMsg.m_ClientId = FakeId;
546 NewClientInfoMsg.m_Local = ClientId == m_ClientId;
547 NewClientInfoMsg.m_Team = pPlayer->GetTeam();
548 NewClientInfoMsg.m_pName = Server()->ClientName(ClientId);
549 NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientId);
550 NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientId);
551 NewClientInfoMsg.m_Silent = 1;
552
553 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
554 {
555 NewClientInfoMsg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p];
556 NewClientInfoMsg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
557 NewClientInfoMsg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
558 }
559
560 Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD | MSGFLAG_NOTRANSLATE, ClientId: m_ClientId);
561}
562
563void CPlayer::SendDisconnect(int FakeId)
564{
565 if(!Server()->IsSixup(ClientId: m_ClientId))
566 return;
567
568 protocol7::CNetMsg_Sv_ClientDrop ClientDropMsg;
569 ClientDropMsg.m_ClientId = FakeId;
570 ClientDropMsg.m_pReason = "";
571 ClientDropMsg.m_Silent = 1;
572
573 Server()->SendPackMsg(pMsg: &ClientDropMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD | MSGFLAG_NOTRANSLATE, ClientId: m_ClientId);
574}
575
576void CPlayer::OnDisconnect()
577{
578 KillCharacter();
579
580 m_Moderating = false;
581}
582
583void CPlayer::OnPredictedInput(const CNetObj_PlayerInput *pNewInput)
584{
585 // skip the input if chat is active
586 if((m_PlayerFlags & PLAYERFLAG_CHATTING) && (pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING))
587 return;
588
589 AfkTimer();
590
591 m_NumInputs++;
592
593 if(m_pCharacter && !m_Paused && !(pNewInput->m_PlayerFlags & PLAYERFLAG_SPEC_CAM))
594 m_pCharacter->OnPredictedInput(pNewInput);
595
596 // Magic number when we can hope that client has successfully identified itself
597 if(m_NumInputs == 20 && g_Config.m_SvClientSuggestion[0] != '\0' && GetClientVersion() <= VERSION_DDNET_OLD)
598 GameServer()->SendBroadcast(pText: g_Config.m_SvClientSuggestion, ClientId: m_ClientId);
599}
600
601void CPlayer::OnDirectInput(const CNetObj_PlayerInput *pNewInput)
602{
603 Server()->SetClientFlags(ClientId: m_ClientId, Flags: pNewInput->m_PlayerFlags);
604
605 AfkTimer();
606
607 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)
608 m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY);
609
610 // check for activity
611 // if a player is killed, their scoreboard opens automatically, so ignore that flag
612 CNetObj_PlayerInput NewWithoutScoreboard = *pNewInput;
613 CNetObj_PlayerInput LastWithoutScoreboard = *m_pLastTarget;
614 NewWithoutScoreboard.m_PlayerFlags &= ~PLAYERFLAG_SCOREBOARD;
615 LastWithoutScoreboard.m_PlayerFlags &= ~PLAYERFLAG_SCOREBOARD;
616 if(mem_comp(a: &NewWithoutScoreboard, b: &LastWithoutScoreboard, size: sizeof(CNetObj_PlayerInput)))
617 {
618 mem_copy(dest: m_pLastTarget, source: pNewInput, size: sizeof(CNetObj_PlayerInput));
619 // Ignore the first direct input and keep the player afk as it is sent automatically
620 if(m_LastTargetInit)
621 UpdatePlaytime();
622 m_LastActionTick = Server()->Tick();
623 m_LastTargetInit = true;
624 }
625}
626
627void CPlayer::OnPredictedEarlyInput(const CNetObj_PlayerInput *pNewInput)
628{
629 m_PlayerFlags = pNewInput->m_PlayerFlags;
630
631 if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (pNewInput->m_Fire & 1))
632 m_Spawning = true;
633
634 // skip the input if chat is active
635 if(m_PlayerFlags & PLAYERFLAG_CHATTING)
636 return;
637
638 if(m_pCharacter && !m_Paused && !(m_PlayerFlags & PLAYERFLAG_SPEC_CAM))
639 m_pCharacter->OnDirectInput(pNewInput);
640}
641
642int CPlayer::GetClientVersion() const
643{
644 return m_pGameServer->GetClientVersion(ClientId: m_ClientId);
645}
646
647CCharacter *CPlayer::GetCharacter()
648{
649 if(m_pCharacter && m_pCharacter->IsAlive())
650 return m_pCharacter;
651 return nullptr;
652}
653
654const CCharacter *CPlayer::GetCharacter() const
655{
656 if(m_pCharacter && m_pCharacter->IsAlive())
657 return m_pCharacter;
658 return nullptr;
659}
660
661void CPlayer::KillCharacter(int Weapon, bool SendKillMsg)
662{
663 if(m_pCharacter)
664 {
665 m_pCharacter->Die(Killer: m_ClientId, Weapon, SendKillMsg);
666
667 delete m_pCharacter;
668 m_pCharacter = nullptr;
669 }
670}
671
672void CPlayer::Respawn(bool WeakHook)
673{
674 if(m_Team != TEAM_SPECTATORS)
675 {
676 m_WeakHookSpawn = WeakHook;
677 m_Spawning = true;
678 }
679}
680
681CCharacter *CPlayer::ForceSpawn(vec2 Pos)
682{
683 m_Spawning = false;
684 m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(ClientId: m_ClientId));
685 m_pCharacter->Spawn(pPlayer: this, Pos);
686 m_Team = TEAM_GAME;
687 return m_pCharacter;
688}
689
690void CPlayer::SetTeam(int Team, bool DoChatMsg)
691{
692 KillCharacter();
693
694 m_Team = Team;
695 m_LastSetTeam = Server()->Tick();
696 m_LastActionTick = Server()->Tick();
697 SetSpectatorId(SPEC_FREEVIEW);
698
699 protocol7::CNetMsg_Sv_Team Msg;
700 Msg.m_ClientId = m_ClientId;
701 Msg.m_Team = m_Team;
702 Msg.m_Silent = !DoChatMsg;
703 Msg.m_CooldownTick = m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay;
704 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1);
705
706 if(Team == TEAM_SPECTATORS)
707 {
708 // update spectator modes
709 for(auto &pPlayer : GameServer()->m_apPlayers)
710 {
711 if(pPlayer && pPlayer->m_SpectatorId == m_ClientId)
712 pPlayer->SetSpectatorId(SPEC_FREEVIEW);
713 }
714 }
715
716 Server()->ExpireServerInfo();
717}
718
719bool CPlayer::SetTimerType(int TimerType)
720{
721 if(TimerType == TIMERTYPE_DEFAULT)
722 {
723 if(Server()->IsSixup(ClientId: m_ClientId))
724 m_TimerType = TIMERTYPE_SIXUP;
725 else
726 SetTimerType(g_Config.m_SvDefaultTimerType);
727
728 return true;
729 }
730
731 if(Server()->IsSixup(ClientId: m_ClientId))
732 {
733 if(TimerType == TIMERTYPE_SIXUP || TimerType == TIMERTYPE_NONE)
734 {
735 m_TimerType = TimerType;
736 return true;
737 }
738 else
739 return false;
740 }
741
742 if(TimerType == TIMERTYPE_GAMETIMER)
743 {
744 if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
745 m_TimerType = TimerType;
746 else
747 return false;
748 }
749 else if(TimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST)
750 {
751 if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
752 m_TimerType = TimerType;
753 else
754 {
755 m_TimerType = TIMERTYPE_BROADCAST;
756 return false;
757 }
758 }
759 else
760 m_TimerType = TimerType;
761
762 return true;
763}
764
765void CPlayer::TryRespawn()
766{
767 vec2 SpawnPos;
768
769 if(!GameServer()->m_pController->CanSpawn(Team: m_Team, pOutPos: &SpawnPos, ClientId: m_ClientId))
770 return;
771
772 m_WeakHookSpawn = false;
773 m_Spawning = false;
774 m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(ClientId: m_ClientId));
775 m_ViewPos = SpawnPos;
776 m_pCharacter->Spawn(pPlayer: this, Pos: SpawnPos);
777 GameServer()->CreatePlayerSpawn(Pos: SpawnPos, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
778
779 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
780 m_pCharacter->SetSolo(true);
781}
782
783void CPlayer::UpdatePlaytime()
784{
785 m_LastPlaytime = time_get();
786}
787
788void CPlayer::AfkTimer()
789{
790 SetAfk(g_Config.m_SvMaxAfkTime != 0 && m_LastPlaytime < time_get() - time_freq() * g_Config.m_SvMaxAfkTime);
791}
792
793void CPlayer::SetAfk(bool Afk)
794{
795 if(m_Afk != Afk)
796 {
797 Server()->ExpireServerInfo();
798 m_Afk = Afk;
799 }
800}
801
802void CPlayer::SetInitialAfk(bool Afk)
803{
804 if(g_Config.m_SvMaxAfkTime == 0)
805 {
806 SetAfk(false);
807 return;
808 }
809
810 SetAfk(Afk);
811
812 // Ensure that the AFK state is not reset again automatically
813 if(Afk)
814 m_LastPlaytime = time_get() - time_freq() * g_Config.m_SvMaxAfkTime - 1;
815 else
816 m_LastPlaytime = time_get();
817}
818
819int CPlayer::GetDefaultEmote() const
820{
821 if(m_OverrideEmoteReset >= 0)
822 return m_OverrideEmote;
823
824 return m_DefEmote;
825}
826
827void CPlayer::OverrideDefaultEmote(int Emote, int Tick)
828{
829 m_OverrideEmote = Emote;
830 m_OverrideEmoteReset = Tick;
831 m_LastEyeEmote = Server()->Tick();
832}
833
834bool CPlayer::CanOverrideDefaultEmote() const
835{
836 return m_LastEyeEmote == 0 || m_LastEyeEmote + (int64_t)g_Config.m_SvEyeEmoteChangeDelay * Server()->TickSpeed() < Server()->Tick();
837}
838
839bool CPlayer::CanSpec() const
840{
841 return m_pCharacter->IsGrounded() && m_pCharacter->m_Pos == m_pCharacter->m_PrevPos;
842}
843
844void CPlayer::ProcessPause()
845{
846 if(m_ForcePauseTime && m_ForcePauseTime < Server()->Tick())
847 {
848 m_ForcePauseTime = 0;
849 GameServer()->SendChatTarget(To: m_ClientId, pText: "The force pause timer is now over, you can exit with /spec");
850 }
851
852 if(m_Paused == PAUSE_SPEC && !m_pCharacter->IsPaused() && CanSpec())
853 {
854 m_pCharacter->Pause(Pause: true);
855 GameServer()->CreateDeath(Pos: m_pCharacter->m_Pos, ClientId: m_ClientId, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
856 GameServer()->CreateSound(Pos: m_pCharacter->m_Pos, Sound: SOUND_PLAYER_DIE, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
857 }
858}
859
860int CPlayer::Pause(int State, bool Force)
861{
862 if(State < PAUSE_NONE || State > PAUSE_SPEC) // Invalid pause state passed
863 return 0;
864
865 if(!m_pCharacter)
866 return 0;
867
868 char aBuf[128];
869 if(State != m_Paused)
870 {
871 // Get to wanted state
872 switch(State)
873 {
874 case PAUSE_PAUSED:
875 case PAUSE_NONE:
876 if(m_pCharacter->IsPaused()) // First condition might be unnecessary
877 {
878 if(!Force && m_LastPause && m_LastPause + (int64_t)g_Config.m_SvSpecFrequency * Server()->TickSpeed() > Server()->Tick())
879 {
880 GameServer()->SendChatTarget(To: m_ClientId, pText: "Can't /spec that quickly.");
881 return m_Paused; // Do not update state. Do not collect $200
882 }
883 m_pCharacter->Pause(Pause: false);
884 m_ViewPos = m_pCharacter->m_Pos;
885 GameServer()->CreatePlayerSpawn(Pos: m_pCharacter->m_Pos, Mask: GameServer()->m_pController->GetMaskForPlayerWorldEvent(Asker: m_ClientId));
886 }
887 [[fallthrough]];
888 case PAUSE_SPEC:
889 if(g_Config.m_SvPauseMessages)
890 {
891 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: (State > PAUSE_NONE) ? "'%s' speced" : "'%s' resumed", Server()->ClientName(ClientId: m_ClientId));
892 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
893 }
894 break;
895 }
896
897 // Update state
898 m_Paused = State;
899 m_LastPause = Server()->Tick();
900
901 // Sixup needs a teamchange
902 protocol7::CNetMsg_Sv_Team Msg;
903 Msg.m_ClientId = m_ClientId;
904 Msg.m_CooldownTick = Server()->Tick();
905 Msg.m_Silent = true;
906 Msg.m_Team = m_Paused ? protocol7::TEAM_SPECTATORS : m_Team;
907
908 GameServer()->Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: m_ClientId);
909
910 if(m_Paused == PAUSE_NONE)
911 {
912 GameServer()->m_PlayerMapping.ResetSeeOthers(ClientId: m_ClientId);
913 }
914 }
915
916 return m_Paused;
917}
918
919int CPlayer::ForcePause(int Time)
920{
921 m_ForcePauseTime = Server()->Tick() + Server()->TickSpeed() * Time;
922
923 if(g_Config.m_SvPauseMessages)
924 {
925 char aBuf[128];
926 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' was force-paused for %ds", Server()->ClientName(ClientId: m_ClientId), Time);
927 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
928 }
929
930 return Pause(State: PAUSE_SPEC, Force: true);
931}
932
933int CPlayer::IsPaused() const
934{
935 return m_ForcePauseTime ? m_ForcePauseTime : -1 * m_Paused;
936}
937
938bool CPlayer::IsPlaying() const
939{
940 return m_pCharacter && m_pCharacter->IsAlive();
941}
942
943void CPlayer::SpectatePlayerName(const char *pName)
944{
945 auto ClientId = GameServer()->FindClientIdByName(pName);
946 if(!ClientId.has_value())
947 return;
948
949 SetSpectatorId(ClientId.value());
950}
951
952void CPlayer::SetSpectatorId(int Id)
953{
954 m_SpectatorId = Id;
955 GameServer()->m_PlayerMapping.ResetSeeOthers(ClientId: m_ClientId);
956}
957
958void CPlayer::ProcessScoreResult(CScorePlayerResult &Result)
959{
960 if(Result.m_Success) // SQL request was successful
961 {
962 switch(Result.m_MessageKind)
963 {
964 case CScorePlayerResult::DIRECT:
965 for(auto &aMessage : Result.m_Data.m_aaMessages)
966 {
967 if(aMessage[0] == 0)
968 break;
969 GameServer()->SendChatTarget(To: m_ClientId, pText: aMessage);
970 }
971 break;
972 case CScorePlayerResult::ALL:
973 {
974 bool PrimaryMessage = true;
975 for(auto &aMessage : Result.m_Data.m_aaMessages)
976 {
977 if(aMessage[0] == 0)
978 break;
979
980 if(GameServer()->ProcessSpamProtection(ClientId: m_ClientId) && PrimaryMessage)
981 break;
982
983 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aMessage, SpamProtectionClientId: -1);
984 PrimaryMessage = false;
985 }
986 break;
987 }
988 case CScorePlayerResult::BROADCAST:
989 if(Result.m_Data.m_aBroadcast[0] != 0)
990 GameServer()->SendBroadcast(pText: Result.m_Data.m_aBroadcast, ClientId: -1);
991 break;
992 case CScorePlayerResult::MAP_VOTE:
993 GameServer()->m_VoteType = CGameContext::VOTE_TYPE_OPTION;
994 GameServer()->m_LastMapVote = time_get();
995
996 char aCmd[256];
997 str_format(buffer: aCmd, buffer_size: sizeof(aCmd),
998 format: "sv_reset_file types/%s/flexreset.cfg; change_map \"%s\"",
999 Result.m_Data.m_MapVote.m_aServer, Result.m_Data.m_MapVote.m_aMap);
1000
1001 char aChatmsg[512];
1002 str_format(buffer: aChatmsg, buffer_size: sizeof(aChatmsg), format: "'%s' called vote to change server option '%s' (%s)",
1003 Server()->ClientName(ClientId: m_ClientId), Result.m_Data.m_MapVote.m_aMap, "/map");
1004
1005 GameServer()->CallVote(ClientId: m_ClientId, pDesc: Result.m_Data.m_MapVote.m_aMap, pCmd: aCmd, pReason: "/map", pChatmsg: aChatmsg);
1006 break;
1007 case CScorePlayerResult::PLAYER_INFO:
1008 {
1009 if(Result.m_Data.m_Info.m_Time.has_value())
1010 {
1011 GameServer()->Score()->PlayerData(Id: m_ClientId)->Set(Time: Result.m_Data.m_Info.m_Time.value(), aTimeCp: Result.m_Data.m_Info.m_aTimeCp);
1012 Server()->SetClientScore(ClientId: m_ClientId, Score: Result.m_Data.m_Info.m_Time.value());
1013 // update map best time if player's time is better
1014 if(!GameServer()->m_pController->m_CurrentRecord.has_value() ||
1015 Result.m_Data.m_Info.m_Time.value() < GameServer()->m_pController->m_CurrentRecord.value())
1016 {
1017 GameServer()->Score()->LoadBestTime();
1018 }
1019 }
1020 Server()->ExpireServerInfo();
1021 int Birthday = Result.m_Data.m_Info.m_Birthday;
1022 if(Birthday != 0 && !m_BirthdayAnnounced && GetCharacter())
1023 {
1024 char aBuf[512];
1025 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1026 format: "Happy DDNet birthday to %s for finishing their first map %d year%s ago!",
1027 Server()->ClientName(ClientId: m_ClientId), Birthday, Birthday > 1 ? "s" : "");
1028 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: m_ClientId);
1029 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1030 format: "Happy DDNet birthday, %s!\nYou have finished your first map exactly %d year%s ago!",
1031 Server()->ClientName(ClientId: m_ClientId), Birthday, Birthday > 1 ? "s" : "");
1032 GameServer()->SendBroadcast(pText: aBuf, ClientId: m_ClientId);
1033 m_BirthdayAnnounced = true;
1034
1035 GameServer()->CreateBirthdayEffect(Pos: GetCharacter()->m_Pos, Mask: GetCharacter()->TeamMask());
1036 }
1037 GameServer()->SendRecord(ClientId: m_ClientId);
1038 break;
1039 }
1040 case CScorePlayerResult::PLAYER_TIMECP:
1041 GameServer()->Score()->PlayerData(Id: m_ClientId)->SetBestTimeCp(Result.m_Data.m_Info.m_aTimeCp);
1042 char aBuf[128], aTime[32];
1043 str_time_float(secs: Result.m_Data.m_Info.m_Time.value(), format: ETimeFormat::HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1044 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);
1045 GameServer()->SendChatTarget(To: m_ClientId, pText: aBuf);
1046 break;
1047 }
1048 }
1049}
1050
1051vec2 CPlayer::CCameraInfo::ConvertTargetToWorld(vec2 Position, vec2 Target) const
1052{
1053 vec2 TargetCameraOffset(0, 0);
1054 float l = length(a: Target);
1055
1056 if(l > 0.0001f) // make sure that this isn't 0
1057 {
1058 float OffsetAmount = std::max(a: l - m_Deadzone, b: 0.0f) * (m_FollowFactor / 100.0f);
1059 TargetCameraOffset = normalize_pre_length(v: Target, len: l) * OffsetAmount;
1060 }
1061
1062 return Position + (Target - TargetCameraOffset) * m_Zoom + TargetCameraOffset;
1063}
1064
1065void CPlayer::UpdateNetworkClipRadius()
1066{
1067 // Keep the full show distance as radius while not viewing the own character, it covers the camera
1068 // offset the client applies when spectating and must not depend on the spectated client's values
1069 const CCharacter *pChr = GetCharacter();
1070 if(!m_CameraInfo.m_HasCameraInfo || !pChr || m_Paused)
1071 {
1072 m_NetworkClipRadius = m_ShowDistance;
1073 return;
1074 }
1075
1076 // With the dynamic camera the client camera is offset from the view position towards the cursor,
1077 // this mirrors the offset computation of the client, see CCamera::UpdateCamera.
1078 // The aim target is not validated, so it's limited to the maximum distance a client can aim at.
1079 const float TargetDistance = std::min(a: length(a: vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY)), b: 2000.0f); // cl_dyncam_max_distance
1080 const float DyncamOffset = std::max(a: TargetDistance - m_CameraInfo.m_Deadzone, b: 0.0f) * (m_CameraInfo.m_FollowFactor / 100.0f);
1081
1082 // Border of 10 blocks outside of view range so that no entities randomly appear even with high latency.
1083 // ShowDistance / 2 because it's the radius. This heavily decreases bandwidth on some maps and was likely a bug
1084 // or misunderstanding introduced by whoever added showdistance. Now it's like this since forever and was noticed because
1085 // the playermapping algorithm depends on the NetworkClipped function to determine when to add a new tee.
1086 const float Border = 32.f * 10.f;
1087 m_NetworkClipRadius = m_ShowDistance / 2.f + vec2(Border + DyncamOffset, Border + DyncamOffset);
1088}
1089
1090void CPlayer::CCameraInfo::Write(const CNetMsg_Cl_CameraInfo *Msg)
1091{
1092 // Clamp to the ranges a client can configure, these values also affect other clients spectating this player
1093 m_HasCameraInfo = true;
1094 m_Zoom = Msg->m_Zoom / 1000.0f;
1095 m_Deadzone = std::clamp(val: Msg->m_Deadzone, lo: 0, hi: 3000); // cl_mouse_deadzone, cl_dyncam_deadzone
1096 m_FollowFactor = std::clamp(val: Msg->m_FollowFactor, lo: 0, hi: 200); // cl_mouse_followfactor, cl_dyncam_follow_factor
1097}
1098
1099void CPlayer::CCameraInfo::Reset()
1100{
1101 m_HasCameraInfo = false;
1102 m_Zoom = 1.0f;
1103 m_Deadzone = 0.0f;
1104 m_FollowFactor = 0.0f;
1105}
1106