1. Pré-réquis
Pour suivre ce tuto vous devrez connaître un minimum le protocole TCP et son utilisation en C++. Dans ce tuto je donnerai des codes exemples en C++ "standard". Vous aurez aussi besoin de connaitres les opérateurs binaires (http://www.bien-programmer.fr/bits.htm) pour le début de ce tuto.
2. Introduction au protocole D.
Rappel important :
1 octet : 8bits; short : 2 octets; int : 4 octets; 1 byte = 1 octet = 1 char = un élément d'un QByteArray
unsigned : ne peut être négatif (mais fait la même taille que son équivalent "normal")
Un petit exemple :
char : 1 octet : -128 à 127. Le type char est l'équivalent du type byte en java.
unsigned char : 1 octet : 0 à 255
Il faut savoir qu'on ne peut pas passer de tableaux composés d'unsigned char aux fonctions send ou recv
Ne vous affolez pas si vous avez des erreurs d'id de paquets négatifs plus tard au cours de ce tuto, c'est sûrement qu'il vous faut faire un cast en unsigned char.
Pourquoi vous ai-je dit tout cela ?
Et bien tout simplement car c'est la base des paquets de Dofus, vous vous en servirez pendant tout le développement du bot. Je m'explique : voilà un exemple de paquet que vous pourriez recevoir :
03 E9 05 08 0f 02 62 63
C'est un paquet que j'ai inventé de toute pièce pour l'exemple. Pour afficher vos paquets de cette façon vous devez traiter chaque octets (char) de ce que vous avez reçu comme un nombre et pas comme un caractère:
for(int i = 0; i < sizeof(buffer); i++)
{
printf("%02x",(unsigned char)buffer);//on affiche chaque valeur en hexa, le cast est nécessaire pour avoir des valeurs positives (voir début tuto)
}
Donc pour lire un paquet vous aurez d'abord besoin de lire son header (les 2 premiers octets). Ce header contient l'id (codé sur 14bits) et la "taille de la taille du paquet". Ensuite à partir de cette taille de taille vous devrez lire la taillle du paquet pour pouvoir séparer vos paquets. L'id vous servira à aller chercher dans les sources de Dofus la façon de désérialiser (en gros l'ordre de lecture).
Pour notre paquet d'exemple imaginions que vous trouviez dans les sources ceci :
override public deserializeAs_machin(param1:IDataInput)
{
this.machin = param1.readShort();
this.string = param1.readUTF();
}
Vous sauriez qu'il faut donc lire un short puis une chaîne encodée en utf-8. Mais comment faire ?
Il va falloir écrire une classe implémentant les méthodes readShort, readUnsignedShort, readInt, readUTF, etc... Nous allons faire cela ensemble.
Notre classe de désérialisation
Pour commencer ouvrez votre IDE puis créez une nouvelle classe. Nous allons l'appeler DataInput.
Créez un constructeur prenant pour paramètre un tableau de char.
//. h
class DataInput
{
private:
char[] message;
int pos; //nous servira plus tard pour savoir ou nous en sommes dans la lecture
public:
DataInput(char[]);
}
//.cpp
DataInput::DataInput(char[] msg)
{
this->message = msg;
pos = 0;
}
readShort
Il y a 2 façons de lire les short : sachant qu'ils sont codés sur 2 octets, soit vous lisez le premier octet vous le multipliez par 256 puis vous additionnez le tout au deuxième octet soit vous faite un décalage du 1er octet de 8 bits vers la gauche puis vous faites un ou binaire avec le 2ème octet (cela revient au même car faire un décalage d'un bit vers la gauche correspond à multiplier par 2) :
short int DataInput::readShort()
{
short int s = message[pos]*256+message[pos+1];
pos += 2;//on lit 2 octets
return s;
}
//ou
short int DataInput::readShort()
{
short int s = (message[pos]<<8)|message[pos+1];
pos += 2;//on lit 2 octets
return s;
}
readUnsignedShort
Pour lire un unsigned short int il y a aussi plusieurs façons. Soit vous lisez un short mais vous castez chaque octet en unsigned char soit lisez un short et vous lui appliquez & 0xff
unsigned short int DataInput::readUnsignedShort()
{
short int s = (unsigned char)message[pos]*256+(unsigned char)message[pos+1];
pos += 2;//on lit 2 octets
return s;
}
//ou
unsigned short int DataInput::readUnsignedShort()
{
unsigned short int s = (message[pos]<<8)|message[pos+1];
pos += 2;//on lit 2 octets
return s & 0xff;//pour avoir la valeur en unsigned
}
readUTF
Pour lire une chaine de caractères il faut d'abord lire un unsigned short int qui correspond à la taille n de la chaine puis lire n octets.
char[] DataInput::readString()
{
unsigned short int taille = readUnsignedShort();
char chaine[taille+1];//taille+1 car on va lire une chaine donc il faut pouvoir mettre le '\0' de fin
for(int i = 0; i < taille; i++)
{
taille = mesage[pos+i];
}
pos += taille;
return chaine;
}
Je pense que vous avez compris le principe, donc je vous laisse implémenter vous même les méthodes readByte, readUnsignedByte, readInt et readUnsignedInt.
Rappel
un int : 4 octets; un byte = un char
Je vous conseille aussi d'écrire une classe pour chaque paquet dont vous aurez besoin d'extraire des informations. Ces classes doivent hériter d'une classe abstraite que vous pourrez appeller NetworkMessage par exemple, qui demandera d'implémenter les méthodes serialize et deserialize. Vous devrez traduire les méthodes serialize et deserialize directement des sources de dofus en décompilant DofusInvoker.swf. Vous devrez aussi traduire tout les attributs de ces classes sauf isInitialized dont vous n'aurez pas besoin. Les classes des paquets sont celles dont le nom se finissent avec "Message".
Un exemple :
//ActionScript
public class GameMapMovementMessage extends NetworkMessage implements INetworkMessage
{
private var _isInitialized:Boolean = false;
public var keyMovements:Vector.<uint>;
public var actorId:int = 0;
public static const protocolId:uint = 951;
public function GameMapMovementMessage()
{
this.keyMovements = new Vector.<uint>;
return;
}// end function
override public function get isInitialized() : Boolean
{
return this._isInitialized;
}// end function
override public function getMessageId() : uint
{
return 951;
}// end function
public function initGameMapMovementMessage(param1:Vector.<uint> = null, param2:int = 0) : GameMapMovementMessage
{
this.keyMovements = param1;
this.actorId = param2;
this._isInitialized = true;
return this;
}// end function
override public function reset() : void
{
this.keyMovements = new Vector.<uint>;
this.actorId = 0;
this._isInitialized = false;
return;
}// end function
override public function pack(param1:IDataOutput) : void
{
var _loc_2:* = new ByteArray();
this.serialize(_loc_2);
writePacket(param1, this.getMessageId(), _loc_2);
return;
}// end function
override public function unpack(param1:IDataInput, param2:uint) : void
{
this.deserialize(param1);
return;
}// end function
public function serialize(param1:IDataOutput) : void
{
this.serializeAs_GameMapMovementMessage(param1);
return;
}// end function
public function serializeAs_GameMapMovementMessage(param1:IDataOutput) : void
{
param1.writeShort(this.keyMovements.length);
var _loc_2:uint = 0;
while (_loc_2 < this.keyMovements.length)
{
if (this.keyMovements[_loc_2] < 0)
{
throw new Error("Forbidden value (" + this.keyMovements[_loc_2] + ") on element 1 (starting at 1) of keyMovements.");
}
param1.writeShort(this.keyMovements[_loc_2]);
_loc_2 = _loc_2 + 1;
}
param1.writeInt(this.actorId);
return;
}// end function
public function deserialize(param1:IDataInput) : void
{
this.deserializeAs_GameMapMovementMessage(param1);
return;
}// end function
public function deserializeAs_GameMapMovementMessage(param1:IDataInput) : void
{
var _loc_4:uint = 0;
var _loc_2:* = param1.readUnsignedShort();
var _loc_3:uint = 0;
while (_loc_3 < _loc_2)
{
_loc_4 = param1.readShort();
if (_loc_4 < 0)
{
throw new Error("Forbidden value (" + _loc_4 + ") on elements of keyMovements.");
}
this.keyMovements.push(_loc_4);
_loc_3 = _loc_3 + 1;
}
this.actorId = param1.readInt();
return;
}// end function
}
//c++
class GameMapMovementMessage
{
public :
Vector<int> keyMovements;
int actorId:int = 0;
static const unsigned int protocolId = 951;
void deserialize(char[] buffer);
void serialize(char[] buffer);
}
//cpp
void GameMapMovementMessage::deserialize(char[] buffer)
{
DataInput reader(buffer);
int s = reader.readUnsignedShort();
for(int i = 0; i < s; i++)
{
int loc = reader.readShort();
keyMovements.push_back(loc);
}
}
Donc revenons à notre paquet d'exemple:
03 E9 05 08 0f 02 62 63
Pour extraire des informations de ce paquet vous aurez besoin de connaitre son id.
L'id est codé sur les 14 premiers bits du paquets. Pour le connaitre vous devez prendre les 2 premiers octets 03 E9 en héxa (1001 en décimal et 1111101001 en binaire) et de faire un décalage de 2 bits vers la droite pour ne garder que les 14 premiers bits (soit 11111010 en binaire et 250 en décimal):
short int header = (unsigned char)buffer[0] << 8 | (unsigned char)buffer[1]; //on lit les 2 premiers octets
int id = header >> 2;// cet id vous permet de déterminer de quel paquet il s'agit
Pour récupérer la taille de taille il faut récuperer les 2 bits à droite:
int tdetaille = header & 3;// 3 en décimal = 11 en binaire
Avec ces informations on peut récuperer la taille :
int taille;
recv(socket,buffer,tdetaille,0);
if(tdetaille == 0)
taille = 0;
else if(tdetaille == 1)
taille = buffer[0] & 0xff;
else if(tdetaille == 2)
taille = (buffer[0]<<8||buffer[1]) & 0xff;
else //tdetaille = 3
taille = ((buffer[0]<<16|buffer[1]<<8)|buffer[2]))& 0xff;
Ensuite vous pouvez récupérer le message :
recv(socket,buffer,taille,0);
Puis traiter le paquet :
switch(id)
{
case 250:
Machin msg = new Machin;
msg.deserialize(buffer);
break;
}