ddnet/src/game/server/player.cpp
bors[bot] 2b5723cdef
Merge #3492 #3559 #3561 #3565
3492: Support F-Client and replace non-extended extension messages r=Learath2 a=heinrich5991

This continues what #3439 started, and also replaces non-extended messages that are DDRace extensions with extended messages. The DDNet server still sends the old message to old clients and the DDNet client still understands the old messages.

Supersedes #3439.

## Checklist

- [x] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


3559: Don't highlight us if we have no name (alternative fix for #3554) r=heinrich5991 a=def-

<!-- What is the motivation for the changes of this pull request -->

## Checklist

- [x] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


3561: Fix input message position in teehistorian files r=heinrich5991 a=Zwelf

Previously the message was recorded when incoming. But the time when the input should apply wasn't recorded. Now inputs are recorded right before they get applied in the game world.

And record exact tick where load/save result is returned.

## Checklist

- [x] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


3565: Add Arabic language translation by .Yukki r=heinrich5991 a=def-

![screenshot-20210131@093216](https://user-images.githubusercontent.com/2335377/106378755-747d7780-63a7-11eb-9553-6a1898907441.png)

## Checklist

- [x] Tested the change ingame
- [x] Provided screenshots if it is a visual change
- [x] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: fokkonaut <35420825+fokkonaut@users.noreply.github.com>
Co-authored-by: heinrich5991 <heinrich5991@gmail.com>
Co-authored-by: def <dennis@felsin9.de>
Co-authored-by: Zwelf <zwelf@strct.cc>
2021-01-31 14:37:48 +00:00

989 lines
28 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include "player.h"
#include <engine/shared/config.h>
#include "entities/character.h"
#include "gamecontext.h"
#include <engine/server.h>
#include <game/gamecore.h>
#include <game/version.h>
MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS)
IServer *CPlayer::Server() const { return m_pGameServer->Server(); }
CPlayer::CPlayer(CGameContext *pGameServer, int ClientID, int Team)
{
m_pGameServer = pGameServer;
m_ClientID = ClientID;
m_Team = GameServer()->m_pController->ClampTeam(Team);
m_NumInputs = 0;
Reset();
GameServer()->Antibot()->OnPlayerInit(m_ClientID);
}
CPlayer::~CPlayer()
{
GameServer()->Antibot()->OnPlayerDestroy(m_ClientID);
delete m_pLastTarget;
delete m_pCharacter;
m_pCharacter = 0;
}
void CPlayer::Reset()
{
m_DieTick = Server()->Tick();
m_PreviousDieTick = m_DieTick;
m_JoinTick = Server()->Tick();
delete m_pCharacter;
m_pCharacter = 0;
m_SpectatorID = SPEC_FREEVIEW;
m_LastActionTick = Server()->Tick();
m_TeamChangeTick = Server()->Tick();
m_LastInvited = 0;
m_WeakHookSpawn = false;
int *pIdMap = Server()->GetIdMap(m_ClientID);
for(int i = 1; i < VANILLA_MAX_CLIENTS; i++)
{
pIdMap[i] = -1;
}
pIdMap[0] = m_ClientID;
// DDRace
m_LastCommandPos = 0;
m_LastPlaytime = 0;
m_LastEyeEmote = 0;
m_Sent1stAfkWarning = 0;
m_Sent2ndAfkWarning = 0;
m_ChatScore = 0;
m_Moderating = false;
m_EyeEmoteEnabled = true;
if(Server()->IsSixup(m_ClientID))
m_TimerType = TIMERTYPE_SIXUP;
else
m_TimerType = (g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER || g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST) ? TIMERTYPE_BROADCAST : g_Config.m_SvDefaultTimerType;
m_DefEmote = EMOTE_NORMAL;
m_Afk = true;
m_LastWhisperTo = -1;
m_LastSetSpectatorMode = 0;
m_TimeoutCode[0] = '\0';
delete m_pLastTarget;
m_pLastTarget = nullptr;
m_TuneZone = 0;
m_TuneZoneOld = m_TuneZone;
m_Halloween = false;
m_FirstPacket = true;
m_SendVoteIndex = -1;
if(g_Config.m_Events)
{
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
if((timeinfo->tm_mon == 11 && timeinfo->tm_mday == 31) || (timeinfo->tm_mon == 0 && timeinfo->tm_mday == 1))
{ // New Year
m_DefEmote = EMOTE_HAPPY;
}
else if((timeinfo->tm_mon == 9 && timeinfo->tm_mday == 31) || (timeinfo->tm_mon == 10 && timeinfo->tm_mday == 1))
{ // Halloween
m_DefEmote = EMOTE_ANGRY;
m_Halloween = true;
}
else
{
m_DefEmote = EMOTE_NORMAL;
}
}
m_OverrideEmoteReset = -1;
GameServer()->Score()->PlayerData(m_ClientID)->Reset();
m_ShowOthers = g_Config.m_SvShowOthersDefault;
m_ShowAll = g_Config.m_SvShowAllDefault;
m_ShowDistance = vec2(1200, 800);
m_SpecTeam = 0;
m_NinjaJetpack = false;
m_Paused = PAUSE_NONE;
m_DND = false;
m_LastPause = 0;
m_Score = -9999;
m_HasFinishScore = false;
// Variable initialized:
m_Last_Team = 0;
m_LastSQLQuery = 0;
m_ScoreQueryResult = nullptr;
m_ScoreFinishResult = nullptr;
int64 Now = Server()->Tick();
int64 TickSpeed = Server()->TickSpeed();
// If the player joins within ten seconds of the server becoming
// non-empty, allow them to vote immediately. This allows players to
// vote after map changes or when they join an empty server.
//
// Otherwise, block voting in the beginning after joining.
if(Now > GameServer()->m_NonEmptySince + 10 * TickSpeed)
m_FirstVoteTick = Now + g_Config.m_SvJoinVoteDelay * TickSpeed;
else
m_FirstVoteTick = Now;
m_NotEligibleForFinish = false;
m_EligibleForFinishCheck = 0;
m_VotedForPractice = false;
}
static int PlayerFlags_SevenToSix(int Flags)
{
int Six = 0;
if(Flags & protocol7::PLAYERFLAG_CHATTING)
Six |= PLAYERFLAG_CHATTING;
if(Flags & protocol7::PLAYERFLAG_SCOREBOARD)
Six |= PLAYERFLAG_SCOREBOARD;
if(Flags & protocol7::PLAYERFLAG_AIM)
Six |= PLAYERFLAG_AIM;
return Six;
}
static int PlayerFlags_SixToSeven(int Flags)
{
int Seven = 0;
if(Flags & PLAYERFLAG_CHATTING)
Seven |= protocol7::PLAYERFLAG_CHATTING;
if(Flags & PLAYERFLAG_SCOREBOARD)
Seven |= protocol7::PLAYERFLAG_SCOREBOARD;
return Seven;
}
void CPlayer::Tick()
{
#ifdef CONF_DEBUG
if(!g_Config.m_DbgDummies || m_ClientID < MAX_CLIENTS - g_Config.m_DbgDummies)
#endif
if(m_ScoreQueryResult != nullptr && m_ScoreQueryResult->m_Completed)
{
ProcessScoreResult(*m_ScoreQueryResult);
m_ScoreQueryResult = nullptr;
}
if(m_ScoreFinishResult != nullptr && m_ScoreFinishResult->m_Completed)
{
ProcessScoreResult(*m_ScoreFinishResult);
m_ScoreFinishResult = nullptr;
}
if(!Server()->ClientIngame(m_ClientID))
return;
if(m_ChatScore > 0)
m_ChatScore--;
Server()->SetClientScore(m_ClientID, m_Score);
if(m_Moderating && m_Afk)
{
m_Moderating = false;
GameServer()->SendChatTarget(m_ClientID, "Active moderator mode disabled because you are afk.");
if(!GameServer()->PlayerModerating())
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, "Server kick/spec votes are no longer actively moderated.");
}
// do latency stuff
{
IServer::CClientInfo Info;
if(Server()->GetClientInfo(m_ClientID, &Info))
{
m_Latency.m_Accum += Info.m_Latency;
m_Latency.m_AccumMax = maximum(m_Latency.m_AccumMax, Info.m_Latency);
m_Latency.m_AccumMin = minimum(m_Latency.m_AccumMin, Info.m_Latency);
}
// each second
if(Server()->Tick() % Server()->TickSpeed() == 0)
{
m_Latency.m_Avg = m_Latency.m_Accum / Server()->TickSpeed();
m_Latency.m_Max = m_Latency.m_AccumMax;
m_Latency.m_Min = m_Latency.m_AccumMin;
m_Latency.m_Accum = 0;
m_Latency.m_AccumMin = 1000;
m_Latency.m_AccumMax = 0;
}
}
if(Server()->GetNetErrorString(m_ClientID)[0])
{
m_Afk = true;
char aBuf[512];
str_format(aBuf, sizeof(aBuf), "'%s' would have timed out, but can use timeout protection now", Server()->ClientName(m_ClientID));
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf);
Server()->ResetNetErrorString(m_ClientID);
}
if(!GameServer()->m_World.m_Paused)
{
int EarliestRespawnTick = m_PreviousDieTick + Server()->TickSpeed() * 3;
int RespawnTick = maximum(m_DieTick, EarliestRespawnTick) + 2;
if(!m_pCharacter && RespawnTick <= Server()->Tick())
m_Spawning = true;
if(m_pCharacter)
{
if(m_pCharacter->IsAlive())
{
ProcessPause();
if(!m_Paused)
m_ViewPos = m_pCharacter->m_Pos;
}
else if(!m_pCharacter->IsPaused())
{
delete m_pCharacter;
m_pCharacter = 0;
}
}
else if(m_Spawning && !m_WeakHookSpawn)
TryRespawn();
}
else
{
++m_DieTick;
++m_PreviousDieTick;
++m_JoinTick;
++m_LastActionTick;
++m_TeamChangeTick;
}
m_TuneZoneOld = m_TuneZone; // determine needed tunings with viewpos
int CurrentIndex = GameServer()->Collision()->GetMapIndex(m_ViewPos);
m_TuneZone = GameServer()->Collision()->IsTune(CurrentIndex);
if(m_TuneZone != m_TuneZoneOld) // don't send tunings all the time
{
GameServer()->SendTuningParams(m_ClientID, m_TuneZone);
}
if(m_OverrideEmoteReset >= 0 && m_OverrideEmoteReset <= Server()->Tick())
{
m_OverrideEmoteReset = -1;
}
if(m_Halloween && m_pCharacter && !m_pCharacter->IsPaused())
{
if(1200 - ((Server()->Tick() - m_pCharacter->GetLastAction()) % (1200)) < 5)
{
GameServer()->SendEmoticon(GetCID(), EMOTICON_GHOST);
}
}
}
void CPlayer::PostTick()
{
// update latency value
if(m_PlayerFlags & PLAYERFLAG_SCOREBOARD)
{
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS)
m_aActLatency[i] = GameServer()->m_apPlayers[i]->m_Latency.m_Min;
}
}
// update view pos for spectators
if((m_Team == TEAM_SPECTATORS || m_Paused) && m_SpectatorID != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorID] && GameServer()->m_apPlayers[m_SpectatorID]->GetCharacter())
m_ViewPos = GameServer()->m_apPlayers[m_SpectatorID]->GetCharacter()->m_Pos;
}
void CPlayer::PostPostTick()
{
#ifdef CONF_DEBUG
if(!g_Config.m_DbgDummies || m_ClientID < MAX_CLIENTS - g_Config.m_DbgDummies)
#endif
if(!Server()->ClientIngame(m_ClientID))
return;
if(!GameServer()->m_World.m_Paused && !m_pCharacter && m_Spawning && m_WeakHookSpawn)
TryRespawn();
}
void CPlayer::Snap(int SnappingClient)
{
#ifdef CONF_DEBUG
if(!g_Config.m_DbgDummies || m_ClientID < MAX_CLIENTS - g_Config.m_DbgDummies)
#endif
if(!Server()->ClientIngame(m_ClientID))
return;
int id = m_ClientID;
if(SnappingClient > -1 && !Server()->Translate(id, SnappingClient))
return;
CNetObj_ClientInfo *pClientInfo = static_cast<CNetObj_ClientInfo *>(Server()->SnapNewItem(NETOBJTYPE_CLIENTINFO, id, sizeof(CNetObj_ClientInfo)));
if(!pClientInfo)
return;
StrToInts(&pClientInfo->m_Name0, 4, Server()->ClientName(m_ClientID));
StrToInts(&pClientInfo->m_Clan0, 3, Server()->ClientClan(m_ClientID));
pClientInfo->m_Country = Server()->ClientCountry(m_ClientID);
StrToInts(&pClientInfo->m_Skin0, 6, m_TeeInfos.m_SkinName);
pClientInfo->m_UseCustomColor = m_TeeInfos.m_UseCustomColor;
pClientInfo->m_ColorBody = m_TeeInfos.m_ColorBody;
pClientInfo->m_ColorFeet = m_TeeInfos.m_ColorFeet;
int SnappingClientVersion = SnappingClient >= 0 ? GameServer()->GetClientVersion(SnappingClient) : CLIENT_VERSIONNR;
int Latency = SnappingClient == -1 ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aActLatency[m_ClientID];
int Score = abs(m_Score) * -1;
// send 0 if times of others are not shown
if(SnappingClient != m_ClientID && g_Config.m_SvHideScore)
Score = -9999;
if(SnappingClient < 0 || !Server()->IsSixup(SnappingClient))
{
CNetObj_PlayerInfo *pPlayerInfo = static_cast<CNetObj_PlayerInfo *>(Server()->SnapNewItem(NETOBJTYPE_PLAYERINFO, id, sizeof(CNetObj_PlayerInfo)));
if(!pPlayerInfo)
return;
pPlayerInfo->m_Latency = Latency;
pPlayerInfo->m_Score = Score;
pPlayerInfo->m_Local = (int)(m_ClientID == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClient >= VERSION_DDNET_OLD));
pPlayerInfo->m_ClientID = id;
pPlayerInfo->m_Team = (SnappingClientVersion < VERSION_DDNET_OLD || m_Paused != PAUSE_PAUSED || m_ClientID != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS;
if(m_ClientID == SnappingClient && m_Paused == PAUSE_PAUSED && SnappingClientVersion < VERSION_DDNET_OLD)
pPlayerInfo->m_Team = TEAM_SPECTATORS;
}
else
{
protocol7::CNetObj_PlayerInfo *pPlayerInfo = static_cast<protocol7::CNetObj_PlayerInfo *>(Server()->SnapNewItem(NETOBJTYPE_PLAYERINFO, id, sizeof(protocol7::CNetObj_PlayerInfo)));
if(!pPlayerInfo)
return;
pPlayerInfo->m_PlayerFlags = PlayerFlags_SixToSeven(m_PlayerFlags);
if(SnappingClientVersion >= VERSION_DDRACE && (m_PlayerFlags & PLAYERFLAG_AIM))
pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_AIM;
if(Server()->ClientAuthed(m_ClientID))
pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_ADMIN;
// Times are in milliseconds for 0.7
pPlayerInfo->m_Score = Score == -9999 ? -1 : -Score * 1000;
pPlayerInfo->m_Latency = Latency;
}
if(m_ClientID == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused))
{
if(SnappingClient < 0 || !Server()->IsSixup(SnappingClient))
{
CNetObj_SpectatorInfo *pSpectatorInfo = static_cast<CNetObj_SpectatorInfo *>(Server()->SnapNewItem(NETOBJTYPE_SPECTATORINFO, m_ClientID, sizeof(CNetObj_SpectatorInfo)));
if(!pSpectatorInfo)
return;
pSpectatorInfo->m_SpectatorID = m_SpectatorID;
pSpectatorInfo->m_X = m_ViewPos.x;
pSpectatorInfo->m_Y = m_ViewPos.y;
}
else
{
protocol7::CNetObj_SpectatorInfo *pSpectatorInfo = static_cast<protocol7::CNetObj_SpectatorInfo *>(Server()->SnapNewItem(NETOBJTYPE_SPECTATORINFO, m_ClientID, sizeof(protocol7::CNetObj_SpectatorInfo)));
if(!pSpectatorInfo)
return;
pSpectatorInfo->m_SpecMode = m_SpectatorID == SPEC_FREEVIEW ? protocol7::SPEC_FREEVIEW : protocol7::SPEC_PLAYER;
pSpectatorInfo->m_SpectatorID = m_SpectatorID;
pSpectatorInfo->m_X = m_ViewPos.x;
pSpectatorInfo->m_Y = m_ViewPos.y;
}
}
CNetObj_DDNetPlayer *pDDNetPlayer = static_cast<CNetObj_DDNetPlayer *>(Server()->SnapNewItem(NETOBJTYPE_DDNETPLAYER, id, sizeof(CNetObj_DDNetPlayer)));
if(!pDDNetPlayer)
return;
pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(id);
pDDNetPlayer->m_Flags = 0;
if(m_Afk)
pDDNetPlayer->m_Flags |= EXPLAYERFLAG_AFK;
if(m_Paused == PAUSE_SPEC)
pDDNetPlayer->m_Flags |= EXPLAYERFLAG_SPEC;
if(m_Paused == PAUSE_PAUSED)
pDDNetPlayer->m_Flags |= EXPLAYERFLAG_PAUSED;
if(SnappingClient >= 0 && Server()->IsSixup(SnappingClient) && m_pCharacter && m_pCharacter->m_DDRaceState == DDRACE_STARTED &&
GameServer()->m_apPlayers[SnappingClient]->m_TimerType == TIMERTYPE_SIXUP)
{
protocol7::CNetObj_PlayerInfoRace *pRaceInfo = static_cast<protocol7::CNetObj_PlayerInfoRace *>(Server()->SnapNewItem(-protocol7::NETOBJTYPE_PLAYERINFORACE, id, sizeof(protocol7::CNetObj_PlayerInfoRace)));
if(!pRaceInfo)
return;
pRaceInfo->m_RaceStartTick = m_pCharacter->m_StartTime;
}
bool ShowSpec = m_pCharacter && m_pCharacter->IsPaused();
if(SnappingClient >= 0)
{
CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient];
ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(id) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->m_ShowOthers == 1 || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()));
}
if(ShowSpec)
{
CNetObj_SpecChar *pSpecChar = static_cast<CNetObj_SpecChar *>(Server()->SnapNewItem(NETOBJTYPE_SPECCHAR, id, sizeof(CNetObj_SpecChar)));
if(!pSpecChar)
return;
pSpecChar->m_X = m_pCharacter->Core()->m_Pos.x;
pSpecChar->m_Y = m_pCharacter->Core()->m_Pos.y;
}
}
void CPlayer::FakeSnap()
{
if(GetClientVersion() >= VERSION_DDNET_OLD)
return;
if(Server()->IsSixup(m_ClientID))
return;
int FakeID = VANILLA_MAX_CLIENTS - 1;
CNetObj_ClientInfo *pClientInfo = static_cast<CNetObj_ClientInfo *>(Server()->SnapNewItem(NETOBJTYPE_CLIENTINFO, FakeID, sizeof(CNetObj_ClientInfo)));
if(!pClientInfo)
return;
StrToInts(&pClientInfo->m_Name0, 4, " ");
StrToInts(&pClientInfo->m_Clan0, 3, "");
StrToInts(&pClientInfo->m_Skin0, 6, "default");
if(m_Paused != PAUSE_PAUSED)
return;
CNetObj_PlayerInfo *pPlayerInfo = static_cast<CNetObj_PlayerInfo *>(Server()->SnapNewItem(NETOBJTYPE_PLAYERINFO, FakeID, sizeof(CNetObj_PlayerInfo)));
if(!pPlayerInfo)
return;
pPlayerInfo->m_Latency = m_Latency.m_Min;
pPlayerInfo->m_Local = 1;
pPlayerInfo->m_ClientID = FakeID;
pPlayerInfo->m_Score = -9999;
pPlayerInfo->m_Team = TEAM_SPECTATORS;
CNetObj_SpectatorInfo *pSpectatorInfo = static_cast<CNetObj_SpectatorInfo *>(Server()->SnapNewItem(NETOBJTYPE_SPECTATORINFO, FakeID, sizeof(CNetObj_SpectatorInfo)));
if(!pSpectatorInfo)
return;
pSpectatorInfo->m_SpectatorID = m_SpectatorID;
pSpectatorInfo->m_X = m_ViewPos.x;
pSpectatorInfo->m_Y = m_ViewPos.y;
}
void CPlayer::OnDisconnect()
{
KillCharacter();
m_Moderating = false;
}
void CPlayer::OnPredictedInput(CNetObj_PlayerInput *NewInput)
{
// skip the input if chat is active
if((m_PlayerFlags & PLAYERFLAG_CHATTING) && (NewInput->m_PlayerFlags & PLAYERFLAG_CHATTING))
return;
AfkVoteTimer(NewInput);
m_NumInputs++;
if(m_pCharacter && !m_Paused)
m_pCharacter->OnPredictedInput(NewInput);
// Magic number when we can hope that client has successfully identified itself
if(m_NumInputs == 20 && g_Config.m_SvClientSuggestion[0] != '\0' && GetClientVersion() <= VERSION_DDNET_OLD)
GameServer()->SendBroadcast(g_Config.m_SvClientSuggestion, m_ClientID);
else if(m_NumInputs == 200 && Server()->IsSixup(m_ClientID))
GameServer()->SendBroadcast("This server uses an experimental translation from Teeworlds 0.7 to 0.6. Please report bugs on ddnet.tw/discord", m_ClientID);
}
void CPlayer::OnDirectInput(CNetObj_PlayerInput *NewInput)
{
if(Server()->IsSixup(m_ClientID))
NewInput->m_PlayerFlags = PlayerFlags_SevenToSix(NewInput->m_PlayerFlags);
if(NewInput->m_PlayerFlags)
Server()->SetClientFlags(m_ClientID, NewInput->m_PlayerFlags);
if(AfkTimer(NewInput->m_TargetX, NewInput->m_TargetY))
return; // we must return if kicked, as player struct is already deleted
AfkVoteTimer(NewInput);
if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorID == SPEC_FREEVIEW)
m_ViewPos = vec2(NewInput->m_TargetX, NewInput->m_TargetY);
if(NewInput->m_PlayerFlags & PLAYERFLAG_CHATTING)
{
// skip the input if chat is active
if(m_PlayerFlags & PLAYERFLAG_CHATTING)
return;
// reset input
if(m_pCharacter)
m_pCharacter->ResetInput();
m_PlayerFlags = NewInput->m_PlayerFlags;
return;
}
m_PlayerFlags = NewInput->m_PlayerFlags;
if(m_pCharacter && m_Paused)
m_pCharacter->ResetInput();
if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (NewInput->m_Fire & 1))
m_Spawning = true;
// check for activity
if(NewInput->m_Direction || m_LatestActivity.m_TargetX != NewInput->m_TargetX ||
m_LatestActivity.m_TargetY != NewInput->m_TargetY || NewInput->m_Jump ||
NewInput->m_Fire & 1 || NewInput->m_Hook)
{
m_LatestActivity.m_TargetX = NewInput->m_TargetX;
m_LatestActivity.m_TargetY = NewInput->m_TargetY;
m_LastActionTick = Server()->Tick();
}
}
void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *NewInput)
{
// skip the input if chat is active
if((m_PlayerFlags & PLAYERFLAG_CHATTING) && (NewInput->m_PlayerFlags & PLAYERFLAG_CHATTING))
return;
if(m_pCharacter && !m_Paused)
m_pCharacter->OnDirectInput(NewInput);
}
int CPlayer::GetClientVersion() const
{
return m_pGameServer->GetClientVersion(m_ClientID);
}
CCharacter *CPlayer::GetCharacter()
{
if(m_pCharacter && m_pCharacter->IsAlive())
return m_pCharacter;
return 0;
}
void CPlayer::KillCharacter(int Weapon)
{
if(m_pCharacter)
{
m_pCharacter->Die(m_ClientID, Weapon);
delete m_pCharacter;
m_pCharacter = 0;
}
}
void CPlayer::Respawn(bool WeakHook)
{
if(m_Team != TEAM_SPECTATORS)
{
m_WeakHookSpawn = WeakHook;
m_Spawning = true;
}
}
CCharacter *CPlayer::ForceSpawn(vec2 Pos)
{
m_Spawning = false;
m_pCharacter = new(m_ClientID) CCharacter(&GameServer()->m_World);
m_pCharacter->Spawn(this, Pos);
m_Team = 0;
return m_pCharacter;
}
void CPlayer::SetTeam(int Team, bool DoChatMsg)
{
KillCharacter();
m_Team = Team;
m_LastSetTeam = Server()->Tick();
m_LastActionTick = Server()->Tick();
m_SpectatorID = SPEC_FREEVIEW;
protocol7::CNetMsg_Sv_Team Msg;
Msg.m_ClientID = m_ClientID;
Msg.m_Team = m_Team;
Msg.m_Silent = !DoChatMsg;
Msg.m_CooldownTick = m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
if(Team == TEAM_SPECTATORS)
{
// update spectator modes
for(auto &pPlayer : GameServer()->m_apPlayers)
{
if(pPlayer && pPlayer->m_SpectatorID == m_ClientID)
pPlayer->m_SpectatorID = SPEC_FREEVIEW;
}
}
}
bool CPlayer::SetTimerType(int TimerType)
{
if(TimerType == TIMERTYPE_DEFAULT)
{
if(Server()->IsSixup(m_ClientID))
m_TimerType = TIMERTYPE_SIXUP;
else
SetTimerType(g_Config.m_SvDefaultTimerType);
return true;
}
if(Server()->IsSixup(m_ClientID))
{
if(TimerType == TIMERTYPE_SIXUP || TimerType == TIMERTYPE_NONE)
{
m_TimerType = TimerType;
return true;
}
else
return false;
}
if(TimerType == TIMERTYPE_GAMETIMER)
{
if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
m_TimerType = TimerType;
else
return false;
}
else if(TimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST)
{
if(GetClientVersion() >= VERSION_DDNET_GAMETICK)
m_TimerType = TimerType;
else
{
m_TimerType = TIMERTYPE_BROADCAST;
return false;
}
}
else
m_TimerType = TimerType;
return true;
}
void CPlayer::TryRespawn()
{
vec2 SpawnPos;
if(!GameServer()->m_pController->CanSpawn(m_Team, &SpawnPos))
return;
m_WeakHookSpawn = false;
m_Spawning = false;
m_pCharacter = new(m_ClientID) CCharacter(&GameServer()->m_World);
m_pCharacter->Spawn(this, SpawnPos);
GameServer()->CreatePlayerSpawn(SpawnPos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
if(g_Config.m_SvTeam == 3)
m_pCharacter->SetSolo(true);
}
bool CPlayer::AfkTimer(int NewTargetX, int NewTargetY)
{
/*
afk timer (x, y = mouse coordinates)
Since a player has to move the mouse to play, this is a better method than checking
the player's position in the game world, because it can easily be bypassed by just locking a key.
Frozen players could be kicked as well, because they can't move.
It also works for spectators.
returns true if kicked
*/
if(Server()->GetAuthedState(m_ClientID))
return false; // don't kick admins
if(g_Config.m_SvMaxAfkTime == 0)
return false; // 0 = disabled
if(NewTargetX != m_LastTarget_x || NewTargetY != m_LastTarget_y)
{
UpdatePlaytime();
m_LastTarget_x = NewTargetX;
m_LastTarget_y = NewTargetY;
m_Sent1stAfkWarning = 0; // afk timer's 1st warning after 50% of sv_max_afk_time
m_Sent2ndAfkWarning = 0;
}
else
{
if(!m_Paused)
{
// not playing, check how long
if(m_Sent1stAfkWarning == 0 && m_LastPlaytime < time_get() - time_freq() * (int)(g_Config.m_SvMaxAfkTime * 0.5))
{
str_format(m_pAfkMsg, sizeof(m_pAfkMsg),
"You have been afk for %d seconds now. Please note that you get kicked after not playing for %d seconds.",
(int)(g_Config.m_SvMaxAfkTime * 0.5),
g_Config.m_SvMaxAfkTime);
m_pGameServer->SendChatTarget(m_ClientID, m_pAfkMsg);
m_Sent1stAfkWarning = 1;
}
else if(m_Sent2ndAfkWarning == 0 && m_LastPlaytime < time_get() - time_freq() * (int)(g_Config.m_SvMaxAfkTime * 0.9))
{
str_format(m_pAfkMsg, sizeof(m_pAfkMsg),
"You have been afk for %d seconds now. Please note that you get kicked after not playing for %d seconds.",
(int)(g_Config.m_SvMaxAfkTime * 0.9),
g_Config.m_SvMaxAfkTime);
m_pGameServer->SendChatTarget(m_ClientID, m_pAfkMsg);
m_Sent2ndAfkWarning = 1;
}
else if(m_LastPlaytime < time_get() - time_freq() * g_Config.m_SvMaxAfkTime)
{
m_pGameServer->Server()->Kick(m_ClientID, "Away from keyboard");
return true;
}
}
}
return false;
}
void CPlayer::UpdatePlaytime()
{
m_LastPlaytime = time_get();
}
void CPlayer::AfkVoteTimer(CNetObj_PlayerInput *NewTarget)
{
if(g_Config.m_SvMaxAfkVoteTime == 0)
return;
if(!m_pLastTarget)
{
m_pLastTarget = new CNetObj_PlayerInput(*NewTarget);
m_LastPlaytime = 0;
m_Afk = true;
return;
}
else if(mem_comp(NewTarget, m_pLastTarget, sizeof(CNetObj_PlayerInput)) != 0)
{
UpdatePlaytime();
mem_copy(m_pLastTarget, NewTarget, sizeof(CNetObj_PlayerInput));
}
else if(m_LastPlaytime < time_get() - time_freq() * g_Config.m_SvMaxAfkVoteTime)
{
m_Afk = true;
return;
}
m_Afk = false;
}
int CPlayer::GetDefaultEmote() const
{
if(m_OverrideEmoteReset >= 0)
return m_OverrideEmote;
return m_DefEmote;
}
void CPlayer::OverrideDefaultEmote(int Emote, int Tick)
{
m_OverrideEmote = Emote;
m_OverrideEmoteReset = Tick;
m_LastEyeEmote = Server()->Tick();
}
bool CPlayer::CanOverrideDefaultEmote() const
{
return m_LastEyeEmote == 0 || m_LastEyeEmote + (int64_t)g_Config.m_SvEyeEmoteChangeDelay * Server()->TickSpeed() < Server()->Tick();
}
void CPlayer::ProcessPause()
{
if(m_ForcePauseTime && m_ForcePauseTime < Server()->Tick())
{
m_ForcePauseTime = 0;
Pause(PAUSE_NONE, true);
}
if(m_Paused == PAUSE_SPEC && !m_pCharacter->IsPaused() && m_pCharacter->IsGrounded() && m_pCharacter->m_Pos == m_pCharacter->m_PrevPos)
{
m_pCharacter->Pause(true);
GameServer()->CreateDeath(m_pCharacter->m_Pos, m_ClientID, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
GameServer()->CreateSound(m_pCharacter->m_Pos, SOUND_PLAYER_DIE, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
}
}
int CPlayer::Pause(int State, bool Force)
{
if(State < PAUSE_NONE || State > PAUSE_SPEC) // Invalid pause state passed
return 0;
if(!m_pCharacter)
return 0;
char aBuf[128];
if(State != m_Paused)
{
// Get to wanted state
switch(State)
{
case PAUSE_PAUSED:
case PAUSE_NONE:
if(m_pCharacter->IsPaused()) // First condition might be unnecessary
{
if(!Force && m_LastPause && m_LastPause + (int64_t)g_Config.m_SvSpecFrequency * Server()->TickSpeed() > Server()->Tick())
{
GameServer()->SendChatTarget(m_ClientID, "Can't /spec that quickly.");
return m_Paused; // Do not update state. Do not collect $200
}
m_pCharacter->Pause(false);
GameServer()->CreatePlayerSpawn(m_pCharacter->m_Pos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
}
// fall-thru
case PAUSE_SPEC:
if(g_Config.m_SvPauseMessages)
{
str_format(aBuf, sizeof(aBuf), (State > PAUSE_NONE) ? "'%s' speced" : "'%s' resumed", Server()->ClientName(m_ClientID));
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf);
}
break;
}
// Update state
m_Paused = State;
m_LastPause = Server()->Tick();
// Sixup needs a teamchange
protocol7::CNetMsg_Sv_Team Msg;
Msg.m_ClientID = m_ClientID;
Msg.m_CooldownTick = Server()->Tick();
Msg.m_Silent = true;
Msg.m_Team = m_Paused ? protocol7::TEAM_SPECTATORS : m_Team;
GameServer()->Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, m_ClientID);
}
return m_Paused;
}
int CPlayer::ForcePause(int Time)
{
m_ForcePauseTime = Server()->Tick() + Server()->TickSpeed() * Time;
if(g_Config.m_SvPauseMessages)
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "'%s' was force-paused for %ds", Server()->ClientName(m_ClientID), Time);
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf);
}
return Pause(PAUSE_SPEC, true);
}
int CPlayer::IsPaused()
{
return m_ForcePauseTime ? m_ForcePauseTime : -1 * m_Paused;
}
bool CPlayer::IsPlaying()
{
if(m_pCharacter && m_pCharacter->IsAlive())
return true;
return false;
}
void CPlayer::SpectatePlayerName(const char *pName)
{
if(!pName)
return;
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(i != m_ClientID && Server()->ClientIngame(i) && !str_comp(pName, Server()->ClientName(i)))
{
m_SpectatorID = i;
return;
}
}
}
void CPlayer::ProcessScoreResult(CScorePlayerResult &Result)
{
if(Result.m_Success) // SQL request was successful
{
switch(Result.m_MessageKind)
{
case CScorePlayerResult::DIRECT:
for(auto &aMessage : Result.m_Data.m_aaMessages)
{
if(aMessage[0] == 0)
break;
GameServer()->SendChatTarget(m_ClientID, aMessage);
}
break;
case CScorePlayerResult::ALL:
for(auto &aMessage : Result.m_Data.m_aaMessages)
{
if(aMessage[0] == 0)
break;
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aMessage, m_ClientID);
}
break;
case CScorePlayerResult::BROADCAST:
if(Result.m_Data.m_Broadcast[0] != 0)
GameServer()->SendBroadcast(Result.m_Data.m_Broadcast, -1);
break;
case CScorePlayerResult::MAP_VOTE:
GameServer()->m_VoteType = CGameContext::VOTE_TYPE_OPTION;
GameServer()->m_LastMapVote = time_get();
char aCmd[256];
str_format(aCmd, sizeof(aCmd),
"sv_reset_file types/%s/flexreset.cfg; change_map \"%s\"",
Result.m_Data.m_MapVote.m_Server, Result.m_Data.m_MapVote.m_Map);
char aChatmsg[512];
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)",
Server()->ClientName(m_ClientID), Result.m_Data.m_MapVote.m_Map, "/map");
GameServer()->CallVote(m_ClientID, Result.m_Data.m_MapVote.m_Map, aCmd, "/map", aChatmsg);
break;
case CScorePlayerResult::PLAYER_INFO:
GameServer()->Score()->PlayerData(m_ClientID)->Set(Result.m_Data.m_Info.m_Time, Result.m_Data.m_Info.m_CpTime);
m_Score = Result.m_Data.m_Info.m_Score;
m_HasFinishScore = Result.m_Data.m_Info.m_HasFinishScore;
// -9999 stands for no time and isn't displayed in scoreboard, so
// shift the time by a second if the player actually took 9999
// seconds to finish the map.
if(m_HasFinishScore && m_Score == -9999)
m_Score = -10000;
Server()->ExpireServerInfo();
int Birthday = Result.m_Data.m_Info.m_Birthday;
if(Birthday != 0)
{
char aBuf[512];
str_format(aBuf, sizeof(aBuf),
"Happy DDNet birthday to %s for finishing their first map %d year%s ago!",
Server()->ClientName(m_ClientID), Birthday, Birthday > 1 ? "s" : "");
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, m_ClientID);
str_format(aBuf, sizeof(aBuf),
"Happy DDNet birthday, %s!\nYou have finished your first map exactly %d year%s ago!",
Server()->ClientName(m_ClientID), Birthday, Birthday > 1 ? "s" : "");
GameServer()->SendBroadcast(aBuf, m_ClientID);
}
break;
}
}
}