mirror of
https://github.com/ddnet/ddnet.git
synced 2024-10-20 07:48:18 +00:00
0d8a0d3b1d
Call expected server callback functions to simulate clients dynamically connecting and disconnecting when changing the `dbg_dummies` variable. This makes the debug dummies more useful for debugging. Previously, the debug dummies were considered invalid clients, whereas they are now considered to be ingame, so they should behave mostly like real clients being connected to the server. The debug dummies also have correct client names now, e.g. "Debug dummy 42". The game server code is cleaned up by moving all special handling for debug dummies to the engine server function `CServer::UpdateDebugDummies`. The left/right direction inputs for debug dummies are now properly added to the client input array, so their input handling should be consistent with normal clients, which fixes some inconsistent prediction with debug dummies.
4654 lines
134 KiB
C++
4654 lines
134 KiB
C++
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
|
|
/* If you are missing that file, acquire a complete release at teeworlds.com. */
|
|
#include "gamecontext.h"
|
|
|
|
#include <vector>
|
|
|
|
#include "teeinfo.h"
|
|
#include <antibot/antibot_data.h>
|
|
#include <base/logger.h>
|
|
#include <base/math.h>
|
|
#include <base/system.h>
|
|
#include <engine/console.h>
|
|
#include <engine/engine.h>
|
|
#include <engine/map.h>
|
|
#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>
|
|
|
|
#include <game/collision.h>
|
|
#include <game/gamecore.h>
|
|
#include <game/mapitems.h>
|
|
#include <game/version.h>
|
|
|
|
#include <game/generated/protocol7.h>
|
|
#include <game/generated/protocolglue.h>
|
|
|
|
#include "entities/character.h"
|
|
#include "gamemodes/DDRace.h"
|
|
#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);
|
|
}
|
|
}
|
|
|
|
enum
|
|
{
|
|
RESET,
|
|
NO_RESET
|
|
};
|
|
|
|
void CGameContext::Construct(int Resetting)
|
|
{
|
|
m_Resetting = false;
|
|
m_pServer = 0;
|
|
|
|
for(auto &pPlayer : m_apPlayers)
|
|
pPlayer = 0;
|
|
|
|
mem_zero(&m_aLastPlayerInput, sizeof(m_aLastPlayerInput));
|
|
mem_zero(&m_aPlayerHasInput, sizeof(m_aPlayerHasInput));
|
|
|
|
m_pController = 0;
|
|
m_aVoteCommand[0] = 0;
|
|
m_VoteType = VOTE_TYPE_UNKNOWN;
|
|
m_VoteCloseTime = 0;
|
|
m_pVoteOptionFirst = 0;
|
|
m_pVoteOptionLast = 0;
|
|
m_NumVoteOptions = 0;
|
|
m_LastMapVote = 0;
|
|
|
|
m_SqlRandomMapResult = nullptr;
|
|
|
|
m_pScore = nullptr;
|
|
m_NumMutes = 0;
|
|
m_NumVoteMutes = 0;
|
|
|
|
m_LatestLog = 0;
|
|
mem_zero(&m_aLogs, sizeof(m_aLogs));
|
|
|
|
if(Resetting == NO_RESET)
|
|
{
|
|
m_NonEmptySince = 0;
|
|
m_pVoteOptionHeap = new CHeap();
|
|
}
|
|
|
|
m_aDeleteTempfile[0] = 0;
|
|
m_TeeHistorianActive = false;
|
|
}
|
|
|
|
void CGameContext::Destruct(int Resetting)
|
|
{
|
|
for(auto &pPlayer : m_apPlayers)
|
|
delete pPlayer;
|
|
|
|
if(Resetting == NO_RESET)
|
|
delete m_pVoteOptionHeap;
|
|
|
|
if(m_pScore)
|
|
{
|
|
delete m_pScore;
|
|
m_pScore = nullptr;
|
|
}
|
|
}
|
|
|
|
CGameContext::CGameContext()
|
|
{
|
|
Construct(NO_RESET);
|
|
}
|
|
|
|
CGameContext::CGameContext(int Reset)
|
|
{
|
|
Construct(Reset);
|
|
}
|
|
|
|
CGameContext::~CGameContext()
|
|
{
|
|
Destruct(m_Resetting ? RESET : NO_RESET);
|
|
}
|
|
|
|
void CGameContext::Clear()
|
|
{
|
|
CHeap *pVoteOptionHeap = m_pVoteOptionHeap;
|
|
CVoteOptionServer *pVoteOptionFirst = m_pVoteOptionFirst;
|
|
CVoteOptionServer *pVoteOptionLast = m_pVoteOptionLast;
|
|
int NumVoteOptions = m_NumVoteOptions;
|
|
CTuningParams Tuning = m_Tuning;
|
|
|
|
m_Resetting = true;
|
|
this->~CGameContext();
|
|
new(this) CGameContext(RESET);
|
|
|
|
m_pVoteOptionHeap = pVoteOptionHeap;
|
|
m_pVoteOptionFirst = pVoteOptionFirst;
|
|
m_pVoteOptionLast = pVoteOptionLast;
|
|
m_NumVoteOptions = NumVoteOptions;
|
|
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];
|
|
for(auto &LatestInput : pChar->m_aLatestInputs)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGameContext::CreateDamageInd(vec2 Pos, float Angle, int Amount, CClientMask Mask)
|
|
{
|
|
float a = 3 * pi / 2 + Angle;
|
|
//float a = get_angle(dir);
|
|
float s = a - pi / 3;
|
|
float e = a + pi / 3;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
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;
|
|
|
|
float Dmg = Strength * l;
|
|
if(!(int)Dmg)
|
|
continue;
|
|
|
|
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;
|
|
|
|
// 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;
|
|
if(!TeamMask.test(PlayerTeam))
|
|
continue;
|
|
TeamMask.reset(PlayerTeam);
|
|
}
|
|
|
|
pChr->TakeDamage(ForceDir * Dmg * 2, (int)Dmg, Owner, Weapon);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
void CGameContext::CreateSoundGlobal(int Sound, int Target)
|
|
{
|
|
if(Sound < 0)
|
|
return;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
int64_t Now = Server()->Tick();
|
|
CPlayer *pPlayer = m_apPlayers[ClientID];
|
|
|
|
if(!pPlayer)
|
|
return;
|
|
|
|
SendChat(-1, CGameContext::CHAT_ALL, pChatmsg, -1, CHAT_SIX);
|
|
if(!pSixupDesc)
|
|
pSixupDesc = pDesc;
|
|
|
|
m_VoteCreator = ClientID;
|
|
StartVote(pDesc, pCmd, pReason, pSixupDesc);
|
|
pPlayer->m_Vote = 1;
|
|
pPlayer->m_VotePos = m_VotePos = 1;
|
|
pPlayer->m_LastVoteCall = Now;
|
|
}
|
|
|
|
void CGameContext::SendChatTarget(int To, const char *pText, int Flags)
|
|
{
|
|
CNetMsg_Sv_Chat Msg;
|
|
Msg.m_Team = 0;
|
|
Msg.m_ClientID = -1;
|
|
Msg.m_pMessage = pText;
|
|
|
|
if(g_Config.m_SvDemoChat)
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void CGameContext::SendChatTeam(int Team, const char *pText)
|
|
{
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
if(m_apPlayers[i] != nullptr && GetDDRaceTeam(i) == Team)
|
|
SendChatTarget(i, pText);
|
|
}
|
|
|
|
void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, int SpamProtectionClientID, int Flags)
|
|
{
|
|
if(SpamProtectionClientID >= 0 && SpamProtectionClientID < MAX_CLIENTS)
|
|
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);
|
|
|
|
if(Team == CHAT_ALL)
|
|
{
|
|
CNetMsg_Sv_Chat Msg;
|
|
Msg.m_Team = 0;
|
|
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);
|
|
|
|
// send to the clients
|
|
for(int i = 0; i < Server()->MaxClients(); i++)
|
|
{
|
|
if(!m_apPlayers[i])
|
|
continue;
|
|
bool Send = (Server()->IsSixup(i) && (Flags & CHAT_SIXUP)) ||
|
|
(!Server()->IsSixup(i) && (Flags & CHAT_SIX));
|
|
|
|
if(!m_apPlayers[i]->m_DND && Send)
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
|
|
}
|
|
|
|
str_format(aBuf, sizeof(aBuf), "Chat: %s", aText);
|
|
LogEvent(aBuf, ChatterClientID);
|
|
}
|
|
else
|
|
{
|
|
CTeamsCore *pTeams = &m_pController->Teams().m_Core;
|
|
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);
|
|
|
|
// 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)
|
|
{
|
|
CNetMsg_Sv_Emoticon Msg;
|
|
Msg.m_ClientID = ClientID;
|
|
Msg.m_Emoticon = Emoticon;
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientID);
|
|
}
|
|
|
|
void CGameContext::SendWeaponPickup(int ClientID, int Weapon)
|
|
{
|
|
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);
|
|
}
|
|
|
|
void CGameContext::SendBroadcast(const char *pText, int ClientID, bool IsImportant)
|
|
{
|
|
CNetMsg_Sv_Broadcast Msg;
|
|
Msg.m_pMessage = pText;
|
|
|
|
if(ClientID == -1)
|
|
{
|
|
dbg_assert(IsImportant, "broadcast messages to all players must be important");
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
|
|
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
{
|
|
pPlayer->m_LastBroadcastImportance = true;
|
|
pPlayer->m_LastBroadcast = Server()->Tick();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(!m_apPlayers[ClientID])
|
|
return;
|
|
|
|
if(!IsImportant && m_apPlayers[ClientID]->m_LastBroadcastImportance && m_apPlayers[ClientID]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10)
|
|
return;
|
|
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
|
|
m_apPlayers[ClientID]->m_LastBroadcast = Server()->Tick();
|
|
m_apPlayers[ClientID]->m_LastBroadcastImportance = IsImportant;
|
|
}
|
|
|
|
void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc)
|
|
{
|
|
// reset votes
|
|
m_VoteEnforce = VOTE_ENFORCE_UNKNOWN;
|
|
m_VoteEnforcer = -1;
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
{
|
|
pPlayer->m_Vote = 0;
|
|
pPlayer->m_VotePos = 0;
|
|
}
|
|
}
|
|
|
|
// start vote
|
|
m_VoteCloseTime = time_get() + time_freq() * g_Config.m_SvVoteTime;
|
|
str_copy(m_aVoteDescription, pDesc, sizeof(m_aVoteDescription));
|
|
str_copy(m_aSixupVoteDescription, pSixupDesc, sizeof(m_aSixupVoteDescription));
|
|
str_copy(m_aVoteCommand, pCommand, sizeof(m_aVoteCommand));
|
|
str_copy(m_aVoteReason, pReason, sizeof(m_aVoteReason));
|
|
SendVoteSet(-1);
|
|
m_VoteUpdate = true;
|
|
}
|
|
|
|
void CGameContext::EndVote()
|
|
{
|
|
m_VoteCloseTime = 0;
|
|
SendVoteSet(-1);
|
|
}
|
|
|
|
void CGameContext::SendVoteSet(int ClientID)
|
|
{
|
|
::CNetMsg_Sv_VoteSet Msg6;
|
|
protocol7::CNetMsg_Sv_VoteSet Msg7;
|
|
|
|
Msg7.m_ClientID = m_VoteCreator;
|
|
if(m_VoteCloseTime)
|
|
{
|
|
Msg6.m_Timeout = Msg7.m_Timeout = (m_VoteCloseTime - time_get()) / time_freq();
|
|
Msg6.m_pDescription = m_aVoteDescription;
|
|
Msg7.m_pDescription = m_aSixupVoteDescription;
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
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++)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
void CGameContext::SendVoteStatus(int ClientID, int Total, int Yes, int No)
|
|
{
|
|
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;
|
|
}
|
|
|
|
CNetMsg_Sv_VoteStatus Msg = {0};
|
|
Msg.m_Total = Total;
|
|
Msg.m_Yes = Yes;
|
|
Msg.m_No = No;
|
|
Msg.m_Pass = Total - (Yes + No);
|
|
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID);
|
|
}
|
|
|
|
void CGameContext::AbortVoteKickOnDisconnect(int ClientID)
|
|
{
|
|
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)))
|
|
m_VoteEnforce = VOTE_ENFORCE_ABORT;
|
|
}
|
|
|
|
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)
|
|
{
|
|
CTuningParams p;
|
|
if(mem_comp(&p, &m_Tuning, sizeof(p)) != 0)
|
|
{
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "resetting tuning due to pure server");
|
|
m_Tuning = p;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGameContext::SendTuningParams(int ClientID, int Zone)
|
|
{
|
|
if(ClientID == -1)
|
|
{
|
|
for(int i = 0; i < MAX_CLIENTS; ++i)
|
|
{
|
|
if(m_apPlayers[i])
|
|
{
|
|
if(m_apPlayers[i]->GetCharacter())
|
|
{
|
|
if(m_apPlayers[i]->GetCharacter()->m_TuneZone == Zone)
|
|
SendTuningParams(i, Zone);
|
|
}
|
|
else if(m_apPlayers[i]->m_TuneZone == Zone)
|
|
{
|
|
SendTuningParams(i, Zone);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
CheckPureTuning();
|
|
|
|
CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
|
|
int *pParams = 0;
|
|
if(Zone == 0)
|
|
pParams = (int *)&m_Tuning;
|
|
else
|
|
pParams = (int *)&(m_aTuningList[Zone]);
|
|
|
|
for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++)
|
|
{
|
|
if(m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetCharacter())
|
|
{
|
|
if((i == 30) // laser_damage is removed from 0.7
|
|
&& (Server()->IsSixup(ClientID)))
|
|
{
|
|
continue;
|
|
}
|
|
else if((i == 31) // collision
|
|
&& (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOCOLL))
|
|
{
|
|
Msg.AddInt(0);
|
|
}
|
|
else if((i == 32) // hooking
|
|
&& (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHOOK))
|
|
{
|
|
Msg.AddInt(0);
|
|
}
|
|
else if((i == 3) // ground jump impulse
|
|
&& m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOJUMP)
|
|
{
|
|
Msg.AddInt(0);
|
|
}
|
|
else if((i == 33) // jetpack
|
|
&& !(m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_JETPACK))
|
|
{
|
|
Msg.AddInt(0);
|
|
}
|
|
else if((i == 36) // hammer hit
|
|
&& m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHAMMER)
|
|
{
|
|
Msg.AddInt(0);
|
|
}
|
|
else
|
|
{
|
|
Msg.AddInt(pParams[i]);
|
|
}
|
|
}
|
|
else
|
|
Msg.AddInt(pParams[i]); // if everything is normal just send true tunings
|
|
}
|
|
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
void CGameContext::OnTick()
|
|
{
|
|
// check tuning
|
|
CheckPureTuning();
|
|
|
|
if(m_TeeHistorianActive)
|
|
{
|
|
int Error = aio_error(m_pTeeHistorianFile);
|
|
if(Error)
|
|
{
|
|
dbg_msg("teehistorian", "error writing to file, err=%d", Error);
|
|
Server()->SetErrorShutdown("teehistorian io error");
|
|
}
|
|
|
|
if(!m_TeeHistorian.Starting())
|
|
{
|
|
m_TeeHistorian.EndInputs();
|
|
m_TeeHistorian.EndTick();
|
|
}
|
|
m_TeeHistorian.BeginTick(Server()->Tick());
|
|
m_TeeHistorian.BeginPlayers();
|
|
}
|
|
|
|
// copy tuning
|
|
m_World.m_Core.m_aTuning[0] = m_Tuning;
|
|
m_World.Tick();
|
|
|
|
UpdatePlayerMaps();
|
|
|
|
//if(world.paused) // make sure that the game object always updates
|
|
m_pController->Tick();
|
|
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(m_apPlayers[i])
|
|
{
|
|
// send vote options
|
|
ProgressVoteOptions(i);
|
|
|
|
m_apPlayers[i]->Tick();
|
|
m_apPlayers[i]->PostTick();
|
|
}
|
|
}
|
|
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
pPlayer->PostPostTick();
|
|
}
|
|
|
|
// update voting
|
|
if(m_VoteCloseTime)
|
|
{
|
|
// abort the kick-vote on player-leave
|
|
if(m_VoteEnforce == VOTE_ENFORCE_ABORT)
|
|
{
|
|
SendChat(-1, CGameContext::CHAT_ALL, "Vote aborted");
|
|
EndVote();
|
|
}
|
|
else
|
|
{
|
|
int Total = 0, Yes = 0, No = 0;
|
|
bool Veto = false, VetoStop = false;
|
|
if(m_VoteUpdate)
|
|
{
|
|
// count votes
|
|
char aaBuf[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}}, *pIP = NULL;
|
|
bool SinglePlayer = true;
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(m_apPlayers[i])
|
|
{
|
|
Server()->GetClientAddr(i, aaBuf[i], NETADDR_MAXSTRSIZE);
|
|
if(!pIP)
|
|
pIP = aaBuf[i];
|
|
else if(SinglePlayer && str_comp(pIP, aaBuf[i]))
|
|
SinglePlayer = false;
|
|
}
|
|
}
|
|
|
|
// remember checked players, only the first player with a specific ip will be handled
|
|
bool aVoteChecked[MAX_CLIENTS] = {false};
|
|
int64_t Now = Server()->Tick();
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(!m_apPlayers[i] || aVoteChecked[i])
|
|
continue;
|
|
|
|
if((IsKickVote() || IsSpecVote()) && (m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS ||
|
|
(GetPlayerChar(m_VoteCreator) && GetPlayerChar(i) &&
|
|
GetPlayerChar(m_VoteCreator)->Team() != GetPlayerChar(i)->Team())))
|
|
continue;
|
|
|
|
if(m_apPlayers[i]->IsAfk() && i != m_VoteCreator)
|
|
continue;
|
|
|
|
// can't vote in kick and spec votes in the beginning after joining
|
|
if((IsKickVote() || IsSpecVote()) && Now < m_apPlayers[i]->m_FirstVoteTick)
|
|
continue;
|
|
|
|
// connecting clients with spoofed ips can clog slots without being ingame
|
|
if(!Server()->ClientIngame(i))
|
|
continue;
|
|
|
|
// don't count votes by blacklisted clients
|
|
if(g_Config.m_SvDnsblVote && !m_pServer->DnsblWhite(i) && !SinglePlayer)
|
|
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++)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// look through all players with same IP again, including the current player
|
|
for(int j = i; j < MAX_CLIENTS; j++)
|
|
{
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(g_Config.m_SvVoteMaxTotal && Total > g_Config.m_SvVoteMaxTotal &&
|
|
(IsKickVote() || IsSpecVote()))
|
|
Total = g_Config.m_SvVoteMaxTotal;
|
|
|
|
if((Yes > Total / (100.0f / g_Config.m_SvVoteYesPercentage)) && !Veto)
|
|
m_VoteEnforce = VOTE_ENFORCE_YES;
|
|
else if(No >= Total - Total / (100.0f / g_Config.m_SvVoteYesPercentage))
|
|
m_VoteEnforce = VOTE_ENFORCE_NO;
|
|
|
|
if(VetoStop)
|
|
m_VoteEnforce = VOTE_ENFORCE_NO;
|
|
|
|
m_VoteWillPass = Yes > (Yes + No) / (100.0f / g_Config.m_SvVoteYesPercentage);
|
|
}
|
|
|
|
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))
|
|
{
|
|
Server()->SetRconCID(IServer::RCON_CID_VOTE);
|
|
Console()->ExecuteLine(m_aVoteCommand);
|
|
Server()->SetRconCID(IServer::RCON_CID_SERV);
|
|
EndVote();
|
|
SendChat(-1, CGameContext::CHAT_ALL, "Vote passed", -1, CHAT_SIX);
|
|
|
|
if(m_apPlayers[m_VoteCreator] && !IsKickVote() && !IsSpecVote())
|
|
m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0;
|
|
}
|
|
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);
|
|
EndVote();
|
|
}
|
|
else if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN)
|
|
{
|
|
EndVote();
|
|
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed enforced by authorized player", -1, CHAT_SIX);
|
|
}
|
|
//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))
|
|
{
|
|
EndVote();
|
|
if(VetoStop || (m_VoteWillPass && Veto))
|
|
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed because of veto. Find an empty server instead", -1, CHAT_SIX);
|
|
else
|
|
SendChat(-1, CGameContext::CHAT_ALL, "Vote failed", -1, CHAT_SIX);
|
|
}
|
|
else if(m_VoteUpdate)
|
|
{
|
|
m_VoteUpdate = false;
|
|
SendVoteStatus(-1, Total, Yes, No);
|
|
}
|
|
}
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
|
|
if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0)
|
|
{
|
|
const char *pLine = Server()->GetAnnouncementLine(g_Config.m_SvAnnouncementFileName);
|
|
if(pLine)
|
|
SendChat(-1, CGameContext::CHAT_ALL, pLine);
|
|
}
|
|
|
|
for(auto &Switcher : Switchers())
|
|
{
|
|
for(int j = 0; j < MAX_CLIENTS; ++j)
|
|
{
|
|
if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDOPEN)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
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
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
void CGameContext::OnClientDirectInput(int ClientID, void *pInput)
|
|
{
|
|
if(!m_World.m_Paused)
|
|
m_apPlayers[ClientID]->OnDirectInput((CNetObj_PlayerInput *)pInput);
|
|
|
|
int Flags = ((CNetObj_PlayerInput *)pInput)->m_PlayerFlags;
|
|
if((Flags & 256) || (Flags & 512))
|
|
{
|
|
Server()->Kick(ClientID, "please update your client or use DDNet client");
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
if(!m_World.m_Paused)
|
|
m_apPlayers[ClientID]->OnPredictedInput(pApplyInput);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if(!m_World.m_Paused)
|
|
m_apPlayers[ClientID]->OnPredictedEarlyInput(pApplyInput);
|
|
|
|
if(m_TeeHistorianActive)
|
|
{
|
|
m_TeeHistorian.RecordPlayerInput(ClientID, m_apPlayers[ClientID]->GetUniqueCID(), pApplyInput);
|
|
}
|
|
}
|
|
|
|
struct CVoteOptionServer *CGameContext::GetVoteOption(int Index)
|
|
{
|
|
CVoteOptionServer *pCurrent;
|
|
for(pCurrent = m_pVoteOptionFirst;
|
|
Index > 0 && pCurrent;
|
|
Index--, pCurrent = pCurrent->m_pNext)
|
|
;
|
|
|
|
if(Index > 0)
|
|
return 0;
|
|
return pCurrent;
|
|
}
|
|
|
|
void CGameContext::ProgressVoteOptions(int ClientID)
|
|
{
|
|
CPlayer *pPl = m_apPlayers[ClientID];
|
|
|
|
if(pPl->m_SendVoteIndex == -1)
|
|
return; // we didn't start sending options yet
|
|
|
|
if(pPl->m_SendVoteIndex > m_NumVoteOptions)
|
|
return; // shouldn't happen / fail silently
|
|
|
|
int VotesLeft = m_NumVoteOptions - pPl->m_SendVoteIndex;
|
|
int NumVotesToSend = minimum(g_Config.m_SvSendVotesPerTick, VotesLeft);
|
|
|
|
if(!VotesLeft)
|
|
{
|
|
// 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);
|
|
|
|
while(CurIndex < NumVotesToSend && pCurrent != NULL)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if(m_TeeHistorianActive)
|
|
{
|
|
m_TeeHistorian.RecordPlayerReady(ClientID);
|
|
}
|
|
m_pController->OnPlayerConnect(m_apPlayers[ClientID]);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
{
|
|
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);
|
|
}
|
|
|
|
IServer::CClientInfo Info;
|
|
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);
|
|
|
|
if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF)
|
|
{
|
|
if(g_Config.m_SvShowOthers)
|
|
SendChatTarget(ClientID, "You can see other players. To disable this use DDNet client and type /showothers");
|
|
|
|
m_apPlayers[ClientID]->m_ShowOthers = g_Config.m_SvShowOthersDefault;
|
|
}
|
|
}
|
|
m_VoteUpdate = true;
|
|
|
|
// send active vote
|
|
if(m_VoteCloseTime)
|
|
SendVoteSet(ClientID);
|
|
|
|
Server()->ExpireServerInfo();
|
|
|
|
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];
|
|
}
|
|
|
|
// update client infos (others before local)
|
|
for(int i = 0; i < Server()->MaxClients(); ++i)
|
|
{
|
|
if(i == ClientID || !m_apPlayers[i] || !Server()->ClientIngame(i))
|
|
continue;
|
|
|
|
CPlayer *pPlayer = m_apPlayers[i];
|
|
|
|
if(Server()->IsSixup(i))
|
|
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// local info
|
|
if(Server()->IsSixup(ClientID))
|
|
{
|
|
NewClientInfoMsg.m_Local = 1;
|
|
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
|
|
}
|
|
|
|
// 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);
|
|
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);
|
|
}
|
|
|
|
LogEvent("Connect", ClientID);
|
|
}
|
|
|
|
bool CGameContext::OnClientDataPersist(int ClientID, void *pData)
|
|
{
|
|
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;
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Check which team the player should be on
|
|
const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(ClientID);
|
|
|
|
if(m_apPlayers[ClientID])
|
|
delete m_apPlayers[ClientID];
|
|
m_apPlayers[ClientID] = new(ClientID) CPlayer(this, NextUniqueClientID, ClientID, StartTeam);
|
|
m_apPlayers[ClientID]->SetInitialAfk(Afk);
|
|
NextUniqueClientID += 1;
|
|
|
|
SendMotd(ClientID);
|
|
SendSettings(ClientID);
|
|
|
|
Server()->ExpireServerInfo();
|
|
}
|
|
|
|
void CGameContext::OnClientDrop(int ClientID, const char *pReason)
|
|
{
|
|
LogEvent("Disconnect", ClientID);
|
|
|
|
AbortVoteKickOnDisconnect(ClientID);
|
|
m_pController->OnPlayerDisconnect(m_apPlayers[ClientID], pReason);
|
|
delete m_apPlayers[ClientID];
|
|
m_apPlayers[ClientID] = 0;
|
|
|
|
m_VoteUpdate = true;
|
|
|
|
// update spectator modes
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer && pPlayer->m_SpectatorID == ClientID)
|
|
pPlayer->m_SpectatorID = SPEC_FREEVIEW;
|
|
}
|
|
|
|
// update conversation targets
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer && pPlayer->m_LastWhisperTo == ClientID)
|
|
pPlayer->m_LastWhisperTo = -1;
|
|
}
|
|
|
|
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);
|
|
|
|
Server()->ExpireServerInfo();
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
// First update the teams state.
|
|
m_pController->Teams().SendTeamsState(ClientID);
|
|
|
|
// Then send records.
|
|
SendRecord(ClientID);
|
|
|
|
// And report correct tunings.
|
|
if(ClientVersion < VERSION_DDNET_EARLY_VERSION)
|
|
SendTuningParams(ClientID, pPlayer->m_TuneZone);
|
|
|
|
// Tell old clients to update.
|
|
if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0')
|
|
SendBroadcast(g_Config.m_SvClientSuggestionOld, ClientID);
|
|
// 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)
|
|
{
|
|
if(Server()->IsSixup(ClientID) && *pMsgID < OFFSET_UUID)
|
|
{
|
|
void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgID, pUnpacker);
|
|
if(!pRawMsg)
|
|
return 0;
|
|
|
|
CPlayer *pPlayer = m_apPlayers[ClientID];
|
|
static char s_aRawMsg[1024];
|
|
|
|
if(*pMsgID == protocol7::NETMSGTYPE_CL_SAY)
|
|
{
|
|
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;
|
|
|
|
if(pMsg7->m_Target >= 0)
|
|
{
|
|
if(ProcessSpamProtection(ClientID))
|
|
return 0;
|
|
|
|
// 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;
|
|
}
|
|
|
|
pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM;
|
|
pMsg->m_pMessage = pMsg7->m_pMessage;
|
|
}
|
|
else if(*pMsgID == protocol7::NETMSGTYPE_CL_STARTINFO)
|
|
{
|
|
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;
|
|
}
|
|
else if(*pMsgID == protocol7::NETMSGTYPE_CL_SKINCHANGE)
|
|
{
|
|
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;
|
|
|
|
pPlayer->m_LastChangeInfo = Server()->Tick();
|
|
|
|
CTeeInfo Info(pMsg->m_apSkinPartNames, pMsg->m_aUseCustomColors, pMsg->m_aSkinPartColors);
|
|
Info.FromSixup();
|
|
pPlayer->m_TeeInfos = Info;
|
|
|
|
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);
|
|
|
|
return 0;
|
|
}
|
|
else if(*pMsgID == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE)
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
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);
|
|
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;
|
|
}
|
|
else if(*pMsgID == protocol7::NETMSGTYPE_CL_EMOTICON)
|
|
{
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
*pMsgID = Msg_SevenToSix(*pMsgID);
|
|
|
|
return s_aRawMsg;
|
|
}
|
|
else
|
|
return m_NetObjHandler.SecureUnpackMsg(*pMsgID, pUnpacker);
|
|
}
|
|
|
|
void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size)
|
|
{
|
|
str_copy(pCensoredMessage, pMessage, Size);
|
|
|
|
for(auto &Item : m_vCensorlist)
|
|
{
|
|
char *pCurLoc = pCensoredMessage;
|
|
do
|
|
{
|
|
pCurLoc = (char *)str_utf8_find_nocase(pCurLoc, Item.c_str());
|
|
if(pCurLoc)
|
|
{
|
|
for(int i = 0; i < (int)Item.length(); i++)
|
|
{
|
|
pCurLoc[i] = '*';
|
|
}
|
|
pCurLoc++;
|
|
}
|
|
} while(pCurLoc);
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
void *pRawMsg = PreProcessMsg(&MsgID, pUnpacker, ClientID);
|
|
|
|
if(!pRawMsg)
|
|
return;
|
|
|
|
if(Server()->ClientIngame(ClientID))
|
|
{
|
|
switch(MsgID)
|
|
{
|
|
case NETMSGTYPE_CL_SAY:
|
|
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;
|
|
}
|
|
}
|
|
if(MsgID == NETMSGTYPE_CL_STARTINFO)
|
|
{
|
|
OnStartInfoNetMessage(static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientID);
|
|
}
|
|
}
|
|
|
|
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());
|
|
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(str_comp(Server()->ClientClan(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);
|
|
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)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
const char *pParamName = pResult->GetString(0);
|
|
|
|
char aBuf[256];
|
|
if(pResult->NumArguments() == 2)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
const char *pParamName = pResult->GetString(0);
|
|
float OldValue;
|
|
|
|
char aBuf[256];
|
|
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);
|
|
return;
|
|
}
|
|
|
|
float NewValue = absolute(OldValue - pResult->GetFloat(1)) < 0.0001f ? pResult->GetFloat(2) : pResult->GetFloat(1);
|
|
|
|
pSelf->Tuning()->Set(pParamName, NewValue);
|
|
pSelf->Tuning()->Get(pParamName, &NewValue);
|
|
|
|
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)
|
|
{
|
|
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");
|
|
}
|
|
}
|
|
|
|
void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
char aBuf[256];
|
|
for(int i = 0; i < CTuningParams::Num(); i++)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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);
|
|
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++)
|
|
{
|
|
float Value;
|
|
pSelf->TuningList()[List].Get(i, &Value);
|
|
str_format(aBuf, sizeof(aBuf), "zone %d: %s %.2f", List, CTuningParams::Name(i), Value);
|
|
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)
|
|
{
|
|
pSelf->TuningList()[List] = TuningParams;
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "Tunezone %d reset", List);
|
|
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
|
|
pSelf->SendTuningParams(-1, List);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < NUM_TUNEZONES; i++)
|
|
{
|
|
*(pSelf->TuningList() + i) = TuningParams;
|
|
pSelf->SendTuningParams(-1, i);
|
|
}
|
|
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);
|
|
|
|
if(in_range(Switch, (int)pSelf->Switchers().size() - 1))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
pSelf->m_pController->ChangeMap(pResult->NumArguments() ? pResult->GetString(0) : "");
|
|
}
|
|
|
|
void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
|
|
int Stars = pResult->NumArguments() ? pResult->GetInteger(0) : -1;
|
|
|
|
pSelf->m_pScore->RandomMap(pSelf->m_VoteCreator, Stars);
|
|
}
|
|
|
|
void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
|
|
int Stars = pResult->NumArguments() ? pResult->GetInteger(0) : -1;
|
|
|
|
pSelf->m_pScore->RandomUnfinishedMap(pSelf->m_VoteCreator, Stars);
|
|
}
|
|
|
|
void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
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)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
|
|
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')
|
|
{
|
|
aBuf[j] = '\n';
|
|
i++;
|
|
}
|
|
else if(i != j)
|
|
{
|
|
aBuf[j] = aBuf[i];
|
|
}
|
|
}
|
|
aBuf[j] = '\0';
|
|
|
|
pSelf->SendBroadcast(aBuf, -1);
|
|
}
|
|
|
|
void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
pSelf->SendChat(-1, CGameContext::CHAT_ALL, pResult->GetString(0));
|
|
}
|
|
|
|
void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
int ClientID = clamp(pResult->GetInteger(0), 0, (int)MAX_CLIENTS - 1);
|
|
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)
|
|
pSelf->m_apPlayers[ClientID]->Pause(CPlayer::PAUSE_NONE, true);
|
|
}
|
|
|
|
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);
|
|
|
|
for(auto &pPlayer : pSelf->m_apPlayers)
|
|
if(pPlayer)
|
|
pSelf->m_pController->DoTeamChange(pPlayer, Team, false);
|
|
}
|
|
|
|
void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
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];
|
|
str_format(aBuf, sizeof(aBuf), "skipped invalid command '%s'", pCommand);
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
|
|
return;
|
|
}
|
|
while(*pDescription == ' ')
|
|
pDescription++;
|
|
if(str_length(pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0)
|
|
{
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "skipped invalid option '%s'", pDescription);
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
|
|
return;
|
|
}
|
|
|
|
// check for duplicate entry
|
|
CVoteOptionServer *pOption = m_pVoteOptionFirst;
|
|
while(pOption)
|
|
{
|
|
if(str_comp_nocase(pDescription, pOption->m_aDescription) == 0)
|
|
{
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "option '%s' already exists", pDescription);
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
|
|
return;
|
|
}
|
|
pOption = pOption->m_pNext;
|
|
}
|
|
|
|
// add the option
|
|
++m_NumVoteOptions;
|
|
int Len = str_length(pCommand);
|
|
|
|
pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len, alignof(CVoteOptionServer));
|
|
pOption->m_pNext = 0;
|
|
pOption->m_pPrev = m_pVoteOptionLast;
|
|
if(pOption->m_pPrev)
|
|
pOption->m_pPrev->m_pNext = pOption;
|
|
m_pVoteOptionLast = pOption;
|
|
if(!m_pVoteOptionFirst)
|
|
m_pVoteOptionFirst = pOption;
|
|
|
|
str_copy(pOption->m_aDescription, pDescription, sizeof(pOption->m_aDescription));
|
|
mem_copy(pOption->m_aCommand, pCommand, Len + 1);
|
|
}
|
|
|
|
void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
const char *pDescription = pResult->GetString(0);
|
|
|
|
// check for valid option
|
|
CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
|
|
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);
|
|
return;
|
|
}
|
|
|
|
// start reloading vote option list
|
|
// clear vote options
|
|
CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
|
|
pSelf->Server()->SendPackMsg(&VoteClearOptionsMsg, MSGFLAG_VITAL, -1);
|
|
|
|
// reset sending of vote options
|
|
for(auto &pPlayer : pSelf->m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
pPlayer->m_SendVoteIndex = 0;
|
|
}
|
|
|
|
// TODO: improve this
|
|
// remove the option
|
|
--pSelf->m_NumVoteOptions;
|
|
|
|
CHeap *pVoteOptionHeap = new CHeap();
|
|
CVoteOptionServer *pVoteOptionFirst = 0;
|
|
CVoteOptionServer *pVoteOptionLast = 0;
|
|
int NumVoteOptions = pSelf->m_NumVoteOptions;
|
|
for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext)
|
|
{
|
|
if(pSrc == pOption)
|
|
continue;
|
|
|
|
// copy option
|
|
int Len = str_length(pSrc->m_aCommand);
|
|
CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len);
|
|
pDst->m_pNext = 0;
|
|
pDst->m_pPrev = pVoteOptionLast;
|
|
if(pDst->m_pPrev)
|
|
pDst->m_pPrev->m_pNext = pDst;
|
|
pVoteOptionLast = pDst;
|
|
if(!pVoteOptionFirst)
|
|
pVoteOptionFirst = pDst;
|
|
|
|
str_copy(pDst->m_aDescription, pSrc->m_aDescription, sizeof(pDst->m_aDescription));
|
|
mem_copy(pDst->m_aCommand, pSrc->m_aCommand, Len + 1);
|
|
}
|
|
|
|
// clean up
|
|
delete pSelf->m_pVoteOptionHeap;
|
|
pSelf->m_pVoteOptionHeap = pVoteOptionHeap;
|
|
pSelf->m_pVoteOptionFirst = pVoteOptionFirst;
|
|
pSelf->m_pVoteOptionLast = pVoteOptionLast;
|
|
pSelf->m_NumVoteOptions = NumVoteOptions;
|
|
}
|
|
|
|
void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
const char *pType = pResult->GetString(0);
|
|
const char *pValue = pResult->GetString(1);
|
|
const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(2)[0] ? pResult->GetString(2) : "No reason given";
|
|
char aBuf[128] = {0};
|
|
|
|
if(str_comp_nocase(pType, "option") == 0)
|
|
{
|
|
CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
|
|
while(pOption)
|
|
{
|
|
if(str_comp_nocase(pValue, pOption->m_aDescription) == 0)
|
|
{
|
|
str_format(aBuf, sizeof(aBuf), "authorized player forced server option '%s' (%s)", pValue, pReason);
|
|
pSelf->SendChatTarget(-1, aBuf, CHAT_SIX);
|
|
pSelf->Console()->ExecuteLine(pOption->m_aCommand);
|
|
break;
|
|
}
|
|
|
|
pOption = pOption->m_pNext;
|
|
}
|
|
|
|
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);
|
|
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");
|
|
return;
|
|
}
|
|
|
|
if(!g_Config.m_SvVoteKickBantime)
|
|
{
|
|
str_format(aBuf, sizeof(aBuf), "kick %d %s", KickID, pReason);
|
|
pSelf->Console()->ExecuteLine(aBuf);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// reset sending of vote options
|
|
for(auto &pPlayer : pSelf->m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
pPlayer->m_SendVoteIndex = 0;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
std::vector<CMapNameItem> vMapList;
|
|
pSelf->Storage()->ListDirectory(IStorage::TYPE_ALL, "maps", MapScan, &vMapList);
|
|
std::sort(vMapList.begin(), vMapList.end());
|
|
|
|
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];
|
|
char *pDst = aMapEscaped;
|
|
str_escape(&pDst, Item.m_aName, aMapEscaped + sizeof(aMapEscaped));
|
|
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)
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
|
|
if(str_comp_nocase(pResult->GetString(0), "yes") == 0)
|
|
pSelf->ForceVote(pResult->m_ClientID, true);
|
|
else if(str_comp_nocase(pResult->GetString(0), "no") == 0)
|
|
pSelf->ForceVote(pResult->m_ClientID, false);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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, "\"");
|
|
|
|
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);
|
|
}
|
|
|
|
void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
|
|
{
|
|
pfnCallback(pResult, pCallbackUserData);
|
|
if(pResult->NumArguments())
|
|
{
|
|
CGameContext *pSelf = (CGameContext *)pUserData;
|
|
pSelf->SendMotd(-1);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void CGameContext::OnConsoleInit()
|
|
{
|
|
m_pServer = Kernel()->RequestInterface<IServer>();
|
|
m_pConfig = Kernel()->RequestInterface<IConfigManager>()->Values();
|
|
m_pConsole = Kernel()->RequestInterface<IConsole>();
|
|
m_pEngine = Kernel()->RequestInterface<IEngine>();
|
|
m_pStorage = Kernel()->RequestInterface<IStorage>();
|
|
|
|
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");
|
|
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");
|
|
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");
|
|
|
|
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>
|
|
}
|
|
|
|
void CGameContext::OnInit(const void *pPersistentData)
|
|
{
|
|
const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData;
|
|
|
|
m_pServer = Kernel()->RequestInterface<IServer>();
|
|
m_pConfig = Kernel()->RequestInterface<IConfigManager>()->Values();
|
|
m_pConsole = Kernel()->RequestInterface<IConsole>();
|
|
m_pEngine = Kernel()->RequestInterface<IEngine>();
|
|
m_pStorage = Kernel()->RequestInterface<IStorage>();
|
|
m_pAntibot = Kernel()->RequestInterface<IAntibot>();
|
|
m_World.SetGameServer(this);
|
|
m_Events.SetGameServer(this);
|
|
|
|
m_GameUuid = RandomUuid();
|
|
Console()->SetTeeHistorianCommandCallback(CommandCallback, this);
|
|
|
|
uint64_t aSeed[2];
|
|
secure_random_fill(aSeed, sizeof(aSeed));
|
|
m_Prng.Seed(aSeed);
|
|
m_World.m_Core.m_pPrng = &m_Prng;
|
|
|
|
DeleteTempfile();
|
|
|
|
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);
|
|
|
|
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++)
|
|
{
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|
|
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;
|
|
g_Config.m_SvOldTeleportHook = 0;
|
|
g_Config.m_SvOldTeleportWeapons = 0;
|
|
g_Config.m_SvTeleportHoldHook = 0;
|
|
g_Config.m_SvTeam = SV_TEAM_ALLOWED;
|
|
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;
|
|
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);
|
|
}
|
|
}
|
|
|
|
if(!str_comp(Config()->m_SvGametype, "mod"))
|
|
m_pController = new CGameControllerMod(this);
|
|
else
|
|
m_pController = new CGameControllerDDRace(this);
|
|
|
|
const char *pCensorFilename = "censorlist.txt";
|
|
IOHANDLE File = Storage()->OpenFile(pCensorFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL);
|
|
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);
|
|
}
|
|
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);
|
|
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);
|
|
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());
|
|
}
|
|
|
|
// 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));
|
|
|
|
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)
|
|
{
|
|
g_Config.m_SvOldLaser = 1;
|
|
dbg_msg("front_layer", "found old laser tile");
|
|
}
|
|
else if(FrontIndex == TILE_NPC)
|
|
{
|
|
m_Tuning.Set("player_collision", 0);
|
|
dbg_msg("front_layer", "found no collision tile");
|
|
}
|
|
else if(FrontIndex == TILE_EHOOK)
|
|
{
|
|
g_Config.m_SvEndlessDrag = 1;
|
|
dbg_msg("front_layer", "found unlimited hook time tile");
|
|
}
|
|
else if(FrontIndex == TILE_NOHIT)
|
|
{
|
|
g_Config.m_SvHit = 0;
|
|
dbg_msg("front_layer", "found no weapons hitting others tile");
|
|
}
|
|
else if(FrontIndex == TILE_NPH)
|
|
{
|
|
m_Tuning.Set("player_hooking", 0);
|
|
dbg_msg("front_layer", "found no player hooking tile");
|
|
}
|
|
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;
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGameContext::DeleteTempfile()
|
|
{
|
|
if(m_aDeleteTempfile[0] != 0)
|
|
{
|
|
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);
|
|
Offset += Length;
|
|
free(Line);
|
|
}
|
|
|
|
CDataFileReader Reader;
|
|
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)
|
|
{
|
|
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);
|
|
if(Error)
|
|
{
|
|
dbg_msg("teehistorian", "error closing file, err=%d", Error);
|
|
Server()->SetErrorShutdown("teehistorian close error");
|
|
}
|
|
aio_free(m_pTeeHistorianFile);
|
|
}
|
|
|
|
DeleteTempfile();
|
|
Console()->ResetGameSettings();
|
|
Collision()->Dest();
|
|
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)
|
|
{
|
|
// add tuning to demo
|
|
CTuningParams StandardTuning;
|
|
if(Server()->IsRecording(ClientID > -1 ? ClientID : MAX_CLIENTS) && mem_comp(&StandardTuning, &m_Tuning, sizeof(CTuningParams)) != 0)
|
|
{
|
|
CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
|
|
int *pParams = (int *)&m_Tuning;
|
|
for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++)
|
|
Msg.AddInt(pParams[i]);
|
|
Server()->SendMsg(&Msg, MSGFLAG_RECORD | MSGFLAG_NOSEND, ClientID);
|
|
}
|
|
|
|
m_pController->Snap(ClientID);
|
|
|
|
for(auto &pPlayer : m_apPlayers)
|
|
{
|
|
if(pPlayer)
|
|
pPlayer->Snap(ClientID);
|
|
}
|
|
|
|
if(ClientID > -1)
|
|
m_apPlayers[ClientID]->FakeSnap();
|
|
|
|
m_World.Snap(ClientID);
|
|
m_Events.Snap(ClientID);
|
|
}
|
|
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
|
|
{
|
|
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; }
|
|
|
|
IGameServer *CreateGameServer() { return new CGameContext; }
|
|
|
|
void CGameContext::OnSetAuthed(int ClientID, int Level)
|
|
{
|
|
if(m_apPlayers[ClientID])
|
|
{
|
|
char aBuf[512], aIP[NETADDR_MAXSTRSIZE];
|
|
Server()->GetClientAddr(ClientID, aIP, sizeof(aIP));
|
|
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))
|
|
{
|
|
m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN;
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "CGameContext", "Vote aborted by authorized login.");
|
|
}
|
|
}
|
|
if(m_TeeHistorianActive)
|
|
{
|
|
if(Level)
|
|
{
|
|
m_TeeHistorian.RecordAuthLogin(ClientID, Level, Server()->GetAuthName(ClientID));
|
|
}
|
|
else
|
|
{
|
|
m_TeeHistorian.RecordAuthLogout(ClientID);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
bool CGameContext::ProcessSpamProtection(int ClientID, bool RespectChatInitialDelay)
|
|
{
|
|
if(!m_apPlayers[ClientID])
|
|
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())
|
|
return true;
|
|
else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientID))
|
|
{
|
|
SendChatTarget(ClientID, "Players are not allowed to chat from VPNs at this time");
|
|
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))
|
|
{
|
|
if(RespectChatInitialDelay || m_aMutes[i].m_InitialChatDelay)
|
|
{
|
|
Muted = m_aMutes[i];
|
|
Expires = (m_aMutes[i].m_Expire - Server()->Tick()) / Server()->TickSpeed();
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Expires > 0)
|
|
{
|
|
char aBuf[128];
|
|
if(Muted.m_InitialChatDelay)
|
|
str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will be able to talk in %d seconds.", Expires);
|
|
else
|
|
str_format(aBuf, sizeof(aBuf), "You are not permitted to talk for the next %d seconds.", Expires);
|
|
SendChatTarget(ClientID, aBuf);
|
|
return true;
|
|
}
|
|
|
|
if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientID]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold)
|
|
{
|
|
Mute(&Addr, g_Config.m_SvSpamMuteDuration, Server()->ClientName(ClientID));
|
|
m_apPlayers[ClientID]->m_ChatScore = 0;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int CGameContext::GetDDRaceTeam(int ClientID)
|
|
{
|
|
return m_pController->Teams().m_Core.Team(ClientID);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
bool CheckClientID2(int ClientID)
|
|
{
|
|
return ClientID >= 0 && ClientID < MAX_CLIENTS;
|
|
}
|
|
|
|
void CGameContext::Whisper(int ClientID, char *pStr)
|
|
{
|
|
if(ProcessSpamProtection(ClientID))
|
|
return;
|
|
|
|
pStr = str_skip_whitespaces(pStr);
|
|
|
|
char *pName;
|
|
int Victim;
|
|
bool Error = false;
|
|
|
|
// add token
|
|
if(*pStr == '"')
|
|
{
|
|
pStr++;
|
|
|
|
pName = pStr;
|
|
char *pDst = pStr; // we might have to process escape data
|
|
while(true)
|
|
{
|
|
if(pStr[0] == '"')
|
|
{
|
|
break;
|
|
}
|
|
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)
|
|
{
|
|
Error = true;
|
|
break;
|
|
}
|
|
|
|
*pDst = *pStr;
|
|
pDst++;
|
|
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;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pName = pStr;
|
|
while(true)
|
|
{
|
|
if(pStr[0] == 0)
|
|
{
|
|
Error = true;
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
pStr++;
|
|
}
|
|
}
|
|
|
|
if(pStr[0] != ' ')
|
|
{
|
|
Error = true;
|
|
}
|
|
|
|
*pStr = 0;
|
|
pStr++;
|
|
|
|
if(Error)
|
|
{
|
|
SendChatTarget(ClientID, "Invalid whisper");
|
|
return;
|
|
}
|
|
|
|
if(Victim >= MAX_CLIENTS || !CheckClientID2(Victim))
|
|
{
|
|
char aBuf[256];
|
|
str_format(aBuf, sizeof(aBuf), "No player with name \"%s\" found", pName);
|
|
SendChatTarget(ClientID, aBuf);
|
|
return;
|
|
}
|
|
|
|
WhisperID(ClientID, Victim, pStr);
|
|
}
|
|
|
|
void CGameContext::WhisperID(int ClientID, int VictimID, const char *pMessage)
|
|
{
|
|
if(!CheckClientID2(ClientID))
|
|
return;
|
|
|
|
if(!CheckClientID2(VictimID))
|
|
return;
|
|
|
|
if(m_apPlayers[ClientID])
|
|
m_apPlayers[ClientID]->m_LastWhisperTo = VictimID;
|
|
|
|
char aCensoredMessage[256];
|
|
CensorMessage(aCensoredMessage, pMessage, sizeof(aCensoredMessage));
|
|
|
|
char aBuf[256];
|
|
|
|
if(Server()->IsSixup(ClientID))
|
|
{
|
|
protocol7::CNetMsg_Sv_Chat Msg;
|
|
Msg.m_ClientID = ClientID;
|
|
Msg.m_Mode = protocol7::CHAT_WHISPER;
|
|
Msg.m_pMessage = aCensoredMessage;
|
|
Msg.m_TargetID = VictimID;
|
|
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID);
|
|
}
|
|
else if(GetClientVersion(ClientID) >= VERSION_DDNET_WHISPER)
|
|
{
|
|
CNetMsg_Sv_Chat Msg;
|
|
Msg.m_Team = CHAT_WHISPER_SEND;
|
|
Msg.m_ClientID = VictimID;
|
|
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
|
|
{
|
|
str_format(aBuf, sizeof(aBuf), "[→ %s] %s", Server()->ClientName(VictimID), aCensoredMessage);
|
|
SendChatTarget(ClientID, aBuf);
|
|
}
|
|
|
|
if(Server()->IsSixup(VictimID))
|
|
{
|
|
protocol7::CNetMsg_Sv_Chat Msg;
|
|
Msg.m_ClientID = ClientID;
|
|
Msg.m_Mode = protocol7::CHAT_WHISPER;
|
|
Msg.m_pMessage = aCensoredMessage;
|
|
Msg.m_TargetID = VictimID;
|
|
|
|
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimID);
|
|
}
|
|
else if(GetClientVersion(VictimID) >= VERSION_DDNET_WHISPER)
|
|
{
|
|
CNetMsg_Sv_Chat Msg2;
|
|
Msg2.m_Team = CHAT_WHISPER_RECV;
|
|
Msg2.m_ClientID = ClientID;
|
|
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
|
|
{
|
|
str_format(aBuf, sizeof(aBuf), "[← %s] %s", Server()->ClientName(ClientID), aCensoredMessage);
|
|
SendChatTarget(VictimID, aBuf);
|
|
}
|
|
}
|
|
|
|
void CGameContext::Converse(int ClientID, char *pStr)
|
|
{
|
|
CPlayer *pPlayer = m_apPlayers[ClientID];
|
|
if(!pPlayer)
|
|
return;
|
|
|
|
if(ProcessSpamProtection(ClientID))
|
|
return;
|
|
|
|
if(pPlayer->m_LastWhisperTo < 0)
|
|
SendChatTarget(ClientID, "You do not have an ongoing conversation. Whisper to someone to start one");
|
|
else
|
|
{
|
|
WhisperID(ClientID, pPlayer->m_LastWhisperTo, pStr);
|
|
}
|
|
}
|
|
|
|
bool CGameContext::IsVersionBanned(int Version)
|
|
{
|
|
char aVersion[16];
|
|
str_from_int(Version, aVersion);
|
|
|
|
return str_in_list(g_Config.m_SvBannedVersions, ",", aVersion);
|
|
}
|
|
|
|
void CGameContext::List(int ClientID, const char *pFilter)
|
|
{
|
|
int Total = 0;
|
|
char aBuf[256];
|
|
int Bufcnt = 0;
|
|
if(pFilter[0])
|
|
str_format(aBuf, sizeof(aBuf), "Listing players with \"%s\" in name:", pFilter);
|
|
else
|
|
str_copy(aBuf, "Listing all players:");
|
|
SendChatTarget(ClientID, aBuf);
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(m_apPlayers[i])
|
|
{
|
|
Total++;
|
|
const char *pName = Server()->ClientName(i);
|
|
if(str_utf8_find_nocase(pName, pFilter) == NULL)
|
|
continue;
|
|
if(Bufcnt + str_length(pName) + 4 > 256)
|
|
{
|
|
SendChatTarget(ClientID, aBuf);
|
|
Bufcnt = 0;
|
|
}
|
|
if(Bufcnt != 0)
|
|
{
|
|
str_format(&aBuf[Bufcnt], sizeof(aBuf) - Bufcnt, ", %s", pName);
|
|
Bufcnt += 2 + str_length(pName);
|
|
}
|
|
else
|
|
{
|
|
str_copy(&aBuf[Bufcnt], pName, sizeof(aBuf) - Bufcnt);
|
|
Bufcnt += str_length(pName);
|
|
}
|
|
}
|
|
}
|
|
if(Bufcnt != 0)
|
|
SendChatTarget(ClientID, aBuf);
|
|
str_format(aBuf, sizeof(aBuf), "%d players online", Total);
|
|
SendChatTarget(ClientID, aBuf);
|
|
}
|
|
|
|
int CGameContext::GetClientVersion(int ClientID) const
|
|
{
|
|
return Server()->GetClientVersion(ClientID);
|
|
}
|
|
|
|
CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version)
|
|
{
|
|
CClientMask Mask;
|
|
for(int i = 0; i < MAX_CLIENTS; ++i)
|
|
{
|
|
if(GetClientVersion(i) >= Version)
|
|
continue;
|
|
Mask.set(i);
|
|
}
|
|
return Mask;
|
|
}
|
|
|
|
bool CGameContext::PlayerModerating() const
|
|
{
|
|
return std::any_of(std::begin(m_apPlayers), std::end(m_apPlayers), [](const CPlayer *pPlayer) { return pPlayer && pPlayer->m_Moderating; });
|
|
}
|
|
|
|
void CGameContext::ForceVote(int EnforcerID, bool Success)
|
|
{
|
|
// check if there is a vote running
|
|
if(!m_VoteCloseTime)
|
|
return;
|
|
|
|
m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
|
|
m_VoteEnforcer = EnforcerID;
|
|
|
|
char aBuf[256];
|
|
const char *pOption = Success ? "yes" : "no";
|
|
str_format(aBuf, sizeof(aBuf), "authorized player forced vote %s", pOption);
|
|
SendChatTarget(-1, aBuf);
|
|
str_format(aBuf, sizeof(aBuf), "forcing vote %s", pOption);
|
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
|
|
}
|
|
|
|
bool CGameContext::RateLimitPlayerVote(int ClientID)
|
|
{
|
|
int64_t Now = Server()->Tick();
|
|
int64_t TickSpeed = Server()->TickSpeed();
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
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()));
|
|
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());
|
|
}
|