Dissection des stats ServerHL

Écrit le 03/07/2003 par *OgGiZ*
Dernière mise à jour : 30/01/2006

Dissection des stats ServerHL

Pour commencer rajoutez wsock32.lib dans votre liste de .lib de votre projet. Voici les deux seuls headers dont nous avons besoin.

#include <stdio.h>
#include <winsock.h>

Les packets de retour d'Half-Life possèdent un indicatif qui nous permet de savoir quel message est retourné !

#define S2A_INFO_DETAILED      (int)'m'
#define S2A_PLAYER             (int)'D'
#define S2A_RULES              (int)'E'

Viennent ensuite nos propres defines dont nous aurons besoin par la suite :

#define BUFFER_SIZE            2048
#define MAX_PLAYERS            32
#define MAX_RULES              200

Nous allons stocker toutes les informations du serveur dans des structures pour plus de facilité ! Voici donc ici le code des différentes structures. Je sais qu'on aurait pu économiser de la taille mémoire en faisant des alloc, new etc, mais quand j'ai écrit le code je voulais avant tout avoir quelque chose qui marche !

struct rule {
    char name[201];          // rule name
    char value[201];         // rule value
};

struct rules {
    short num;               // number of rules
    rule rl[MAX_RULES];      // list of all rules
};

struct player {
    byte index;              // index du joueur
    char name[201];          // nom du joueur
    long frags;              // frags
    float time;              // temps de partie
};

struct players {
    byte num;                // nombre de joueurs
    player pl[MAX_PLAYERS];  // liste des joueurs
};

struct details {
    char address[201];       // addresse du serveur
    char hostname[201];      // nom de l'host
    char mapname[201];       // la map
    char gamedir[201];       // le gamedir (cf: /cstrike/)
    char gamedesc[201];      // "Description" spécifiée dans le liblist.gam
    byte plnum;              // nombre de joueurs
    byte maxplayers;         // nombre max de joueurs
    byte protocol;           // protocole (le 7 normalement)
    byte typeofserver;       // dedicated ou pas
    byte os;                 // linux ou windows
    byte password;           // 1 si ya un pass, sinon 0

    char info[201];          // info sur le mod
    char ftp[201];           // ftp de d/l
    long modversion;         // version du mod
    long modsize;            // taille du mod
    byte serverside;         // uniquement serverside? 1 sinon 0
    byte hasclient;          // y'a t-il un client.dll ?
};

Nous allons avoir besoin du fichier parsemsg.cpp de valve !

void BEGIN_READ( void *buf, int size );
int READ_CHAR( void );
int READ_BYTE( void );
int READ_SHORT( void );
int READ_WORD( void );
int READ_LONG( void );
float READ_FLOAT( void );
char* READ_STRING( void );
float READ_COORD( void );
float READ_ANGLE( void );
float READ_HIRESANGLE( void );

Voici ici les variables cruciales au bon fonctionnement du programme. Nous stockerons les stats du serveur dans les data_....

details data_details;
rules  data_rules;
players data_players;

WSADATA      WSAData;
SOCKET        socket_desc;
SOCKADDR_IN  local_sin;
SOCKADDR_IN  host_sin;

Bon là on commence la programmation Socket, commençons par initialiser le device et notre petit socket UDP

bool NET_Init ()
{
    if (WSAStartup(0x0101, &WSAData))
        return false;

    if ( (socket_desc = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET)
        return false;

    return true;
}

Ensuite il faut déterminer l'adresse ip de l'hôte si on a fourni un nom (cf: cstrike1.leserveur.com). Et ensuite de connecter notre socket dessus :

bool NET_Connect (char *address, int port)
{
    struct hostent  *hp;
    unsigned int  addr;

    memset(&local_sin, 0, sizeof(local_sin));

    local_sin.sin_family = AF_INET;
    local_sin.sin_addr.s_addr = INADDR_ANY;
    local_sin.sin_port = htons(port);

    if (isalpha (address[0]))
        hp = gethostbyname (address);
    else
    {
        addr = inet_addr(address);
        hp = gethostbyaddr((char *)&addr, 4, AF_INET);
    }

    if (!hp)
        return false;

    memset(&host_sin, 0, sizeof(host_sin));

    host_sin.sin_family = AF_INET;
    memcpy(&(host_sin.sin_addr), hp->h_addr, hp->h_length);
    host_sin.sin_port = htons(port);

    if (bind(socket_desc, (struct sockaddr *)&local_sin, sizeof(local_sin)) == SOCKET_ERROR)
        return false;

    return true;
}

Deux fonctions à présent pour recevoir et envoyer des données sur notre socket !

bool NET_Send (char *pChar)
{
    sendto(socket_desc, pChar, strlen(pChar), 0, (struct sockaddr *)&host_sin, sizeof(host_sin));
    return true;
}

int NET_Receive (char *pChar)
{
    return recvfrom(socket_desc, pChar, BUFFER_SIZE, 0, NULL, NULL);
}

Petit détail important à présent est que pour récupérer les informations d'un serveur il faut d'abord mettre 4 caractères (char)-1 suivis de la commande. Voici donc une fonction NET_Query spécialement faites pour HL :]

