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