List of network game messages
There are three types of messages with overlapping message ids:
system messages,
game messages and
control messages
This list is only covering game messages.
When the client and server exchange packets.
An integer field which usually does not exceed one byte.
Is indicating which type of message was sent.
This field is called the message id.
These message ids are defined in generated/protocol.h
which is generated by
datasrc/network.py
which also includes the message payload.
There can be multiple messages in so called chunks in one teeworlds packet.
The message names give a hint about who is sending and receiving them.
The format is always
NETMSGTYPE_<sender>_<name>
Where
sender
can be one of those:
- SV - Server sending to client
- CL - Client sending to server
- DE - used for Demos only and is not actually sent
In the list below you will find those message.
With their name, id and payload.
Argument name |
Type |
Note |
Message |
String |
The message of the day (motd) that will be displayed to clients
that connect. (The one with the dark background)
|
Argument name |
Type |
Note |
Mode |
Int |
Chat mode can be one of those
- 1 - CHAT_ALL
- 2 - CHAT_TEAM
- 3 - CHAT_WHISPER
|
Client ID |
Int |
The id of the message author.
|
Target ID |
Int |
Recipient client id of the chat message.
If there is no specific target (no whisper message) it is set to -1 which stands for ALL.
And set to the target if it is a whipser message.
|
Message |
String |
The text of the chat message.
|
Argument name |
Type |
Note |
Client ID |
Int |
ID of the client that switched team
|
Team |
Int |
Target team the client switched to. Can be one of those:
- -1 - TEAM_SPECTATORS
- 0 - TEAM_RED
- 1 - TEAM_BLUE
If the game mode has no teams and a player joins the game
it uses 0 which is technically TEAM_RED.
|
Silent |
Int |
Boolean flag that decides if the message is visible in chat.
Can be one of those values:
- 0 - Message shown (not silent)
- 1 - Message hidden (silent)
For example if the value 0 is sent it is not silent. And a message like this
is shown in the clients chat:
*** 'nameless tee' joined the spectators
|
Cooldown Tick |
Int |
The server disallows changing teams too fast. This field informs the client who changed team
when he can change the team again. This is used to show a cooldown in the client menu.
The message in the ui then looks like this:
Teams are locked. Time to wait before changing team: 00:01
|
A sample message sent over the network could look like this:
40 06 0c 08 00 40 00 91 3b @....@..;
\______/ ^ ^ ^ ^ \___/
| | | | | |
chunk | | | | Cooldown Tick (3793)
header | | | |
flags: vital | | | Silent (0/false)
size: 6 | | |
seq: 12 | | TEAM_SPECTATORS (-1)
| |
| ClientID (0)
|
NETMSGTYPE_SV_TEAM (msg=4 system=false)
Sender: | Server |
Recipient: | Client |
Message ID: | 5 |
Response to: |
Tee death
|
Expected response: | None |
Flags: | MSGFLAG_VITAL |
Argument name |
Type |
Note |
Killer |
Int |
Client ID of the killer. Can be the same as the Victim.
For example on a selfkill with grenade but also when a tee
dies in a spike (death tile) or falls out of the world.
|
Victim |
Int |
Client ID of the killed.
|
Weapon |
Int |
Weapon the tee was killed with.
Can be one of those:
- -3WEAPON_GAME (team switching etc)
- -2WEAPON_SELF (console kill command)
- -1WEAPON_WORLD (death tiles etc)
- 0WEAPON_HAMMER
- 1WEAPON_GUN
- 2WEAPON_SHOTGUN
- 3WEAPON_GRENADE
- 4WEAPON_LASER
- 5WEAPON_NINJA
|
Mode special |
Int |
For CTF, if the guy is carrying a flag for example.
Only when the sv_gametype is ctf this mode is non zero.
It is set in
ctf.cpp
when a flag is involved on death.
|
This message is used by the server to inform the client about kills.
The client will then render them in the kill feed in the top right corner.
The server sends this message to all clients when a tee dies.
void CCharacter::Die(int Killer, int Weapon)
{
// we got to wait 0.5 secs before respawning
m_Alive = false;
m_pPlayer->m_RespawnTick = Server()->Tick()+Server()->TickSpeed()/2;
int ModeSpecial = GameServer()->m_pController->OnCharacterDeath(this, (Killer < 0) ? 0 : GameServer()->m_apPlayers[Killer], Weapon);
char aBuf[256];
if(Killer < 0)
{
str_format(aBuf, sizeof(aBuf), "kill killer='%d:%d:' victim='%d:%d:%s' weapon=%d special=%d",
Killer, - 1 - Killer,
m_pPlayer->GetCID(), m_pPlayer->GetTeam(), Server()->ClientName(m_pPlayer->GetCID()), Weapon, ModeSpecial
);
}
else
{
str_format(aBuf, sizeof(aBuf), "kill killer='%d:%d:%s' victim='%d:%d:%s' weapon=%d special=%d",
Killer, GameServer()->m_apPlayers[Killer]->GetTeam(), Server()->ClientName(Killer),
m_pPlayer->GetCID(), m_pPlayer->GetTeam(), Server()->ClientName(m_pPlayer->GetCID()), Weapon, ModeSpecial
);
}
GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", aBuf);
// send the kill message
CNetMsg_Sv_KillMsg Msg;
Msg.m_Victim = m_pPlayer->GetCID();
Msg.m_ModeSpecial = ModeSpecial;
for(int i = 0 ; i < MAX_CLIENTS; i++)
{
if(!Server()->ClientIngame(i))
continue;
if(Killer < 0 && Server()->GetClientVersion(i) < MIN_KILLMESSAGE_CLIENTVERSION)
{
Msg.m_Killer = 0;
Msg.m_Weapon = WEAPON_WORLD;
}
else
{
Msg.m_Killer = Killer;
Msg.m_Weapon = Weapon;
}
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, i);
}
// a nice sound
GameServer()->CreateSound(m_Pos, SOUND_PLAYER_DIE);
// this is for auto respawn after 3 secs
m_pPlayer->m_DieTick = Server()->Tick();
GameWorld()->RemoveEntity(this);
GameWorld()->m_Core.m_apCharacters[m_pPlayer->GetCID()] = 0;
GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCID());
}
The client side file that used to handle this was called
killmessages.cpp
and was renamed to a more generic
infomessages.cpp
when 0.7 introduced
race related messages in the "killfeed".
void CInfoMessages::OnMessage(int MsgType, void *pRawMsg)
{
// [..]
bool Race = m_pClient->m_GameInfo.m_GameFlags&GAMEFLAG_RACE;
if(MsgType == NETMSGTYPE_SV_KILLMSG)
{
// [..]
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
// unpack messages
CInfoMsg Kill;
Kill.m_Player1ID = pMsg->m_Victim;
if(Config()->m_ClShowsocial)
{
Kill.m_Player1NameCursor.m_FontSize = 36.0f;
TextRender()->TextDeferred(&Kill.m_Player1NameCursor, m_pClient->m_aClients[Kill.m_Player1ID].m_aName, -1);
}
Kill.m_Player1RenderInfo = m_pClient->m_aClients[Kill.m_Player1ID].m_RenderInfo;
Kill.m_Player2ID = pMsg->m_Killer;
// [..]
Kill.m_Weapon = pMsg->m_Weapon;
Kill.m_ModeSpecial = pMsg->m_ModeSpecial;
Kill.m_FlagCarrierBlue = m_pClient->m_Snap.m_pGameDataFlag ? m_pClient->m_Snap.m_pGameDataFlag->m_FlagCarrierBlue : -1;
AddInfoMsg(INFOMSG_KILL, Kill);
}
// [..]
In a update of 0.7 support for the community mod race was added.
So in
hud.cpp
the client also resets the checkpoint time
of the tee that died. Which is not used by the standard gametypes.
void CHud::OnMessage(int MsgType, void *pRawMsg)
{
// [..]
else if(MsgType == NETMSGTYPE_SV_KILLMSG && (m_pClient->m_GameInfo.m_GameFlags&GAMEFLAG_RACE))
{
// reset checkpoint time on death
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
if(pMsg->m_Victim == m_pClient->m_LocalClientID)
m_CheckpointTime = 0;
}
}
The client does also process this message in
CStats::OnMessage()
Sender: | Server |
Recipient: | Client |
Message ID: | 6 |
Response to: |
TODO
|
Expected response: | None |
Flags: | MSGFLAG_VITAL |
All the fields of this message are defined in the
tuning.h
file.
Unpacked by the client in
CGameClient::OnMessage()
void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker)
{
Client()->RecordGameMessage(true);
// special messages
if(MsgId == NETMSGTYPE_SV_TUNEPARAMS && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
Client()->RecordGameMessage(false);
// unpack the new tuning
CTuningParams NewTuning;
int *pParams = (int *)&NewTuning;
for(unsigned i = 0; i < sizeof(CTuningParams)/sizeof(int); i++)
pParams[i] = pUnpacker->GetInt();
// check for unpacking errors
if(pUnpacker->Error())
return;
m_ServerMode = SERVERMODE_PURE;
// apply new tuning
m_Tuning = NewTuning;
return;
}
Sender: | Unused (should be Server) |
Recipient: | Unused (should be Client) |
Message ID: | 7 |
Response to: |
Unused |
Expected response: | Unused |
Flags: | Unused |
Argument name |
Type |
Note |
Unused |
Argument name |
Type |
Note |
None |
It is sent by the server as a response to
NETMSGTYPE_CL_STARTINFO
in the method
CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
CNetMsg_Sv_ReadyToEnter m;
Server()->SendPackMsg(&m, MSGFLAG_VITAL|MSGFLAG_FLUSH, ClientID);
It is processed by the client in
CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker)
else if(MsgId == NETMSGTYPE_SV_READYTOENTER)
{
Client()->EnterGame();
}
Which calls
CClient::EnterGame()
which then calls
CClient::SendEnterGame()
void CClient::SendEnterGame()
{
CMsgPacker Msg(NETMSG_ENTERGAME, true);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
}
Where it responds with
NETMSG_ENTERGAME
Sender: | Server |
Recipient: | Client |
Message ID: | 9 |
Response to: |
Sent when tees touch/collect one of those weapons:
grenade, shotgun or laser
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Argument name |
Type |
Note |
Client ID |
Int |
Client ID of the tee who sent the emoticon.
|
Emoticon |
Int |
- 0 - oop!
- 1 - alert
- 2 - heart
- 3 - tear
- 4 - ...
- 5 - music
- 6 - sorry
- 7 - ghost
- 8 - annoyed
- 9 - angry
- 10 - devil
- 11 - swearing
- 12 - zzZ
- 13 - WTF
- 14 - happy
- 15 - ??
|
Argument name |
Type |
Note |
None |
This message is used by the server to empty all vote entries in the client menu.
Sender: | Server |
Recipient: | Client |
Message ID: | 12 |
Response to: |
TODO
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
One of the few messages with a dynamic amount of fields.
The first field is always a int determining how many string fields are following.
The strings represent the descriptions of the votes that will show up in the client
menu vote option list.
else if(MsgId == NETMSGTYPE_SV_VOTEOPTIONLISTADD)
{
int NumOptions = pUnpacker->GetInt();
for(int i = 0; i < NumOptions; i++)
{
const char *pDescription = pUnpacker->GetString(CUnpacker::SANITIZE_CC);
if(pUnpacker->Error())
return;
m_pVoting->AddOption(pDescription);
}
}
Sent by the server in
CGameContext::OnMessage()
CVoteOptionServer *pCurrent = m_pVoteOptionFirst;
while(pCurrent)
{
// count options for actual packet
int NumOptions = 0;
for(CVoteOptionServer *p = pCurrent; p && NumOptions < MAX_VOTE_OPTION_ADD; p = p->m_pNext, ++NumOptions);
// pack and send vote list packet
CMsgPacker Msg(NETMSGTYPE_SV_VOTEOPTIONLISTADD);
Msg.AddInt(NumOptions);
while(pCurrent && NumOptions--)
{
Msg.AddString(pCurrent->m_aDescription, VOTE_DESC_LENGTH);
pCurrent = pCurrent->m_pNext;
}
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
Sender: | Server |
Recipient: | Client |
Message ID: | 13 |
Response to: |
TODO
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Same use case as the
NETMSGTYPE_SV_VOTEOPTIONLISTADD
but for single vote option entries.
else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONADD)
{
CNetMsg_Sv_VoteOptionAdd *pMsg = (CNetMsg_Sv_VoteOptionAdd *)pRawMsg;
AddOption(pMsg->m_pDescription);
}
Sender: | Server |
Recipient: | Client |
Message ID: | 14 |
Response to: |
TODO
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Sender: | Server |
Recipient: | Client |
Message ID: | 15 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: |
MSGFLAG_VITAL
|
Indicates that a new vote was started. The client will
render the current vote status and the server is ready
to receive yes or no votes via
NETMSGTYPE_CL_VOTE.
Sender: | Server |
Recipient: | Client |
Message ID: | 16 |
Response to: |
TODO
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Sender: | Server |
Recipient: | Client |
Message ID: | 17 |
Response to: |
NETMSG_READY |
NETMSG_RCON_CMD
(lock_teams, sv_vote_kick, sv_vote_kick_min, sv_vote_spectate, sv_teambalance_time, sv_player_slots, sv_max_clients)
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Argument name |
Type |
Note |
Kick vote
|
Int |
If it is set to 1 kick votes are allowed.
If it is set to 0 kick votes are not allowed.
The client shows the "Server does not allow voting to kick players" message.
In the menu when the user tries to start a kick vote.
The value can be set by a server admin using the sv_vote_kick setting.
If that value is updated via rcon on a running server this network message is sent
to inform the clients.
|
Kick minimum
|
Int |
Minimum amount of players needed on the server to allow kick votes to be started.
|
Spectator vote
|
Int |
If it is set to 1 spectator votes are allowed.
If it is set to 0 spectator votes are not allowed.
The client shows the "Server does not allow voting to move players to spectators" message.
In the menu when the user tries to start a move to spectator vote.
The value can be set by a server admin using the sv_vote_spectate setting.
If that value is updated via rcon on a running server this network message is sent
to inform the clients.
|
Team lock
|
Int |
If it is set to 1 changing team is allowed.
If it is set to 0 changing team is not allowed.
If the teams are locked the client shows a "Teams are locked" in the menu.
Where the join red/blue/spec buttons are.
|
Team balance
|
Int |
TODO
|
Player slots
|
Int |
This value is used to fill in the number in the "Only %d active players are allowed"
string that is shown to the clients when they try to join the game but the maximum amount
of in game players is already reached.
The value can be set by a server admin using the sv_player_slots setting.
If that value is updated via rcon on a running server this network message is sent
to inform the clients.
|
What is sent in NETMSGTYPE_SV_CLIENTINFO?
In
skins.cpp
all the skin part names are listed in order.
const char * const CSkins::ms_apSkinPartNames[NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};
Every skin part has his own name and color.
Thats why the argument list has 6 fields with all the names
followed by 6 fields if that part is using a custom color or not
followed by 6 fields with the corresponding custom color values.
When is NETMSGTYPE_SV_CLIENTINFO sent?
Every time a new client connects its info will be sent to all
clients that were already connected.
And also the info of every client that is already connected
will be sent to the client that just connected.
void CGameContext::OnClientEnter(int ClientID)
{
// [..]
// update client infos (others before local)
CNetMsg_Sv_ClientInfo NewClientInfoMsg;
NewClientInfoMsg.m_ClientID = ClientID;
NewClientInfoMsg.m_Local = 0;
NewClientInfoMsg.m_Team = m_apPlayers[ClientID]->GetTeam();
NewClientInfoMsg.m_pName = Server()->ClientName(ClientID);
NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientID);
NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientID);
NewClientInfoMsg.m_Silent = false;
if(Config()->m_SvSilentSpectatorMode && m_apPlayers[ClientID]->GetTeam() == TEAM_SPECTATORS)
NewClientInfoMsg.m_Silent = true;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
NewClientInfoMsg.m_apSkinPartNames[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aaSkinPartNames[p];
NewClientInfoMsg.m_aUseCustomColors[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aUseCustomColors[p];
NewClientInfoMsg.m_aSkinPartColors[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aSkinPartColors[p];
}
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(i == ClientID || !m_apPlayers[i] || (!Server()->ClientIngame(i) && !m_apPlayers[i]->IsDummy()))
continue;
// new info for others
if(Server()->ClientIngame(i))
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL|MSGFLAG_NORECORD, i);
// existing infos for new player
CNetMsg_Sv_ClientInfo ClientInfoMsg;
ClientInfoMsg.m_ClientID = i;
ClientInfoMsg.m_Local = 0;
ClientInfoMsg.m_Team = m_apPlayers[i]->GetTeam();
ClientInfoMsg.m_pName = Server()->ClientName(i);
ClientInfoMsg.m_pClan = Server()->ClientClan(i);
ClientInfoMsg.m_Country = Server()->ClientCountry(i);
ClientInfoMsg.m_Silent = false;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
ClientInfoMsg.m_apSkinPartNames[p] = m_apPlayers[i]->m_TeeInfos.m_aaSkinPartNames[p];
ClientInfoMsg.m_aUseCustomColors[p] = m_apPlayers[i]->m_TeeInfos.m_aUseCustomColors[p];
ClientInfoMsg.m_aSkinPartColors[p] = m_apPlayers[i]->m_TeeInfos.m_aSkinPartColors[p];
}
Server()->SendPackMsg(&ClientInfoMsg, MSGFLAG_VITAL|MSGFLAG_NORECORD, ClientID);
}
// local info
NewClientInfoMsg.m_Local = 1;
Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL|MSGFLAG_NORECORD, ClientID);
// [..]
}
Sender: | Server |
Recipient: | Client |
Message ID: | 19 |
Response to: |
Round end |
NETMSG_RCON_CMD sv_scorelimit, sv_timelimit, sv_maprotation, sv_matches_per_map |
NETMSG_ENTERGAME
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL &
MSGFLAG_NORECORD
|
Sent by the server in
UpdateGameInfo()
void IGameController::UpdateGameInfo(int ClientID)
{
CNetMsg_Sv_GameInfo GameInfoMsg;
GameInfoMsg.m_GameFlags = m_GameFlags;
GameInfoMsg.m_ScoreLimit = m_GameInfo.m_ScoreLimit;
GameInfoMsg.m_TimeLimit = m_GameInfo.m_TimeLimit;
GameInfoMsg.m_MatchNum = m_GameInfo.m_MatchNum;
GameInfoMsg.m_MatchCurrent = m_GameInfo.m_MatchCurrent;
CNetMsg_Sv_GameInfo GameInfoMsgNoRace = GameInfoMsg;
GameInfoMsgNoRace.m_GameFlags &= ~GAMEFLAG_RACE;
if(ClientID == -1)
{
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(!GameServer()->m_apPlayers[i] || !Server()->ClientIngame(i))
continue;
CNetMsg_Sv_GameInfo *pInfoMsg = (Server()->GetClientVersion(i) < CGameContext::MIN_RACE_CLIENTVERSION) ? &GameInfoMsgNoRace : &GameInfoMsg;
Server()->SendPackMsg(pInfoMsg, MSGFLAG_VITAL|MSGFLAG_NORECORD, i);
}
}
else
{
CNetMsg_Sv_GameInfo *pInfoMsg = (Server()->GetClientVersion(ClientID) < CGameContext::MIN_RACE_CLIENTVERSION) ? &GameInfoMsgNoRace : &GameInfoMsg;
Server()->SendPackMsg(pInfoMsg, MSGFLAG_VITAL|MSGFLAG_NORECORD, ClientID);
}
}
Unpacked by the client in
gameclient.cpp
else if(MsgId == NETMSGTYPE_SV_GAMEINFO && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
Client()->RecordGameMessage(false);
CNetMsg_Sv_GameInfo *pMsg = (CNetMsg_Sv_GameInfo *)pRawMsg;
m_GameInfo.m_GameFlags = pMsg->m_GameFlags;
m_GameInfo.m_ScoreLimit = pMsg->m_ScoreLimit;
m_GameInfo.m_TimeLimit = pMsg->m_TimeLimit;
m_GameInfo.m_MatchNum = pMsg->m_MatchNum;
m_GameInfo.m_MatchCurrent = pMsg->m_MatchCurrent;
}
Sender: | Server |
Recipient: | Client |
Message ID: | 20 |
Response to: |
Either triggered by a disconnecting client via
NET_CTRLMSG_CLOSE.
Or sent by the server if it disconnects a client due to kick or timeout.
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL &
MSGFLAG_NORECORD
|
If one client leaves the server sends this message to all remaining clients.
To inform them about the updated connected player list. Which also causes the client
to print the
'nameless tee' has left the game
chat message.
In 0.6 this used to be a regular chat message the server sent to the client.
Now in 0.7 this is a network message allowing the client to localize the message locally.
The client is just informed about who left the game and then translates the "left the game" string into
the language the client is running in.
Sent by the server in
CGameContext::OnClientDrop()
void CGameContext::OnClientDrop(int ClientID, const char *pReason)
{
AbortVoteOnDisconnect(ClientID);
m_pController->OnPlayerDisconnect(m_apPlayers[ClientID]);
// update clients on drop
if(Server()->ClientIngame(ClientID) || IsClientBot(ClientID))
{
if(Server()->DemoRecorder_IsRecording())
{
CNetMsg_De_ClientLeave Msg;
Msg.m_pName = Server()->ClientName(ClientID);
Msg.m_pReason = pReason;
Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, -1);
}
CNetMsg_Sv_ClientDrop Msg;
Msg.m_ClientID = ClientID;
Msg.m_pReason = pReason;
Msg.m_Silent = false;
if(Config()->m_SvSilentSpectatorMode && m_apPlayers[ClientID]->GetTeam() == TEAM_SPECTATORS)
Msg.m_Silent = true;
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_NORECORD, -1);
}
// [..]
}
Unpacked by the client in
CGameClient::OnMessage()
else if(MsgId == NETMSGTYPE_SV_CLIENTDROP && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
Client()->RecordGameMessage(false);
CNetMsg_Sv_ClientDrop *pMsg = (CNetMsg_Sv_ClientDrop *)pRawMsg;
if(m_LocalClientID == pMsg->m_ClientID || !m_aClients[pMsg->m_ClientID].m_Active)
{
if(Config()->m_Debug)
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", "invalid clientdrop");
return;
}
if(!pMsg->m_Silent)
{
DoLeaveMessage(m_aClients[pMsg->m_ClientID].m_aName, pMsg->m_ClientID, pMsg->m_pReason);
if(m_pDemoRecorder->IsRecording())
{
CNetMsg_De_ClientLeave Msg;
Msg.m_pName = m_aClients[pMsg->m_ClientID].m_aName;
Msg.m_ClientID = pMsg->m_ClientID;
Msg.m_pReason = pMsg->m_pReason;
Client()->SendPackMsg(&Msg, MSGFLAG_NOSEND | MSGFLAG_RECORD);
}
}
// [..]
}
Sender: | Server |
Recipient: | Client |
Message ID: | 21 |
Response to: |
NETMSGTYPE_CL_READYCHANGE can trigger GAMEMSG_GAME_PAUSED.
NETMSGTYPE_CL_SETSPECTATORMODE can trigger GAMEMSG_SPEC_INVALIDID.
NETMSG_RCON_CMD swap_teams triggers GAMEMSG_TEAM_SWAP.
NETMSG_RCON_CMD set_team_all triggers GAMEMSG_TEAM_ALL.
The other messages depend on gameplay state and are not direct responses to net messages from the client (auto team balance, flag cap/grab/return/capture).
|
Expected response: |
None
|
Flags: |
MSGFLAG_VITAL
|
Argument name |
Type |
Note |
Game message ID
|
Int |
Can be one of those values:
- 0 - GAMEMSG_TEAM_SWAP
- 1 - GAMEMSG_SPEC_INVALIDID
- 2 - GAMEMSG_TEAM_SHUFFLE
- 3 - GAMEMSG_TEAM_BALANCE
- 4 - GAMEMSG_CTF_DROP
- 5 - GAMEMSG_CTF_RETURN
|
Argument name |
Type |
Note |
Game message ID
|
Int |
Can be one of those values:
- 6 - GAMEMSG_TEAM_ALL
- 7 - GAMEMSG_TEAM_BALANCE_VICTIM
- 8 - GAMEMSG_CTF_GRAB
- 10 - GAMEMSG_GAME_PAUSED
|
Parameter 1
|
Int |
The parameter depends on the type of game message:
- GAMEMSG_TEAM_ALL => Team
- GAMEMSG_TEAM_BALANCE_VICTIM => Team
- GAMEMSG_CTF_GRAB => Team
- GAMEMSG_GAME_PAUSED => ClientID (pause initiator)
|
Argument name |
Type |
Note |
Game message ID
|
Int |
There is only one game message with 3 parameters:
|
Parameter 1
|
Int |
Flag:
- 0 - red
*** The red flag was captured by 'ChillerDragon' (0.02 seconds)
- 1 - blue
*** The blue flag was captured by 'ChillerDragon' (0.02 seconds)
|
Parameter 2
|
Int |
This is the ClientID of the player who captured the flag.
|
Parameter 3
|
Int |
This is the capture time and it will be converted to a float like This
float Time = aParaI[2] / (float)Client()->GameTickSpeed();
|
GAMEMSG_SPEC_INVALIDID and GAMEMSG_TEAM_BALANCE_VICTIM are sent to specific ClientIDs and all other game messages are
broadcasted to all connected players.
The amount of parameters depends on the
Game message ID
The server has 3 overloaded methods to send this message
with either 0, 1 or 3 parameters
void CGameContext::SendGameMsg(int GameMsgID, int ClientID)
{
CMsgPacker Msg(NETMSGTYPE_SV_GAMEMSG);
Msg.AddInt(GameMsgID);
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
void CGameContext::SendGameMsg(int GameMsgID, int ParaI1, int ClientID)
{
CMsgPacker Msg(NETMSGTYPE_SV_GAMEMSG);
Msg.AddInt(GameMsgID);
Msg.AddInt(ParaI1);
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
void CGameContext::SendGameMsg(int GameMsgID, int ParaI1, int ParaI2, int ParaI3, int ClientID)
{
CMsgPacker Msg(NETMSGTYPE_SV_GAMEMSG);
Msg.AddInt(GameMsgID);
Msg.AddInt(ParaI1);
Msg.AddInt(ParaI2);
Msg.AddInt(ParaI3);
Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
Server and client have a shared list of pre agreed "Game message ID" to "parameter"
mappings.
The client unpacks the message like this:
else if(MsgId == NETMSGTYPE_SV_GAMEMSG)
{
int GameMsgID = pUnpacker->GetInt();
// check for valid gamemsgid
if(GameMsgID < 0 || GameMsgID >= NUM_GAMEMSGS)
return;
int aParaI[3];
int NumParaI = 0;
// get paras
switch(gs_GameMsgList[GameMsgID].m_ParaType)
{
case PARA_I: NumParaI = 1; break;
case PARA_II: NumParaI = 2; break;
case PARA_III: NumParaI = 3; break;
}
for(int i = 0; i < NumParaI; i++)
{
aParaI[i] = pUnpacker->GetInt();
}
// check for unpacking errors
if(pUnpacker->Error())
return;
// handle special messages
char aBuf[256];
bool TeamPlay = m_GameInfo.m_GameFlags&GAMEFLAG_TEAMS;
if(gs_GameMsgList[GameMsgID].m_Action == DO_SPECIAL)
{
switch(GameMsgID)
{
case GAMEMSG_CTF_DROP:
m_pSounds->Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_DROP);
break;
case GAMEMSG_CTF_RETURN:
m_pSounds->Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_RETURN);
break;
case GAMEMSG_TEAM_ALL:
{
const char *pMsg = "";
switch(GetStrTeam(aParaI[0], TeamPlay))
{
case STR_TEAM_GAME: pMsg = Localize("All players were moved to the game"); break;
case STR_TEAM_RED: pMsg = Localize("All players were moved to the red team"); break;
case STR_TEAM_BLUE: pMsg = Localize("All players were moved to the blue team"); break;
case STR_TEAM_SPECTATORS: pMsg = Localize("All players were moved to the spectators"); break;
}
m_pBroadcast->DoClientBroadcast(pMsg);
}
break;
case GAMEMSG_TEAM_BALANCE_VICTIM:
{
const char *pMsg = "";
switch(GetStrTeam(aParaI[0], TeamPlay))
{
case STR_TEAM_RED: pMsg = Localize("You were moved to the red team due to team balancing"); break;
case STR_TEAM_BLUE: pMsg = Localize("You were moved to the blue team due to team balancing"); break;
}
m_pBroadcast->DoClientBroadcast(pMsg);
}
break;
case GAMEMSG_CTF_GRAB:
if(m_LocalClientID != -1 && (m_aClients[m_LocalClientID].m_Team != aParaI[0] || (m_Snap.m_SpecInfo.m_Active &&
((m_Snap.m_SpecInfo.m_SpectatorID != -1 && m_aClients[m_Snap.m_SpecInfo.m_SpectatorID].m_Team != aParaI[0]) ||
(m_Snap.m_SpecInfo.m_SpecMode == SPEC_FLAGRED && aParaI[0] != TEAM_RED) ||
(m_Snap.m_SpecInfo.m_SpecMode == SPEC_FLAGBLUE && aParaI[0] != TEAM_BLUE)))))
m_pSounds->Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_GRAB_PL);
else
m_pSounds->Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_GRAB_EN);
break;
case GAMEMSG_GAME_PAUSED:
{
int ClientID = clamp(aParaI[0], 0, MAX_CLIENTS - 1);
char aLabel[64];
GetPlayerLabel(aLabel, sizeof(aLabel), ClientID, m_aClients[ClientID].m_aName);
str_format(aBuf, sizeof(aBuf), Localize("'%s' initiated a pause"), aLabel);
m_pChat->AddLine(aBuf);
}
break;
case GAMEMSG_CTF_CAPTURE:
m_pSounds->Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_CAPTURE);
int ClientID = clamp(aParaI[1], 0, MAX_CLIENTS - 1);
m_pStats->OnFlagCapture(ClientID);
char aLabel[64];
GetPlayerLabel(aLabel, sizeof(aLabel), ClientID, m_aClients[ClientID].m_aName);
float Time = aParaI[2] / (float)Client()->GameTickSpeed();
if(Time <= 60)
{
if(aParaI[0])
{
str_format(aBuf, sizeof(aBuf), Localize("The blue flag was captured by '%s' (%.2f seconds)"), aLabel, Time);
}
else
{
str_format(aBuf, sizeof(aBuf), Localize("The red flag was captured by '%s' (%.2f seconds)"), aLabel, Time);
}
}
else
{
if(aParaI[0])
{
str_format(aBuf, sizeof(aBuf), Localize("The blue flag was captured by '%s'"), aLabel);
}
else
{
str_format(aBuf, sizeof(aBuf), Localize("The red flag was captured by '%s'"), aLabel);
}
}
m_pChat->AddLine(aBuf);
}
return;
}
// build message
const char *pText = "";
if(NumParaI == 0)
{
pText = Localize(gs_GameMsgList[GameMsgID].m_pText);
}
// handle message
switch(gs_GameMsgList[GameMsgID].m_Action)
{
case DO_CHAT:
m_pChat->AddLine(pText);
break;
case DO_BROADCAST:
m_pBroadcast->DoClientBroadcast(pText);
break;
}
}
Sender: | [ Client ] (not send over the network just used for demos) |
Recipient: | [ Client ] (not send over the network just used for demos) |
Message ID: | 22 |
Response to: |
NETMSGTYPE_SV_CLIENTINFO
|
Expected response: |
None
|
Flags: |
MSGFLAG_NOSEND & MSGFLAG_RECORD
|
This message is only used when the client is recording a demo.
There is no udp packet being sent to the server or anywhere else.
This is just added by the client to the demo file to be later read
from the demo file by the client again.
The client basically translates the
NETMSGTYPE_SV_CLIENTINFO
message to this
NETMSGTYPE_DE_CLIENTENTER message.
Note that it only happens when the client is recording a demo
if(m_pDemoRecorder->IsRecording())
if(MsgId == NETMSGTYPE_SV_CLIENTINFO && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
// [..]
if(pMsg->m_Local)
{
// [..]
}
else
{
// [..]
if(m_LocalClientID != -1 && !pMsg->m_Silent)
{
if(m_pDemoRecorder->IsRecording())
{
CNetMsg_De_ClientEnter Msg;
Msg.m_pName = pMsg->m_pName;
Msg.m_ClientID = pMsg->m_ClientID;
Msg.m_Team = pMsg->m_Team;
Client()->SendPackMsg(&Msg, MSGFLAG_NOSEND|MSGFLAG_RECORD);
}
}
}
}
And then the client only unpacks this message when in demo playback mode
Client()->State() == IClient::STATE_DEMOPLAYBACK
else if(MsgId == NETMSGTYPE_DE_CLIENTENTER && Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
CNetMsg_De_ClientEnter *pMsg = (CNetMsg_De_ClientEnter *)pRawMsg;
DoEnterMessage(pMsg->m_pName, pMsg->m_ClientID, pMsg->m_Team);
m_pStats->OnPlayerEnter(pMsg->m_ClientID, pMsg->m_Team);
}
Sender: | [ Client, Server] (not send over the network just used for demos) |
Recipient: | [ Client ] (not send over the network just used for demos) |
Message ID: | 23 |
Response to: |
TODO
|
Expected response: |
None
|
Flags: |
MSGFLAG_NOSEND & MSGFLAG_RECORD
|
Sender: | Client |
Recipient: | Server |
Message ID: | 24 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: |
TODO
|
Argument name |
Type |
Note |
Mode
|
Int |
Chat mode can be one of those
- 1 - CHAT_ALL
- 2 - CHAT_TEAM
- 3 - CHAT_WHISPER
|
Target
|
Int |
This is the client ID of the receiving party. It should be set to -1 if it is not a whisper message.
|
Message
|
String |
Message that will be printed in chat. The server trims right and cuts off after 128 utf8-characters.
|
Sender: | Client |
Recipient: | Server |
Message ID: | 25 |
Response to: |
The client running the console command team or clicking one of the team join buttons in the menu.
|
Expected response: |
NETMSGTYPE_SV_TEAM (Only on success. If the team is full, there is a ratelimit or any other error the server will not respond)
|
Flags: |
MSGFLAG_VITAL
|
Argument name |
Type |
Note |
Team
|
Int |
Can be one of those:
- -1 - TEAM_SPECTATORS
- 0 - TEAM_RED
- 1 - TEAM_BLUE
|
Unpacked by the server in
gamecontext.cpp
else if(MsgID == NETMSGTYPE_CL_SETTEAM && m_pController->IsTeamChangeAllowed())
{
CNetMsg_Cl_SetTeam *pMsg = (CNetMsg_Cl_SetTeam *)pRawMsg;
if(pPlayer->GetTeam() == pMsg->m_Team ||
(Config()->m_SvSpamprotection && pPlayer->m_LastSetTeamTick && pPlayer->m_LastSetTeamTick+Server()->TickSpeed()*3 > Server()->Tick()) ||
(pMsg->m_Team != TEAM_SPECTATORS && m_LockTeams) || pPlayer->m_TeamChangeTick > Server()->Tick())
return;
pPlayer->m_LastSetTeamTick = Server()->Tick();
// Switch team on given client and kill/respawn him
if(m_pController->CanJoinTeam(pMsg->m_Team, ClientID) && m_pController->CanChangeTeam(pPlayer, pMsg->m_Team))
{
if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
m_VoteUpdate = true;
pPlayer->m_TeamChangeTick = Server()->Tick()+Server()->TickSpeed()*3;
m_pController->DoTeamChange(pPlayer, pMsg->m_Team);
}
}
The server never responds with an error message. In case of failed team change the server will silently drop the team change request.
The client is supposed to do the error handling locally. Because it knows about all the cases where team changes will fail.
So the client deactivates the menu buttons locally if a team change is impossible. This allows the client to fully translate the error messages.
So the client keeps track of the server settings like
locked teams and in game slots.
And it also has to check if the teams are balanced
by counting the amount of team members locally
before doing a team change request.
Argument name |
Type |
Note |
Mode
|
Int |
The value should be one of those
- 0 - SPEC_FREEVIEW
- 1 - SPEC_PLAYER
- 2 - SPEC_FLAGRED
- 3 - SPEC_FLAGBLUE
|
Spectator ID
|
Int |
Should be either the ClientId when the mode is SPEC_PLAYER
or -1 in all other modes.
|
On success the server will update the snap item
obj_spectator_info.
On error the server will respond with
NETMSGTYPE_SV_GAMEMSG (GAMEMSG_SPEC_INVALIDID).
Sent by the client if the spectate next or spectate previous bind is triggerd.
Or if the client user picks a player in the specate menu. Or selects free view in the spectate menu.
Or if the user "clicks" on a player in free view.
In
src/game/client/components/spectator.cpp there is a
Spectate()
method used by the client to send this message.
void CSpectator::Spectate(int SpecMode, int SpectatorID)
{
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
m_pClient->m_DemoSpecMode = clamp(SpecMode, 0, NUM_SPECMODES-1);
m_pClient->m_DemoSpecID = clamp(SpectatorID, -1, MAX_CLIENTS-1);
return;
}
if(m_pClient->m_Snap.m_SpecInfo.m_SpecMode == SpecMode && (SpecMode != SPEC_PLAYER || m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SpectatorID))
return;
CNetMsg_Cl_SetSpectatorMode Msg;
Msg.m_SpecMode = SpecMode;
Msg.m_SpectatorID = SpectatorID;
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
}
Unpacked by the server in
gamecontext.cpp
else if (MsgID == NETMSGTYPE_CL_SETSPECTATORMODE && !m_World.m_Paused)
{
CNetMsg_Cl_SetSpectatorMode *pMsg = (CNetMsg_Cl_SetSpectatorMode *)pRawMsg;
if(Config()->m_SvSpamprotection && pPlayer->m_LastSetSpectatorModeTick && pPlayer->m_LastSetSpectatorModeTick+Server()->TickSpeed() > Server()->Tick())
return;
pPlayer->m_LastSetSpectatorModeTick = Server()->Tick();
if(!pPlayer->SetSpectatorID(pMsg->m_SpecMode, pMsg->m_SpectatorID))
SendGameMsg(GAMEMSG_SPEC_INVALIDID, ClientID);
}
Sent by the client in
SendStartInfo()
void CGameClient::SendStartInfo()
{
CNetMsg_Cl_StartInfo Msg;
Msg.m_pName = Config()->m_PlayerName;
Msg.m_pClan = Config()->m_PlayerClan;
Msg.m_Country = Config()->m_PlayerCountry;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
Msg.m_apSkinPartNames[p] = CSkins::ms_apSkinVariables[p];
Msg.m_aUseCustomColors[p] = *CSkins::ms_apUCCVariables[p];
Msg.m_aSkinPartColors[p] = *CSkins::ms_apColorVariables[p];
}
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
}
Which is called in
OnConnected()
void CGameClient::OnConnected()
{
m_Layers.Init(Kernel());
m_Collision.Init(Layers());
for(int i = 0; i < m_All.m_Num; i++)
{
m_All.m_paComponents[i]->OnMapLoad();
m_All.m_paComponents[i]->OnReset();
}
m_ServerMode = SERVERMODE_PURE;
// send the inital info
SendStartInfo();
}
Unpacked by the server in
CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
Requests selfkill. The server might block it. Processed by the server in
gamecontext.cpp
else if (MsgID == NETMSGTYPE_CL_KILL && !m_World.m_Paused)
{
if(pPlayer->m_LastKillTick && pPlayer->m_LastKillTick+Server()->TickSpeed()*3 > Server()->Tick())
return;
pPlayer->m_LastKillTick = Server()->Tick();
pPlayer->KillCharacter(WEAPON_SELF);
}
Sender: | Client |
Recipient: | Server |
Message ID: | 29 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: | MSGFLAG_VITAL |
Message to request game pause and unpause.
Will pause the game for everyone if one person sends it and if the server allows it.
Will unpause the game if everyone send it.
Needs
sv_player_ready_mode
set to
1
on the server side.
void CGameClient::SendReadyChange()
{
CNetMsg_Cl_ReadyChange Msg;
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
}
else if (MsgID == NETMSGTYPE_CL_READYCHANGE)
{
if(pPlayer->m_LastReadyChangeTick && pPlayer->m_LastReadyChangeTick+Server()->TickSpeed()*1 > Server()->Tick())
return;
pPlayer->m_LastReadyChangeTick = Server()->Tick();
m_pController->OnPlayerReadyChange(pPlayer);
}
Sender: | Client |
Recipient: | Server |
Message ID: | 30 |
Response to: |
User picking emote in emote wheel or
using the local console command emote (emote id)
|
Expected response: |
NETMSGTYPE_SV_EMOTICON
|
Flags: | MSGFLAG_VITAL |
Argument name |
Type |
Note |
Emoticon |
Int |
- 0 - oop!
- 1 - alert
- 2 - heart
- 3 - tear
- 4 - ...
- 5 - music
- 6 - sorry
- 7 - ghost
- 8 - annoyed
- 9 - angry
- 10 - devil
- 11 - swearing
- 12 - zzZ
- 13 - WTF
- 14 - happy
- 15 - ??
|
void CEmoticon::Emote(int Emoticon)
{
CNetMsg_Cl_Emoticon Msg;
Msg.m_Emoticon = Emoticon;
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
}
else if (MsgID == NETMSGTYPE_CL_EMOTICON && !m_World.m_Paused)
{
CNetMsg_Cl_Emoticon *pMsg = (CNetMsg_Cl_Emoticon *)pRawMsg;
if(Config()->m_SvSpamprotection && pPlayer->m_LastEmoteTick && pPlayer->m_LastEmoteTick+Server()->TickSpeed()*3 > Server()->Tick())
return;
pPlayer->m_LastEmoteTick = Server()->Tick();
SendEmoticon(ClientID, pMsg->m_Emoticon);
}
Sender: | Client |
Recipient: | Server |
Message ID: | 31 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: | MSGFLAG_VITAL |
Argument name |
Type |
Note |
Vote |
Int |
The value can be one of those
- -1 - VOTE_CHOICE_NO
- (0 - VOTE_CHOICE_PASS) server internal only
- 1 - VOTE_CHOICE_YES
|
void CVoting::Vote(int Choice)
{
CNetMsg_Cl_Vote Msg = { Choice };
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
}
else if(MsgID == NETMSGTYPE_CL_VOTE)
{
if(!m_VoteCloseTime)
return;
if(pPlayer->m_Vote == VOTE_CHOICE_PASS)
{
CNetMsg_Cl_Vote *pMsg = (CNetMsg_Cl_Vote *)pRawMsg;
if(pMsg->m_Vote == VOTE_CHOICE_PASS)
return;
pPlayer->m_Vote = pMsg->m_Vote;
pPlayer->m_VotePos = ++m_VotePos;
m_VoteUpdate = true;
}
else if(m_VoteCreator == pPlayer->GetCID())
{
CNetMsg_Cl_Vote *pMsg = (CNetMsg_Cl_Vote *)pRawMsg;
if(pMsg->m_Vote != VOTE_CHOICE_NO || m_VoteCancelTime<time_get())
return;
m_VoteCloseTime = -1;
}
}
Sender: | Client |
Recipient: | Server |
Message ID: | 32 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: | MSGFLAG_VITAL |
Sent by the client via
Callvote()
void CVoting::Callvote(const char *pType, const char *pValue, const char *pReason, bool ForceVote)
{
CNetMsg_Cl_CallVote Msg = {0};
Msg.m_Type = pType;
Msg.m_Value = pValue;
Msg.m_Reason = pReason;
Msg.m_Force = ForceVote;
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
}
Unpacked by the server in
CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
Sender: | Server |
Recipient: | Client |
Message ID: | 33 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: |
MSGFLAG_VITAL
&
MSGFLAG_NORECORD
|
void CGameContext::SendSkinChange(int ClientID, int TargetID)
{
CNetMsg_Sv_SkinChange Msg;
Msg.m_ClientID = ClientID;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
Msg.m_apSkinPartNames[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aaSkinPartNames[p];
Msg.m_aUseCustomColors[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aUseCustomColors[p];
Msg.m_aSkinPartColors[p] = m_apPlayers[ClientID]->m_TeeInfos.m_aSkinPartColors[p];
}
Server()->SendPackMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_NORECORD, TargetID);
}
else if(MsgId == NETMSGTYPE_SV_SKINCHANGE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
Client()->RecordGameMessage(false);
CNetMsg_Sv_SkinChange *pMsg = (CNetMsg_Sv_SkinChange *)pRawMsg;
if(!m_aClients[pMsg->m_ClientID].m_Active)
{
if(Config()->m_Debug)
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", "invalid skin info");
return;
}
for(int i = 0; i < NUM_SKINPARTS; i++)
{
str_utf8_copy_num(m_aClients[pMsg->m_ClientID].m_aaSkinPartNames[i], pMsg->m_apSkinPartNames[i], sizeof(m_aClients[pMsg->m_ClientID].m_aaSkinPartNames[i]), MAX_SKIN_LENGTH);
m_aClients[pMsg->m_ClientID].m_aUseCustomColors[i] = pMsg->m_aUseCustomColors[i];
m_aClients[pMsg->m_ClientID].m_aSkinPartColors[i] = pMsg->m_aSkinPartColors[i];
}
m_aClients[pMsg->m_ClientID].UpdateRenderInfo(this, pMsg->m_ClientID, true);
}
Sender: | Client |
Recipient: | Server |
Message ID: | 34 |
Response to: |
TODO
|
Expected response: |
TODO
|
Flags: |
MSGFLAG_VITAL &
MSGFLAG_FLUSH &
MSGFLAG_NORECORD
|
void CGameClient::SendSkinChange()
{
CNetMsg_Cl_SkinChange Msg;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
Msg.m_apSkinPartNames[p] = CSkins::ms_apSkinVariables[p];
Msg.m_aUseCustomColors[p] = *CSkins::ms_apUCCVariables[p];
Msg.m_aSkinPartColors[p] = *CSkins::ms_apColorVariables[p];
}
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_NORECORD|MSGFLAG_FLUSH);
m_LastSkinChangeTime = Client()->LocalTime();
}
else if(MsgID == NETMSGTYPE_CL_SKINCHANGE)
{
if(pPlayer->m_LastChangeInfoTick && pPlayer->m_LastChangeInfoTick+Server()->TickSpeed()*5 > Server()->Tick())
return;
pPlayer->m_LastChangeInfoTick = Server()->Tick();
CNetMsg_Cl_SkinChange *pMsg = (CNetMsg_Cl_SkinChange *)pRawMsg;
for(int p = 0; p < NUM_SKINPARTS; p++)
{
str_utf8_copy_num(pPlayer->m_TeeInfos.m_aaSkinPartNames[p], pMsg->m_apSkinPartNames[p], sizeof(pPlayer->m_TeeInfos.m_aaSkinPartNames[p]), MAX_SKIN_LENGTH);
pPlayer->m_TeeInfos.m_aUseCustomColors[p] = pMsg->m_aUseCustomColors[p];
pPlayer->m_TeeInfos.m_aSkinPartColors[p] = pMsg->m_aSkinPartColors[p];
}
// update all clients
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(!m_apPlayers[i] || (!Server()->ClientIngame(i) && !m_apPlayers[i]->IsDummy()) || Server()->GetClientVersion(i) < MIN_SKINCHANGE_CLIENTVERSION)
continue;
SendSkinChange(pPlayer->GetCID(), i);
}
m_pController->OnPlayerInfoChange(pPlayer);
}
Sender: | [Server] (vanilla servers do not send it) |
Recipient: | Client |
Message ID: | 35 |
Response to: |
Decided by mod implementations.
Should indicate that a tee finished the race.
|
Expected response: |
None
|
Flags: |
Unused
|
else if(MsgType == NETMSGTYPE_SV_RACEFINISH && Race)
{
CNetMsg_Sv_RaceFinish *pMsg = (CNetMsg_Sv_RaceFinish *)pRawMsg;
char aBuf[256];
char aTime[32];
char aLabel[64];
FormatTime(aTime, sizeof(aTime), pMsg->m_Time, m_pClient->RacePrecision());
m_pClient->GetPlayerLabel(aLabel, sizeof(aLabel), pMsg->m_ClientID, m_pClient->m_aClients[pMsg->m_ClientID].m_aName);
str_format(aBuf, sizeof(aBuf), "%2d: %s: finished in %s", pMsg->m_ClientID, m_pClient->m_aClients[pMsg->m_ClientID].m_aName, aTime);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "race", aBuf);
if(pMsg->m_RecordPersonal || pMsg->m_RecordServer)
{
if(pMsg->m_RecordServer)
str_format(aBuf, sizeof(aBuf), Localize("'%s' has set a new map record: %s"), aLabel, aTime);
else // m_RecordPersonal
str_format(aBuf, sizeof(aBuf), Localize("'%s' has set a new personal record: %s"), aLabel, aTime);
if(pMsg->m_Diff < 0)
{
char aImprovement[64];
char aDiff[32];
FormatTimeDiff(aDiff, sizeof(aDiff), absolute(pMsg->m_Diff), m_pClient->RacePrecision(), false);
str_format(aImprovement, sizeof(aImprovement), Localize(" (%s seconds faster)"), aDiff);
str_append(aBuf, aImprovement, sizeof(aBuf));
}
m_pClient->m_pChat->AddLine(aBuf);
}
if(m_pClient->m_Snap.m_pGameDataRace && m_pClient->m_Snap.m_pGameDataRace->m_RaceFlags&RACEFLAG_FINISHMSG_AS_CHAT)
{
if(!pMsg->m_RecordPersonal && !pMsg->m_RecordServer) // don't print the time twice
{
str_format(aBuf, sizeof(aBuf), Localize("'%s' finished in: %s"), aLabel, aTime);
m_pClient->m_pChat->AddLine(aBuf);
}
}
else
{
CInfoMsg Finish;
Finish.m_Player1ID = pMsg->m_ClientID;
Finish.m_Player1RenderInfo = m_pClient->m_aClients[Finish.m_Player1ID].m_RenderInfo;
Finish.m_TimeCursor.m_FontSize = 36.0f;
if(pMsg->m_RecordServer)
TextRender()->TextColor(1.0f, 0.5f, 0.0f, 1.0f);
else if(pMsg->m_RecordPersonal)
TextRender()->TextColor(0.2f, 0.6f, 1.0f, 1.0f);
TextRender()->TextDeferred(&Finish.m_TimeCursor, aTime, -1);
if(Config()->m_ClShowsocial)
{
Finish.m_Player1NameCursor.m_FontSize = 36.0f;
TextRender()->TextDeferred(&Finish.m_Player1NameCursor, m_pClient->m_aClients[pMsg->m_ClientID].m_aName, -1);
}
FormatTimeDiff(aTime, sizeof(aTime), pMsg->m_Diff, m_pClient->RacePrecision());
str_format(aBuf, sizeof(aBuf), "(%s)", aTime);
Finish.m_DiffCursor.m_FontSize = 36.0f;
if(pMsg->m_Diff < 0)
TextRender()->TextColor(0.5f, 1.0f, 0.5f, 1.0f);
else
TextRender()->TextColor(1.0f, 0.5f, 0.5f, 1.0f);
TextRender()->TextDeferred(&Finish.m_DiffCursor, aBuf, -1);
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
Finish.m_Time = pMsg->m_Time;
Finish.m_Diff = pMsg->m_Diff;
Finish.m_RecordPersonal = pMsg->m_RecordPersonal;
Finish.m_RecordServer = pMsg->m_RecordServer;
AddInfoMsg(INFOMSG_FINISH, Finish);
}
}
Sender: | [Server] (vanilla servers do not send it) |
Recipient: | Client |
Message ID: | 36 |
Response to: |
Decided by mod implementations.
Should indicate that a tee reached a race checkpoint.
|
Expected response: |
None
|
Flags: |
Unused
|
Argument name |
Type |
Note |
Diff
|
Int |
TODO
|
void CHud::OnMessage(int MsgType, void *pRawMsg)
{
if(MsgType == NETMSGTYPE_SV_CHECKPOINT)
{
CNetMsg_Sv_Checkpoint *pMsg = (CNetMsg_Sv_Checkpoint *)pRawMsg;
m_CheckpointDiff = pMsg->m_Diff;
m_CheckpointTime = time_get();
}
// [..]
}
Sender: | Server |
Recipient: | Client |
Message ID: | 37 |
Response to: |
TODO
|
Expected response: | None |
Flags: | MSGFLAG_VITAL |
Argument name |
Type |
Note |
Name |
String |
Name of the command. If a user types /commandname in the chat.
The command will be run.
|
Args format |
String |
The argument format specifies how many and what type of arguments this chat command takes.
Valid types are:
- s - string word split
- r - string consume till the end
- i - integer
And then a leading ? can indicate if it is a optional argument.
And [details] following the type can give more details on what the value should be.
An example for a say command could be:
r[message]
|
Help text |
String |
Command description displayed to the user when autocompleting chat commands.
|
This can be seen as the chat equivalent to the rcon counter part
NETMSG_RCON_CMD_ADD
void CChat::OnMessage(int MsgType, void *pRawMsg)
{
// [..]
else if(MsgType == NETMSGTYPE_SV_COMMANDINFO)
{
CNetMsg_Sv_CommandInfo *pMsg = (CNetMsg_Sv_CommandInfo *)pRawMsg;
if(!m_CommandManager.AddCommand(pMsg->m_Name, pMsg->m_HelpText, pMsg->m_ArgsFormat, ServerCommandCallback, this))
dbg_msg("chat_commands", "adding server chat command: name='%s' args='%s' help='%s'", pMsg->m_Name, pMsg->m_ArgsFormat, pMsg->m_HelpText);
else
dbg_msg("chat_commands", "failed to add command '%s'", pMsg->m_Name);
}
// [..]
}
Sender: | Server |
Recipient: | Client |
Message ID: | 38 |
Response to: |
TODO
|
Expected response: | None |
Flags: | MSGFLAG_VITAL |
Argument name |
Type |
Note |
Name |
String |
Name of the command to be deleted from the clients autocompletion list.
|
void CChat::OnMessage(int MsgType, void *pRawMsg)
{
// [..]
else if(MsgType == NETMSGTYPE_SV_COMMANDINFOREMOVE)
{
CNetMsg_Sv_CommandInfoRemove *pMsg = (CNetMsg_Sv_CommandInfoRemove *)pRawMsg;
if(!m_CommandManager.RemoveCommand(pMsg->m_Name))
{
dbg_msg("chat_commands", "removed chat command: name='%s'", pMsg->m_Name);
}
}
}
Sender: | Client |
Recipient: | Server |
Message ID: | 39 |
Response to: |
Client typing a command into chat.
|
Expected response: | Depends on command |
Flags: | MSGFLAG_VITAL |
The client fills in command and arguments and sends it via the
ServerCommandCallback()
method
void CChat::ServerCommandCallback(IConsole::IResult *pResult, void *pContext)
{
CCommandManager::SCommandContext *pComContext = (CCommandManager::SCommandContext *)pContext;
CChat *pChatData = (CChat *)pComContext->m_pContext;
CNetMsg_Cl_Command Msg;
Msg.m_Name = pComContext->m_pCommand;
Msg.m_Arguments = pComContext->m_pArgs;
pChatData->Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
pChatData->m_pHistoryEntry = 0x0;
pChatData->Disable();
pChatData->m_pClient->OnRelease();
}
The server then calls the right callback method matching the command name
else if (MsgID == NETMSGTYPE_CL_COMMAND)
{
CNetMsg_Cl_Command *pMsg = (CNetMsg_Cl_Command*)pRawMsg;
CommandManager()->OnCommand(pMsg->m_Name, pMsg->m_Arguments, ClientID);
}