bool NET_Query (char *pChar)
{
    char cmd[201];
    sprintf (cmd, "%c%c%c%c%s", (char)-1, (char)-1, (char)-1, (char)-1, pChar);
    return NET_Send (cmd);
}

À présent il ne nous reste plus qu'a fermer notre socket.

void NET_Close ()
{
    closesocket(socket_desc);
    WSACleanup ();
}

Voici maintenant le code qui télécharge les stats du serveur. Ici nous allons utiliser les fonctions du parsemsg.cpp de valve. Pour avoir l'ordre de « caption » je me suis référé au fichier protocol.txt fourni avec le SDK ! Ce code sert uniquement à faire le tri dans ce que renvoie le serveur et de le stocker dans nos structures :

bool GetMessageDetails (void *pBuf, int size)
{
    strcpy (data_details.address, READ_STRING ());
    strcpy (data_details.hostname, READ_STRING ());
    strcpy (data_details.mapname, READ_STRING ());
    strcpy (data_details.gamedir, READ_STRING ());
    strcpy (data_details.gamedesc, READ_STRING ());
    data_details.plnum = READ_BYTE ();
    data_details.maxplayers = READ_BYTE ();
    data_details.protocol = READ_BYTE ();
    data_details.typeofserver = READ_BYTE ();
    data_details.os = READ_BYTE ();
    data_details.password = READ_BYTE ();

    strcpy (data_details.info, READ_STRING ());
    strcpy (data_details.ftp, READ_STRING ());
    data_details.modversion = READ_LONG ();
    data_details.modsize = READ_LONG ();
    data_details.serverside = READ_BYTE ();
    data_details.hasclient = READ_BYTE ();

    return true;
}

bool GetMessagePlayers (void *pBuf, int size)
{
    data_players.num = READ_SHORT ();

    for (int i=0;i<data_players.num;i++)
    {
        data_players.pl[i].index = READ_BYTE ();
        strcpy (data_players.pl[i].name, READ_STRING ());
        data_players.pl[i].frags = READ_LONG ();
        data_players.pl[i].time = READ_FLOAT ();
    }

    return true;
}

bool GetMessageRules (void *pBuf, int size)
{
    data_rules.num = READ_SHORT ();

    for (int i=0;i<data_rules.num;i++)
    {
        strcpy (data_rules.rl[i].name, READ_STRING ());
        strcpy (data_rules.rl[i].value, READ_STRING ());
    }

    return true;
}

Mais comment diable faisons-nous pour avoir ces informations me direz-vous ? En effet vous savez connecter votre socket et trier les informations ! Voici donc la manip : Connexion, NET_Querry d'un des 3 messages « details », « players » et « rules » puis déconnexion.

bool GetPackage (char *address, int port, char *msgs)
{
    if (!NET_Init ())
        return false;

    if (!NET_Connect (address, port))
        return false;

    char msg[BUFFER_SIZE];
    strcpy (msg, msgs);

    NET_Query (msg);
    NET_Receive (msg);

    if (!GetMessage (msg, BUFFER_SIZE))
        return false;

    NET_Close ();

    return true;
}

Vous avez probablement remarqué l'utilisation d'une fonction GetMessage (char*, int) que nous n'avons pas encore écrite ;) Vous remarquez également que nous avons des #define S2A machin brol qui semblent être très intéressants! En fait valve envoie d'abord un byte -1 suivis d'un byte « type » et enfin tout le contenu. Le byte « type » permet de savoir si il s'agit d'un players, details ou rules !

bool GetMessage (void *pBuf, int size)
{
    BEGIN_READ (pBuf, size);
    long check = READ_LONG ();
       
    if (check != -1)
        return false;

    byte b = READ_BYTE ();

    if (b == S2A_INFO_DETAILED)
        return GetMessageDetails (pBuf, size);

    if (b == S2A_PLAYER)
        return GetMessagePlayers (pBuf, size);

    if (b == S2A_RULES)
        return GetMessageRules (pBuf, size);

    return false;
}

Pour récupérer les stats d'un serveur il faut donc faire :

GetPackage (address, port, "details");
GetPackage (address, port, "rules");
GetPackage (address, port, "players");

et après vous pouvez exploiter data_details, data_players et data_rules ! Cela vous permet (par ex) d'écrire un programme qui rejoint automatiquement un serveur lorsqu'il y a de la place ! Ou encore d'occuper de la place sur le serveur avec un challen... hum ;)