Bonjour à tous,
Je ne vais pas souvent sur ce forum mais je devrais :p
C'est surement le seul forum publique où il y a des vrais programmeurs, qui ont de l'ambition et qui ont envie d'apprendre.
C'est pourquoi j'ai décidé de poster ici mon tutoriel sur le protocole de dofus 2.0 qui explique en détails comment l'exploiter, tutoriel qui n'était jusqu'a maintenant que sur mon forum privé.
J'espère que ça vous aidera et donnera envie d'exploiter dofus 2.0 ;)
Bonjour à tous,
Je vais vous présenter le protocole de dofus en détail pour pouvoir correctement l'exploiter. Un sujet du même genre a déjà été fait par Lorrio mais c'est plus un complément et de plus il est pour pimper :p
Pour ceux qui ont déjà connu le protocole 1.29 je vais vous expliquer les nuances entre les 2.
Ce qu'il faut savoir tout d'abords est que le protocole de Dofus 1.29 a été fait comme de la me***. Complètement fait à l'arrache. Donc forcément le protocole de Dofus 2.0 est bien plus rapide. (Je ferais une comparaison après)
Le 1er protocole est basé sur des chaînes de caractères. C'est à dire que pour envoyer un paquet avec comme info "Bonjour" et le nombre 123 il va écrire quelque chose de ce genre "Bonjour|123" (j'ai omis le header). Décortiquons un peu :
Il faut savoir qu’une chaîne de caractères c'est un tableau d’octets (bytes). Chaque octet correspond à un caractère. Exemple le caractère A correspond à 65 (B à 66, etc.).
Donc pour écrire la chaîne "123" on aura dans le buffer (tampon) 3 octets : 49 50 51 (ou en hexa 0x31 0x32 0x33). Notez que je sépare chaque octet par un espace.
Donc pour écrire "Bonjour|123" on aura dans notre paquet 11 octets : 66 78 78 74 79 85 82 124 49 50 51
Comparons avec le protocole de Dofus 2
Celui-ci n'est plus basé sur des chaînes de caractères et est nettement plus efficace. Il n'est pas crypté non plus attention ! (chose que je vois souvent).
Ce qu'il faut comprendre c'est qu’au lieu de transcrire chaque caractère en octet (1 caractère par octet) il les encode selon la technique de stockage native.
Qu'est ce que ça veut dire ?
Et bien un exemple permettra de comprendre. Un nombre courant est un int (integer), il est encodé sur 4 octets. Je ne vais pas vous expliquer en détail comment c'est encodé, mais voici quelques exemples
Un integer de valeur 1 sera encodé de cette manière : 0 0 0 1 (4 octets dans tous les cas), 256 de cette manière : 0 0 1 0, et 255 de celle-ci 0 0 0 255. Le nombre maximum est donc 4 294 967 295 (en hex 0xFFFFFFFF) soit 255 255 255 255.
Et donc dofus va envoyer l'integer directement sous forme d'octets. De cette manière au lieu d'envoyer 49 50 51 pour envoyer 123 il enverra 0 0 0 123.
Ici ce n'est pas avantageux niveau taille naturellement, mais pour envoyer par exemple 4 294 967 295 au lieu d'envoyer 10 octets (10 caractères) il en enverra que 4 (255 255 255 255). Ce qui est déjà nettement avantageux.
Bien sûr l'avantage majeur est dans la conversion, convertir "123" (chaîne de caractères) en integer est beaucoup plus long que de dire que les 4 octets correspondent à un integer (juste à copier dans la mémoire).
L'autre avantage est qu'il n'y a plus besoin de séparateur tel que "|" vu qu’il sait qu'un integer est sur 4 octets, il lira juste 4 octets. (Je reviendrai sur ce point après)
Maintenant que vous avez compris le système, vous aurez besoin de comprendre comment est formée une chaîne de caractères avec le nouveau protocole . Car s’il n'y a plus de délimiteur et qu’une chaîne de caractères peut naturellement être sur un nombre inconnue d'octets. Il faut donc le mentionner avant.
C'est pourquoi avant toute chaîne de caractères la taille de celle-ci est encodée en temps que short (sur 2 octets)
"Abc" sera encodé de la manière suivante : 00 03 (taille) 65 66 67 (chaîne)
Notre paquet de départ "Bonjour|123" sera donc encodé de cette manière au final : 00 07 66 78 78 74 79 85 82 00 00 00 123
Vous remarquerez qu'il n'y a pas de délimiteur , le programme sait que juste après la chaîne il y a un integer, inutile de séparer. Il n'y a plus de split !
En code ça donne quoi ?
Et bien on utilise (sous .net) les classes BinaryWriter et BinaryReader (en faite non, mais j'expliquerai après) pour écrire le paquet. C'est ces classes qui se chargent d'écrire/lire les octets (comme quoi c'est plus facile hein ;))
Par exemple
var buffer = new byte[128]; // attention je mets 128 en taille, mais il ne faut SURTOUT pas faire comme ça, il faut un buffer dynamique pour éviter de prendre de la mémoire inutilement
var writer = new BinaryWriter(new MemoryStream(buffer)); // il va écrire dans le flux de mémoire qui va écrire dans le buffer
writer.Write("Bonjour");
writer.Write(123);
/*...*/
var reader = new BinaryReader(new MemoryStream(buffer)); // cette fois-ci le flux lit le buffer seulement
string bonjour = reader.ReadString();
int nombre = reader.ReadInt32();
// bonjour contient "bonjour" et nombre contient 123
Voilà vous avez tout compris sur l'encodage.
Désormais je vais vous faire remarquer une petite nuance qui est toujours source de bugs au début. Il y a 2 manières de stocker un nombre en mémoire. En vérité il y a 2 sens. Sois remplir de gauche à droite soit de droite à gauche. Exemple 0 0 0 1 ou 1 0 0 0, c'est 2 fois le nombre 1, mais si on lit dans le mauvais sens on aura pas 1 mais 268 435 456
Ce sens on l'appelle l'endianness (de l'anglais). Il peut être petit (Little) ou grand (Big). Le grand endian place le plus grand nombre en tête c'est-à-dire à droite, l'inverse pour le petit
Big : 00 00 00 01
Little : 01 00 00 00
Problème Dofus 2.0 utilise un grand endian alors qu’en .net tous les lecteurs binaires sont en petit endian (BinaryReader/Writer en autres). De base il n'y en a pas qui supporte les 2 modes.
Résultat nous devons impérativement exporter une librairie tierce pour pouvoir lire/écrire en Big Endian. Je n'ai pas de conseil spécial à ce sujet, à vous de trouver la bonne ;)
Attaquons maintenant la structure concrète d'un paquet dofus 2.0 :
[HEADER:[HI-HEADER:2 bytes (PacketId 6 bits + LengthType 2 bits)][LENGTH:LengthType (1/2/3) bytes]][MESSAGE:LENGTH bytes]
Le header donne des infos sur le paquet :
- l'id du paquet (qui permet de déterminer de quel paquet il s'agit)
- sur combien d'octets la taille est encodée (le type de taille)
- sa taille (la taille du contenu, header exclu)
L'id du paquet et l'information sur le nombre d'octets sur lesquels la taille est encodée, sont encodés sur 2 octets confondus. Je les ai nommés Hi-Header (pour header de tête)
C'est-à-dire que l'id est en vérité encodée sur 8+6 bits (1 octet + 3 quarts) et le type de taille sur 2 bits (il peut donc contenir le nombre 0, 1, 2 ou 3)
Cela permet sans doute de raccourcir un peu la taille du header.
Il faut donc lire un short (2 octets) et séparer les 2 premiers bits du nombre. Pour cela nous faisons un décalage de 2 bits vers la droite (noté >>) (ce qui supprime les bits) pour obtenir l'id du paquet, et appliqué un "mask" (masque --") en utilisant l'opérateur AND sur les 2 premiers bits pour avoir 2 bits seulement.
En détail voici la démarche
J'ai un paquet d'id 2 (en binaire 10) et avec un type de taille 1 (01) également
Je vais avoir donc (en bits) 0000 0000 0000 1001
Ce qui correspond au chiffre 5 (mais on s'en fiche)
En appliquant l'opérateur >> 2 on obtiendra 0000 0000 0000 0010 -> soit 2 (notre id)
En appliquant l'opérateur & (AND) avec le chiffre 3 ( soit & 3) on obtiendra 0000 0000 0000 0001
En faite 3 correspond à 0111, l'opérateur AND garde que les bits lorsqu’ils valent tous les deux 1 et non 0.
Exemple 2 & 3 <=> 0010 & 0011 => 0010 (soit 2)
Ou plus utile 7 & 3 <=> 1110 & 0011 => 0010 (soit 2)
Donc AND permet de tronquer en quelque sorte le nombre et ignorer des bits.
En code nous aurons ça
short hiheader = reader.ReadInt16();
short packetId = hiheader >> 2;
short lenType = hiheader & 3;
Maintenant la taille. La taille est encodée sur 1, 2 ou 3 octets (s) suivant la taille du paquet (un paquet de plus de 255 octets aura une taille sur 2 octets minimum). C'est "lenType" qui informe, combien d'octets on doit lire.
Si lenType vaut 1 on lit un seul octet (ReadByte())
Si lenType vaut 2 on lit 2 octets (ReadInt16())
Si lenType vaut 3 on lit 3 octets, et c'est là que ça se complique. La technique est de en faites faire un integer (donc 4 octets) et de remplir que 3 octets de ce integer. Je ne vais pas m'enfoncer là dedans voici le code
switch (lenType)
{
case 0:
{
length = 0;
}
case 1:
{
length = m_buffer.ReadByte();
}
case 2:
{
length = m_buffer.ReadUShort();
}
case 3:
{
length = (uint)(((m_buffer.ReadByte() & 255) << 16) + ((m_buffer.ReadByte() & 255) << 8) + (m_buffer.ReadByte() & 255));
}
default:
{
length = 0;
}
}
Note : Je précise que la taille n'inclus pas le header, juste le contenu du paquet, donc si un paquet contient juste un integer à lire la taille vaudra 4.
Voilà, désormais vous savez, lire correctement un paquet dans Dofus 2.0
Je vais faire une petite partie sur comment connaître la structure d'un paquet (s’il faut lire un integer puis un short ou une chaîne puis un short puis un integer etc.)
Pour cela il faut regarder dans les sources de dofus dans le dossier \com\ankamagames\dofus\network\messages
Chaque fichier correspond à un paquet, exemple avec IdentificationMessage qui est le premier paquet que le client envoie.
Tout d'abord on voit
public static const protocolId:uint = 4;
C'est donc le paquet avec l'id 4.
Puis ensuite on regarde la fonction serializeAs_IdentificationMessage
this.version.serializeAs_Version(param1);
param1.writeUTF(this.login);
param1.writeUTF(this.password);
param1.writeBoolean(this.autoconnect);
La fonction indique comment écrire le paquet (deserialize pour le lire)
this.version.serializeAs_Version(param1), il appelle tout d'abord la fonction pour sérialiser un objet de type Version, on va donc dans \com\ankamagames\dofus\network\types\version et on regarde cette classe
if (this.major < 0)
{
throw new Error("Forbidden value (" + this.major + ") on element major.");
}
param1.writeByte(this.major);
if (this.minor < 0)
{
throw new Error("Forbidden value (" + this.minor + ") on element minor.");
}
param1.writeByte(this.minor);
if (this.release < 0)
{
throw new Error("Forbidden value (" + this.release + ") on element release.");
}
param1.writeByte(this.release);
if (this.revision < 0 || this.revision > 65535)
{
throw new Error("Forbidden value (" + this.revision + ") on element revision.");
}
param1.writeShort(this.revision);
if (this.patch < 0)
{
throw new Error("Forbidden value (" + this.patch + ") on element patch.");
}
param1.writeByte(this.patch);
param1.writeByte(this.buildType);
Version est donc sérialisée de la façon suivante : major (sur 1 octect), minor (byte), release (byte), revision (short), patch (byte) et buildType (byte)
On retourne dans IdentificationMessage, après la version, est sérialiser la chaîne qui contient le login, puis le password et enfin sur un bool (1 seul octet) si l'autoconnect est activé.
Au final pour écrire ce message on fera :
writer.WriteByte(version.major);
writer.WriteByte(version.minor);
writer.WriteByte(version.release);
writer.WriteShort(version.revision);
writer.WriteByte(version.patch);
writer.WriteByte(version.buildType);
writer.WriteString(login);
writer.WriteString(password);
wirter.WriteBoolean(autoconnect);
Puis à envoyer et c'est bon =)
Voilà je pense avoir fait le tour sur le protocole de Dofus 2.0, maintenant à vous de vous débrouiller :)
Bonne chance :)