mirror of
https://github.com/ddnet/ddnet.git
synced 2024-10-20 15:58:18 +00:00
0d8a0d3b1d
Call expected server callback functions to simulate clients dynamically connecting and disconnecting when changing the `dbg_dummies` variable. This makes the debug dummies more useful for debugging. Previously, the debug dummies were considered invalid clients, whereas they are now considered to be ingame, so they should behave mostly like real clients being connected to the server. The debug dummies also have correct client names now, e.g. "Debug dummy 42". The game server code is cleaned up by moving all special handling for debug dummies to the engine server function `CServer::UpdateDebugDummies`. The left/right direction inputs for debug dummies are now properly added to the client input array, so their input handling should be consistent with normal clients, which fixes some inconsistent prediction with debug dummies.
935 lines
25 KiB
C++
935 lines
25 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 "entities/character.h"
|
|
#include "gamecontext.h"
|
|
#include "gamecontroller.h"
|
|
#include "score.h"
|
|
|
|
#include <base/system.h>
|
|
|
|
#include <engine/antibot.h>
|
|
#include <engine/server.h>
|
|
#include <engine/shared/config.h>
|
|
|
|
#include <game/gamecore.h>
|
|
#include <game/teamscore.h>
|
|
|
|
MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS)
|
|
|
|
IServer *CPlayer::Server() const { return m_pGameServer->Server(); }
|
|
|
|
CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientID, int ClientID, int Team) :
|
|
m_UniqueClientID(UniqueClientID)
|
|
{
|
|
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_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_aTimeoutCode[0] = '\0';
|
|
delete m_pLastTarget;
|
|
m_pLastTarget = new CNetObj_PlayerInput({0});
|
|
m_LastTargetInit = false;
|
|
m_TuneZone = 0;
|
|
m_TuneZoneOld = m_TuneZone;
|
|
m_Halloween = false;
|
|
m_FirstPacket = true;
|
|
|
|
m_SendVoteIndex = -1;
|
|
|
|
if(g_Config.m_Events)
|
|
{
|
|
const ETimeSeason Season = time_season();
|
|
if(Season == SEASON_NEWYEAR)
|
|
{
|
|
m_DefEmote = EMOTE_HAPPY;
|
|
}
|
|
else if(Season == SEASON_HALLOWEEN)
|
|
{
|
|
m_DefEmote = EMOTE_ANGRY;
|
|
m_Halloween = true;
|
|
}
|
|
else
|
|
{
|
|
m_DefEmote = EMOTE_NORMAL;
|
|
}
|
|
}
|
|
m_OverrideEmoteReset = -1;
|
|
|
|
GameServer()->Score()->PlayerData(m_ClientID)->Reset();
|
|
|
|
m_Last_KickVote = 0;
|
|
m_Last_Team = 0;
|
|
m_ShowOthers = g_Config.m_SvShowOthersDefault;
|
|
m_ShowAll = g_Config.m_SvShowAllDefault;
|
|
m_ShowDistance = vec2(1200, 800);
|
|
m_SpecTeam = false;
|
|
m_NinjaJetpack = false;
|
|
|
|
m_Paused = PAUSE_NONE;
|
|
m_DND = false;
|
|
|
|
m_LastPause = 0;
|
|
m_Score.reset();
|
|
|
|
// Variable initialized:
|
|
m_Last_Team = 0;
|
|
m_LastSQLQuery = 0;
|
|
m_ScoreQueryResult = nullptr;
|
|
m_ScoreFinishResult = nullptr;
|
|
|
|
int64_t Now = Server()->Tick();
|
|
int64_t 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;
|
|
m_SwapTargetsClientID = -1;
|
|
m_BirthdayAnnounced = false;
|
|
}
|
|
|
|
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()
|
|
{
|
|
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])
|
|
{
|
|
SetAfk(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, -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CPlayer::PostTick()
|
|
{
|
|
// update latency value
|
|
if(m_PlayerFlags & PLAYERFLAG_IN_MENU)
|
|
m_aCurLatency[m_ClientID] = GameServer()->m_apPlayers[m_ClientID]->m_Latency.m_Min;
|
|
|
|
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_aCurLatency[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()
|
|
{
|
|
if(!Server()->ClientIngame(m_ClientID))
|
|
return;
|
|
|
|
if(!GameServer()->m_World.m_Paused && !m_pCharacter && m_Spawning && m_WeakHookSpawn)
|
|
TryRespawn();
|
|
}
|
|
|
|
void CPlayer::Snap(int SnappingClient)
|
|
{
|
|
if(!Server()->ClientIngame(m_ClientID))
|
|
return;
|
|
|
|
int id = m_ClientID;
|
|
if(!Server()->Translate(id, SnappingClient))
|
|
return;
|
|
|
|
CNetObj_ClientInfo *pClientInfo = Server()->SnapNewItem<CNetObj_ClientInfo>(id);
|
|
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_aSkinName);
|
|
pClientInfo->m_UseCustomColor = m_TeeInfos.m_UseCustomColor;
|
|
pClientInfo->m_ColorBody = m_TeeInfos.m_ColorBody;
|
|
pClientInfo->m_ColorFeet = m_TeeInfos.m_ColorFeet;
|
|
|
|
int SnappingClientVersion = GameServer()->GetClientVersion(SnappingClient);
|
|
int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientID];
|
|
|
|
int Score;
|
|
// This is the time sent to the player while ingame (do not confuse to the one reported to the master server).
|
|
// Due to clients expecting this as a negative value, we have to make sure it's negative.
|
|
// Special numbers:
|
|
// -9999: means no time and isn't displayed in the scoreboard.
|
|
if(m_Score.has_value())
|
|
{
|
|
// shift the time by a second if the player actually took 9999
|
|
// seconds to finish the map.
|
|
if(m_Score.value() == 9999)
|
|
Score = -10000;
|
|
else
|
|
Score = -m_Score.value();
|
|
}
|
|
else
|
|
{
|
|
Score = -9999;
|
|
}
|
|
|
|
// send 0 if times of others are not shown
|
|
if(SnappingClient != m_ClientID && g_Config.m_SvHideScore)
|
|
Score = -9999;
|
|
|
|
if(!Server()->IsSixup(SnappingClient))
|
|
{
|
|
CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem<CNetObj_PlayerInfo>(id);
|
|
if(!pPlayerInfo)
|
|
return;
|
|
|
|
pPlayerInfo->m_Latency = Latency;
|
|
pPlayerInfo->m_Score = Score;
|
|
pPlayerInfo->m_Local = (int)(m_ClientID == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD));
|
|
pPlayerInfo->m_ClientID = id;
|
|
pPlayerInfo->m_Team = m_Team;
|
|
if(SnappingClientVersion < VERSION_DDNET_INDEPENDENT_SPECTATORS_TEAM)
|
|
{
|
|
// 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.
|
|
pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientID != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
protocol7::CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem<protocol7::CNetObj_PlayerInfo>(id);
|
|
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(!Server()->IsSixup(SnappingClient))
|
|
{
|
|
CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem<CNetObj_SpectatorInfo>(m_ClientID);
|
|
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 = Server()->SnapNewItem<protocol7::CNetObj_SpectatorInfo>(m_ClientID);
|
|
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 = Server()->SnapNewItem<CNetObj_DDNetPlayer>(id);
|
|
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(Server()->IsSixup(SnappingClient) && m_pCharacter && m_pCharacter->m_DDRaceState == DDRACE_STARTED &&
|
|
GameServer()->m_apPlayers[SnappingClient]->m_TimerType == TIMERTYPE_SIXUP)
|
|
{
|
|
protocol7::CNetObj_PlayerInfoRace *pRaceInfo = Server()->SnapNewItem<protocol7::CNetObj_PlayerInfoRace>(id);
|
|
if(!pRaceInfo)
|
|
return;
|
|
pRaceInfo->m_RaceStartTick = m_pCharacter->m_StartTime;
|
|
}
|
|
|
|
bool ShowSpec = m_pCharacter && m_pCharacter->IsPaused() && m_pCharacter->CanSnapCharacter(SnappingClient);
|
|
|
|
if(SnappingClient != SERVER_DEMO_CLIENT)
|
|
{
|
|
CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient];
|
|
ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(m_ClientID) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()));
|
|
}
|
|
|
|
if(ShowSpec)
|
|
{
|
|
CNetObj_SpecChar *pSpecChar = Server()->SnapNewItem<CNetObj_SpecChar>(id);
|
|
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 = Server()->SnapNewItem<CNetObj_ClientInfo>(FakeID);
|
|
|
|
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 = Server()->SnapNewItem<CNetObj_PlayerInfo>(FakeID);
|
|
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 = Server()->SnapNewItem<CNetObj_SpectatorInfo>(FakeID);
|
|
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 *pNewInput)
|
|
{
|
|
// skip the input if chat is active
|
|
if((m_PlayerFlags & PLAYERFLAG_CHATTING) && (pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING))
|
|
return;
|
|
|
|
AfkTimer();
|
|
|
|
m_NumInputs++;
|
|
|
|
if(m_pCharacter && !m_Paused)
|
|
m_pCharacter->OnPredictedInput(pNewInput);
|
|
|
|
// 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.org/discord", m_ClientID);
|
|
}
|
|
|
|
void CPlayer::OnDirectInput(CNetObj_PlayerInput *pNewInput)
|
|
{
|
|
Server()->SetClientFlags(m_ClientID, pNewInput->m_PlayerFlags);
|
|
|
|
AfkTimer();
|
|
|
|
if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorID == SPEC_FREEVIEW)
|
|
m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY);
|
|
|
|
// check for activity
|
|
if(mem_comp(pNewInput, m_pLastTarget, sizeof(CNetObj_PlayerInput)))
|
|
{
|
|
mem_copy(m_pLastTarget, pNewInput, sizeof(CNetObj_PlayerInput));
|
|
// Ignore the first direct input and keep the player afk as it is sent automatically
|
|
if(m_LastTargetInit)
|
|
UpdatePlaytime();
|
|
m_LastActionTick = Server()->Tick();
|
|
m_LastTargetInit = true;
|
|
}
|
|
}
|
|
|
|
void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput)
|
|
{
|
|
m_PlayerFlags = pNewInput->m_PlayerFlags;
|
|
|
|
if(!m_pCharacter && m_Team != TEAM_SPECTATORS && (pNewInput->m_Fire & 1))
|
|
m_Spawning = true;
|
|
|
|
// skip the input if chat is active
|
|
if(m_PlayerFlags & PLAYERFLAG_CHATTING)
|
|
return;
|
|
|
|
if(m_pCharacter && !m_Paused)
|
|
m_pCharacter->OnDirectInput(pNewInput);
|
|
}
|
|
|
|
int CPlayer::GetClientVersion() const
|
|
{
|
|
return m_pGameServer->GetClientVersion(m_ClientID);
|
|
}
|
|
|
|
CCharacter *CPlayer::GetCharacter()
|
|
{
|
|
if(m_pCharacter && m_pCharacter->IsAlive())
|
|
return m_pCharacter;
|
|
return 0;
|
|
}
|
|
|
|
const CCharacter *CPlayer::GetCharacter() const
|
|
{
|
|
if(m_pCharacter && m_pCharacter->IsAlive())
|
|
return m_pCharacter;
|
|
return 0;
|
|
}
|
|
|
|
void CPlayer::KillCharacter(int Weapon, bool SendKillMsg)
|
|
{
|
|
if(m_pCharacter)
|
|
{
|
|
m_pCharacter->Die(m_ClientID, Weapon, SendKillMsg);
|
|
|
|
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, GameServer()->GetLastPlayerInput(m_ClientID));
|
|
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, GameServer()->GetDDRaceTeam(m_ClientID)))
|
|
return;
|
|
|
|
m_WeakHookSpawn = false;
|
|
m_Spawning = false;
|
|
m_pCharacter = new(m_ClientID) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(m_ClientID));
|
|
m_ViewPos = SpawnPos;
|
|
m_pCharacter->Spawn(this, SpawnPos);
|
|
GameServer()->CreatePlayerSpawn(SpawnPos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
|
|
|
|
if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
|
|
m_pCharacter->SetSolo(true);
|
|
}
|
|
|
|
void CPlayer::UpdatePlaytime()
|
|
{
|
|
m_LastPlaytime = time_get();
|
|
}
|
|
|
|
void CPlayer::AfkTimer()
|
|
{
|
|
SetAfk(g_Config.m_SvMaxAfkTime != 0 && m_LastPlaytime < time_get() - time_freq() * g_Config.m_SvMaxAfkTime);
|
|
}
|
|
|
|
void CPlayer::SetAfk(bool Afk)
|
|
{
|
|
if(m_Afk != Afk)
|
|
{
|
|
Server()->ExpireServerInfo();
|
|
m_Afk = Afk;
|
|
}
|
|
}
|
|
|
|
void CPlayer::SetInitialAfk(bool Afk)
|
|
{
|
|
if(g_Config.m_SvMaxAfkTime == 0)
|
|
{
|
|
SetAfk(false);
|
|
return;
|
|
}
|
|
|
|
SetAfk(Afk);
|
|
|
|
// Ensure that the AFK state is not reset again automatically
|
|
if(Afk)
|
|
m_LastPlaytime = time_get() - time_freq() * g_Config.m_SvMaxAfkTime - 1;
|
|
else
|
|
m_LastPlaytime = time_get();
|
|
}
|
|
|
|
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);
|
|
m_ViewPos = m_pCharacter->m_Pos;
|
|
GameServer()->CreatePlayerSpawn(m_pCharacter->m_Pos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID));
|
|
}
|
|
[[fallthrough]];
|
|
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()
|
|
{
|
|
return m_pCharacter && m_pCharacter->IsAlive();
|
|
}
|
|
|
|
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:
|
|
{
|
|
bool PrimaryMessage = true;
|
|
for(auto &aMessage : Result.m_Data.m_aaMessages)
|
|
{
|
|
if(aMessage[0] == 0)
|
|
break;
|
|
|
|
if(GameServer()->ProcessSpamProtection(m_ClientID) && PrimaryMessage)
|
|
break;
|
|
|
|
GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aMessage, -1);
|
|
PrimaryMessage = false;
|
|
}
|
|
break;
|
|
}
|
|
case CScorePlayerResult::BROADCAST:
|
|
if(Result.m_Data.m_aBroadcast[0] != 0)
|
|
GameServer()->SendBroadcast(Result.m_Data.m_aBroadcast, -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_aServer, Result.m_Data.m_MapVote.m_aMap);
|
|
|
|
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_aMap, "/map");
|
|
|
|
GameServer()->CallVote(m_ClientID, Result.m_Data.m_MapVote.m_aMap, aCmd, "/map", aChatmsg);
|
|
break;
|
|
case CScorePlayerResult::PLAYER_INFO:
|
|
if(Result.m_Data.m_Info.m_Time.has_value())
|
|
{
|
|
GameServer()->Score()->PlayerData(m_ClientID)->Set(Result.m_Data.m_Info.m_Time.value(), Result.m_Data.m_Info.m_aTimeCp);
|
|
m_Score = Result.m_Data.m_Info.m_Time;
|
|
}
|
|
Server()->ExpireServerInfo();
|
|
int Birthday = Result.m_Data.m_Info.m_Birthday;
|
|
if(Birthday != 0 && !m_BirthdayAnnounced)
|
|
{
|
|
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);
|
|
m_BirthdayAnnounced = true;
|
|
}
|
|
GameServer()->SendRecord(m_ClientID);
|
|
break;
|
|
}
|
|
}
|
|
}
|