ddnet/src/game/server/gamecontext.cpp

4666 lines
134 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (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. */
2023-02-23 09:48:11 +00:00
#include "gamecontext.h"
#include <vector>
2011-08-31 11:56:04 +00:00
#include "teeinfo.h"
#include <antibot/antibot_data.h>
#include <base/logger.h>
#include <base/math.h>
2023-02-23 09:48:11 +00:00
#include <base/system.h>
2010-05-29 07:25:38 +00:00
#include <engine/console.h>
#include <engine/engine.h>
#include <engine/map.h>
2018-08-16 19:42:10 +00:00
#include <engine/server/server.h>
#include <engine/shared/config.h>
#include <engine/shared/datafile.h>
#include <engine/shared/json.h>
#include <engine/shared/linereader.h>
#include <engine/shared/memheap.h>
#include <engine/storage.h>
2022-06-16 16:06:35 +00:00
2010-05-29 07:25:38 +00:00
#include <game/collision.h>
#include <game/gamecore.h>
2022-06-16 16:06:35 +00:00
#include <game/mapitems.h>
#include <game/version.h>
2020-04-16 08:46:43 +00:00
#include <game/generated/protocol7.h>
#include <game/generated/protocolglue.h>
#include "entities/character.h"
#include "gamemodes/DDRace.h"
2023-11-16 10:39:55 +00:00
#include "gamemodes/mod.h"
#include "player.h"
#include "score.h"
// Not thread-safe!
class CClientChatLogger : public ILogger
{
CGameContext *m_pGameServer;
int m_ClientID;
ILogger *m_pOuterLogger;
public:
CClientChatLogger(CGameContext *pGameServer, int ClientID, ILogger *pOuterLogger) :
m_pGameServer(pGameServer),
m_ClientID(ClientID),
m_pOuterLogger(pOuterLogger)
{
}
void Log(const CLogMessage *pMessage) override;
};
void CClientChatLogger::Log(const CLogMessage *pMessage)
{
if(str_comp(pMessage->m_aSystem, "chatresp") == 0)
{
if(m_Filter.Filters(pMessage))
{
return;
}
m_pGameServer->SendChatTarget(m_ClientID, pMessage->Message());
}
else
{
m_pOuterLogger->Log(pMessage);
}
}
2010-05-29 07:25:38 +00:00
enum
{
RESET,
NO_RESET
};
2010-05-29 07:25:38 +00:00
void CGameContext::Construct(int Resetting)
{
2021-07-22 19:49:26 +00:00
m_Resetting = false;
2010-05-29 07:25:38 +00:00
m_pServer = 0;
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
pPlayer = 0;
mem_zero(&m_aLastPlayerInput, sizeof(m_aLastPlayerInput));
mem_zero(&m_aPlayerHasInput, sizeof(m_aPlayerHasInput));
2010-05-29 07:25:38 +00:00
m_pController = 0;
2021-07-22 19:49:26 +00:00
m_aVoteCommand[0] = 0;
2020-07-01 12:02:47 +00:00
m_VoteType = VOTE_TYPE_UNKNOWN;
2010-05-29 07:25:38 +00:00
m_VoteCloseTime = 0;
m_pVoteOptionFirst = 0;
m_pVoteOptionLast = 0;
m_NumVoteOptions = 0;
2014-03-04 01:48:58 +00:00
m_LastMapVote = 0;
2010-05-29 07:25:38 +00:00
m_SqlRandomMapResult = nullptr;
m_pScore = nullptr;
m_NumMutes = 0;
m_NumVoteMutes = 0;
m_LatestLog = 0;
mem_zero(&m_aLogs, sizeof(m_aLogs));
2022-11-29 10:47:36 +00:00
if(Resetting == NO_RESET)
2022-02-09 16:07:02 +00:00
{
m_NonEmptySince = 0;
2010-05-29 07:25:38 +00:00
m_pVoteOptionHeap = new CHeap();
2022-02-09 16:07:02 +00:00
}
m_aDeleteTempfile[0] = 0;
m_TeeHistorianActive = false;
2010-05-29 07:25:38 +00:00
}
void CGameContext::Destruct(int Resetting)
2010-05-29 07:25:38 +00:00
{
for(auto &pPlayer : m_apPlayers)
delete pPlayer;
if(Resetting == NO_RESET)
delete m_pVoteOptionHeap;
if(m_pScore)
{
delete m_pScore;
m_pScore = nullptr;
}
2010-05-29 07:25:38 +00:00
}
CGameContext::CGameContext()
{
Construct(NO_RESET);
2008-09-23 07:43:41 +00:00
}
2021-07-22 19:49:26 +00:00
CGameContext::CGameContext(int Reset)
{
Construct(Reset);
}
2010-05-29 07:25:38 +00:00
CGameContext::~CGameContext()
2008-09-23 07:43:41 +00:00
{
2021-07-22 19:49:26 +00:00
Destruct(m_Resetting ? RESET : NO_RESET);
}
2010-05-29 07:25:38 +00:00
void CGameContext::Clear()
{
2010-05-29 07:25:38 +00:00
CHeap *pVoteOptionHeap = m_pVoteOptionHeap;
2011-03-26 16:44:34 +00:00
CVoteOptionServer *pVoteOptionFirst = m_pVoteOptionFirst;
CVoteOptionServer *pVoteOptionLast = m_pVoteOptionLast;
int NumVoteOptions = m_NumVoteOptions;
2010-05-29 07:25:38 +00:00
CTuningParams Tuning = m_Tuning;
2021-07-22 19:49:26 +00:00
m_Resetting = true;
this->~CGameContext();
new(this) CGameContext(RESET);
2010-05-29 07:25:38 +00:00
m_pVoteOptionHeap = pVoteOptionHeap;
m_pVoteOptionFirst = pVoteOptionFirst;
m_pVoteOptionLast = pVoteOptionLast;
m_NumVoteOptions = NumVoteOptions;
2010-05-29 07:25:38 +00:00
m_Tuning = Tuning;
}
void CGameContext::TeeHistorianWrite(const void *pData, int DataSize, void *pUser)
{
CGameContext *pSelf = (CGameContext *)pUser;
aio_write(pSelf->m_pTeeHistorianFile, pData, DataSize);
}
void CGameContext::CommandCallback(int ClientID, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser)
{
CGameContext *pSelf = (CGameContext *)pUser;
if(pSelf->m_TeeHistorianActive)
{
pSelf->m_TeeHistorian.RecordConsoleCommand(ClientID, FlagMask, pCmd, pResult);
}
}
CNetObj_PlayerInput CGameContext::GetLastPlayerInput(int ClientID) const
{
dbg_assert(0 <= ClientID && ClientID < MAX_CLIENTS, "invalid ClientID");
return m_aLastPlayerInput[ClientID];
}
class CCharacter *CGameContext::GetPlayerChar(int ClientID)
{
if(ClientID < 0 || ClientID >= MAX_CLIENTS || !m_apPlayers[ClientID])
return 0;
return m_apPlayers[ClientID]->GetCharacter();
}
bool CGameContext::EmulateBug(int Bug)
{
return m_MapBugs.Contains(Bug);
}
void CGameContext::FillAntibot(CAntibotRoundData *pData)
{
if(!pData->m_Map.m_pTiles)
{
Collision()->FillAntibot(&pData->m_Map);
}
pData->m_Tick = Server()->Tick();
mem_zero(pData->m_aCharacters, sizeof(pData->m_aCharacters));
for(int i = 0; i < MAX_CLIENTS; i++)
{
CAntibotCharacterData *pChar = &pData->m_aCharacters[i];
2020-10-26 14:14:07 +00:00
for(auto &LatestInput : pChar->m_aLatestInputs)
{
2020-10-26 14:14:07 +00:00
LatestInput.m_TargetX = -1;
LatestInput.m_TargetY = -1;
}
pChar->m_Alive = false;
pChar->m_Pause = false;
pChar->m_Team = -1;
pChar->m_Pos = vec2(-1, -1);
pChar->m_Vel = vec2(0, 0);
pChar->m_Angle = -1;
pChar->m_HookedPlayer = -1;
pChar->m_SpawnTick = -1;
pChar->m_WeaponChangeTick = -1;
if(m_apPlayers[i])
{
str_copy(pChar->m_aName, Server()->ClientName(i), sizeof(pChar->m_aName));
CCharacter *pGameChar = m_apPlayers[i]->GetCharacter();
pChar->m_Alive = (bool)pGameChar;
pChar->m_Pause = m_apPlayers[i]->IsPaused();
pChar->m_Team = m_apPlayers[i]->GetTeam();
if(pGameChar)
{
pGameChar->FillAntibot(pChar);
}
}
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreateDamageInd(vec2 Pos, float Angle, int Amount, CClientMask Mask)
{
2022-11-06 09:48:06 +00:00
float a = 3 * pi / 2 + Angle;
//float a = get_angle(dir);
float s = a - pi / 3;
float e = a + pi / 3;
2010-05-29 07:25:38 +00:00
for(int i = 0; i < Amount; i++)
{
float f = mix(s, e, (i + 1) / (float)(Amount + 2));
CNetEvent_DamageInd *pEvent = m_Events.Create<CNetEvent_DamageInd>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
pEvent->m_Angle = (int)(f * 256.0f);
}
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreateHammerHit(vec2 Pos, CClientMask Mask)
{
// create the event
CNetEvent_HammerHit *pEvent = m_Events.Create<CNetEvent_HammerHit>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask)
{
// create the event
CNetEvent_Explosion *pEvent = m_Events.Create<CNetEvent_Explosion>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
}
2014-04-13 14:41:02 +00:00
// deal damage
CEntity *apEnts[MAX_CLIENTS];
float Radius = 135.0f;
float InnerRadius = 48.0f;
int Num = m_World.FindEntities(Pos, Radius, apEnts, MAX_CLIENTS, CGameWorld::ENTTYPE_CHARACTER);
2023-01-24 08:27:29 +00:00
CClientMask TeamMask = CClientMask().set();
for(int i = 0; i < Num; i++)
{
auto *pChr = static_cast<CCharacter *>(apEnts[i]);
vec2 Diff = pChr->m_Pos - Pos;
vec2 ForceDir(0, 1);
float l = length(Diff);
if(l)
ForceDir = normalize(Diff);
l = 1 - clamp((l - InnerRadius) / (Radius - InnerRadius), 0.0f, 1.0f);
float Strength;
if(Owner == -1 || !m_apPlayers[Owner] || !m_apPlayers[Owner]->m_TuneZone)
Strength = Tuning()->m_ExplosionStrength;
else
Strength = TuningList()[m_apPlayers[Owner]->m_TuneZone].m_ExplosionStrength;
2018-04-04 17:48:46 +00:00
float Dmg = Strength * l;
if(!(int)Dmg)
continue;
2018-04-04 17:48:46 +00:00
if((GetPlayerChar(Owner) ? !GetPlayerChar(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit) || NoDamage || Owner == pChr->GetPlayer()->GetCID())
{
if(Owner != -1 && pChr->IsAlive() && !pChr->CanCollide(Owner))
continue;
if(Owner == -1 && ActivatedTeam != -1 && pChr->IsAlive() && pChr->Team() != ActivatedTeam)
continue;
2018-04-04 17:48:46 +00:00
// Explode at most once per team
int PlayerTeam = pChr->Team();
if((GetPlayerChar(Owner) ? GetPlayerChar(Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit) || NoDamage)
{
if(PlayerTeam == TEAM_SUPER)
continue;
2023-01-24 08:27:29 +00:00
if(!TeamMask.test(PlayerTeam))
continue;
2023-01-24 08:27:29 +00:00
TeamMask.reset(PlayerTeam);
2018-04-04 18:03:05 +00:00
}
pChr->TakeDamage(ForceDir * Dmg * 2, (int)Dmg, Owner, Weapon);
}
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreatePlayerSpawn(vec2 Pos, CClientMask Mask)
{
// create the event
CNetEvent_Spawn *pEvent = m_Events.Create<CNetEvent_Spawn>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreateDeath(vec2 Pos, int ClientID, CClientMask Mask)
{
// create the event
CNetEvent_Death *pEvent = m_Events.Create<CNetEvent_Death>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
pEvent->m_ClientID = ClientID;
}
}
2023-01-24 08:27:29 +00:00
void CGameContext::CreateSound(vec2 Pos, int Sound, CClientMask Mask)
{
if(Sound < 0)
return;
// create a sound
CNetEvent_SoundWorld *pEvent = m_Events.Create<CNetEvent_SoundWorld>(Mask);
if(pEvent)
{
pEvent->m_X = (int)Pos.x;
pEvent->m_Y = (int)Pos.y;
pEvent->m_SoundID = Sound;
}
}
2010-05-29 07:25:38 +00:00
void CGameContext::CreateSoundGlobal(int Sound, int Target)
{
if(Sound < 0)
return;
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_SoundGlobal Msg;
Msg.m_SoundID = Sound;
if(Target == -2)
Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, -1);
else
{
int Flag = MSGFLAG_VITAL;
if(Target != -1)
Flag |= MSGFLAG_NORECORD;
Server()->SendPackMsg(&Msg, Flag, Target);
}
}
bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapID, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber)
{
if(Context.GetClientVersion() >= VERSION_DDNET_MULTI_LASER)
{
CNetObj_DDNetLaser *pObj = Server()->SnapNewItem<CNetObj_DDNetLaser>(SnapID);
if(!pObj)
return false;
pObj->m_ToX = (int)To.x;
pObj->m_ToY = (int)To.y;
pObj->m_FromX = (int)From.x;
pObj->m_FromY = (int)From.y;
pObj->m_StartTick = StartTick;
pObj->m_Owner = Owner;
pObj->m_Type = LaserType;
pObj->m_Subtype = Subtype;
pObj->m_SwitchNumber = SwitchNumber;
pObj->m_Flags = 0;
}
else
{
CNetObj_Laser *pObj = Server()->SnapNewItem<CNetObj_Laser>(SnapID);
if(!pObj)
return false;
pObj->m_X = (int)To.x;
pObj->m_Y = (int)To.y;
pObj->m_FromX = (int)From.x;
pObj->m_FromY = (int)From.y;
pObj->m_StartTick = StartTick;
}
return true;
}
bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapID, const vec2 &Pos, int Type, int SubType, int SwitchNumber)
{
if(Context.IsSixup())
{
protocol7::CNetObj_Pickup *pPickup = Server()->SnapNewItem<protocol7::CNetObj_Pickup>(SnapID);
if(!pPickup)
return false;
pPickup->m_X = (int)Pos.x;
pPickup->m_Y = (int)Pos.y;
if(Type == POWERUP_WEAPON)
pPickup->m_Type = SubType == WEAPON_SHOTGUN ? protocol7::PICKUP_SHOTGUN : SubType == WEAPON_GRENADE ? protocol7::PICKUP_GRENADE : protocol7::PICKUP_LASER;
else if(Type == POWERUP_NINJA)
pPickup->m_Type = protocol7::PICKUP_NINJA;
}
else if(Context.GetClientVersion() >= VERSION_DDNET_ENTITY_NETOBJS)
{
CNetObj_DDNetPickup *pPickup = Server()->SnapNewItem<CNetObj_DDNetPickup>(SnapID);
if(!pPickup)
return false;
pPickup->m_X = (int)Pos.x;
pPickup->m_Y = (int)Pos.y;
pPickup->m_Type = Type;
pPickup->m_Subtype = SubType;
pPickup->m_SwitchNumber = SwitchNumber;
}
else
{
CNetObj_Pickup *pPickup = Server()->SnapNewItem<CNetObj_Pickup>(SnapID);
if(!pPickup)
return false;
pPickup->m_X = (int)Pos.x;
pPickup->m_Y = (int)Pos.y;
pPickup->m_Type = Type;
if(Context.GetClientVersion() < VERSION_DDNET_WEAPON_SHIELDS)
{
if(Type >= POWERUP_ARMOR_SHOTGUN && Type <= POWERUP_ARMOR_LASER)
{
pPickup->m_Type = POWERUP_ARMOR;
}
}
pPickup->m_Subtype = SubType;
}
return true;
}
2020-08-04 17:14:37 +00:00
void CGameContext::CallVote(int ClientID, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc)
{
// check if a vote is already running
if(m_VoteCloseTime)
return;
2021-06-23 05:05:49 +00:00
int64_t Now = Server()->Tick();
CPlayer *pPlayer = m_apPlayers[ClientID];
if(!pPlayer)
return;
2020-08-04 17:14:37 +00:00
SendChat(-1, CGameContext::CHAT_ALL, pChatmsg, -1, CHAT_SIX);
if(!pSixupDesc)
pSixupDesc = pDesc;
m_VoteCreator = ClientID;
2020-08-04 17:14:37 +00:00
StartVote(pDesc, pCmd, pReason, pSixupDesc);
pPlayer->m_Vote = 1;
pPlayer->m_VotePos = m_VotePos = 1;
pPlayer->m_LastVoteCall = Now;
}
2020-06-22 15:08:08 +00:00
void CGameContext::SendChatTarget(int To, const char *pText, int Flags)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Chat Msg;
Msg.m_Team = 0;
Msg.m_ClientID = -1;
2010-05-29 07:25:38 +00:00
Msg.m_pMessage = pText;
2020-06-24 16:22:41 +00:00
if(g_Config.m_SvDemoChat)
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);
2020-06-24 16:22:41 +00:00
if(To == -1)
{
for(int i = 0; i < Server()->MaxClients(); i++)
{
if(!((Server()->IsSixup(i) && (Flags & CHAT_SIXUP)) ||
(!Server()->IsSixup(i) && (Flags & CHAT_SIX))))
continue;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
}
}
else
{
if(!((Server()->IsSixup(To) && (Flags & CHAT_SIXUP)) ||
(!Server()->IsSixup(To) && (Flags & CHAT_SIX))))
return;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, To);
}
}
2014-07-26 12:46:31 +00:00
void CGameContext::SendChatTeam(int Team, const char *pText)
{
for(int i = 0; i < MAX_CLIENTS; i++)
if(m_apPlayers[i] != nullptr && GetDDRaceTeam(i) == Team)
2014-07-26 12:46:31 +00:00
SendChatTarget(i, pText);
}
2020-06-12 13:25:58 +00:00
void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, int SpamProtectionClientID, int Flags)
{
if(SpamProtectionClientID >= 0 && SpamProtectionClientID < MAX_CLIENTS)
2011-02-23 21:39:53 +00:00
if(ProcessSpamProtection(SpamProtectionClientID))
return;
char aBuf[256], aText[256];
str_copy(aText, pText, sizeof(aText));
if(ChatterClientID >= 0 && ChatterClientID < MAX_CLIENTS)
str_format(aBuf, sizeof(aBuf), "%d:%d:%s: %s", ChatterClientID, Team, Server()->ClientName(ChatterClientID), aText);
else if(ChatterClientID == -2)
{
str_format(aBuf, sizeof(aBuf), "### %s", aText);
str_copy(aText, aBuf, sizeof(aText));
ChatterClientID = -1;
}
else
str_format(aBuf, sizeof(aBuf), "*** %s", aText);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, Team != CHAT_ALL ? "teamchat" : "chat", aBuf);
2010-05-29 07:25:38 +00:00
if(Team == CHAT_ALL)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Chat Msg;
Msg.m_Team = 0;
Msg.m_ClientID = ChatterClientID;
Msg.m_pMessage = aText;
2013-12-22 17:30:13 +00:00
// pack one for the recording only
if(g_Config.m_SvDemoChat)
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);
2013-12-22 17:30:13 +00:00
// send to the clients
for(int i = 0; i < Server()->MaxClients(); i++)
2013-12-22 17:30:13 +00:00
{
if(!m_apPlayers[i])
continue;
2020-06-12 19:22:54 +00:00
bool Send = (Server()->IsSixup(i) && (Flags & CHAT_SIXUP)) ||
(!Server()->IsSixup(i) && (Flags & CHAT_SIX));
2020-06-12 19:22:54 +00:00
if(!m_apPlayers[i]->m_DND && Send)
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
2013-12-22 17:30:13 +00:00
}
2022-11-29 10:47:36 +00:00
2023-11-19 21:36:47 +00:00
str_format(aBuf, sizeof(aBuf), "Chat: %s", aText);
2022-11-29 10:47:36 +00:00
LogEvent(aBuf, ChatterClientID);
}
else
{
CTeamsCore *pTeams = &m_pController->Teams().m_Core;
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Chat Msg;
Msg.m_Team = 1;
Msg.m_ClientID = ChatterClientID;
Msg.m_pMessage = aText;
// pack one for the recording only
if(g_Config.m_SvDemoChat)
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);
2011-01-29 00:59:50 +00:00
// send to the clients
for(int i = 0; i < Server()->MaxClients(); i++)
{
if(m_apPlayers[i] != 0)
{
if(Team == CHAT_SPEC)
{
if(m_apPlayers[i]->GetTeam() == CHAT_SPEC)
{
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
}
}
else
{
if(pTeams->Team(i) == Team && m_apPlayers[i]->GetTeam() != CHAT_SPEC)
{
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
}
}
}
}
}
}
void CGameContext::SendStartWarning(int ClientID, const char *pMessage)
{
CCharacter *pChr = GetPlayerChar(ClientID);
if(pChr && pChr->m_LastStartWarning < Server()->Tick() - 3 * Server()->TickSpeed())
{
SendChatTarget(ClientID, pMessage);
pChr->m_LastStartWarning = Server()->Tick();
}
}
void CGameContext::SendEmoticon(int ClientID, int Emoticon, int TargetClientID)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Emoticon Msg;
Msg.m_ClientID = ClientID;
2010-05-29 07:25:38 +00:00
Msg.m_Emoticon = Emoticon;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientID);
}
void CGameContext::SendWeaponPickup(int ClientID, int Weapon)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_WeaponPickup Msg;
Msg.m_Weapon = Weapon;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
void CGameContext::SendMotd(int ClientID)
{
CNetMsg_Sv_Motd Msg;
Msg.m_pMessage = g_Config.m_SvMotd;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
void CGameContext::SendSettings(int ClientID)
{
protocol7::CNetMsg_Sv_ServerSettings Msg;
Msg.m_KickVote = g_Config.m_SvVoteKick;
Msg.m_KickMin = g_Config.m_SvVoteKickMin;
Msg.m_SpecVote = g_Config.m_SvVoteSpectate;
Msg.m_TeamLock = 0;
Msg.m_TeamBalance = 0;
Msg.m_PlayerSlots = g_Config.m_SvMaxClients - g_Config.m_SvSpectatorSlots;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
}
2018-03-07 16:22:41 +00:00
void CGameContext::SendBroadcast(const char *pText, int ClientID, bool IsImportant)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Broadcast Msg;
Msg.m_pMessage = pText;
2018-03-07 16:22:41 +00:00
2018-03-07 17:10:57 +00:00
if(ClientID == -1)
2018-03-07 16:22:41 +00:00
{
dbg_assert(IsImportant, "broadcast messages to all players must be important");
2018-03-07 16:22:41 +00:00
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
2018-03-07 16:22:41 +00:00
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
2018-03-07 16:22:41 +00:00
{
2020-10-26 14:14:07 +00:00
pPlayer->m_LastBroadcastImportance = true;
pPlayer->m_LastBroadcast = Server()->Tick();
2018-03-07 16:22:41 +00:00
}
}
return;
}
2018-03-07 17:10:57 +00:00
if(!m_apPlayers[ClientID])
2018-03-07 16:22:41 +00:00
return;
2018-03-15 21:35:25 +00:00
if(!IsImportant && m_apPlayers[ClientID]->m_LastBroadcastImportance && m_apPlayers[ClientID]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10)
2018-03-07 16:22:41 +00:00
return;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
2018-03-07 16:22:41 +00:00
m_apPlayers[ClientID]->m_LastBroadcast = Server()->Tick();
m_apPlayers[ClientID]->m_LastBroadcastImportance = IsImportant;
}
2020-08-04 17:14:37 +00:00
void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc)
2008-09-24 14:47:03 +00:00
{
// reset votes
2010-05-29 07:25:38 +00:00
m_VoteEnforce = VOTE_ENFORCE_UNKNOWN;
2011-12-26 09:15:43 +00:00
m_VoteEnforcer = -1;
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
2008-09-24 14:47:03 +00:00
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
2010-05-29 07:25:38 +00:00
{
2020-10-26 14:14:07 +00:00
pPlayer->m_Vote = 0;
pPlayer->m_VotePos = 0;
2010-05-29 07:25:38 +00:00
}
2008-09-24 14:47:03 +00:00
}
2008-09-24 14:47:03 +00:00
// start vote
2014-05-19 00:23:01 +00:00
m_VoteCloseTime = time_get() + time_freq() * g_Config.m_SvVoteTime;
2010-05-29 07:25:38 +00:00
str_copy(m_aVoteDescription, pDesc, sizeof(m_aVoteDescription));
2020-08-04 17:14:37 +00:00
str_copy(m_aSixupVoteDescription, pSixupDesc, sizeof(m_aSixupVoteDescription));
2010-05-29 07:25:38 +00:00
str_copy(m_aVoteCommand, pCommand, sizeof(m_aVoteCommand));
str_copy(m_aVoteReason, pReason, sizeof(m_aVoteReason));
2010-05-29 07:25:38 +00:00
SendVoteSet(-1);
m_VoteUpdate = true;
2008-09-24 14:47:03 +00:00
}
2010-05-29 07:25:38 +00:00
void CGameContext::EndVote()
2008-09-25 12:41:37 +00:00
{
2010-05-29 07:25:38 +00:00
m_VoteCloseTime = 0;
SendVoteSet(-1);
2008-09-25 12:41:37 +00:00
}
void CGameContext::SendVoteSet(int ClientID)
2008-09-24 14:47:03 +00:00
{
2020-07-01 12:02:47 +00:00
::CNetMsg_Sv_VoteSet Msg6;
protocol7::CNetMsg_Sv_VoteSet Msg7;
Msg7.m_ClientID = m_VoteCreator;
2010-05-29 07:25:38 +00:00
if(m_VoteCloseTime)
2008-09-24 14:47:03 +00:00
{
Msg6.m_Timeout = Msg7.m_Timeout = (m_VoteCloseTime - time_get()) / time_freq();
2020-08-04 17:14:37 +00:00
Msg6.m_pDescription = m_aVoteDescription;
Msg7.m_pDescription = m_aSixupVoteDescription;
2020-07-01 12:02:47 +00:00
Msg6.m_pReason = Msg7.m_pReason = m_aVoteReason;
int &Type = (Msg7.m_Type = protocol7::VOTE_UNKNOWN);
if(IsKickVote())
Type = protocol7::VOTE_START_KICK;
else if(IsSpecVote())
Type = protocol7::VOTE_START_SPEC;
else if(IsOptionVote())
Type = protocol7::VOTE_START_OP;
2008-09-24 14:47:03 +00:00
}
else
{
2020-07-01 12:02:47 +00:00
Msg6.m_Timeout = Msg7.m_Timeout = 0;
Msg6.m_pDescription = Msg7.m_pDescription = "";
Msg6.m_pReason = Msg7.m_pReason = "";
int &Type = (Msg7.m_Type = protocol7::VOTE_UNKNOWN);
if(m_VoteEnforce == VOTE_ENFORCE_NO || m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN)
Type = protocol7::VOTE_END_FAIL;
else if(m_VoteEnforce == VOTE_ENFORCE_YES || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
Type = protocol7::VOTE_END_PASS;
else if(m_VoteEnforce == VOTE_ENFORCE_ABORT)
Type = protocol7::VOTE_END_ABORT;
if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
Msg7.m_ClientID = -1;
}
if(ClientID == -1)
{
for(int i = 0; i < Server()->MaxClients(); i++)
2020-07-01 12:02:47 +00:00
{
if(!m_apPlayers[i])
continue;
if(!Server()->IsSixup(i))
Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, i);
else
Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, i);
}
}
else
{
if(!Server()->IsSixup(ClientID))
Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, ClientID);
else
Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, ClientID);
2008-09-24 14:47:03 +00:00
}
}
void CGameContext::SendVoteStatus(int ClientID, int Total, int Yes, int No)
2008-09-24 14:47:03 +00:00
{
if(ClientID == -1)
{
for(int i = 0; i < MAX_CLIENTS; ++i)
if(Server()->ClientIngame(i))
SendVoteStatus(i, Total, Yes, No);
return;
}
if(Total > VANILLA_MAX_CLIENTS && m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetClientVersion() <= VERSION_DDRACE)
{
Yes = (Yes * VANILLA_MAX_CLIENTS) / (float)Total;
No = (No * VANILLA_MAX_CLIENTS) / (float)Total;
Total = VANILLA_MAX_CLIENTS;
}
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_VoteStatus Msg = {0};
Msg.m_Total = Total;
Msg.m_Yes = Yes;
Msg.m_No = No;
Msg.m_Pass = Total - (Yes + No);
2010-05-29 07:25:38 +00:00
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
2010-05-29 07:25:38 +00:00
}
void CGameContext::AbortVoteKickOnDisconnect(int ClientID)
2010-05-29 07:25:38 +00:00
{
if(m_VoteCloseTime && ((str_startswith(m_aVoteCommand, "kick ") && str_toint(&m_aVoteCommand[5]) == ClientID) ||
(str_startswith(m_aVoteCommand, "set_team ") && str_toint(&m_aVoteCommand[9]) == ClientID)))
2020-07-01 12:02:47 +00:00
m_VoteEnforce = VOTE_ENFORCE_ABORT;
2010-05-29 07:25:38 +00:00
}
void CGameContext::CheckPureTuning()
{
// might not be created yet during start up
if(!m_pController)
return;
if(str_comp(m_pController->m_pGameType, "DM") == 0 ||
str_comp(m_pController->m_pGameType, "TDM") == 0 ||
str_comp(m_pController->m_pGameType, "CTF") == 0)
2008-09-24 14:47:03 +00:00
{
2011-01-29 00:59:50 +00:00
CTuningParams p;
if(mem_comp(&p, &m_Tuning, sizeof(p)) != 0)
2008-09-24 14:47:03 +00:00
{
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "resetting tuning due to pure server");
2011-01-29 00:59:50 +00:00
m_Tuning = p;
2008-09-24 14:47:03 +00:00
}
}
2008-09-24 14:47:03 +00:00
}
void CGameContext::SendTuningParams(int ClientID, int Zone)
{
if(ClientID == -1)
2014-03-29 13:54:43 +00:00
{
2017-04-22 17:04:16 +00:00
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(m_apPlayers[i])
2014-03-29 13:54:43 +00:00
{
2017-04-22 17:04:16 +00:00
if(m_apPlayers[i]->GetCharacter())
2014-03-29 13:54:43 +00:00
{
if(m_apPlayers[i]->GetCharacter()->m_TuneZone == Zone)
2014-03-29 13:54:43 +00:00
SendTuningParams(i, Zone);
2017-04-22 17:04:16 +00:00
}
else if(m_apPlayers[i]->m_TuneZone == Zone)
2017-04-22 17:04:16 +00:00
{
SendTuningParams(i, Zone);
2014-03-29 13:54:43 +00:00
}
}
2017-04-22 17:04:16 +00:00
}
return;
2014-03-29 13:54:43 +00:00
}
2015-07-09 00:08:14 +00:00
2010-05-29 07:25:38 +00:00
CheckPureTuning();
2010-05-29 07:25:38 +00:00
CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
int *pParams = 0;
if(Zone == 0)
pParams = (int *)&m_Tuning;
else
pParams = (int *)&(m_aTuningList[Zone]);
2014-06-26 15:37:06 +00:00
for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++)
2017-04-22 17:04:16 +00:00
{
if(m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetCharacter())
{
2020-09-25 14:41:17 +00:00
if((i == 30) // laser_damage is removed from 0.7
&& (Server()->IsSixup(ClientID)))
{
continue;
}
2020-09-25 14:41:17 +00:00
else if((i == 31) // collision
&& (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOCOLL))
{
2017-04-22 17:04:16 +00:00
Msg.AddInt(0);
}
else if((i == 32) // hooking
&& (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHOOK))
2017-04-22 17:04:16 +00:00
{
Msg.AddInt(0);
}
else if((i == 3) // ground jump impulse
&& m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOJUMP)
2017-04-22 17:04:16 +00:00
{
Msg.AddInt(0);
}
else if((i == 33) // jetpack
&& !(m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_JETPACK))
2017-04-22 17:04:16 +00:00
{
Msg.AddInt(0);
}
else if((i == 36) // hammer hit
&& m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHAMMER)
2017-04-22 17:04:16 +00:00
{
Msg.AddInt(0);
2014-02-19 21:30:57 +00:00
}
else
2017-04-22 17:04:16 +00:00
{
Msg.AddInt(pParams[i]);
}
}
2017-04-22 17:04:16 +00:00
else
Msg.AddInt(pParams[i]); // if everything is normal just send true tunings
}
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
2015-07-09 00:08:14 +00:00
void CGameContext::OnPreTickTeehistorian()
{
if(!m_TeeHistorianActive)
return;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_apPlayers[i] != nullptr)
m_TeeHistorian.RecordPlayerTeam(i, GetDDRaceTeam(i));
else
m_TeeHistorian.RecordPlayerTeam(i, 0);
}
for(int i = 0; i < MAX_CLIENTS; i++)
{
m_TeeHistorian.RecordTeamPractice(i, m_pController->Teams().IsPractice(i));
}
}
2010-05-29 07:25:38 +00:00
void CGameContext::OnTick()
{
2010-05-29 07:25:38 +00:00
// check tuning
CheckPureTuning();
if(m_TeeHistorianActive)
{
int Error = aio_error(m_pTeeHistorianFile);
2017-10-10 02:07:38 +00:00
if(Error)
{
dbg_msg("teehistorian", "error writing to file, err=%d", Error);
2017-10-13 00:25:50 +00:00
Server()->SetErrorShutdown("teehistorian io error");
2017-10-10 02:07:38 +00:00
}
if(!m_TeeHistorian.Starting())
{
m_TeeHistorian.EndInputs();
m_TeeHistorian.EndTick();
}
m_TeeHistorian.BeginTick(Server()->Tick());
m_TeeHistorian.BeginPlayers();
}
2010-05-29 07:25:38 +00:00
// copy tuning
m_World.m_Core.m_aTuning[0] = m_Tuning;
2010-05-29 07:25:38 +00:00
m_World.Tick();
UpdatePlayerMaps();
//if(world.paused) // make sure that the game object always updates
2010-05-29 07:25:38 +00:00
m_pController->Tick();
for(int i = 0; i < MAX_CLIENTS; i++)
{
2010-05-29 07:25:38 +00:00
if(m_apPlayers[i])
{
2014-10-26 18:39:42 +00:00
// send vote options
ProgressVoteOptions(i);
2010-05-29 07:25:38 +00:00
m_apPlayers[i]->Tick();
m_apPlayers[i]->PostTick();
}
}
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
pPlayer->PostPostTick();
}
2008-09-24 14:47:03 +00:00
// update voting
2010-05-29 07:25:38 +00:00
if(m_VoteCloseTime)
2008-09-24 14:47:03 +00:00
{
// abort the kick-vote on player-leave
2020-07-01 12:02:47 +00:00
if(m_VoteEnforce == VOTE_ENFORCE_ABORT)
2008-09-24 14:47:03 +00:00
{
2010-05-29 07:25:38 +00:00
SendChat(-1, CGameContext::CHAT_ALL, "Vote aborted");
EndVote();
}
else
{
2010-05-29 07:25:38 +00:00
int Total = 0, Yes = 0, No = 0;
bool Veto = false, VetoStop = false;
2010-05-29 07:25:38 +00:00
if(m_VoteUpdate)
2008-09-24 14:47:03 +00:00
{
2010-05-29 07:25:38 +00:00
// count votes
2019-05-01 11:54:51 +00:00
char aaBuf[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}}, *pIP = NULL;
bool SinglePlayer = true;
2010-05-29 07:25:38 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
2019-05-01 11:54:51 +00:00
{
2010-05-29 07:25:38 +00:00
if(m_apPlayers[i])
2019-05-01 11:54:51 +00:00
{
Server()->GetClientAddr(i, aaBuf[i], NETADDR_MAXSTRSIZE);
2019-05-01 11:54:51 +00:00
if(!pIP)
pIP = aaBuf[i];
2019-05-01 19:57:14 +00:00
else if(SinglePlayer && str_comp(pIP, aaBuf[i]))
2019-05-01 11:54:51 +00:00
SinglePlayer = false;
}
}
// remember checked players, only the first player with a specific ip will be handled
bool aVoteChecked[MAX_CLIENTS] = {false};
2021-06-23 05:05:49 +00:00
int64_t Now = Server()->Tick();
2010-05-29 07:25:38 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!m_apPlayers[i] || aVoteChecked[i])
2010-05-29 07:25:38 +00:00
continue;
2020-07-01 12:02:47 +00:00
if((IsKickVote() || IsSpecVote()) && (m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS ||
(GetPlayerChar(m_VoteCreator) && GetPlayerChar(i) &&
GetPlayerChar(m_VoteCreator)->Team() != GetPlayerChar(i)->Team())))
2010-05-29 07:25:38 +00:00
continue;
if(m_apPlayers[i]->IsAfk() && i != m_VoteCreator)
2013-08-18 01:27:30 +00:00
continue;
// can't vote in kick and spec votes in the beginning after joining
2020-07-01 12:02:47 +00:00
if((IsKickVote() || IsSpecVote()) && Now < m_apPlayers[i]->m_FirstVoteTick)
continue;
2018-08-16 19:42:10 +00:00
// connecting clients with spoofed ips can clog slots without being ingame
if(!Server()->ClientIngame(i))
2018-08-16 19:42:10 +00:00
continue;
2016-09-05 09:38:11 +00:00
// don't count votes by blacklisted clients
2019-05-01 11:54:51 +00:00
if(g_Config.m_SvDnsblVote && !m_pServer->DnsblWhite(i) && !SinglePlayer)
2016-09-05 09:38:11 +00:00
continue;
int CurVote = m_apPlayers[i]->m_Vote;
int CurVotePos = m_apPlayers[i]->m_VotePos;
// only allow IPs to vote once, but keep veto ability
// check for more players with the same ip (only use the vote of the one who voted first)
for(int j = i + 1; j < MAX_CLIENTS; j++)
2010-05-29 07:25:38 +00:00
{
if(!m_apPlayers[j] || aVoteChecked[j] || str_comp(aaBuf[j], aaBuf[i]) != 0)
continue;
// count the latest vote by this ip
if(CurVotePos < m_apPlayers[j]->m_VotePos)
{
CurVote = m_apPlayers[j]->m_Vote;
CurVotePos = m_apPlayers[j]->m_VotePos;
2010-05-29 07:25:38 +00:00
}
aVoteChecked[j] = true;
}
Total++;
if(CurVote > 0)
Yes++;
else if(CurVote < 0)
No++;
// veto right for players who have been active on server for long and who're not afk
if(!IsKickVote() && !IsSpecVote() && g_Config.m_SvVoteVetoTime)
{
2020-12-28 10:53:16 +00:00
// look through all players with same IP again, including the current player
for(int j = i; j < MAX_CLIENTS; j++)
{
2020-12-28 10:53:16 +00:00
// no need to check ip address of current player
if(i != j && (!m_apPlayers[j] || str_comp(aaBuf[j], aaBuf[i]) != 0))
continue;
if(m_apPlayers[j] && !m_apPlayers[j]->IsAfk() && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS &&
((Server()->Tick() - m_apPlayers[j]->m_JoinTick) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime ||
(m_apPlayers[j]->GetCharacter() && m_apPlayers[j]->GetCharacter()->m_DDRaceState == DDRACE_STARTED &&
(Server()->Tick() - m_apPlayers[j]->GetCharacter()->m_StartTime) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime)))
{
if(CurVote == 0)
Veto = true;
else if(CurVote < 0)
VetoStop = true;
break;
}
}
}
}
2010-05-29 07:25:38 +00:00
if(g_Config.m_SvVoteMaxTotal && Total > g_Config.m_SvVoteMaxTotal &&
(IsKickVote() || IsSpecVote()))
2014-03-22 18:53:43 +00:00
Total = g_Config.m_SvVoteMaxTotal;
2019-07-08 21:08:42 +00:00
if((Yes > Total / (100.0f / g_Config.m_SvVoteYesPercentage)) && !Veto)
2018-01-13 21:22:55 +00:00
m_VoteEnforce = VOTE_ENFORCE_YES;
2019-07-08 21:08:42 +00:00
else if(No >= Total - Total / (100.0f / g_Config.m_SvVoteYesPercentage))
2010-05-29 07:25:38 +00:00
m_VoteEnforce = VOTE_ENFORCE_NO;
if(VetoStop)
m_VoteEnforce = VOTE_ENFORCE_NO;
2019-07-08 21:08:42 +00:00
m_VoteWillPass = Yes > (Yes + No) / (100.0f / g_Config.m_SvVoteYesPercentage);
2008-09-24 14:47:03 +00:00
}
2018-01-13 15:46:31 +00:00
if(time_get() > m_VoteCloseTime && !g_Config.m_SvVoteMajority)
m_VoteEnforce = (m_VoteWillPass && !Veto) ? VOTE_ENFORCE_YES : VOTE_ENFORCE_NO;
// / Ensure minimum time for vote to end when moderating.
if(m_VoteEnforce == VOTE_ENFORCE_YES && !(PlayerModerating() &&
(IsKickVote() || IsSpecVote()) && time_get() < m_VoteCloseTime))
2010-05-29 07:25:38 +00:00
{
Server()->SetRconCID(IServer::RCON_CID_VOTE);
Console()->ExecuteLine(m_aVoteCommand);
Server()->SetRconCID(IServer::RCON_CID_SERV);
EndVote();
2020-07-12 07:34:36 +00:00
SendChat(-1, CGameContext::CHAT_ALL, "Vote passed", -1, CHAT_SIX);
2020-07-01 12:02:47 +00:00
if(m_apPlayers[m_VoteCreator] && !IsKickVote() && !IsSpecVote())
m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0;
2010-05-29 07:25:38 +00:00
}
2010-10-11 11:10:39 +00:00
else if(m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
{
Console()->ExecuteLine(m_aVoteCommand, m_VoteEnforcer);
SendChat(-1, CGameContext::CHAT_ALL, "Vote passed enforced by authorized player", -1, CHAT_SIX);
2010-10-11 11:10:39 +00:00
EndVote();
}
else if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN)
{
EndVote();
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed enforced by authorized player", -1, CHAT_SIX);
2010-10-11 11:10:39 +00:00
}
//else if(m_VoteEnforce == VOTE_ENFORCE_NO || time_get() > m_VoteCloseTime)
else if(m_VoteEnforce == VOTE_ENFORCE_NO || (time_get() > m_VoteCloseTime && g_Config.m_SvVoteMajority))
2010-05-29 07:25:38 +00:00
{
EndVote();
if(VetoStop || (m_VoteWillPass && Veto))
2020-07-12 07:34:36 +00:00
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed because of veto. Find an empty server instead", -1, CHAT_SIX);
else
2020-07-12 07:34:36 +00:00
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed", -1, CHAT_SIX);
2010-05-29 07:25:38 +00:00
}
else if(m_VoteUpdate)
{
m_VoteUpdate = false;
SendVoteStatus(-1, Total, Yes, No);
2010-05-29 07:25:38 +00:00
}
}
}
2011-02-23 21:39:53 +00:00
for(int i = 0; i < m_NumMutes; i++)
{
if(m_aMutes[i].m_Expire <= Server()->Tick())
{
m_NumMutes--;
m_aMutes[i] = m_aMutes[m_NumMutes];
}
}
for(int i = 0; i < m_NumVoteMutes; i++)
{
if(m_aVoteMutes[i].m_Expire <= Server()->Tick())
{
m_NumVoteMutes--;
m_aVoteMutes[i] = m_aVoteMutes[m_NumVoteMutes];
}
}
2010-11-22 20:43:22 +00:00
2011-02-23 21:39:53 +00:00
if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0)
2010-11-22 20:43:22 +00:00
{
const char *pLine = Server()->GetAnnouncementLine(g_Config.m_SvAnnouncementFileName);
if(pLine)
SendChat(-1, CGameContext::CHAT_ALL, pLine);
2011-02-23 21:39:53 +00:00
}
for(auto &Switcher : Switchers())
2020-05-27 15:49:07 +00:00
{
for(int j = 0; j < MAX_CLIENTS; ++j)
2010-11-22 20:43:22 +00:00
{
if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDOPEN)
2010-11-22 20:43:22 +00:00
{
Switcher.m_aStatus[j] = false;
Switcher.m_aEndTick[j] = 0;
Switcher.m_aType[j] = TILE_SWITCHCLOSE;
}
else if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDCLOSE)
{
Switcher.m_aStatus[j] = true;
Switcher.m_aEndTick[j] = 0;
Switcher.m_aType[j] = TILE_SWITCHOPEN;
2010-11-22 20:43:22 +00:00
}
}
2020-05-27 15:49:07 +00:00
}
2010-11-22 20:43:22 +00:00
if(m_SqlRandomMapResult != nullptr && m_SqlRandomMapResult->m_Completed)
{
if(m_SqlRandomMapResult->m_Success)
{
if(PlayerExists(m_SqlRandomMapResult->m_ClientID) && m_SqlRandomMapResult->m_aMessage[0] != '\0')
SendChatTarget(m_SqlRandomMapResult->m_ClientID, m_SqlRandomMapResult->m_aMessage);
2021-09-13 09:47:47 +00:00
if(m_SqlRandomMapResult->m_aMap[0] != '\0')
Server()->ChangeMap(m_SqlRandomMapResult->m_aMap);
else
m_LastMapVote = 0;
}
m_SqlRandomMapResult = nullptr;
}
// Record player position at the end of the tick
if(m_TeeHistorianActive)
{
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_apPlayers[i] && m_apPlayers[i]->GetCharacter())
{
CNetObj_CharacterCore Char;
m_apPlayers[i]->GetCharacter()->GetCore().Write(&Char);
m_TeeHistorian.RecordPlayer(i, &Char);
}
else
{
m_TeeHistorian.RecordDeadPlayer(i);
}
}
m_TeeHistorian.EndPlayers();
m_TeeHistorian.BeginInputs();
}
// Warning: do not put code in this function directly above or below this comment
2010-05-29 07:25:38 +00:00
}
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;
}
2010-05-29 07:25:38 +00:00
// Server hooks
void CGameContext::OnClientPrepareInput(int ClientID, void *pInput)
{
auto *pPlayerInput = (CNetObj_PlayerInput *)pInput;
if(Server()->IsSixup(ClientID))
pPlayerInput->m_PlayerFlags = PlayerFlags_SevenToSix(pPlayerInput->m_PlayerFlags);
}
2010-05-29 07:25:38 +00:00
void CGameContext::OnClientDirectInput(int ClientID, void *pInput)
{
if(!m_World.m_Paused)
m_apPlayers[ClientID]->OnDirectInput((CNetObj_PlayerInput *)pInput);
2020-09-23 14:52:50 +00:00
int Flags = ((CNetObj_PlayerInput *)pInput)->m_PlayerFlags;
if((Flags & 256) || (Flags & 512))
{
Server()->Kick(ClientID, "please update your client or use DDNet client");
}
2010-05-29 07:25:38 +00:00
}
void CGameContext::OnClientPredictedInput(int ClientID, void *pInput)
{
// early return if no input at all has been sent by a player
if(pInput == nullptr && !m_aPlayerHasInput[ClientID])
return;
// set to last sent input when no new input has been sent
CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput;
if(pApplyInput == nullptr)
{
pApplyInput = &m_aLastPlayerInput[ClientID];
}
2010-05-29 07:25:38 +00:00
if(!m_World.m_Paused)
m_apPlayers[ClientID]->OnPredictedInput(pApplyInput);
2010-05-29 07:25:38 +00:00
}
2019-01-29 19:32:11 +00:00
void CGameContext::OnClientPredictedEarlyInput(int ClientID, void *pInput)
{
// early return if no input at all has been sent by a player
if(pInput == nullptr && !m_aPlayerHasInput[ClientID])
return;
// set to last sent input when no new input has been sent
CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput;
if(pApplyInput == nullptr)
{
pApplyInput = &m_aLastPlayerInput[ClientID];
}
else
{
// Store input in this function and not in `OnClientPredictedInput`,
// because this function is called on all inputs, while
// `OnClientPredictedInput` is only called on the first input of each
// tick.
mem_copy(&m_aLastPlayerInput[ClientID], pApplyInput, sizeof(m_aLastPlayerInput[ClientID]));
m_aPlayerHasInput[ClientID] = true;
}
2019-01-29 19:32:11 +00:00
if(!m_World.m_Paused)
m_apPlayers[ClientID]->OnPredictedEarlyInput(pApplyInput);
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerInput(ClientID, m_apPlayers[ClientID]->GetUniqueCID(), pApplyInput);
}
2019-01-29 19:32:11 +00:00
}
2015-04-18 20:29:28 +00:00
struct CVoteOptionServer *CGameContext::GetVoteOption(int Index)
2014-10-26 18:39:42 +00:00
{
CVoteOptionServer *pCurrent;
for(pCurrent = m_pVoteOptionFirst;
Index > 0 && pCurrent;
Index--, pCurrent = pCurrent->m_pNext)
;
2014-10-26 18:39:42 +00:00
if(Index > 0)
2014-10-26 18:39:42 +00:00
return 0;
return pCurrent;
}
void CGameContext::ProgressVoteOptions(int ClientID)
{
CPlayer *pPl = m_apPlayers[ClientID];
if(pPl->m_SendVoteIndex == -1)
2014-10-27 12:34:29 +00:00
return; // we didn't start sending options yet
2014-10-26 18:39:42 +00:00
if(pPl->m_SendVoteIndex > m_NumVoteOptions)
2014-10-27 12:34:29 +00:00
return; // shouldn't happen / fail silently
2014-10-26 18:39:42 +00:00
int VotesLeft = m_NumVoteOptions - pPl->m_SendVoteIndex;
2019-04-26 19:36:49 +00:00
int NumVotesToSend = minimum(g_Config.m_SvSendVotesPerTick, VotesLeft);
2014-10-26 18:39:42 +00:00
if(!VotesLeft)
2014-10-26 18:39:42 +00:00
{
// player has up to date vote option list
return;
}
// build vote option list msg
int CurIndex = 0;
CNetMsg_Sv_VoteOptionListAdd OptionMsg;
OptionMsg.m_pDescription0 = "";
OptionMsg.m_pDescription1 = "";
OptionMsg.m_pDescription2 = "";
OptionMsg.m_pDescription3 = "";
OptionMsg.m_pDescription4 = "";
OptionMsg.m_pDescription5 = "";
OptionMsg.m_pDescription6 = "";
OptionMsg.m_pDescription7 = "";
OptionMsg.m_pDescription8 = "";
OptionMsg.m_pDescription9 = "";
OptionMsg.m_pDescription10 = "";
OptionMsg.m_pDescription11 = "";
OptionMsg.m_pDescription12 = "";
OptionMsg.m_pDescription13 = "";
OptionMsg.m_pDescription14 = "";
// get current vote option by index
CVoteOptionServer *pCurrent = GetVoteOption(pPl->m_SendVoteIndex);
2015-06-02 09:55:16 +00:00
while(CurIndex < NumVotesToSend && pCurrent != NULL)
2014-10-26 18:39:42 +00:00
{
switch(CurIndex)
{
case 0: OptionMsg.m_pDescription0 = pCurrent->m_aDescription; break;
case 1: OptionMsg.m_pDescription1 = pCurrent->m_aDescription; break;
case 2: OptionMsg.m_pDescription2 = pCurrent->m_aDescription; break;
case 3: OptionMsg.m_pDescription3 = pCurrent->m_aDescription; break;
case 4: OptionMsg.m_pDescription4 = pCurrent->m_aDescription; break;
case 5: OptionMsg.m_pDescription5 = pCurrent->m_aDescription; break;
case 6: OptionMsg.m_pDescription6 = pCurrent->m_aDescription; break;
case 7: OptionMsg.m_pDescription7 = pCurrent->m_aDescription; break;
case 8: OptionMsg.m_pDescription8 = pCurrent->m_aDescription; break;
case 9: OptionMsg.m_pDescription9 = pCurrent->m_aDescription; break;
case 10: OptionMsg.m_pDescription10 = pCurrent->m_aDescription; break;
case 11: OptionMsg.m_pDescription11 = pCurrent->m_aDescription; break;
case 12: OptionMsg.m_pDescription12 = pCurrent->m_aDescription; break;
case 13: OptionMsg.m_pDescription13 = pCurrent->m_aDescription; break;
case 14: OptionMsg.m_pDescription14 = pCurrent->m_aDescription; break;
2014-10-26 18:39:42 +00:00
}
CurIndex++;
pCurrent = pCurrent->m_pNext;
}
// send msg
OptionMsg.m_NumOptions = NumVotesToSend;
Server()->SendPackMsg(&OptionMsg, MSGFLAG_VITAL, ClientID);
pPl->m_SendVoteIndex += NumVotesToSend;
}
void CGameContext::OnClientEnter(int ClientID)
2010-05-29 07:25:38 +00:00
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerReady(ClientID);
}
m_pController->OnPlayerConnect(m_apPlayers[ClientID]);
2020-06-16 15:54:01 +00:00
if(Server()->IsSixup(ClientID))
{
{
protocol7::CNetMsg_Sv_GameInfo Msg;
Msg.m_GameFlags = protocol7::GAMEFLAG_RACE;
Msg.m_MatchCurrent = 1;
Msg.m_MatchNum = 0;
Msg.m_ScoreLimit = 0;
Msg.m_TimeLimit = 0;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
}
// /team is essential
{
protocol7::CNetMsg_Sv_CommandInfoRemove Msg;
Msg.m_pName = "team";
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
}
for(const IConsole::CCommandInfo *pCmd = Console()->FirstCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT);
pCmd; pCmd = pCmd->NextCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT))
{
if(!str_comp_nocase(pCmd->m_pName, "w") || !str_comp_nocase(pCmd->m_pName, "whisper"))
continue;
const char *pName = pCmd->m_pName;
if(!str_comp_nocase(pCmd->m_pName, "r"))
pName = "rescue";
protocol7::CNetMsg_Sv_CommandInfo Msg;
Msg.m_pName = pName;
Msg.m_pArgsFormat = pCmd->m_pParams;
Msg.m_pHelpText = pCmd->m_pHelp;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
}
2020-06-16 15:54:01 +00:00
}
2018-12-17 13:55:58 +00:00
{
int Empty = -1;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!Server()->ClientIngame(i))
{
Empty = i;
break;
}
}
CNetMsg_Sv_Chat Msg;
Msg.m_Team = 0;
Msg.m_ClientID = Empty;
Msg.m_pMessage = "Do you know someone who uses a bot? Please report them to the moderators.";
m_apPlayers[ClientID]->m_EligibleForFinishCheck = time_get();
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
2018-12-17 13:55:58 +00:00
}
IServer::CClientInfo Info;
Handle `CServer::GetClientInfo` return to fix use of undefined value ``` /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:790:2: error: Undefined or garbage value returned to caller [clang-analyzer-core.uninitialized.UndefReturn,-warnings-as-errors] return Info.m_DDNetVersion; ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:785:5: note: Assuming 'ClientID' is not equal to SERVER_DEMO_CLIENT if(ClientID == SERVER_DEMO_CLIENT) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:785:2: note: Taking false branch if(ClientID == SERVER_DEMO_CLIENT) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:789:2: note: Calling 'CServer::GetClientInfo' GetClientInfo(ClientID, &Info); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:13: note: Assuming 'ClientID' is >= 0 dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:13: note: Left side of '&&' is true dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:30: note: Assuming 'ClientID' is < MAX_CLIENTS dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:2: note: '?' condition is true dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:31: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:7: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:647:13: note: 'pInfo' is not equal to null dbg_assert(pInfo != 0, "info can not be null"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:647:2: note: '?' condition is true dbg_assert(pInfo != 0, "info can not be null"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:31: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:7: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:649:5: note: Assuming field 'm_State' is not equal to STATE_INGAME if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:649:2: note: Taking false branch if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:667:2: note: Returning without writing to 'pInfo->m_DDNetVersion' return 0; ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:789:2: note: Returning from 'CServer::GetClientInfo' GetClientInfo(ClientID, &Info); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:790:2: note: Undefined or garbage value returned to caller return Info.m_DDNetVersion; ^ ```
2022-07-31 10:53:26 +00:00
if(Server()->GetClientInfo(ClientID, &Info) && Info.m_GotDDNetVersion)
{
if(OnClientDDNetVersionKnown(ClientID))
return; // kicked
}
if(!Server()->ClientPrevIngame(ClientID))
{
if(g_Config.m_SvWelcome[0] != 0)
SendChatTarget(ClientID, g_Config.m_SvWelcome);
2022-03-03 21:56:32 +00:00
if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF)
2013-10-20 21:22:49 +00:00
{
if(g_Config.m_SvShowOthers)
SendChatTarget(ClientID, "You can see other players. To disable this use DDNet client and type /showothers");
2013-10-20 21:22:49 +00:00
m_apPlayers[ClientID]->m_ShowOthers = g_Config.m_SvShowOthersDefault;
2013-10-20 21:22:49 +00:00
}
}
2010-05-29 07:25:38 +00:00
m_VoteUpdate = true;
2011-08-26 18:03:30 +00:00
2014-03-28 11:52:03 +00:00
// send active vote
if(m_VoteCloseTime)
SendVoteSet(ClientID);
Server()->ExpireServerInfo();
2020-03-29 02:36:38 +00:00
2020-06-10 16:12:10 +00:00
CPlayer *pNewPlayer = m_apPlayers[ClientID];
mem_zero(&m_aLastPlayerInput[ClientID], sizeof(m_aLastPlayerInput[ClientID]));
m_aPlayerHasInput[ClientID] = false;
// new info for others
protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg;
NewClientInfoMsg.m_ClientID = ClientID;
NewClientInfoMsg.m_Local = 0;
NewClientInfoMsg.m_Team = pNewPlayer->GetTeam();
NewClientInfoMsg.m_pName = Server()->ClientName(ClientID);
NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientID);
NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientID);
NewClientInfoMsg.m_Silent = false;
for(int p = 0; p < 6; p++)
{
NewClientInfoMsg.m_apSkinPartNames[p] = pNewPlayer->m_TeeInfos.m_apSkinPartNames[p];
NewClientInfoMsg.m_aUseCustomColors[p] = pNewPlayer->m_TeeInfos.m_aUseCustomColors[p];
NewClientInfoMsg.m_aSkinPartColors[p] = pNewPlayer->m_TeeInfos.m_aSkinPartColors[p];
}
2020-04-16 08:46:43 +00:00
// update client infos (others before local)
for(int i = 0; i < Server()->MaxClients(); ++i)
2020-03-29 02:36:38 +00:00
{
if(i == ClientID || !m_apPlayers[i] || !Server()->ClientIngame(i))
continue;
2020-06-10 16:12:10 +00:00
CPlayer *pPlayer = m_apPlayers[i];
2020-04-13 12:19:07 +00:00
if(Server()->IsSixup(i))
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
2020-03-29 02:36:38 +00:00
if(Server()->IsSixup(ClientID))
{
// existing infos for new player
protocol7::CNetMsg_Sv_ClientInfo ClientInfoMsg;
ClientInfoMsg.m_ClientID = i;
ClientInfoMsg.m_Local = 0;
ClientInfoMsg.m_Team = pPlayer->GetTeam();
ClientInfoMsg.m_pName = Server()->ClientName(i);
ClientInfoMsg.m_pClan = Server()->ClientClan(i);
ClientInfoMsg.m_Country = Server()->ClientCountry(i);
ClientInfoMsg.m_Silent = 0;
for(int p = 0; p < 6; p++)
{
ClientInfoMsg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p];
ClientInfoMsg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
ClientInfoMsg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
}
Server()->SendPackMsg(&ClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
2020-03-29 02:36:38 +00:00
}
}
// local info
if(Server()->IsSixup(ClientID))
{
NewClientInfoMsg.m_Local = 1;
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
2020-03-29 02:36:38 +00:00
}
// initial chat delay
if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientID]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed())
{
char aBuf[128];
NETADDR Addr;
Server()->GetClientAddr(ClientID, &Addr);
2023-11-19 21:36:47 +00:00
str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will need to wait %d seconds before talking.", g_Config.m_SvChatInitialDelay);
SendChatTarget(ClientID, aBuf);
Mute(&Addr, g_Config.m_SvChatInitialDelay, Server()->ClientName(ClientID), "Initial chat delay", true);
}
2022-11-29 10:47:36 +00:00
LogEvent("Connect", ClientID);
2010-05-29 07:25:38 +00:00
}
bool CGameContext::OnClientDataPersist(int ClientID, void *pData)
2010-05-29 07:25:38 +00:00
{
CPersistentClientData *pPersistent = (CPersistentClientData *)pData;
if(!m_apPlayers[ClientID])
{
return false;
}
pPersistent->m_IsSpectator = m_apPlayers[ClientID]->GetTeam() == TEAM_SPECTATORS;
pPersistent->m_IsAfk = m_apPlayers[ClientID]->IsAfk();
return true;
}
void CGameContext::OnClientConnected(int ClientID, void *pData)
{
CPersistentClientData *pPersistentData = (CPersistentClientData *)pData;
bool Spec = false;
bool Afk = true;
if(pPersistentData)
{
Spec = pPersistentData->m_IsSpectator;
Afk = pPersistentData->m_IsAfk;
}
{
bool Empty = true;
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
{
// connecting clients with spoofed ips can clog slots without being ingame
if(pPlayer && Server()->ClientIngame(pPlayer->GetCID()))
{
Empty = false;
break;
}
}
if(Empty)
{
m_NonEmptySince = Server()->Tick();
}
}
2010-05-29 07:25:38 +00:00
// Check which team the player should be on
const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(ClientID);
2010-05-29 07:25:38 +00:00
2021-12-18 10:20:22 +00:00
if(m_apPlayers[ClientID])
delete m_apPlayers[ClientID];
m_apPlayers[ClientID] = new(ClientID) CPlayer(this, NextUniqueClientID, ClientID, StartTeam);
2023-10-30 18:43:35 +00:00
m_apPlayers[ClientID]->SetInitialAfk(Afk);
NextUniqueClientID += 1;
SendMotd(ClientID);
SendSettings(ClientID);
2020-06-16 15:19:53 +00:00
Server()->ExpireServerInfo();
2010-05-29 07:25:38 +00:00
}
void CGameContext::OnClientDrop(int ClientID, const char *pReason)
2010-05-29 07:25:38 +00:00
{
2022-11-29 10:47:36 +00:00
LogEvent("Disconnect", ClientID);
AbortVoteKickOnDisconnect(ClientID);
m_pController->OnPlayerDisconnect(m_apPlayers[ClientID], pReason);
delete m_apPlayers[ClientID];
m_apPlayers[ClientID] = 0;
2010-05-29 07:25:38 +00:00
m_VoteUpdate = true;
// update spectator modes
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
{
2020-10-26 14:14:07 +00:00
if(pPlayer && pPlayer->m_SpectatorID == ClientID)
pPlayer->m_SpectatorID = SPEC_FREEVIEW;
}
2013-10-18 09:42:35 +00:00
// update conversation targets
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
2013-10-18 09:42:35 +00:00
{
2020-10-26 14:14:07 +00:00
if(pPlayer && pPlayer->m_LastWhisperTo == ClientID)
pPlayer->m_LastWhisperTo = -1;
2013-10-18 09:42:35 +00:00
}
2020-06-16 16:24:43 +00:00
protocol7::CNetMsg_Sv_ClientDrop Msg;
Msg.m_ClientID = ClientID;
Msg.m_pReason = pReason;
Msg.m_Silent = false;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
2020-06-16 16:24:43 +00:00
Server()->ExpireServerInfo();
2010-05-29 07:25:38 +00:00
}
void CGameContext::TeehistorianRecordAntibot(const void *pData, int DataSize)
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordAntibot(pData, DataSize);
}
}
void CGameContext::TeehistorianRecordPlayerJoin(int ClientID, bool Sixup)
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerJoin(ClientID, !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7);
}
}
void CGameContext::TeehistorianRecordPlayerDrop(int ClientID, const char *pReason)
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerDrop(ClientID, pReason);
}
}
void CGameContext::TeehistorianRecordPlayerRejoin(int ClientID)
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerRejoin(ClientID);
}
}
bool CGameContext::OnClientDDNetVersionKnown(int ClientID)
{
IServer::CClientInfo Info;
Handle `CServer::GetClientInfo` return to fix use of undefined value ``` /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:790:2: error: Undefined or garbage value returned to caller [clang-analyzer-core.uninitialized.UndefReturn,-warnings-as-errors] return Info.m_DDNetVersion; ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:785:5: note: Assuming 'ClientID' is not equal to SERVER_DEMO_CLIENT if(ClientID == SERVER_DEMO_CLIENT) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:785:2: note: Taking false branch if(ClientID == SERVER_DEMO_CLIENT) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:789:2: note: Calling 'CServer::GetClientInfo' GetClientInfo(ClientID, &Info); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:13: note: Assuming 'ClientID' is >= 0 dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:13: note: Left side of '&&' is true dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:30: note: Assuming 'ClientID' is < MAX_CLIENTS dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:646:2: note: '?' condition is true dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "client_id is not valid"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:31: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:7: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:647:13: note: 'pInfo' is not equal to null dbg_assert(pInfo != 0, "info can not be null"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:38: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:27: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:647:2: note: '?' condition is true dbg_assert(pInfo != 0, "info can not be null"); ^ /home/runner/work/ddnet/ddnet/src/base/tl/../system.h:58:31: note: expanded from macro 'dbg_assert' #define dbg_assert(test, msg) assert(test) ^ /usr/include/assert.h:93:7: note: expanded from macro 'assert' (static_cast <bool> (expr) \ ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:649:5: note: Assuming field 'm_State' is not equal to STATE_INGAME if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:649:2: note: Taking false branch if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:667:2: note: Returning without writing to 'pInfo->m_DDNetVersion' return 0; ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:789:2: note: Returning from 'CServer::GetClientInfo' GetClientInfo(ClientID, &Info); ^ /home/runner/work/ddnet/ddnet/src/engine/server/server.cpp:790:2: note: Undefined or garbage value returned to caller return Info.m_DDNetVersion; ^ ```
2022-07-31 10:53:26 +00:00
dbg_assert(Server()->GetClientInfo(ClientID, &Info), "failed to get client info");
int ClientVersion = Info.m_DDNetVersion;
dbg_msg("ddnet", "cid=%d version=%d", ClientID, ClientVersion);
if(m_TeeHistorianActive)
{
if(Info.m_pConnectionID && Info.m_pDDNetVersionStr)
{
m_TeeHistorian.RecordDDNetVersion(ClientID, *Info.m_pConnectionID, ClientVersion, Info.m_pDDNetVersionStr);
}
else
{
m_TeeHistorian.RecordDDNetVersionOld(ClientID, ClientVersion);
}
}
// Autoban known bot versions.
if(g_Config.m_SvBannedVersions[0] != '\0' && IsVersionBanned(ClientVersion))
{
Server()->Kick(ClientID, "unsupported client");
return true;
}
CPlayer *pPlayer = m_apPlayers[ClientID];
if(ClientVersion >= VERSION_DDNET_GAMETICK)
pPlayer->m_TimerType = g_Config.m_SvDefaultTimerType;
2020-05-27 17:45:30 +00:00
// First update the teams state.
m_pController->Teams().SendTeamsState(ClientID);
2020-05-27 17:45:30 +00:00
// Then send records.
SendRecord(ClientID);
2020-05-27 17:45:30 +00:00
// And report correct tunings.
if(ClientVersion < VERSION_DDNET_EARLY_VERSION)
SendTuningParams(ClientID, pPlayer->m_TuneZone);
2020-05-27 17:45:30 +00:00
// Tell old clients to update.
if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0')
SendBroadcast(g_Config.m_SvClientSuggestionOld, ClientID);
2020-05-27 17:45:30 +00:00
// Tell known bot clients that they're botting and we know it.
if(((ClientVersion >= 15 && ClientVersion < 100) || ClientVersion == 502) && g_Config.m_SvClientSuggestionBot[0] != '\0')
SendBroadcast(g_Config.m_SvClientSuggestionBot, ClientID);
return false;
}
void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientID)
2010-05-29 07:25:38 +00:00
{
if(Server()->IsSixup(ClientID) && *pMsgID < OFFSET_UUID)
{
void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgID, pUnpacker);
2020-03-29 02:36:38 +00:00
if(!pRawMsg)
2020-04-16 08:46:43 +00:00
return 0;
2020-03-29 02:36:38 +00:00
2020-06-10 16:12:10 +00:00
CPlayer *pPlayer = m_apPlayers[ClientID];
static char s_aRawMsg[1024];
if(*pMsgID == protocol7::NETMSGTYPE_CL_SAY)
{
2020-06-10 16:12:10 +00:00
protocol7::CNetMsg_Cl_Say *pMsg7 = (protocol7::CNetMsg_Cl_Say *)pRawMsg;
// Should probably use a placement new to start the lifetime of the object to avoid future weirdness
::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;
2020-04-16 08:46:43 +00:00
2020-06-12 13:14:56 +00:00
if(pMsg7->m_Target >= 0)
{
if(ProcessSpamProtection(ClientID))
return 0;
2020-06-12 13:14:56 +00:00
// Should we maybe recraft the message so that it can go through the usual path?
WhisperID(ClientID, pMsg7->m_Target, pMsg7->m_pMessage);
return 0;
}
2020-04-16 08:46:43 +00:00
2020-06-10 16:12:10 +00:00
pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM;
2020-04-16 08:46:43 +00:00
pMsg->m_pMessage = pMsg7->m_pMessage;
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_STARTINFO)
2020-06-10 16:12:10 +00:00
{
protocol7::CNetMsg_Cl_StartInfo *pMsg7 = (protocol7::CNetMsg_Cl_StartInfo *)pRawMsg;
::CNetMsg_Cl_StartInfo *pMsg = (::CNetMsg_Cl_StartInfo *)s_aRawMsg;
pMsg->m_pName = pMsg7->m_pName;
pMsg->m_pClan = pMsg7->m_pClan;
pMsg->m_Country = pMsg7->m_Country;
CTeeInfo Info(pMsg7->m_apSkinPartNames, pMsg7->m_aUseCustomColors, pMsg7->m_aSkinPartColors);
Info.FromSixup();
pPlayer->m_TeeInfos = Info;
str_copy(s_aRawMsg + sizeof(*pMsg), Info.m_aSkinName, sizeof(s_aRawMsg) - sizeof(*pMsg));
pMsg->m_pSkin = s_aRawMsg + sizeof(*pMsg);
pMsg->m_UseCustomColor = pPlayer->m_TeeInfos.m_UseCustomColor;
pMsg->m_ColorBody = pPlayer->m_TeeInfos.m_ColorBody;
pMsg->m_ColorFeet = pPlayer->m_TeeInfos.m_ColorFeet;
2020-06-10 16:12:10 +00:00
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_SKINCHANGE)
2020-06-10 16:12:10 +00:00
{
protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg;
if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo &&
pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
return 0;
2020-06-10 16:12:10 +00:00
pPlayer->m_LastChangeInfo = Server()->Tick();
2020-06-10 16:12:10 +00:00
CTeeInfo Info(pMsg->m_apSkinPartNames, pMsg->m_aUseCustomColors, pMsg->m_aSkinPartColors);
Info.FromSixup();
pPlayer->m_TeeInfos = Info;
2020-04-16 08:46:43 +00:00
protocol7::CNetMsg_Sv_SkinChange Msg;
Msg.m_ClientID = ClientID;
for(int p = 0; p < 6; p++)
{
Msg.m_apSkinPartNames[p] = pMsg->m_apSkinPartNames[p];
Msg.m_aSkinPartColors[p] = pMsg->m_aSkinPartColors[p];
Msg.m_aUseCustomColors[p] = pMsg->m_aUseCustomColors[p];
}
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
2020-06-10 16:12:10 +00:00
return 0;
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE)
2020-06-12 20:53:41 +00:00
{
protocol7::CNetMsg_Cl_SetSpectatorMode *pMsg7 = (protocol7::CNetMsg_Cl_SetSpectatorMode *)pRawMsg;
::CNetMsg_Cl_SetSpectatorMode *pMsg = (::CNetMsg_Cl_SetSpectatorMode *)s_aRawMsg;
if(pMsg7->m_SpecMode == protocol7::SPEC_FREEVIEW)
pMsg->m_SpectatorID = SPEC_FREEVIEW;
else if(pMsg7->m_SpecMode == protocol7::SPEC_PLAYER)
pMsg->m_SpectatorID = pMsg7->m_SpectatorID;
else
pMsg->m_SpectatorID = SPEC_FREEVIEW; // Probably not needed
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_SETTEAM)
2020-06-16 15:19:53 +00:00
{
protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg;
::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg;
pMsg->m_Team = pMsg7->m_Team;
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_COMMAND)
{
protocol7::CNetMsg_Cl_Command *pMsg7 = (protocol7::CNetMsg_Cl_Command *)pRawMsg;
::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;
str_format(s_aRawMsg + sizeof(*pMsg), sizeof(s_aRawMsg) - sizeof(*pMsg), "/%s %s", pMsg7->m_pName, pMsg7->m_pArguments);
pMsg->m_pMessage = s_aRawMsg + sizeof(*pMsg);
dbg_msg("debug", "line='%s'", s_aRawMsg + sizeof(*pMsg));
pMsg->m_Team = 0;
*pMsgID = NETMSGTYPE_CL_SAY;
return s_aRawMsg;
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_CALLVOTE)
2020-06-20 11:22:46 +00:00
{
protocol7::CNetMsg_Cl_CallVote *pMsg7 = (protocol7::CNetMsg_Cl_CallVote *)pRawMsg;
::CNetMsg_Cl_CallVote *pMsg = (::CNetMsg_Cl_CallVote *)s_aRawMsg;
int Authed = Server()->GetAuthedState(ClientID);
if(pMsg7->m_Force)
{
str_format(s_aRawMsg, sizeof(s_aRawMsg), "force_vote \"%s\" \"%s\" \"%s\"", pMsg7->m_pType, pMsg7->m_pValue, pMsg7->m_pReason);
2020-06-20 11:22:46 +00:00
Console()->SetAccessLevel(Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER);
Console()->ExecuteLine(s_aRawMsg, ClientID, false);
Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN);
return 0;
}
pMsg->m_pValue = pMsg7->m_pValue;
pMsg->m_pReason = pMsg7->m_pReason;
pMsg->m_pType = pMsg7->m_pType;
2020-06-20 11:22:46 +00:00
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_EMOTICON)
2020-06-23 09:24:09 +00:00
{
protocol7::CNetMsg_Cl_Emoticon *pMsg7 = (protocol7::CNetMsg_Cl_Emoticon *)pRawMsg;
::CNetMsg_Cl_Emoticon *pMsg = (::CNetMsg_Cl_Emoticon *)s_aRawMsg;
pMsg->m_Emoticon = pMsg7->m_Emoticon;
}
else if(*pMsgID == protocol7::NETMSGTYPE_CL_VOTE)
2020-07-07 10:16:53 +00:00
{
protocol7::CNetMsg_Cl_Vote *pMsg7 = (protocol7::CNetMsg_Cl_Vote *)pRawMsg;
::CNetMsg_Cl_Vote *pMsg = (::CNetMsg_Cl_Vote *)s_aRawMsg;
pMsg->m_Vote = pMsg7->m_Vote;
}
2020-06-10 16:12:10 +00:00
*pMsgID = Msg_SevenToSix(*pMsgID);
2020-06-10 16:12:10 +00:00
return s_aRawMsg;
2020-04-16 08:46:43 +00:00
}
2020-06-10 16:12:10 +00:00
else
return m_NetObjHandler.SecureUnpackMsg(*pMsgID, pUnpacker);
2020-04-16 08:46:43 +00:00
}
2020-09-18 15:37:27 +00:00
void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size)
{
str_copy(pCensoredMessage, pMessage, Size);
for(auto &Item : m_vCensorlist)
2020-09-18 15:37:27 +00:00
{
2020-10-04 15:25:14 +00:00
char *pCurLoc = pCensoredMessage;
do
{
pCurLoc = (char *)str_utf8_find_nocase(pCurLoc, Item.c_str());
2020-10-04 15:25:14 +00:00
if(pCurLoc)
{
for(int i = 0; i < (int)Item.length(); i++)
{
pCurLoc[i] = '*';
}
2020-10-04 15:25:14 +00:00
pCurLoc++;
}
} while(pCurLoc);
2020-09-18 15:37:27 +00:00
}
}
2020-04-16 08:46:43 +00:00
void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
{
if(m_TeeHistorianActive)
{
if(m_NetObjHandler.TeeHistorianRecordMsg(MsgID))
{
m_TeeHistorian.RecordPlayerMessage(ClientID, pUnpacker->CompleteData(), pUnpacker->CompleteSize());
}
}
2020-06-10 16:12:10 +00:00
void *pRawMsg = PreProcessMsg(&MsgID, pUnpacker, ClientID);
2020-06-21 22:06:33 +00:00
if(!pRawMsg)
return;
if(Server()->ClientIngame(ClientID))
2010-05-29 07:25:38 +00:00
{
switch(MsgID)
{
case NETMSGTYPE_CL_SAY:
2023-08-15 20:17:06 +00:00
OnSayNetMessage(static_cast<CNetMsg_Cl_Say *>(pRawMsg), ClientID, pUnpacker);
break;
case NETMSGTYPE_CL_CALLVOTE:
OnCallVoteNetMessage(static_cast<CNetMsg_Cl_CallVote *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_VOTE:
OnVoteNetMessage(static_cast<CNetMsg_Cl_Vote *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_SETTEAM:
OnSetTeamNetMessage(static_cast<CNetMsg_Cl_SetTeam *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_ISDDNETLEGACY:
OnIsDDNetLegacyNetMessage(static_cast<CNetMsg_Cl_IsDDNetLegacy *>(pRawMsg), ClientID, pUnpacker);
break;
case NETMSGTYPE_CL_SHOWOTHERSLEGACY:
OnShowOthersLegacyNetMessage(static_cast<CNetMsg_Cl_ShowOthersLegacy *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_SHOWOTHERS:
OnShowOthersNetMessage(static_cast<CNetMsg_Cl_ShowOthers *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_SHOWDISTANCE:
OnShowDistanceNetMessage(static_cast<CNetMsg_Cl_ShowDistance *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_SETSPECTATORMODE:
OnSetSpectatorModeNetMessage(static_cast<CNetMsg_Cl_SetSpectatorMode *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_CHANGEINFO:
OnChangeInfoNetMessage(static_cast<CNetMsg_Cl_ChangeInfo *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_EMOTICON:
OnEmoticonNetMessage(static_cast<CNetMsg_Cl_Emoticon *>(pRawMsg), ClientID);
break;
case NETMSGTYPE_CL_KILL:
OnKillNetMessage(static_cast<CNetMsg_Cl_Kill *>(pRawMsg), ClientID);
break;
default:
break;
}
2010-05-29 07:25:38 +00:00
}
if(MsgID == NETMSGTYPE_CL_STARTINFO)
2010-05-29 07:25:38 +00:00
{
OnStartInfoNetMessage(static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientID);
2010-05-29 07:25:38 +00:00
}
}
2011-01-29 00:59:50 +00:00
2023-08-15 20:17:06 +00:00
void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, const CUnpacker *pUnpacker)
{
if(!str_utf8_check(pMsg->m_pMessage))
{
return;
}
CPlayer *pPlayer = m_apPlayers[ClientID];
bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get();
if(Check && str_comp(pMsg->m_pMessage, "xd sure chillerbot.png is lyfe") == 0 && pMsg->m_Team == 0)
{
if(m_TeeHistorianActive)
{
m_TeeHistorian.RecordPlayerMessage(ClientID, pUnpacker->CompleteData(), pUnpacker->CompleteSize());
}
pPlayer->m_NotEligibleForFinish = true;
dbg_msg("hack", "bot detected, cid=%d", ClientID);
return;
}
int Team = pMsg->m_Team;
// trim right and set maximum length to 256 utf8-characters
int Length = 0;
const char *p = pMsg->m_pMessage;
const char *pEnd = 0;
while(*p)
{
const char *pStrOld = p;
int Code = str_utf8_decode(&p);
// check if unicode is not empty
if(!str_utf8_isspace(Code))
{
pEnd = 0;
}
else if(pEnd == 0)
pEnd = pStrOld;
if(++Length >= 256)
{
*(const_cast<char *>(p)) = 0;
break;
}
}
if(pEnd != 0)
*(const_cast<char *>(pEnd)) = 0;
// drop empty and autocreated spam messages (more than 32 characters per second)
if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick())))
return;
int GameTeam = GetDDRaceTeam(pPlayer->GetCID());
2023-08-15 20:17:06 +00:00
if(Team)
Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? CHAT_SPEC : GameTeam);
else
Team = CHAT_ALL;
if(pMsg->m_pMessage[0] == '/')
{
if(str_startswith_nocase(pMsg->m_pMessage + 1, "w "))
{
char aWhisperMsg[256];
str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256);
Whisper(pPlayer->GetCID(), aWhisperMsg);
}
else if(str_startswith_nocase(pMsg->m_pMessage + 1, "whisper "))
{
char aWhisperMsg[256];
str_copy(aWhisperMsg, pMsg->m_pMessage + 9, 256);
Whisper(pPlayer->GetCID(), aWhisperMsg);
}
else if(str_startswith_nocase(pMsg->m_pMessage + 1, "c "))
{
char aWhisperMsg[256];
str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256);
Converse(pPlayer->GetCID(), aWhisperMsg);
}
else if(str_startswith_nocase(pMsg->m_pMessage + 1, "converse "))
{
char aWhisperMsg[256];
str_copy(aWhisperMsg, pMsg->m_pMessage + 10, 256);
Converse(pPlayer->GetCID(), aWhisperMsg);
}
else
{
if(g_Config.m_SvSpamprotection && !str_startswith(pMsg->m_pMessage + 1, "timeout ") && pPlayer->m_aLastCommands[0] && pPlayer->m_aLastCommands[0] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[1] && pPlayer->m_aLastCommands[1] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[2] && pPlayer->m_aLastCommands[2] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[3] && pPlayer->m_aLastCommands[3] + Server()->TickSpeed() > Server()->Tick())
return;
int64_t Now = Server()->Tick();
pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now;
pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4;
Console()->SetFlagMask(CFGFLAG_CHAT);
int Authed = Server()->GetAuthedState(ClientID);
if(Authed)
Console()->SetAccessLevel(Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER);
else
Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_USER);
{
CClientChatLogger Logger(this, ClientID, log_get_scope_logger());
CLogScope Scope(&Logger);
Console()->ExecuteLine(pMsg->m_pMessage + 1, ClientID, false);
}
// m_apPlayers[ClientID] can be NULL, if the player used a
// timeout code and replaced another client.
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "%d used %s", ClientID, pMsg->m_pMessage);
Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "chat-command", aBuf);
Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN);
Console()->SetFlagMask(CFGFLAG_SERVER);
}
}
else
{
pPlayer->UpdatePlaytime();
char aCensoredMessage[256];
CensorMessage(aCensoredMessage, pMsg->m_pMessage, sizeof(aCensoredMessage));
SendChat(ClientID, Team, aCensoredMessage, ClientID);
}
}
void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientID)
{
if(RateLimitPlayerVote(ClientID) || m_VoteCloseTime)
return;
m_apPlayers[ClientID]->UpdatePlaytime();
m_VoteType = VOTE_TYPE_UNKNOWN;
char aChatmsg[512] = {0};
char aDesc[VOTE_DESC_LENGTH] = {0};
char aSixupDesc[VOTE_DESC_LENGTH] = {0};
char aCmd[VOTE_CMD_LENGTH] = {0};
char aReason[VOTE_REASON_LENGTH] = "No reason given";
if(!str_utf8_check(pMsg->m_pType) || !str_utf8_check(pMsg->m_pReason) || !str_utf8_check(pMsg->m_pValue))
{
return;
}
if(pMsg->m_pReason[0])
{
str_copy(aReason, pMsg->m_pReason, sizeof(aReason));
}
if(str_comp_nocase(pMsg->m_pType, "option") == 0)
{
int Authed = Server()->GetAuthedState(ClientID);
CVoteOptionServer *pOption = m_pVoteOptionFirst;
while(pOption)
{
if(str_comp_nocase(pMsg->m_pValue, pOption->m_aDescription) == 0)
{
if(!Console()->LineIsValid(pOption->m_aCommand))
{
SendChatTarget(ClientID, "Invalid option");
return;
}
if((str_find(pOption->m_aCommand, "sv_map ") != 0 || str_find(pOption->m_aCommand, "change_map ") != 0 || str_find(pOption->m_aCommand, "random_map") != 0 || str_find(pOption->m_aCommand, "random_unfinished_map") != 0) && RateLimitPlayerMapVote(ClientID))
{
return;
}
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientID),
pOption->m_aDescription, aReason);
str_copy(aDesc, pOption->m_aDescription);
if((str_endswith(pOption->m_aCommand, "random_map") || str_endswith(pOption->m_aCommand, "random_unfinished_map")) && str_length(aReason) == 1 && aReason[0] >= '0' && aReason[0] <= '5')
{
int Stars = aReason[0] - '0';
str_format(aCmd, sizeof(aCmd), "%s %d", pOption->m_aCommand, Stars);
}
else
{
str_copy(aCmd, pOption->m_aCommand);
}
m_LastMapVote = time_get();
break;
}
pOption = pOption->m_pNext;
}
if(!pOption)
{
if(Authed != AUTHED_ADMIN) // allow admins to call any vote they want
{
str_format(aChatmsg, sizeof(aChatmsg), "'%s' isn't an option on this server", pMsg->m_pValue);
SendChatTarget(ClientID, aChatmsg);
return;
}
else
{
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s'", Server()->ClientName(ClientID), pMsg->m_pValue);
str_copy(aDesc, pMsg->m_pValue);
str_copy(aCmd, pMsg->m_pValue);
}
}
m_VoteType = VOTE_TYPE_OPTION;
}
else if(str_comp_nocase(pMsg->m_pType, "kick") == 0)
{
int Authed = Server()->GetAuthedState(ClientID);
if(!g_Config.m_SvVoteKick && !Authed) // allow admins to call kick votes even if they are forbidden
{
SendChatTarget(ClientID, "Server does not allow voting to kick players");
return;
}
if(!Authed && time_get() < m_apPlayers[ClientID]->m_Last_KickVote + (time_freq() * g_Config.m_SvVoteKickDelay))
{
str_format(aChatmsg, sizeof(aChatmsg), "There's a %d second wait time between kick votes for each player please wait %d second(s)",
g_Config.m_SvVoteKickDelay,
(int)((m_apPlayers[ClientID]->m_Last_KickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq()));
SendChatTarget(ClientID, aChatmsg);
return;
}
if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientID))
{
char aaAddresses[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}};
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_apPlayers[i])
{
Server()->GetClientAddr(i, aaAddresses[i], NETADDR_MAXSTRSIZE);
}
}
int NumPlayers = 0;
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(m_apPlayers[i] && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(i))
{
NumPlayers++;
for(int j = 0; j < i; j++)
{
if(m_apPlayers[j] && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(j))
{
if(str_comp(aaAddresses[i], aaAddresses[j]) == 0)
{
NumPlayers--;
break;
}
}
}
}
}
if(NumPlayers < g_Config.m_SvVoteKickMin)
{
str_format(aChatmsg, sizeof(aChatmsg), "Kick voting requires %d players", g_Config.m_SvVoteKickMin);
SendChatTarget(ClientID, aChatmsg);
return;
}
}
int KickID = str_toint(pMsg->m_pValue);
if(KickID < 0 || KickID >= MAX_CLIENTS || !m_apPlayers[KickID])
{
SendChatTarget(ClientID, "Invalid client id to kick");
return;
}
if(KickID == ClientID)
{
SendChatTarget(ClientID, "You can't kick yourself");
return;
}
if(!Server()->ReverseTranslate(KickID, ClientID))
{
return;
}
int KickedAuthed = Server()->GetAuthedState(KickID);
if(KickedAuthed > Authed)
{
SendChatTarget(ClientID, "You can't kick authorized players");
char aBufKick[128];
str_format(aBufKick, sizeof(aBufKick), "'%s' called for vote to kick you", Server()->ClientName(ClientID));
SendChatTarget(KickID, aBufKick);
return;
}
// Don't allow kicking if a player has no character
if(!GetPlayerChar(ClientID) || !GetPlayerChar(KickID) || GetDDRaceTeam(ClientID) != GetDDRaceTeam(KickID))
{
SendChatTarget(ClientID, "You can kick only your team member");
return;
}
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to kick '%s' (%s)", Server()->ClientName(ClientID), Server()->ClientName(KickID), aReason);
str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", KickID, Server()->ClientName(KickID));
if(!GetDDRaceTeam(ClientID))
{
if(!g_Config.m_SvVoteKickBantime)
{
str_format(aCmd, sizeof(aCmd), "kick %d Kicked by vote", KickID);
str_format(aDesc, sizeof(aDesc), "Kick '%s'", Server()->ClientName(KickID));
}
else
{
char aAddrStr[NETADDR_MAXSTRSIZE] = {0};
Server()->GetClientAddr(KickID, aAddrStr, sizeof(aAddrStr));
str_format(aCmd, sizeof(aCmd), "ban %s %d Banned by vote", aAddrStr, g_Config.m_SvVoteKickBantime);
str_format(aDesc, sizeof(aDesc), "Ban '%s'", Server()->ClientName(KickID));
}
}
else
{
str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team_ddr %d 0", KickID, GetDDRaceTeam(KickID), KickID);
str_format(aDesc, sizeof(aDesc), "Move '%s' to team 0", Server()->ClientName(KickID));
}
m_apPlayers[ClientID]->m_Last_KickVote = time_get();
m_VoteType = VOTE_TYPE_KICK;
m_VoteVictim = KickID;
}
else if(str_comp_nocase(pMsg->m_pType, "spectate") == 0)
{
if(!g_Config.m_SvVoteSpectate)
{
SendChatTarget(ClientID, "Server does not allow voting to move players to spectators");
return;
}
int SpectateID = str_toint(pMsg->m_pValue);
if(SpectateID < 0 || SpectateID >= MAX_CLIENTS || !m_apPlayers[SpectateID] || m_apPlayers[SpectateID]->GetTeam() == TEAM_SPECTATORS)
{
SendChatTarget(ClientID, "Invalid client id to move");
return;
}
if(SpectateID == ClientID)
{
SendChatTarget(ClientID, "You can't move yourself");
return;
}
if(!Server()->ReverseTranslate(SpectateID, ClientID))
{
return;
}
if(!GetPlayerChar(ClientID) || !GetPlayerChar(SpectateID) || GetDDRaceTeam(ClientID) != GetDDRaceTeam(SpectateID))
{
SendChatTarget(ClientID, "You can only move your team member to spectators");
return;
}
str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", SpectateID, Server()->ClientName(SpectateID));
if(g_Config.m_SvPauseable && g_Config.m_SvVotePause)
{
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to pause '%s' for %d seconds (%s)", Server()->ClientName(ClientID), Server()->ClientName(SpectateID), g_Config.m_SvVotePauseTime, aReason);
str_format(aDesc, sizeof(aDesc), "Pause '%s' (%ds)", Server()->ClientName(SpectateID), g_Config.m_SvVotePauseTime);
str_format(aCmd, sizeof(aCmd), "uninvite %d %d; force_pause %d %d", SpectateID, GetDDRaceTeam(SpectateID), SpectateID, g_Config.m_SvVotePauseTime);
}
else
{
str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to move '%s' to spectators (%s)", Server()->ClientName(ClientID), Server()->ClientName(SpectateID), aReason);
str_format(aDesc, sizeof(aDesc), "Move '%s' to spectators", Server()->ClientName(SpectateID));
str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team %d -1 %d", SpectateID, GetDDRaceTeam(SpectateID), SpectateID, g_Config.m_SvVoteSpectateRejoindelay);
}
m_VoteType = VOTE_TYPE_SPECTATE;
m_VoteVictim = SpectateID;
}
if(aCmd[0] && str_comp_nocase(aCmd, "info") != 0)
CallVote(ClientID, aDesc, aCmd, aReason, aChatmsg, aSixupDesc[0] ? aSixupDesc : 0);
}
void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientID)
{
if(!m_VoteCloseTime)
return;
CPlayer *pPlayer = m_apPlayers[ClientID];
if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick())
return;
int64_t Now = Server()->Tick();
pPlayer->m_LastVoteTry = Now;
pPlayer->UpdatePlaytime();
if(!pMsg->m_Vote)
return;
pPlayer->m_Vote = pMsg->m_Vote;
pPlayer->m_VotePos = ++m_VotePos;
m_VoteUpdate = true;
CNetMsg_Sv_YourVote Msg = {pMsg->m_Vote};
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientID)
{
if(m_World.m_Paused)
return;
CPlayer *pPlayer = m_apPlayers[ClientID];
if(pPlayer->GetTeam() == pMsg->m_Team || (g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick()))
return;
// Kill Protection
CCharacter *pChr = pPlayer->GetCharacter();
if(pChr)
{
int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == DDRACE_STARTED)
{
SendChatTarget(ClientID, "Kill Protection enabled. If you really want to join the spectators, first type /kill");
return;
}
}
if(pPlayer->m_TeamChangeTick > Server()->Tick())
{
pPlayer->m_LastSetTeam = Server()->Tick();
int TimeLeft = (pPlayer->m_TeamChangeTick - Server()->Tick()) / Server()->TickSpeed();
char aTime[32];
str_time((int64_t)TimeLeft * 100, TIME_HOURS, aTime, sizeof(aTime));
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "Time to wait before changing team: %s", aTime);
SendBroadcast(aBuf, ClientID);
return;
}
// Switch team on given client and kill/respawn them
if(m_pController->CanJoinTeam(pMsg->m_Team, ClientID))
{
if(pPlayer->IsPaused())
SendChatTarget(ClientID, "Use /pause first then you can kill");
else
{
if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
m_VoteUpdate = true;
m_pController->DoTeamChange(pPlayer, pMsg->m_Team);
pPlayer->m_TeamChangeTick = Server()->Tick();
}
}
else
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "Only %d active players are allowed", Server()->MaxClients() - g_Config.m_SvSpectatorSlots);
SendBroadcast(aBuf, ClientID);
}
}
void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientID, CUnpacker *pUnpacker)
{
IServer::CClientInfo Info;
if(Server()->GetClientInfo(ClientID, &Info) && Info.m_GotDDNetVersion)
{
return;
}
int DDNetVersion = pUnpacker->GetInt();
if(pUnpacker->Error() || DDNetVersion < 0)
{
DDNetVersion = VERSION_DDRACE;
}
Server()->SetClientDDNetVersion(ClientID, DDNetVersion);
OnClientDDNetVersionKnown(ClientID);
}
void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientID)
{
if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
pPlayer->m_ShowOthers = pMsg->m_Show;
}
}
void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientID)
{
if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
pPlayer->m_ShowOthers = pMsg->m_Show;
}
}
void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientID)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y);
}
void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientID)
{
if(m_World.m_Paused)
return;
int SpectatorID = clamp(pMsg->m_SpectatorID, (int)SPEC_FOLLOW, MAX_CLIENTS - 1);
if(SpectatorID >= 0)
if(!Server()->ReverseTranslate(SpectatorID, ClientID))
return;
CPlayer *pPlayer = m_apPlayers[ClientID];
if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick()))
return;
pPlayer->m_LastSetSpectatorMode = Server()->Tick();
pPlayer->UpdatePlaytime();
if(SpectatorID >= 0 && (!m_apPlayers[SpectatorID] || m_apPlayers[SpectatorID]->GetTeam() == TEAM_SPECTATORS))
SendChatTarget(ClientID, "Invalid spectator id used");
else
pPlayer->m_SpectatorID = SpectatorID;
}
void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientID)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
return;
bool SixupNeedsUpdate = false;
if(!str_utf8_check(pMsg->m_pName) || !str_utf8_check(pMsg->m_pClan) || !str_utf8_check(pMsg->m_pSkin))
{
return;
}
pPlayer->m_LastChangeInfo = Server()->Tick();
pPlayer->UpdatePlaytime();
// set infos
if(Server()->WouldClientNameChange(ClientID, pMsg->m_pName) && !ProcessSpamProtection(ClientID))
{
char aOldName[MAX_NAME_LENGTH];
str_copy(aOldName, Server()->ClientName(ClientID), sizeof(aOldName));
Server()->SetClientName(ClientID, pMsg->m_pName);
char aChatText[256];
str_format(aChatText, sizeof(aChatText), "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientID));
SendChat(-1, CGameContext::CHAT_ALL, aChatText);
// reload scores
Score()->PlayerData(ClientID)->Reset();
m_apPlayers[ClientID]->m_Score.reset();
Score()->LoadPlayerData(ClientID);
SixupNeedsUpdate = true;
LogEvent("Name change", ClientID);
}
if(Server()->WouldClientClanChange(ClientID, pMsg->m_pClan))
{
SixupNeedsUpdate = true;
Server()->SetClientClan(ClientID, pMsg->m_pClan);
}
if(Server()->ClientCountry(ClientID) != pMsg->m_Country)
SixupNeedsUpdate = true;
Server()->SetClientCountry(ClientID, pMsg->m_Country);
str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin, sizeof(pPlayer->m_TeeInfos.m_aSkinName));
pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
if(!Server()->IsSixup(ClientID))
pPlayer->m_TeeInfos.ToSixup();
if(SixupNeedsUpdate)
{
protocol7::CNetMsg_Sv_ClientDrop Drop;
Drop.m_ClientID = ClientID;
Drop.m_pReason = "";
Drop.m_Silent = true;
protocol7::CNetMsg_Sv_ClientInfo Info;
Info.m_ClientID = ClientID;
Info.m_pName = Server()->ClientName(ClientID);
Info.m_Country = pMsg->m_Country;
Info.m_pClan = pMsg->m_pClan;
Info.m_Local = 0;
Info.m_Silent = true;
Info.m_Team = pPlayer->GetTeam();
for(int p = 0; p < 6; p++)
{
Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p];
Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
Info.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
}
for(int i = 0; i < Server()->MaxClients(); i++)
{
if(i != ClientID)
{
Server()->SendPackMsg(&Drop, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
Server()->SendPackMsg(&Info, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
}
}
}
else
{
protocol7::CNetMsg_Sv_SkinChange Msg;
Msg.m_ClientID = ClientID;
for(int p = 0; p < 6; p++)
{
Msg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p];
Msg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
Msg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
}
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
}
Server()->ExpireServerInfo();
}
void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientID)
{
if(m_World.m_Paused)
return;
CPlayer *pPlayer = m_apPlayers[ClientID];
auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) {
return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000);
};
if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmote, (int64_t)g_Config.m_SvEmoticonMsDelay))
return;
CCharacter *pChr = pPlayer->GetCharacter();
// player needs a character to send emotes
if(!pChr)
return;
pPlayer->m_LastEmote = Server()->Tick();
pPlayer->UpdatePlaytime();
// check if the global emoticon is prevented and emotes are only send to nearby players
if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmoteGlobal, (int64_t)g_Config.m_SvGlobalEmoticonMsDelay))
{
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(m_apPlayers[i] && pChr->CanSnapCharacter(i) && pChr->IsSnappingCharacterInView(i))
{
SendEmoticon(ClientID, pMsg->m_Emoticon, i);
}
}
}
else
{
// else send emoticons to all players
pPlayer->m_LastEmoteGlobal = Server()->Tick();
SendEmoticon(ClientID, pMsg->m_Emoticon, -1);
}
if(g_Config.m_SvEmotionalTees && pPlayer->m_EyeEmoteEnabled)
{
int EmoteType = EMOTE_NORMAL;
switch(pMsg->m_Emoticon)
{
case EMOTICON_EXCLAMATION:
case EMOTICON_GHOST:
case EMOTICON_QUESTION:
case EMOTICON_WTF:
EmoteType = EMOTE_SURPRISE;
break;
case EMOTICON_DOTDOT:
case EMOTICON_DROP:
case EMOTICON_ZZZ:
EmoteType = EMOTE_BLINK;
break;
case EMOTICON_EYES:
case EMOTICON_HEARTS:
case EMOTICON_MUSIC:
EmoteType = EMOTE_HAPPY;
break;
case EMOTICON_OOP:
case EMOTICON_SORRY:
case EMOTICON_SUSHI:
EmoteType = EMOTE_PAIN;
break;
case EMOTICON_DEVILTEE:
case EMOTICON_SPLATTEE:
case EMOTICON_ZOMG:
EmoteType = EMOTE_ANGRY;
break;
default:
break;
}
pChr->SetEmote(EmoteType, Server()->Tick() + 2 * Server()->TickSpeed());
}
}
void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientID)
{
if(m_World.m_Paused)
return;
if(m_VoteCloseTime && m_VoteCreator == ClientID && GetDDRaceTeam(ClientID) && (IsKickVote() || IsSpecVote()))
{
SendChatTarget(ClientID, "You are running a vote please try again after the vote is done!");
return;
}
CPlayer *pPlayer = m_apPlayers[ClientID];
if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick())
return;
if(pPlayer->IsPaused())
return;
CCharacter *pChr = pPlayer->GetCharacter();
if(!pChr)
return;
// Kill Protection
int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == DDRACE_STARTED)
{
SendChatTarget(ClientID, "Kill Protection enabled. If you really want to kill, type /kill");
return;
}
pPlayer->m_LastKill = Server()->Tick();
pPlayer->KillCharacter(WEAPON_SELF);
pPlayer->Respawn();
}
void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientID)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
if(pPlayer->m_IsReady)
return;
if(!str_utf8_check(pMsg->m_pName))
{
Server()->Kick(ClientID, "name is not valid utf8");
return;
}
if(!str_utf8_check(pMsg->m_pClan))
{
Server()->Kick(ClientID, "clan is not valid utf8");
return;
}
if(!str_utf8_check(pMsg->m_pSkin))
{
Server()->Kick(ClientID, "skin is not valid utf8");
return;
}
pPlayer->m_LastChangeInfo = Server()->Tick();
// set start infos
Server()->SetClientName(ClientID, pMsg->m_pName);
// trying to set client name can delete the player object, check if it still exists
if(!m_apPlayers[ClientID])
{
return;
}
Server()->SetClientClan(ClientID, pMsg->m_pClan);
// trying to set client clan can delete the player object, check if it still exists
if(!m_apPlayers[ClientID])
{
return;
}
Server()->SetClientCountry(ClientID, pMsg->m_Country);
str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin, sizeof(pPlayer->m_TeeInfos.m_aSkinName));
pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
if(!Server()->IsSixup(ClientID))
pPlayer->m_TeeInfos.ToSixup();
// send clear vote options
CNetMsg_Sv_VoteClearOptions ClearMsg;
Server()->SendPackMsg(&ClearMsg, MSGFLAG_VITAL, ClientID);
// begin sending vote options
pPlayer->m_SendVoteIndex = 0;
// send tuning parameters to client
SendTuningParams(ClientID, pPlayer->m_TuneZone);
// client is ready to enter
pPlayer->m_IsReady = true;
CNetMsg_Sv_ReadyToEnter m;
Server()->SendPackMsg(&m, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID);
Server()->ExpireServerInfo();
}
void CGameContext::ConTuneParam(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
const char *pParamName = pResult->GetString(0);
char aBuf[256];
if(pResult->NumArguments() == 2)
2010-05-29 07:25:38 +00:00
{
float NewValue = pResult->GetFloat(1);
if(pSelf->Tuning()->Set(pParamName, NewValue) && pSelf->Tuning()->Get(pParamName, &NewValue))
{
str_format(aBuf, sizeof(aBuf), "%s changed to %.2f", pParamName, NewValue);
pSelf->SendTuningParams(-1);
}
else
{
str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
}
2010-05-29 07:25:38 +00:00
}
else
{
float Value;
if(pSelf->Tuning()->Get(pParamName, &Value))
{
str_format(aBuf, sizeof(aBuf), "%s %.2f", pParamName, Value);
}
else
{
str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
}
}
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
2010-05-29 07:25:38 +00:00
}
2018-12-20 08:18:22 +00:00
void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
const char *pParamName = pResult->GetString(0);
float OldValue;
char aBuf[256];
2018-12-20 08:18:22 +00:00
if(!pSelf->Tuning()->Get(pParamName, &OldValue))
{
str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
2018-12-20 08:18:22 +00:00
return;
}
float NewValue = absolute(OldValue - pResult->GetFloat(1)) < 0.0001f ? pResult->GetFloat(2) : pResult->GetFloat(1);
2018-12-20 08:18:22 +00:00
pSelf->Tuning()->Set(pParamName, NewValue);
pSelf->Tuning()->Get(pParamName, &NewValue);
2018-12-20 08:18:22 +00:00
str_format(aBuf, sizeof(aBuf), "%s changed to %.2f", pParamName, NewValue);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
pSelf->SendTuningParams(-1);
}
void CGameContext::ConTuneReset(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
if(pResult->NumArguments())
{
const char *pParamName = pResult->GetString(0);
float DefaultValue = 0.0f;
char aBuf[256];
CTuningParams TuningParams;
if(TuningParams.Get(pParamName, &DefaultValue) && pSelf->Tuning()->Set(pParamName, DefaultValue) && pSelf->Tuning()->Get(pParamName, &DefaultValue))
{
str_format(aBuf, sizeof(aBuf), "%s reset to %.2f", pParamName, DefaultValue);
pSelf->SendTuningParams(-1);
}
else
{
str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
}
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
}
else
{
pSelf->ResetTuning();
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", "Tuning reset");
}
2010-05-29 07:25:38 +00:00
}
void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
char aBuf[256];
for(int i = 0; i < CTuningParams::Num(); i++)
2010-05-29 07:25:38 +00:00
{
2022-08-09 14:34:52 +00:00
float Value;
pSelf->Tuning()->Get(i, &Value);
str_format(aBuf, sizeof(aBuf), "%s %.2f", CTuningParams::Name(i), Value);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
2010-05-29 07:25:38 +00:00
}
}
void CGameContext::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
int List = pResult->GetInteger(0);
const char *pParamName = pResult->GetString(1);
float NewValue = pResult->GetFloat(2);
2015-07-09 00:08:14 +00:00
if(List >= 0 && List < NUM_TUNEZONES)
{
char aBuf[256];
if(pSelf->TuningList()[List].Set(pParamName, NewValue) && pSelf->TuningList()[List].Get(pParamName, &NewValue))
{
str_format(aBuf, sizeof(aBuf), "%s in zone %d changed to %.2f", pParamName, List, NewValue);
2014-03-29 13:54:43 +00:00
pSelf->SendTuningParams(-1, List);
}
else
{
str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
}
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
}
}
void CGameContext::ConTuneDumpZone(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
int List = pResult->GetInteger(0);
char aBuf[256];
if(List >= 0 && List < NUM_TUNEZONES)
{
for(int i = 0; i < CTuningParams::Num(); i++)
2014-03-23 17:56:07 +00:00
{
2022-08-09 14:34:52 +00:00
float Value;
pSelf->TuningList()[List].Get(i, &Value);
str_format(aBuf, sizeof(aBuf), "zone %d: %s %.2f", List, CTuningParams::Name(i), Value);
2014-03-23 17:56:07 +00:00
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
}
}
}
void CGameContext::ConTuneResetZone(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
CTuningParams TuningParams;
if(pResult->NumArguments())
{
int List = pResult->GetInteger(0);
if(List >= 0 && List < NUM_TUNEZONES)
{
2014-03-23 17:56:07 +00:00
pSelf->TuningList()[List] = TuningParams;
char aBuf[256];
2018-07-10 09:29:02 +00:00
str_format(aBuf, sizeof(aBuf), "Tunezone %d reset", List);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
2014-03-29 13:54:43 +00:00
pSelf->SendTuningParams(-1, List);
}
}
else
{
for(int i = 0; i < NUM_TUNEZONES; i++)
2014-03-29 13:54:43 +00:00
{
*(pSelf->TuningList() + i) = TuningParams;
2014-03-29 13:54:43 +00:00
pSelf->SendTuningParams(-1, i);
}
2018-07-10 09:29:02 +00:00
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", "All Tunezones reset");
}
}
void CGameContext::ConTuneSetZoneMsgEnter(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
if(pResult->NumArguments())
{
int List = pResult->GetInteger(0);
if(List >= 0 && List < NUM_TUNEZONES)
{
str_copy(pSelf->m_aaZoneEnterMsg[List], pResult->GetString(1), sizeof(pSelf->m_aaZoneEnterMsg[List]));
}
}
}
void CGameContext::ConTuneSetZoneMsgLeave(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
if(pResult->NumArguments())
{
int List = pResult->GetInteger(0);
if(List >= 0 && List < NUM_TUNEZONES)
{
str_copy(pSelf->m_aaZoneLeaveMsg[List], pResult->GetString(1), sizeof(pSelf->m_aaZoneLeaveMsg[List]));
}
}
}
void CGameContext::ConMapbug(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
const char *pMapBugName = pResult->GetString(0);
if(pSelf->m_pController)
{
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "mapbugs", "can't add map bugs after the game started");
return;
}
switch(pSelf->m_MapBugs.Update(pMapBugName))
{
case MAPBUGUPDATE_OK:
break;
case MAPBUGUPDATE_OVERRIDDEN:
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "mapbugs", "map-internal setting overridden by database");
break;
case MAPBUGUPDATE_NOTFOUND:
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "unknown map bug '%s', ignoring", pMapBugName);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "mapbugs", aBuf);
}
break;
default:
dbg_assert(0, "unreachable");
}
}
void CGameContext::ConSwitchOpen(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
int Switch = pResult->GetInteger(0);
2022-06-02 18:52:11 +00:00
if(in_range(Switch, (int)pSelf->Switchers().size() - 1))
{
2022-02-13 19:57:27 +00:00
pSelf->Switchers()[Switch].m_Initial = false;
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "switch %d opened by default", Switch);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
}
}
2012-01-09 23:49:31 +00:00
void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
pSelf->m_World.m_Paused ^= 1;
}
void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
pSelf->m_pController->ChangeMap(pResult->NumArguments() ? pResult->GetString(0) : "");
2010-05-29 07:25:38 +00:00
}
2013-12-06 23:15:56 +00:00
void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
2019-08-27 16:33:37 +00:00
int Stars = pResult->NumArguments() ? pResult->GetInteger(0) : -1;
2013-12-06 23:15:56 +00:00
pSelf->m_pScore->RandomMap(pSelf->m_VoteCreator, Stars);
2013-12-06 23:15:56 +00:00
}
2014-06-20 20:40:23 +00:00
void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
2019-08-27 16:33:37 +00:00
int Stars = pResult->NumArguments() ? pResult->GetInteger(0) : -1;
pSelf->m_pScore->RandomUnfinishedMap(pSelf->m_VoteCreator, Stars);
2014-06-20 20:40:23 +00:00
}
void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
if(pResult->NumArguments())
pSelf->m_pController->DoWarmup(pResult->GetInteger(0));
else
pSelf->m_pController->StartRound();
}
void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
2014-04-18 22:40:21 +00:00
char aBuf[1024];
str_copy(aBuf, pResult->GetString(0), sizeof(aBuf));
int i, j;
for(i = 0, j = 0; aBuf[i]; i++, j++)
{
if(aBuf[i] == '\\' && aBuf[i + 1] == 'n')
2014-04-18 22:40:21 +00:00
{
aBuf[j] = '\n';
i++;
}
else if(i != j)
2014-04-18 22:40:21 +00:00
{
aBuf[j] = aBuf[i];
}
}
aBuf[j] = '\0';
pSelf->SendBroadcast(aBuf, -1);
2010-05-29 07:25:38 +00:00
}
void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
pSelf->SendChat(-1, CGameContext::CHAT_ALL, pResult->GetString(0));
}
void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
int ClientID = clamp(pResult->GetInteger(0), 0, (int)MAX_CLIENTS - 1);
2010-05-29 07:25:38 +00:00
int Team = clamp(pResult->GetInteger(1), -1, 1);
int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(2) : 0;
if(!pSelf->m_apPlayers[ClientID])
return;
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "moved client %d to team %d", ClientID, Team);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
pSelf->m_apPlayers[ClientID]->Pause(CPlayer::PAUSE_NONE, false); // reset /spec and /pause to allow rejoin
pSelf->m_apPlayers[ClientID]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60;
pSelf->m_pController->DoTeamChange(pSelf->m_apPlayers[ClientID], Team);
if(Team == TEAM_SPECTATORS)
2017-04-08 23:16:48 +00:00
pSelf->m_apPlayers[ClientID]->Pause(CPlayer::PAUSE_NONE, true);
2010-05-29 07:25:38 +00:00
}
void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
int Team = clamp(pResult->GetInteger(0), -1, 1);
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "All players were moved to the %s", pSelf->m_pController->GetTeamName(Team));
pSelf->SendChat(-1, CGameContext::CHAT_ALL, aBuf);
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : pSelf->m_apPlayers)
if(pPlayer)
pSelf->m_pController->DoTeamChange(pPlayer, Team, false);
2011-09-04 09:13:30 +00:00
}
void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
2011-03-25 08:49:21 +00:00
const char *pDescription = pResult->GetString(0);
const char *pCommand = pResult->GetString(1);
pSelf->AddVote(pDescription, pCommand);
}
void CGameContext::AddVote(const char *pDescription, const char *pCommand)
{
if(m_NumVoteOptions == MAX_VOTE_OPTIONS)
{
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "maximum number of vote options reached");
return;
}
// check for valid option
if(!Console()->LineIsValid(pCommand) || str_length(pCommand) >= VOTE_CMD_LENGTH)
{
char aBuf[256];
2011-03-26 16:44:34 +00:00
str_format(aBuf, sizeof(aBuf), "skipped invalid command '%s'", pCommand);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
2011-03-26 16:44:34 +00:00
return;
}
2020-11-05 10:47:07 +00:00
while(*pDescription == ' ')
pDescription++;
if(str_length(pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0)
2011-03-26 16:44:34 +00:00
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "skipped invalid option '%s'", pDescription);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
return;
}
2011-03-25 08:49:21 +00:00
// check for duplicate entry
CVoteOptionServer *pOption = m_pVoteOptionFirst;
while(pOption)
{
2011-03-25 10:49:35 +00:00
if(str_comp_nocase(pDescription, pOption->m_aDescription) == 0)
{
char aBuf[256];
2011-03-25 08:49:21 +00:00
str_format(aBuf, sizeof(aBuf), "option '%s' already exists", pDescription);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
return;
}
pOption = pOption->m_pNext;
}
2011-03-25 08:49:21 +00:00
// add the option
++m_NumVoteOptions;
2011-03-25 08:49:21 +00:00
int Len = str_length(pCommand);
2021-05-27 17:35:20 +00:00
pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len, alignof(CVoteOptionServer));
2010-05-29 07:25:38 +00:00
pOption->m_pNext = 0;
pOption->m_pPrev = m_pVoteOptionLast;
2010-05-29 07:25:38 +00:00
if(pOption->m_pPrev)
pOption->m_pPrev->m_pNext = pOption;
m_pVoteOptionLast = pOption;
if(!m_pVoteOptionFirst)
m_pVoteOptionFirst = pOption;
2011-03-25 08:49:21 +00:00
str_copy(pOption->m_aDescription, pDescription, sizeof(pOption->m_aDescription));
mem_copy(pOption->m_aCommand, pCommand, Len + 1);
2010-05-29 07:25:38 +00:00
}
void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData)
2011-03-25 10:49:35 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
const char *pDescription = pResult->GetString(0);
2011-03-25 10:49:35 +00:00
// check for valid option
2011-03-26 16:44:34 +00:00
CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
2011-03-25 10:49:35 +00:00
while(pOption)
{
if(str_comp_nocase(pDescription, pOption->m_aDescription) == 0)
break;
pOption = pOption->m_pNext;
}
if(!pOption)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "option '%s' does not exist", pDescription);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
2011-03-25 10:49:35 +00:00
return;
}
2011-04-04 14:58:53 +00:00
2014-10-27 12:34:29 +00:00
// start reloading vote option list
// clear vote options
CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
pSelf->Server()->SendPackMsg(&VoteClearOptionsMsg, MSGFLAG_VITAL, -1);
// reset sending of vote options
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : pSelf->m_apPlayers)
2014-10-27 12:34:29 +00:00
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
pPlayer->m_SendVoteIndex = 0;
2014-10-27 12:34:29 +00:00
}
2011-03-25 10:49:35 +00:00
// TODO: improve this
// remove the option
--pSelf->m_NumVoteOptions;
2011-03-25 10:49:35 +00:00
2011-03-25 11:06:45 +00:00
CHeap *pVoteOptionHeap = new CHeap();
2011-03-26 16:44:34 +00:00
CVoteOptionServer *pVoteOptionFirst = 0;
CVoteOptionServer *pVoteOptionLast = 0;
int NumVoteOptions = pSelf->m_NumVoteOptions;
2011-03-26 16:44:34 +00:00
for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext)
2011-03-25 10:49:35 +00:00
{
if(pSrc == pOption)
continue;
// copy option
int Len = str_length(pSrc->m_aCommand);
2011-03-26 16:44:34 +00:00
CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len);
2011-03-25 10:49:35 +00:00
pDst->m_pNext = 0;
pDst->m_pPrev = pVoteOptionLast;
if(pDst->m_pPrev)
pDst->m_pPrev->m_pNext = pDst;
pVoteOptionLast = pDst;
if(!pVoteOptionFirst)
pVoteOptionFirst = pDst;
2011-03-25 10:49:35 +00:00
str_copy(pDst->m_aDescription, pSrc->m_aDescription, sizeof(pDst->m_aDescription));
mem_copy(pDst->m_aCommand, pSrc->m_aCommand, Len + 1);
2011-03-25 11:06:45 +00:00
}
// clean up
delete pSelf->m_pVoteOptionHeap;
2011-03-25 10:49:35 +00:00
pSelf->m_pVoteOptionHeap = pVoteOptionHeap;
pSelf->m_pVoteOptionFirst = pVoteOptionFirst;
pSelf->m_pVoteOptionLast = pVoteOptionLast;
pSelf->m_NumVoteOptions = NumVoteOptions;
2010-05-29 07:25:38 +00:00
}
void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData)
2011-03-25 11:06:45 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
const char *pType = pResult->GetString(0);
const char *pValue = pResult->GetString(1);
2011-03-26 15:24:12 +00:00
const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(2)[0] ? pResult->GetString(2) : "No reason given";
2011-03-25 11:06:45 +00:00
char aBuf[128] = {0};
if(str_comp_nocase(pType, "option") == 0)
{
2011-03-26 16:44:34 +00:00
CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
2011-03-25 11:06:45 +00:00
while(pOption)
{
if(str_comp_nocase(pValue, pOption->m_aDescription) == 0)
{
2020-11-25 01:50:10 +00:00
str_format(aBuf, sizeof(aBuf), "authorized player forced server option '%s' (%s)", pValue, pReason);
2020-07-12 07:34:36 +00:00
pSelf->SendChatTarget(-1, aBuf, CHAT_SIX);
pSelf->Console()->ExecuteLine(pOption->m_aCommand);
2011-03-25 11:06:45 +00:00
break;
}
pOption = pOption->m_pNext;
}
2011-03-25 11:06:45 +00:00
if(!pOption)
{
str_format(aBuf, sizeof(aBuf), "'%s' isn't an option on this server", pValue);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
2011-03-25 11:06:45 +00:00
return;
}
}
else if(str_comp_nocase(pType, "kick") == 0)
{
int KickID = str_toint(pValue);
if(KickID < 0 || KickID >= MAX_CLIENTS || !pSelf->m_apPlayers[KickID])
{
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to kick");
2011-03-25 11:06:45 +00:00
return;
}
if(!g_Config.m_SvVoteKickBantime)
2011-03-25 11:06:45 +00:00
{
str_format(aBuf, sizeof(aBuf), "kick %d %s", KickID, pReason);
pSelf->Console()->ExecuteLine(aBuf);
2011-03-25 11:06:45 +00:00
}
else
{
char aAddrStr[NETADDR_MAXSTRSIZE] = {0};
pSelf->Server()->GetClientAddr(KickID, aAddrStr, sizeof(aAddrStr));
str_format(aBuf, sizeof(aBuf), "ban %s %d %s", aAddrStr, g_Config.m_SvVoteKickBantime, pReason);
pSelf->Console()->ExecuteLine(aBuf);
2011-03-25 11:06:45 +00:00
}
}
else if(str_comp_nocase(pType, "spectate") == 0)
{
int SpectateID = str_toint(pValue);
if(SpectateID < 0 || SpectateID >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateID] || pSelf->m_apPlayers[SpectateID]->GetTeam() == TEAM_SPECTATORS)
{
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to move");
return;
}
2019-02-27 18:46:01 +00:00
str_format(aBuf, sizeof(aBuf), "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(SpectateID), pReason);
pSelf->SendChatTarget(-1, aBuf);
str_format(aBuf, sizeof(aBuf), "set_team %d -1 %d", SpectateID, g_Config.m_SvVoteSpectateRejoindelay);
pSelf->Console()->ExecuteLine(aBuf);
}
2011-03-25 11:06:45 +00:00
}
void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
pSelf->Server()->SendPackMsg(&VoteClearOptionsMsg, MSGFLAG_VITAL, -1);
pSelf->m_pVoteOptionHeap->Reset();
pSelf->m_pVoteOptionFirst = 0;
pSelf->m_pVoteOptionLast = 0;
pSelf->m_NumVoteOptions = 0;
2014-10-26 18:39:42 +00:00
// reset sending of vote options
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : pSelf->m_apPlayers)
2014-10-26 18:39:42 +00:00
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
pPlayer->m_SendVoteIndex = 0;
2014-10-26 18:39:42 +00:00
}
}
struct CMapNameItem
{
char m_aName[IO_MAX_PATH_LENGTH - 4];
bool operator<(const CMapNameItem &Other) const { return str_comp_nocase(m_aName, Other.m_aName) < 0; }
};
void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
2022-06-15 17:34:41 +00:00
std::vector<CMapNameItem> vMapList;
pSelf->Storage()->ListDirectory(IStorage::TYPE_ALL, "maps", MapScan, &vMapList);
std::sort(vMapList.begin(), vMapList.end());
2022-06-15 17:34:41 +00:00
for(auto &Item : vMapList)
{
char aDescription[64];
str_format(aDescription, sizeof(aDescription), "Map: %s", Item.m_aName);
char aCommand[IO_MAX_PATH_LENGTH * 2 + 10];
char aMapEscaped[IO_MAX_PATH_LENGTH * 2];
2020-10-26 10:28:04 +00:00
char *pDst = aMapEscaped;
str_escape(&pDst, Item.m_aName, aMapEscaped + sizeof(aMapEscaped));
2020-10-26 10:28:04 +00:00
str_format(aCommand, sizeof(aCommand), "change_map \"%s\"", aMapEscaped);
pSelf->AddVote(aDescription, aCommand);
}
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "added maps to votes");
}
int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData)
{
if(IsDir || !str_endswith(pName, ".map"))
return 0;
CMapNameItem Item;
str_truncate(Item.m_aName, sizeof(Item.m_aName), pName, str_length(pName) - str_length(".map"));
static_cast<std::vector<CMapNameItem> *>(pUserData)->push_back(Item);
return 0;
}
void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CGameContext *pSelf = (CGameContext *)pUserData;
2012-01-26 21:17:26 +00:00
2010-05-29 07:25:38 +00:00
if(str_comp_nocase(pResult->GetString(0), "yes") == 0)
2018-01-18 15:17:23 +00:00
pSelf->ForceVote(pResult->m_ClientID, true);
2010-05-29 07:25:38 +00:00
else if(str_comp_nocase(pResult->GetString(0), "no") == 0)
2018-01-18 15:17:23 +00:00
pSelf->ForceVote(pResult->m_ClientID, false);
2010-05-29 07:25:38 +00:00
}
2023-09-26 00:24:00 +00:00
void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData)
{
CGameContext *pSelf = (CGameContext *)pUserData;
int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : 0;
static const int s_EntriesPerPage = 20;
const int Start = Page * s_EntriesPerPage;
const int End = (Page + 1) * s_EntriesPerPage;
char aBuf[512];
int Count = 0;
for(CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; pOption; pOption = pOption->m_pNext, Count++)
{
if(Count < Start || Count >= End)
{
continue;
}
2023-09-26 10:13:49 +00:00
str_copy(aBuf, "add_vote \"");
char *pDst = aBuf + str_length(aBuf);
str_escape(&pDst, pOption->m_aDescription, aBuf + sizeof(aBuf));
str_append(aBuf, "\" \"");
pDst = aBuf + str_length(aBuf);
str_escape(&pDst, pOption->m_aCommand, aBuf + sizeof(aBuf));
str_append(aBuf, "\"");
2023-09-26 00:24:00 +00:00
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "votes", aBuf);
}
str_format(aBuf, sizeof(aBuf), "%d %s, showing entries %d - %d", Count, Count == 1 ? "vote" : "votes", Start, End - 1);
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "votes", aBuf);
}
2010-05-29 07:25:38 +00:00
void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
2010-05-29 07:25:38 +00:00
if(pResult->NumArguments())
{
CGameContext *pSelf = (CGameContext *)pUserData;
pSelf->SendMotd(-1);
2010-05-29 07:25:38 +00:00
}
}
void CGameContext::ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
{
CGameContext *pSelf = (CGameContext *)pUserData;
pSelf->SendSettings(-1);
}
}
2010-05-29 07:25:38 +00:00
void CGameContext::OnConsoleInit()
{
m_pServer = Kernel()->RequestInterface<IServer>();
Refactor config manager, move config variable handling Move all code for handling of config variables from console to config manager. The console no longer depends on the config manager, instead the config manager now depends on the console. Add `struct`s to manage config variables of different types (int, color and string). The config manager now keeps a list of all config variables, so usage of the preprocessor can be avoided except for code to initially create all config variables. Additionally, a separate list of just the game config variables (config variables with `CFGFLAG_GAME`) is kept to optimize the `ResetGameSettings` function, as this function is called whenever connecting to a server and should be fast. Previously, this function was even less efficient because it preformed a linear search for every individual game config variable to find the respective command data. Move console commands that opperate only on config variables (`reset`, `toggle` and `+toggle`) to config manager. Ensure that these commands only opperate on the desired config variables (client or server, respectively) by checking `IConsole::FlagMask`. Add `IConfigManager::SetReadOnly` function to set/unset config variables as read-only instead of manually overriding the command handlers for the `sv_rescue` and `sv_test_cmds` config variables. This also fixes that read-only config variables could still be changed by using the `reset` command. A console message is now printed when trying to change a read-only config variable. Removing the special handling for these two config variables is additionally useful so the console does not need to keep a pointer to config values and manager. Use a `CHeap` for the config variables, their help texts and the previous values of string config variables to avoid many separate allocations as well usage of static variables. Also use the heap to store the unknown commands instead of using `std::string`s. Properly trigger command chain when resetting config variables with the `reset` command and when resetting game settings on map loading. Closes #7461. Format default value for color variables as RGB/RGBA hex with dollar sign prefix. Closes #5523. Add log message when using `reset` with a variable that does not exist. Use `log_error` instead of `dbg_msg` when saving config file fails. Support unlimited number of config save callbacks instead of at most 16. The code also becomes more readable by using an `std::vector` instead of a fixed-size array and a separate num variable. Consistently name `MACRO_CONFIG_*` parameters when declaring the macros. Add `IConsole::CMDLINE_LENGTH` constant to represent the maximum length of the console input and thereby reduce usage of magic numbers for buffer sizes.
2023-11-22 22:07:08 +00:00
m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
m_pConfig = m_pConfigManager->Values();
2010-05-29 07:25:38 +00:00
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pEngine = Kernel()->RequestInterface<IEngine>();
2017-10-13 00:25:50 +00:00
m_pStorage = Kernel()->RequestInterface<IStorage>();
2010-09-16 22:40:44 +00:00
Console()->Register("tune", "s[tuning] ?i[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneParam, this, "Tune variable to value or show current value");
Console()->Register("toggle_tune", "s[tuning] i[value 1] i[value 2]", CFGFLAG_SERVER | CFGFLAG_GAME, ConToggleTuneParam, this, "Toggle tune variable");
Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, ConTuneReset, this, "Reset all or one tuning variable to default");
Console()->Register("tunes", "", CFGFLAG_SERVER, ConTunes, this, "List all tuning variables and their values");
Console()->Register("tune_zone", "i[zone] s[tuning] i[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value");
Console()->Register("tune_zone_dump", "i[zone]", CFGFLAG_SERVER, ConTuneDumpZone, this, "Dump zone tuning in zone x");
Console()->Register("tune_zone_reset", "?i[zone]", CFGFLAG_SERVER, ConTuneResetZone, this, "reset zone tuning in zone x or in all zones");
Console()->Register("tune_zone_enter", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgEnter, this, "which message to display on zone enter; use 0 for normal area");
Console()->Register("tune_zone_leave", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgLeave, this, "which message to display on zone leave; use 0 for normal area");
2022-10-13 09:51:18 +00:00
Console()->Register("mapbug", "s[mapbug]", CFGFLAG_SERVER | CFGFLAG_GAME, ConMapbug, this, "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)");
Console()->Register("switch_open", "i[switch]", CFGFLAG_SERVER | CFGFLAG_GAME, ConSwitchOpen, this, "Whether a switch is deactivated by default (otherwise activated)");
Console()->Register("pause_game", "", CFGFLAG_SERVER, ConPause, this, "Pause/unpause game");
Console()->Register("change_map", "?r[map]", CFGFLAG_SERVER | CFGFLAG_STORE, ConChangeMap, this, "Change map");
Console()->Register("random_map", "?i[stars]", CFGFLAG_SERVER, ConRandomMap, this, "Random map");
Console()->Register("random_unfinished_map", "?i[stars]", CFGFLAG_SERVER, ConRandomUnfinishedMap, this, "Random unfinished map");
Console()->Register("restart", "?i[seconds]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRestart, this, "Restart in x seconds (0 = abort)");
Console()->Register("broadcast", "r[message]", CFGFLAG_SERVER, ConBroadcast, this, "Broadcast message");
Console()->Register("say", "r[message]", CFGFLAG_SERVER, ConSay, this, "Say in chat");
Console()->Register("set_team", "i[id] i[team-id] ?i[delay in minutes]", CFGFLAG_SERVER, ConSetTeam, this, "Set team of player to team");
Console()->Register("set_team_all", "i[team-id]", CFGFLAG_SERVER, ConSetTeamAll, this, "Set team of all players to team");
Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, ConAddVote, this, "Add a voting option");
Console()->Register("remove_vote", "r[name]", CFGFLAG_SERVER, ConRemoveVote, this, "remove a voting option");
Console()->Register("force_vote", "s[name] s[command] ?r[reason]", CFGFLAG_SERVER, ConForceVote, this, "Force a voting option");
Console()->Register("clear_votes", "", CFGFLAG_SERVER, ConClearVotes, this, "Clears the voting options");
Console()->Register("add_map_votes", "", CFGFLAG_SERVER, ConAddMapVotes, this, "Automatically adds voting options for all maps");
Console()->Register("vote", "r['yes'|'no']", CFGFLAG_SERVER, ConVote, this, "Force a vote to yes/no");
2023-09-26 00:24:00 +00:00
Console()->Register("votes", "?i[page]", CFGFLAG_SERVER, ConVotes, this, "Show all votes (page 0 by default, 20 entries per page)");
Console()->Register("dump_antibot", "", CFGFLAG_SERVER, ConDumpAntibot, this, "Dumps the antibot status");
Console()->Register("antibot", "r[command]", CFGFLAG_SERVER, ConAntibot, this, "Sends a command to the antibot");
2010-05-29 07:25:38 +00:00
Console()->Chain("sv_motd", ConchainSpecialMotdupdate, this);
Console()->Chain("sv_vote_kick", ConchainSettingUpdate, this);
Console()->Chain("sv_vote_kick_min", ConchainSettingUpdate, this);
Console()->Chain("sv_vote_spectate", ConchainSettingUpdate, this);
Console()->Chain("sv_spectator_slots", ConchainSettingUpdate, this);
Console()->Chain("sv_max_clients", ConchainSettingUpdate, this);
#define CONSOLE_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, callback, userdata, help);
#include <game/ddracecommands.h>
#define CHAT_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, callback, userdata, help);
#include <game/ddracechat.h>
2010-05-29 07:25:38 +00:00
}
void CGameContext::OnInit(const void *pPersistentData)
2010-05-29 07:25:38 +00:00
{
const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData;
2010-05-29 07:25:38 +00:00
m_pServer = Kernel()->RequestInterface<IServer>();
Refactor config manager, move config variable handling Move all code for handling of config variables from console to config manager. The console no longer depends on the config manager, instead the config manager now depends on the console. Add `struct`s to manage config variables of different types (int, color and string). The config manager now keeps a list of all config variables, so usage of the preprocessor can be avoided except for code to initially create all config variables. Additionally, a separate list of just the game config variables (config variables with `CFGFLAG_GAME`) is kept to optimize the `ResetGameSettings` function, as this function is called whenever connecting to a server and should be fast. Previously, this function was even less efficient because it preformed a linear search for every individual game config variable to find the respective command data. Move console commands that opperate only on config variables (`reset`, `toggle` and `+toggle`) to config manager. Ensure that these commands only opperate on the desired config variables (client or server, respectively) by checking `IConsole::FlagMask`. Add `IConfigManager::SetReadOnly` function to set/unset config variables as read-only instead of manually overriding the command handlers for the `sv_rescue` and `sv_test_cmds` config variables. This also fixes that read-only config variables could still be changed by using the `reset` command. A console message is now printed when trying to change a read-only config variable. Removing the special handling for these two config variables is additionally useful so the console does not need to keep a pointer to config values and manager. Use a `CHeap` for the config variables, their help texts and the previous values of string config variables to avoid many separate allocations as well usage of static variables. Also use the heap to store the unknown commands instead of using `std::string`s. Properly trigger command chain when resetting config variables with the `reset` command and when resetting game settings on map loading. Closes #7461. Format default value for color variables as RGB/RGBA hex with dollar sign prefix. Closes #5523. Add log message when using `reset` with a variable that does not exist. Use `log_error` instead of `dbg_msg` when saving config file fails. Support unlimited number of config save callbacks instead of at most 16. The code also becomes more readable by using an `std::vector` instead of a fixed-size array and a separate num variable. Consistently name `MACRO_CONFIG_*` parameters when declaring the macros. Add `IConsole::CMDLINE_LENGTH` constant to represent the maximum length of the console input and thereby reduce usage of magic numbers for buffer sizes.
2023-11-22 22:07:08 +00:00
m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
m_pConfig = m_pConfigManager->Values();
2010-05-29 07:25:38 +00:00
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pEngine = Kernel()->RequestInterface<IEngine>();
2017-10-13 00:25:50 +00:00
m_pStorage = Kernel()->RequestInterface<IStorage>();
m_pAntibot = Kernel()->RequestInterface<IAntibot>();
2010-05-29 07:25:38 +00:00
m_World.SetGameServer(this);
m_Events.SetGameServer(this);
m_GameUuid = RandomUuid();
Console()->SetTeeHistorianCommandCallback(CommandCallback, this);
2021-06-23 05:05:49 +00:00
uint64_t aSeed[2];
secure_random_fill(aSeed, sizeof(aSeed));
m_Prng.Seed(aSeed);
2020-05-25 13:08:24 +00:00
m_World.m_Core.m_pPrng = &m_Prng;
DeleteTempfile();
2010-05-29 07:25:38 +00:00
for(int i = 0; i < NUM_NETOBJTYPES; i++)
Server()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i));
m_Layers.Init(Kernel());
m_Collision.Init(&m_Layers);
m_World.m_pTuningList = m_aTuningList;
m_World.m_Core.InitSwitchers(m_Collision.m_HighestSwitchNumber);
2010-05-29 07:25:38 +00:00
char aMapName[IO_MAX_PATH_LENGTH];
int MapSize;
SHA256_DIGEST MapSha256;
int MapCrc;
Server()->GetMapInfo(aMapName, sizeof(aMapName), &MapSize, &MapSha256, &MapCrc);
m_MapBugs = GetMapBugs(aMapName, MapSize, MapSha256);
// Reset Tunezones
CTuningParams TuningParams;
for(int i = 0; i < NUM_TUNEZONES; i++)
{
2014-03-23 18:31:28 +00:00
TuningList()[i] = TuningParams;
TuningList()[i].Set("gun_curvature", 0);
TuningList()[i].Set("gun_speed", 1400);
TuningList()[i].Set("shotgun_curvature", 0);
TuningList()[i].Set("shotgun_speed", 500);
TuningList()[i].Set("shotgun_speeddiff", 0);
}
2014-03-23 18:31:28 +00:00
for(int i = 0; i < NUM_TUNEZONES; i++)
{
// Send no text by default when changing tune zones.
m_aaZoneEnterMsg[i][0] = 0;
m_aaZoneLeaveMsg[i][0] = 0;
}
// Reset Tuning
if(g_Config.m_SvTuneReset)
{
ResetTuning();
}
2011-09-05 11:55:47 +00:00
else
{
Tuning()->Set("gun_speed", 1400);
Tuning()->Set("gun_curvature", 0);
Tuning()->Set("shotgun_speed", 500);
Tuning()->Set("shotgun_speeddiff", 0);
Tuning()->Set("shotgun_curvature", 0);
}
if(g_Config.m_SvDDRaceTuneReset)
{
g_Config.m_SvHit = 1;
g_Config.m_SvEndlessDrag = 0;
g_Config.m_SvOldLaser = 0;
2013-11-24 18:23:58 +00:00
g_Config.m_SvOldTeleportHook = 0;
g_Config.m_SvOldTeleportWeapons = 0;
2014-04-04 22:35:18 +00:00
g_Config.m_SvTeleportHoldHook = 0;
g_Config.m_SvTeam = SV_TEAM_ALLOWED;
2022-03-03 21:56:32 +00:00
g_Config.m_SvShowOthersDefault = SHOW_OTHERS_OFF;
for(auto &Switcher : Switchers())
Switcher.m_Initial = true;
}
Console()->ExecuteFile(g_Config.m_SvResetFile, -1);
LoadMapSettings();
m_MapBugs.Dump();
if(g_Config.m_SvSoloServer)
{
g_Config.m_SvTeam = SV_TEAM_FORCED_SOLO;
2022-03-03 21:56:32 +00:00
g_Config.m_SvShowOthersDefault = SHOW_OTHERS_ON;
Tuning()->Set("player_collision", 0);
Tuning()->Set("player_hooking", 0);
for(int i = 0; i < NUM_TUNEZONES; i++)
{
TuningList()[i].Set("player_collision", 0);
TuningList()[i].Set("player_hooking", 0);
}
}
2023-11-16 10:39:55 +00:00
if(!str_comp(Config()->m_SvGametype, "mod"))
m_pController = new CGameControllerMod(this);
else
m_pController = new CGameControllerDDRace(this);
2017-10-10 00:39:29 +00:00
2020-09-18 15:37:27 +00:00
const char *pCensorFilename = "censorlist.txt";
IOHANDLE File = Storage()->OpenFile(pCensorFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL);
2020-09-18 15:37:27 +00:00
if(!File)
{
dbg_msg("censorlist", "failed to open '%s'", pCensorFilename);
}
else
{
CLineReader LineReader;
LineReader.Init(File);
char *pLine;
while((pLine = LineReader.Get()))
{
m_vCensorlist.emplace_back(pLine);
2020-09-18 15:37:27 +00:00
}
io_close(File);
}
m_TeeHistorianActive = g_Config.m_SvTeeHistorian;
if(m_TeeHistorianActive)
{
char aGameUuid[UUID_MAXSTRSIZE];
FormatUuid(m_GameUuid, aGameUuid, sizeof(aGameUuid));
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "teehistorian/%s.teehistorian", aGameUuid);
IOHANDLE THFile = Storage()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!THFile)
{
dbg_msg("teehistorian", "failed to open '%s'", aFilename);
2017-10-13 00:25:50 +00:00
Server()->SetErrorShutdown("teehistorian open error");
return;
}
else
{
dbg_msg("teehistorian", "recording to '%s'", aFilename);
}
m_pTeeHistorianFile = aio_new(THFile);
char aVersion[128];
if(GIT_SHORTREV_HASH)
{
str_format(aVersion, sizeof(aVersion), "%s (%s)", GAME_VERSION, GIT_SHORTREV_HASH);
}
else
{
str_copy(aVersion, GAME_VERSION);
}
CTeeHistorian::CGameInfo GameInfo;
GameInfo.m_GameUuid = m_GameUuid;
GameInfo.m_pServerVersion = aVersion;
GameInfo.m_StartTime = time(0);
2020-05-25 13:08:24 +00:00
GameInfo.m_pPrngDescription = m_Prng.Description();
GameInfo.m_pServerName = g_Config.m_SvName;
GameInfo.m_ServerPort = Server()->Port();
GameInfo.m_pGameType = m_pController->m_pGameType;
GameInfo.m_pConfig = &g_Config;
GameInfo.m_pTuning = Tuning();
GameInfo.m_pUuids = &g_UuidManager;
GameInfo.m_pMapName = aMapName;
GameInfo.m_MapSize = MapSize;
GameInfo.m_MapSha256 = MapSha256;
GameInfo.m_MapCrc = MapCrc;
if(pPersistent)
{
GameInfo.m_HavePrevGameUuid = true;
GameInfo.m_PrevGameUuid = pPersistent->m_PrevGameUuid;
}
else
{
GameInfo.m_HavePrevGameUuid = false;
mem_zero(&GameInfo.m_PrevGameUuid, sizeof(GameInfo.m_PrevGameUuid));
}
m_TeeHistorian.Reset(&GameInfo, TeeHistorianWrite, this);
for(int i = 0; i < MAX_CLIENTS; i++)
{
int Level = Server()->GetAuthedState(i);
if(Level)
{
m_TeeHistorian.RecordAuthInitial(i, Level, Server()->GetAuthName(i));
}
}
}
if(!m_pScore)
{
m_pScore = new CScore(this, ((CServer *)Server())->DbPool());
}
2010-05-29 07:25:38 +00:00
// create all entities from the game layer
CreateAllEntities(true);
if(GIT_SHORTREV_HASH)
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "git-revision", GIT_SHORTREV_HASH);
m_pAntibot->RoundStart(this);
}
void CGameContext::CreateAllEntities(bool Initial)
{
const CMapItemLayerTilemap *pTileMap = m_Layers.GameLayer();
const CTile *pTiles = static_cast<CTile *>(Kernel()->RequestInterface<IMap>()->GetData(pTileMap->m_Data));
const CTile *pFront = nullptr;
if(m_Layers.FrontLayer())
pFront = static_cast<CTile *>(Kernel()->RequestInterface<IMap>()->GetData(m_Layers.FrontLayer()->m_Front));
const CSwitchTile *pSwitch = nullptr;
if(m_Layers.SwitchLayer())
pSwitch = static_cast<CSwitchTile *>(Kernel()->RequestInterface<IMap>()->GetData(m_Layers.SwitchLayer()->m_Switch));
2010-05-29 07:25:38 +00:00
for(int y = 0; y < pTileMap->m_Height; y++)
{
for(int x = 0; x < pTileMap->m_Width; x++)
{
const int Index = y * pTileMap->m_Width + x;
// Game layer
{
const int GameIndex = pTiles[Index].m_Index;
if(GameIndex == TILE_OLDLASER)
{
g_Config.m_SvOldLaser = 1;
dbg_msg("game_layer", "found old laser tile");
}
else if(GameIndex == TILE_NPC)
{
m_Tuning.Set("player_collision", 0);
dbg_msg("game_layer", "found no collision tile");
}
else if(GameIndex == TILE_EHOOK)
{
g_Config.m_SvEndlessDrag = 1;
dbg_msg("game_layer", "found unlimited hook time tile");
}
else if(GameIndex == TILE_NOHIT)
{
g_Config.m_SvHit = 0;
dbg_msg("game_layer", "found no weapons hitting others tile");
}
else if(GameIndex == TILE_NPH)
{
m_Tuning.Set("player_hooking", 0);
dbg_msg("game_layer", "found no player hooking tile");
}
else if(GameIndex >= ENTITY_OFFSET)
{
m_pController->OnEntity(GameIndex - ENTITY_OFFSET, x, y, LAYER_GAME, pTiles[Index].m_Flags, Initial);
}
}
if(pFront)
{
const int FrontIndex = pFront[Index].m_Index;
if(FrontIndex == TILE_OLDLASER)
2011-01-29 00:59:50 +00:00
{
2010-12-01 22:45:04 +00:00
g_Config.m_SvOldLaser = 1;
dbg_msg("front_layer", "found old laser tile");
2011-01-29 00:59:50 +00:00
}
else if(FrontIndex == TILE_NPC)
2011-01-29 00:59:50 +00:00
{
m_Tuning.Set("player_collision", 0);
dbg_msg("front_layer", "found no collision tile");
2011-01-29 00:59:50 +00:00
}
else if(FrontIndex == TILE_EHOOK)
2011-01-29 00:59:50 +00:00
{
g_Config.m_SvEndlessDrag = 1;
dbg_msg("front_layer", "found unlimited hook time tile");
2011-01-29 00:59:50 +00:00
}
else if(FrontIndex == TILE_NOHIT)
2011-01-29 00:59:50 +00:00
{
g_Config.m_SvHit = 0;
dbg_msg("front_layer", "found no weapons hitting others tile");
2011-01-29 00:59:50 +00:00
}
else if(FrontIndex == TILE_NPH)
2011-01-29 00:59:50 +00:00
{
m_Tuning.Set("player_hooking", 0);
dbg_msg("front_layer", "found no player hooking tile");
2011-01-29 00:59:50 +00:00
}
else if(FrontIndex >= ENTITY_OFFSET)
{
m_pController->OnEntity(FrontIndex - ENTITY_OFFSET, x, y, LAYER_FRONT, pFront[Index].m_Flags, Initial);
}
}
if(pSwitch)
{
const int SwitchType = pSwitch[Index].m_Type;
2013-08-22 22:45:48 +00:00
// TODO: Add off by default door here
// if(SwitchType == TILE_DOOR_OFF)
if(SwitchType >= ENTITY_OFFSET)
{
m_pController->OnEntity(SwitchType - ENTITY_OFFSET, x, y, LAYER_SWITCH, pSwitch[Index].m_Flags, Initial, pSwitch[Index].m_Number);
}
2010-05-29 07:25:38 +00:00
}
}
}
}
void CGameContext::DeleteTempfile()
{
if(m_aDeleteTempfile[0] != 0)
{
2017-10-13 00:25:50 +00:00
Storage()->RemoveFile(m_aDeleteTempfile, IStorage::TYPE_SAVE);
m_aDeleteTempfile[0] = 0;
}
}
void CGameContext::OnMapChange(char *pNewMapName, int MapNameSize)
{
char aConfig[IO_MAX_PATH_LENGTH];
str_format(aConfig, sizeof(aConfig), "maps/%s.cfg", g_Config.m_SvMap);
IOHANDLE File = Storage()->OpenFile(aConfig, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL);
if(!File)
{
// No map-specific config, just return.
return;
}
CLineReader LineReader;
LineReader.Init(File);
std::vector<char *> vLines;
char *pLine;
int TotalLength = 0;
while((pLine = LineReader.Get()))
{
int Length = str_length(pLine) + 1;
char *pCopy = (char *)malloc(Length);
mem_copy(pCopy, pLine, Length);
vLines.push_back(pCopy);
TotalLength += Length;
}
io_close(File);
char *pSettings = (char *)malloc(maximum(1, TotalLength));
int Offset = 0;
for(auto &Line : vLines)
{
int Length = str_length(Line) + 1;
mem_copy(pSettings + Offset, Line, Length);
2015-07-15 09:38:42 +00:00
Offset += Length;
free(Line);
}
CDataFileReader Reader;
2017-10-13 00:25:50 +00:00
Reader.Open(Storage(), pNewMapName, IStorage::TYPE_ALL);
CDataFileWriter Writer;
int SettingsIndex = Reader.NumData();
bool FoundInfo = false;
for(int i = 0; i < Reader.NumItems(); i++)
{
int TypeID;
int ItemID;
void *pData = Reader.GetItem(i, &TypeID, &ItemID);
int Size = Reader.GetItemSize(i);
CMapItemInfoSettings MapInfo;
if(TypeID == MAPITEMTYPE_INFO && ItemID == 0)
{
FoundInfo = true;
if(Size >= (int)sizeof(CMapItemInfoSettings))
{
CMapItemInfoSettings *pInfo = (CMapItemInfoSettings *)pData;
if(pInfo->m_Settings > -1)
{
SettingsIndex = pInfo->m_Settings;
char *pMapSettings = (char *)Reader.GetData(SettingsIndex);
int DataSize = Reader.GetDataSize(SettingsIndex);
if(DataSize == TotalLength && mem_comp(pSettings, pMapSettings, DataSize) == 0)
{
// Configs coincide, no need to update map.
free(pSettings);
return;
}
Reader.UnloadData(pInfo->m_Settings);
}
else
{
MapInfo = *pInfo;
MapInfo.m_Settings = SettingsIndex;
pData = &MapInfo;
Size = sizeof(MapInfo);
}
}
else
{
*(CMapItemInfo *)&MapInfo = *(CMapItemInfo *)pData;
MapInfo.m_Settings = SettingsIndex;
pData = &MapInfo;
Size = sizeof(MapInfo);
}
}
Writer.AddItem(TypeID, ItemID, Size, pData);
}
if(!FoundInfo)
{
CMapItemInfoSettings Info;
Info.m_Version = 1;
Info.m_Author = -1;
Info.m_MapVersion = -1;
Info.m_Credits = -1;
Info.m_License = -1;
Info.m_Settings = SettingsIndex;
Writer.AddItem(MAPITEMTYPE_INFO, 0, sizeof(Info), &Info);
}
for(int i = 0; i < Reader.NumData() || i == SettingsIndex; i++)
{
if(i == SettingsIndex)
{
Writer.AddData(TotalLength, pSettings);
continue;
}
const void *pData = Reader.GetData(i);
int Size = Reader.GetDataSize(i);
Writer.AddData(Size, pData);
Reader.UnloadData(i);
}
dbg_msg("mapchange", "imported settings");
free(pSettings);
Reader.Close();
char aTemp[IO_MAX_PATH_LENGTH];
Writer.Open(Storage(), IStorage::FormatTmpPath(aTemp, sizeof(aTemp), pNewMapName));
Writer.Finish();
str_copy(pNewMapName, aTemp, MapNameSize);
str_copy(m_aDeleteTempfile, aTemp, sizeof(m_aDeleteTempfile));
}
void CGameContext::OnShutdown(void *pPersistentData)
2010-05-29 07:25:38 +00:00
{
CPersistentData *pPersistent = (CPersistentData *)pPersistentData;
if(pPersistent)
{
pPersistent->m_PrevGameUuid = m_GameUuid;
}
Antibot()->RoundEnd();
if(m_TeeHistorianActive)
{
m_TeeHistorian.Finish();
aio_close(m_pTeeHistorianFile);
aio_wait(m_pTeeHistorianFile);
int Error = aio_error(m_pTeeHistorianFile);
2017-10-10 02:07:38 +00:00
if(Error)
{
dbg_msg("teehistorian", "error closing file, err=%d", Error);
2017-10-13 00:25:50 +00:00
Server()->SetErrorShutdown("teehistorian close error");
2017-10-10 02:07:38 +00:00
}
aio_free(m_pTeeHistorianFile);
}
// Stop any demos being recorded.
Server()->StopDemos();
DeleteTempfile();
Refactor config manager, move config variable handling Move all code for handling of config variables from console to config manager. The console no longer depends on the config manager, instead the config manager now depends on the console. Add `struct`s to manage config variables of different types (int, color and string). The config manager now keeps a list of all config variables, so usage of the preprocessor can be avoided except for code to initially create all config variables. Additionally, a separate list of just the game config variables (config variables with `CFGFLAG_GAME`) is kept to optimize the `ResetGameSettings` function, as this function is called whenever connecting to a server and should be fast. Previously, this function was even less efficient because it preformed a linear search for every individual game config variable to find the respective command data. Move console commands that opperate only on config variables (`reset`, `toggle` and `+toggle`) to config manager. Ensure that these commands only opperate on the desired config variables (client or server, respectively) by checking `IConsole::FlagMask`. Add `IConfigManager::SetReadOnly` function to set/unset config variables as read-only instead of manually overriding the command handlers for the `sv_rescue` and `sv_test_cmds` config variables. This also fixes that read-only config variables could still be changed by using the `reset` command. A console message is now printed when trying to change a read-only config variable. Removing the special handling for these two config variables is additionally useful so the console does not need to keep a pointer to config values and manager. Use a `CHeap` for the config variables, their help texts and the previous values of string config variables to avoid many separate allocations as well usage of static variables. Also use the heap to store the unknown commands instead of using `std::string`s. Properly trigger command chain when resetting config variables with the `reset` command and when resetting game settings on map loading. Closes #7461. Format default value for color variables as RGB/RGBA hex with dollar sign prefix. Closes #5523. Add log message when using `reset` with a variable that does not exist. Use `log_error` instead of `dbg_msg` when saving config file fails. Support unlimited number of config save callbacks instead of at most 16. The code also becomes more readable by using an `std::vector` instead of a fixed-size array and a separate num variable. Consistently name `MACRO_CONFIG_*` parameters when declaring the macros. Add `IConsole::CMDLINE_LENGTH` constant to represent the maximum length of the console input and thereby reduce usage of magic numbers for buffer sizes.
2023-11-22 22:07:08 +00:00
ConfigManager()->ResetGameSettings();
2010-10-27 10:14:26 +00:00
Collision()->Dest();
2010-05-29 07:25:38 +00:00
delete m_pController;
m_pController = 0;
Clear();
}
void CGameContext::LoadMapSettings()
{
IMap *pMap = Kernel()->RequestInterface<IMap>();
int Start, Num;
pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num);
for(int i = Start; i < Start + Num; i++)
{
int ItemID;
CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemID);
int ItemSize = pMap->GetItemSize(i);
if(!pItem || ItemID != 0)
continue;
if(ItemSize < (int)sizeof(CMapItemInfoSettings))
break;
if(!(pItem->m_Settings > -1))
break;
int Size = pMap->GetDataSize(pItem->m_Settings);
char *pSettings = (char *)pMap->GetData(pItem->m_Settings);
char *pNext = pSettings;
while(pNext < pSettings + Size)
{
int StrSize = str_length(pNext) + 1;
Console()->ExecuteLine(pNext, IConsole::CLIENT_ID_GAME);
pNext += StrSize;
}
pMap->UnloadData(pItem->m_Settings);
break;
}
char aBuf[IO_MAX_PATH_LENGTH];
str_format(aBuf, sizeof(aBuf), "maps/%s.map.cfg", g_Config.m_SvMap);
Console()->ExecuteFile(aBuf, IConsole::CLIENT_ID_NO_GAME);
}
void CGameContext::OnSnap(int ClientID)
{
2012-01-08 23:49:20 +00:00
// add tuning to demo
CTuningParams StandardTuning;
2022-08-25 15:02:40 +00:00
if(Server()->IsRecording(ClientID > -1 ? ClientID : MAX_CLIENTS) && mem_comp(&StandardTuning, &m_Tuning, sizeof(CTuningParams)) != 0)
2012-01-08 23:49:20 +00:00
{
CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
int *pParams = (int *)&m_Tuning;
for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++)
2012-01-08 23:49:20 +00:00
Msg.AddInt(pParams[i]);
Server()->SendMsg(&Msg, MSGFLAG_RECORD | MSGFLAG_NOSEND, ClientID);
2012-01-08 23:49:20 +00:00
}
m_pController->Snap(ClientID);
2020-10-26 14:14:07 +00:00
for(auto &pPlayer : m_apPlayers)
{
2020-10-26 14:14:07 +00:00
if(pPlayer)
pPlayer->Snap(ClientID);
}
if(ClientID > -1)
m_apPlayers[ClientID]->FakeSnap();
m_World.Snap(ClientID);
m_Events.Snap(ClientID);
}
2010-05-29 07:25:38 +00:00
void CGameContext::OnPreSnap() {}
void CGameContext::OnPostSnap()
{
m_Events.Clear();
}
void CGameContext::UpdatePlayerMaps()
{
const auto DistCompare = [](std::pair<float, int> a, std::pair<float, int> b) -> bool {
return (a.first < b.first);
};
if(Server()->Tick() % g_Config.m_SvMapUpdateRate != 0)
return;
std::pair<float, int> Dist[MAX_CLIENTS];
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!Server()->ClientIngame(i))
continue;
if(Server()->GetClientVersion(i) >= VERSION_DDNET_OLD)
continue;
int *pMap = Server()->GetIdMap(i);
// compute distances
for(int j = 0; j < MAX_CLIENTS; j++)
{
Dist[j].second = j;
if(j == i)
continue;
if(!Server()->ClientIngame(j) || !m_apPlayers[j])
{
Dist[j].first = 1e10;
continue;
}
CCharacter *pChr = m_apPlayers[j]->GetCharacter();
if(!pChr)
{
Dist[j].first = 1e9;
continue;
}
if(!pChr->CanSnapCharacter(i))
Dist[j].first = 1e8;
else
Dist[j].first = length_squared(m_apPlayers[i]->m_ViewPos - pChr->GetPos());
}
// always send the player themselves, even if all in same position
Dist[i].first = -1;
std::nth_element(&Dist[0], &Dist[VANILLA_MAX_CLIENTS - 1], &Dist[MAX_CLIENTS], DistCompare);
int Index = 1; // exclude self client id
for(int j = 0; j < VANILLA_MAX_CLIENTS - 1; j++)
{
pMap[j + 1] = -1; // also fill player with empty name to say chat msgs
if(Dist[j].second == i || Dist[j].first > 5e9f)
continue;
pMap[Index++] = Dist[j].second;
}
// sort by real client ids, guarantee order on distance changes, O(Nlog(N)) worst case
// sort just clients in game always except first (self client id) and last (fake client id) indexes
std::sort(&pMap[1], &pMap[minimum(Index, VANILLA_MAX_CLIENTS - 1)]);
}
}
bool CGameContext::IsClientReady(int ClientID) const
{
2022-01-22 16:34:23 +00:00
return m_apPlayers[ClientID] && m_apPlayers[ClientID]->m_IsReady;
}
bool CGameContext::IsClientPlayer(int ClientID) const
{
return m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetTeam() != TEAM_SPECTATORS;
}
CUuid CGameContext::GameUuid() const { return m_GameUuid; }
const char *CGameContext::GameType() const { return m_pController && m_pController->m_pGameType ? m_pController->m_pGameType : ""; }
const char *CGameContext::Version() const { return GAME_VERSION; }
const char *CGameContext::NetVersion() const { return GAME_NETVERSION; }
2010-05-29 07:25:38 +00:00
IGameServer *CreateGameServer() { return new CGameContext; }
void CGameContext::OnSetAuthed(int ClientID, int Level)
2011-01-29 00:59:50 +00:00
{
if(m_apPlayers[ClientID])
2011-01-29 00:59:50 +00:00
{
char aBuf[512], aIP[NETADDR_MAXSTRSIZE];
Server()->GetClientAddr(ClientID, aIP, sizeof(aIP));
2011-07-20 00:41:11 +00:00
str_format(aBuf, sizeof(aBuf), "ban %s %d Banned by vote", aIP, g_Config.m_SvVoteKickBantime);
if(!str_comp_nocase(m_aVoteCommand, aBuf) && Level > Server()->GetAuthedState(m_VoteCreator))
2011-01-29 00:59:50 +00:00
{
2011-12-26 09:15:43 +00:00
m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN;
2019-02-27 18:46:01 +00:00
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "CGameContext", "Vote aborted by authorized login.");
2011-01-29 00:59:50 +00:00
}
}
if(m_TeeHistorianActive)
{
if(Level)
{
m_TeeHistorian.RecordAuthLogin(ClientID, Level, Server()->GetAuthName(ClientID));
}
else
{
m_TeeHistorian.RecordAuthLogout(ClientID);
}
}
2011-01-29 00:59:50 +00:00
}
void CGameContext::SendRecord(int ClientID)
{
CNetMsg_Sv_Record Msg;
CNetMsg_Sv_RecordLegacy MsgLegacy;
MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = Score()->PlayerData(ClientID)->m_BestTime * 100.0f;
MsgLegacy.m_ServerTimeBest = Msg.m_ServerTimeBest = m_pController->m_CurrentRecord * 100.0f; //TODO: finish this
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
if(!Server()->IsSixup(ClientID) && GetClientVersion(ClientID) < VERSION_DDNET_MSG_LEGACY)
{
Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientID);
}
2011-02-23 07:43:05 +00:00
}
2022-12-12 22:32:45 +00:00
bool CGameContext::ProcessSpamProtection(int ClientID, bool RespectChatInitialDelay)
{
2011-09-07 23:02:09 +00:00
if(!m_apPlayers[ClientID])
2022-12-12 22:32:45 +00:00
return false;
if(g_Config.m_SvSpamprotection && m_apPlayers[ClientID]->m_LastChat && m_apPlayers[ClientID]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick())
2022-12-12 22:32:45 +00:00
return true;
else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientID))
{
SendChatTarget(ClientID, "Players are not allowed to chat from VPNs at this time");
2022-12-12 22:32:45 +00:00
return true;
}
else
m_apPlayers[ClientID]->m_LastChat = Server()->Tick();
NETADDR Addr;
Server()->GetClientAddr(ClientID, &Addr);
CMute Muted;
int Expires = 0;
for(int i = 0; i < m_NumMutes && Expires <= 0; i++)
{
if(!net_addr_comp_noport(&Addr, &m_aMutes[i].m_Addr))
2021-03-01 18:44:59 +00:00
{
if(RespectChatInitialDelay || m_aMutes[i].m_InitialChatDelay)
{
Muted = m_aMutes[i];
Expires = (m_aMutes[i].m_Expire - Server()->Tick()) / Server()->TickSpeed();
}
2021-03-01 18:44:59 +00:00
}
}
if(Expires > 0)
{
char aBuf[128];
if(Muted.m_InitialChatDelay)
2023-11-19 21:36:47 +00:00
str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will be able to talk in %d seconds.", Expires);
else
2023-11-19 21:36:47 +00:00
str_format(aBuf, sizeof(aBuf), "You are not permitted to talk for the next %d seconds.", Expires);
SendChatTarget(ClientID, aBuf);
2022-12-12 22:32:45 +00:00
return true;
}
if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientID]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold)
{
2018-10-08 18:29:33 +00:00
Mute(&Addr, g_Config.m_SvSpamMuteDuration, Server()->ClientName(ClientID));
m_apPlayers[ClientID]->m_ChatScore = 0;
2022-12-12 22:32:45 +00:00
return true;
}
2022-12-12 22:32:45 +00:00
return false;
}
2011-04-19 19:44:02 +00:00
int CGameContext::GetDDRaceTeam(int ClientID)
{
return m_pController->Teams().m_Core.Team(ClientID);
2011-04-19 19:44:02 +00:00
}
void CGameContext::ResetTuning()
{
CTuningParams TuningParams;
m_Tuning = TuningParams;
Tuning()->Set("gun_speed", 1400);
Tuning()->Set("gun_curvature", 0);
Tuning()->Set("shotgun_speed", 500);
Tuning()->Set("shotgun_speeddiff", 0);
Tuning()->Set("shotgun_curvature", 0);
SendTuningParams(-1);
}
2013-08-09 13:04:20 +00:00
bool CheckClientID2(int ClientID)
{
2022-01-22 16:34:23 +00:00
return ClientID >= 0 && ClientID < MAX_CLIENTS;
2013-08-09 13:04:20 +00:00
}
void CGameContext::Whisper(int ClientID, char *pStr)
{
2013-08-10 01:40:59 +00:00
if(ProcessSpamProtection(ClientID))
return;
2013-08-09 13:04:20 +00:00
pStr = str_skip_whitespaces(pStr);
char *pName;
int Victim;
2022-12-12 22:32:45 +00:00
bool Error = false;
2013-08-09 13:04:20 +00:00
// add token
if(*pStr == '"')
{
pStr++;
pName = pStr;
char *pDst = pStr; // we might have to process escape data
2022-02-14 23:12:52 +00:00
while(true)
2013-08-09 13:04:20 +00:00
{
if(pStr[0] == '"')
{
2013-08-09 13:04:20 +00:00
break;
}
2013-08-09 13:04:20 +00:00
else if(pStr[0] == '\\')
{
if(pStr[1] == '\\')
pStr++; // skip due to escape
else if(pStr[1] == '"')
pStr++; // skip due to escape
}
else if(pStr[0] == 0)
{
2022-12-12 22:32:45 +00:00
Error = true;
break;
}
2013-08-09 13:04:20 +00:00
*pDst = *pStr;
pDst++;
2013-08-09 13:04:20 +00:00
pStr++;
}
if(!Error)
{
// write null termination
*pDst = 0;
pStr++;
for(Victim = 0; Victim < MAX_CLIENTS; Victim++)
if(str_comp(pName, Server()->ClientName(Victim)) == 0)
break;
}
2013-08-09 13:04:20 +00:00
}
else
{
pName = pStr;
2022-02-14 23:12:52 +00:00
while(true)
2013-08-09 13:04:20 +00:00
{
2013-08-10 02:30:59 +00:00
if(pStr[0] == 0)
{
2022-12-12 22:32:45 +00:00
Error = true;
2013-08-10 02:30:59 +00:00
break;
}
2013-08-09 13:04:20 +00:00
if(pStr[0] == ' ')
{
pStr[0] = 0;
for(Victim = 0; Victim < MAX_CLIENTS; Victim++)
if(str_comp(pName, Server()->ClientName(Victim)) == 0)
break;
pStr[0] = ' ';
if(Victim < MAX_CLIENTS)
break;
2013-08-09 13:04:20 +00:00
}
pStr++;
}
}
if(pStr[0] != ' ')
{
2022-12-12 22:32:45 +00:00
Error = true;
2013-08-09 13:04:20 +00:00
}
*pStr = 0;
pStr++;
if(Error)
2013-08-09 13:04:20 +00:00
{
SendChatTarget(ClientID, "Invalid whisper");
2013-08-09 13:04:20 +00:00
return;
}
if(Victim >= MAX_CLIENTS || !CheckClientID2(Victim))
2013-08-09 13:04:20 +00:00
{
char aBuf[256];
2013-08-09 13:04:20 +00:00
str_format(aBuf, sizeof(aBuf), "No player with name \"%s\" found", pName);
SendChatTarget(ClientID, aBuf);
return;
}
WhisperID(ClientID, Victim, pStr);
2013-10-18 09:42:35 +00:00
}
2020-06-12 13:14:56 +00:00
void CGameContext::WhisperID(int ClientID, int VictimID, const char *pMessage)
2013-10-18 09:42:35 +00:00
{
if(!CheckClientID2(ClientID))
2013-10-18 09:42:35 +00:00
return;
if(!CheckClientID2(VictimID))
2013-10-18 09:42:35 +00:00
return;
if(m_apPlayers[ClientID])
m_apPlayers[ClientID]->m_LastWhisperTo = VictimID;
2013-10-18 09:42:35 +00:00
2020-09-18 15:37:27 +00:00
char aCensoredMessage[256];
CensorMessage(aCensoredMessage, pMessage, sizeof(aCensoredMessage));
2013-10-18 09:42:35 +00:00
char aBuf[256];
2020-06-12 13:14:56 +00:00
if(Server()->IsSixup(ClientID))
{
protocol7::CNetMsg_Sv_Chat Msg;
Msg.m_ClientID = ClientID;
Msg.m_Mode = protocol7::CHAT_WHISPER;
2020-09-18 15:37:27 +00:00
Msg.m_pMessage = aCensoredMessage;
2020-06-12 13:14:56 +00:00
Msg.m_TargetID = VictimID;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
2020-06-12 13:14:56 +00:00
}
else if(GetClientVersion(ClientID) >= VERSION_DDNET_WHISPER)
{
CNetMsg_Sv_Chat Msg;
Msg.m_Team = CHAT_WHISPER_SEND;
Msg.m_ClientID = VictimID;
2020-09-18 15:37:27 +00:00
Msg.m_pMessage = aCensoredMessage;
if(g_Config.m_SvDemoChat)
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
else
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
}
else
{
2020-09-18 15:37:27 +00:00
str_format(aBuf, sizeof(aBuf), "[→ %s] %s", Server()->ClientName(VictimID), aCensoredMessage);
SendChatTarget(ClientID, aBuf);
}
2013-08-09 13:04:20 +00:00
2020-06-12 13:14:56 +00:00
if(Server()->IsSixup(VictimID))
{
protocol7::CNetMsg_Sv_Chat Msg;
Msg.m_ClientID = ClientID;
Msg.m_Mode = protocol7::CHAT_WHISPER;
2020-09-18 15:37:27 +00:00
Msg.m_pMessage = aCensoredMessage;
2020-06-12 13:14:56 +00:00
Msg.m_TargetID = VictimID;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimID);
2020-06-12 13:14:56 +00:00
}
else if(GetClientVersion(VictimID) >= VERSION_DDNET_WHISPER)
{
CNetMsg_Sv_Chat Msg2;
Msg2.m_Team = CHAT_WHISPER_RECV;
Msg2.m_ClientID = ClientID;
2020-09-18 15:37:27 +00:00
Msg2.m_pMessage = aCensoredMessage;
if(g_Config.m_SvDemoChat)
Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL, VictimID);
else
Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimID);
}
else
{
2020-09-18 15:37:27 +00:00
str_format(aBuf, sizeof(aBuf), "[← %s] %s", Server()->ClientName(ClientID), aCensoredMessage);
SendChatTarget(VictimID, aBuf);
}
2013-08-09 13:04:20 +00:00
}
2013-10-18 09:42:35 +00:00
void CGameContext::Converse(int ClientID, char *pStr)
{
CPlayer *pPlayer = m_apPlayers[ClientID];
if(!pPlayer)
2013-10-18 09:42:35 +00:00
return;
2014-04-23 22:43:49 +00:00
if(ProcessSpamProtection(ClientID))
return;
if(pPlayer->m_LastWhisperTo < 0)
2013-10-18 09:42:35 +00:00
SendChatTarget(ClientID, "You do not have an ongoing conversation. Whisper to someone to start one");
else
{
WhisperID(ClientID, pPlayer->m_LastWhisperTo, pStr);
}
}
2019-02-11 17:52:40 +00:00
bool CGameContext::IsVersionBanned(int Version)
{
char aVersion[16];
str_from_int(Version, aVersion);
2019-02-11 17:52:40 +00:00
return str_in_list(g_Config.m_SvBannedVersions, ",", aVersion);
}
2013-12-31 05:13:57 +00:00
2017-03-21 10:24:44 +00:00
void CGameContext::List(int ClientID, const char *pFilter)
2013-12-31 05:13:57 +00:00
{
2017-03-21 10:24:44 +00:00
int Total = 0;
char aBuf[256];
int Bufcnt = 0;
if(pFilter[0])
2017-03-21 10:24:44 +00:00
str_format(aBuf, sizeof(aBuf), "Listing players with \"%s\" in name:", pFilter);
2013-12-31 05:13:57 +00:00
else
str_copy(aBuf, "Listing all players:");
2017-03-21 10:24:44 +00:00
SendChatTarget(ClientID, aBuf);
2013-12-31 05:13:57 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_apPlayers[i])
{
2017-03-21 10:24:44 +00:00
Total++;
const char *pName = Server()->ClientName(i);
if(str_utf8_find_nocase(pName, pFilter) == NULL)
2013-12-31 05:13:57 +00:00
continue;
if(Bufcnt + str_length(pName) + 4 > 256)
2013-12-31 05:13:57 +00:00
{
2017-03-21 10:24:44 +00:00
SendChatTarget(ClientID, aBuf);
Bufcnt = 0;
2013-12-31 05:13:57 +00:00
}
if(Bufcnt != 0)
2013-12-31 05:13:57 +00:00
{
2017-03-21 10:24:44 +00:00
str_format(&aBuf[Bufcnt], sizeof(aBuf) - Bufcnt, ", %s", pName);
Bufcnt += 2 + str_length(pName);
2013-12-31 05:13:57 +00:00
}
else
{
str_copy(&aBuf[Bufcnt], pName, sizeof(aBuf) - Bufcnt);
2017-03-21 10:24:44 +00:00
Bufcnt += str_length(pName);
2013-12-31 05:13:57 +00:00
}
}
}
if(Bufcnt != 0)
2017-03-21 10:24:44 +00:00
SendChatTarget(ClientID, aBuf);
str_format(aBuf, sizeof(aBuf), "%d players online", Total);
SendChatTarget(ClientID, aBuf);
2013-12-31 05:13:57 +00:00
}
int CGameContext::GetClientVersion(int ClientID) const
{
return Server()->GetClientVersion(ClientID);
}
2018-01-05 11:04:06 +00:00
2023-01-24 08:27:29 +00:00
CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version)
{
2023-01-24 08:27:29 +00:00
CClientMask Mask;
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(GetClientVersion(i) >= Version)
continue;
2023-01-24 08:27:29 +00:00
Mask.set(i);
}
return Mask;
}
bool CGameContext::PlayerModerating() const
2018-01-05 11:04:06 +00:00
{
2022-01-23 17:56:37 +00:00
return std::any_of(std::begin(m_apPlayers), std::end(m_apPlayers), [](const CPlayer *pPlayer) { return pPlayer && pPlayer->m_Moderating; });
2018-01-05 11:04:06 +00:00
}
2018-01-18 15:17:23 +00:00
void CGameContext::ForceVote(int EnforcerID, bool Success)
{
// check if there is a vote running
2018-01-25 18:33:40 +00:00
if(!m_VoteCloseTime)
2018-01-18 15:17:23 +00:00
return;
2018-10-07 22:59:07 +00:00
2018-01-18 15:17:23 +00:00
m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
m_VoteEnforcer = EnforcerID;
2018-10-07 22:59:07 +00:00
2018-01-18 15:17:23 +00:00
char aBuf[256];
2018-01-18 17:41:37 +00:00
const char *pOption = Success ? "yes" : "no";
2018-04-07 14:51:17 +00:00
str_format(aBuf, sizeof(aBuf), "authorized player forced vote %s", pOption);
2018-01-18 15:17:23 +00:00
SendChatTarget(-1, aBuf);
2018-01-18 17:30:38 +00:00
str_format(aBuf, sizeof(aBuf), "forcing vote %s", pOption);
2018-01-18 15:17:23 +00:00
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
}
2020-06-14 08:44:14 +00:00
bool CGameContext::RateLimitPlayerVote(int ClientID)
{
2021-06-23 05:05:49 +00:00
int64_t Now = Server()->Tick();
int64_t TickSpeed = Server()->TickSpeed();
2020-06-14 08:44:14 +00:00
CPlayer *pPlayer = m_apPlayers[ClientID];
if(g_Config.m_SvRconVote && !Server()->GetAuthedState(ClientID))
{
SendChatTarget(ClientID, "You can only vote after logging in.");
return true;
}
if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1)
2020-06-14 08:44:14 +00:00
{
if(m_pServer->DnsblPending(ClientID))
{
SendChatTarget(ClientID, "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds.");
return true;
}
else if(m_pServer->DnsblBlack(ClientID))
{
SendChatTarget(ClientID, "You are not allowed to vote because you appear to be using a VPN. Try connecting without a VPN or contacting an admin if you think this is a mistake.");
return true;
}
2020-06-14 08:44:14 +00:00
}
if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + TickSpeed * 3 > Now)
return true;
pPlayer->m_LastVoteTry = Now;
if(m_VoteCloseTime)
{
SendChatTarget(ClientID, "Wait for current vote to end before calling a new one.");
return true;
}
if(Now < pPlayer->m_FirstVoteTick)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "You must wait %d seconds before making your first vote.", (int)((pPlayer->m_FirstVoteTick - Now) / TickSpeed) + 1);
SendChatTarget(ClientID, aBuf);
return true;
}
int TimeLeft = pPlayer->m_LastVoteCall + TickSpeed * g_Config.m_SvVoteDelay - Now;
if(pPlayer->m_LastVoteCall && TimeLeft > 0)
{
char aChatmsg[64];
str_format(aChatmsg, sizeof(aChatmsg), "You must wait %d seconds before making another vote.", (int)(TimeLeft / TickSpeed) + 1);
SendChatTarget(ClientID, aChatmsg);
return true;
}
NETADDR Addr;
Server()->GetClientAddr(ClientID, &Addr);
int VoteMuted = 0;
for(int i = 0; i < m_NumVoteMutes && !VoteMuted; i++)
if(!net_addr_comp_noport(&Addr, &m_aVoteMutes[i].m_Addr))
VoteMuted = (m_aVoteMutes[i].m_Expire - Server()->Tick()) / Server()->TickSpeed();
for(int i = 0; i < m_NumMutes && VoteMuted == 0; i++)
{
if(!net_addr_comp_noport(&Addr, &m_aMutes[i].m_Addr))
VoteMuted = (m_aMutes[i].m_Expire - Server()->Tick()) / Server()->TickSpeed();
}
2020-06-14 08:44:14 +00:00
if(VoteMuted > 0)
{
char aChatmsg[64];
str_format(aChatmsg, sizeof(aChatmsg), "You are not permitted to vote for the next %d seconds.", VoteMuted);
SendChatTarget(ClientID, aChatmsg);
return true;
}
return false;
}
bool CGameContext::RateLimitPlayerMapVote(int ClientID)
{
if(!Server()->GetAuthedState(ClientID) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay))
{
char aChatmsg[512] = {0};
str_format(aChatmsg, sizeof(aChatmsg), "There's a %d second delay between map-votes, please wait %d seconds.",
g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq()));
2020-06-14 08:44:14 +00:00
SendChatTarget(ClientID, aChatmsg);
return true;
}
return false;
}
void CGameContext::OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID)
{
if(BufSize <= 0)
return;
aBuf[0] = '\0';
if(!m_apPlayers[ID])
return;
char aCSkinName[64];
CTeeInfo &TeeInfo = m_apPlayers[ID]->m_TeeInfos;
char aJsonSkin[400];
aJsonSkin[0] = '\0';
if(!Server()->IsSixup(ID))
{
// 0.6
if(TeeInfo.m_UseCustomColor)
{
str_format(aJsonSkin, sizeof(aJsonSkin),
"\"name\":\"%s\","
"\"color_body\":%d,"
"\"color_feet\":%d",
EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_aSkinName),
TeeInfo.m_ColorBody,
TeeInfo.m_ColorFeet);
}
else
{
str_format(aJsonSkin, sizeof(aJsonSkin),
"\"name\":\"%s\"",
EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_aSkinName));
}
}
else
{
const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};
char aPartBuf[64];
for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i)
{
str_format(aPartBuf, sizeof(aPartBuf),
"%s\"%s\":{"
"\"name\":\"%s\"",
i == 0 ? "" : ",",
apPartNames[i],
EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_apSkinPartNames[i]));
str_append(aJsonSkin, aPartBuf);
if(TeeInfo.m_aUseCustomColors[i])
{
str_format(aPartBuf, sizeof(aPartBuf),
",\"color\":%d",
TeeInfo.m_aSkinPartColors[i]);
str_append(aJsonSkin, aPartBuf);
}
str_append(aJsonSkin, "}");
}
}
str_format(aBuf, BufSize,
",\"skin\":{"
"%s"
"},"
"\"afk\":%s,"
"\"team\":%d",
aJsonSkin,
JsonBool(m_apPlayers[ID]->IsAfk()),
m_apPlayers[ID]->GetTeam());
}