Hello à tous, suite à la demande de quelqu'un ici qui désirait un éditeur D2O fonctionnel, je m'y suis attelé à contre cœur (c'est pas vrai c'était super marrant).

La structure

Pour chaque fichier D2O vous aurez toujours (normalement si le fichier n'est pas protégé) 3 bytes à convertir en UTF8 qui vous donneront "D2O" (quelle surprise).

Vous aurez ensuite un int contenant l'index de démarrage de l'index table. Elle sert à aller chercher vos données, elle se décompose toujours de la même manière :

Un int contenant la taille de la table, sur lequel vous devez itérer de 0 à ce nombre en avançant votre pointeur de 8 en 8. Dans chaque itération vous allez récupérer 2 choses, un ID, et une position, conservez les deux.

Déplacez vous donc à cette index que vous avez récupérer juste après "D2O" et extrayez là.

Une fois cette index table récupérée, vous pouvez récupérer les structure des données.

Vous allez donc récupérer un int qui sera le nombre de structures dans le fichier, vous allez donc itérer d'un index de 0 à ce nombre en avançant de 1 par 1.

Pour chaque structure vous aurez :

- classId : int

- className : UTF8

- packageName : UTF8

- fieldsCount : int

Voilà bravo vous avez la moitié de votre structure (gg), maintenant récupérons les champs. Chacun est composé de 2 choses :

- name : UTF8

- type : int

Les types possibles sont les suivant :

int = -1 (méthodes readInt, writeInt)

bool = -2 (méthodes readBool, writeBool)

string = -3 (méthodes readUTF, writeUTF)

double = -4 (méthodes readDouble, writeDouble)

i18n = -5 (se comporte comme un int)

uint = -6 (méthodes readUInt, writeUInt)

list = -99

Précision, si le type est liste, le suivant sera son type interne, attention, il peut aussi être de type liste, et donc vous devez aller aussi profondément que nécessaire (c'est dégueulasse sorti du contexte, bande de porcs).

Si le type est différent de tout ceux là, alors il sera basé sur une structure, et donc le type correspondra à l'id de la structure.

Source au niveau du client (com.ankamagames.jerakine.data.GameDataFileAccessor.as) :


public function initFromIDataInput(stream:IDataInput, moduleName:String) : void
{
         var key:int = 0;
         var pointer:int = 0;
         var count:uint = 0;
         var classIdentifier:int = 0;
         var formatVersion:uint = 0;
         var len:uint = 0;
         if(!this._streams)
         {
            this._streams = new Dictionary();
         }
         if(!this._indexes)
         {
            this._indexes = new Dictionary();
         }
         if(!this._classes)
         {
            this._classes = new Dictionary();
         }
         if(!this._counter)
         {
            this._counter = new Dictionary();
         }
         if(!this._streamStartIndex)
         {
            this._streamStartIndex = new Dictionary();
         }
         if(!this._gameDataProcessor)
         {
            this._gameDataProcessor = new Dictionary();
         }
         this._streams[moduleName] = stream;
         if(!this._streamStartIndex[moduleName])
         {
            this._streamStartIndex[moduleName] = 7;
         }
         var indexes:Dictionary = new Dictionary();
         this._indexes[moduleName] = indexes;
         var contentOffset:uint = 0;
         var headers:String = stream.readMultiByte(3,"ASCII");
         if(headers != "D2O")
         {
            stream["position"] = 0;
            try
            {
               headers = stream.readUTF();
            }
            catch(e:Error)
            {
            }
            if(headers != Signature.ANKAMA_SIGNED_FILE_HEADER)
            {
               throw new Error("Malformated game data file. (AKSF)");
            }
            formatVersion = stream.readShort();
            len = stream.readInt();
            stream["position"] = stream["position"] + len;
            contentOffset = stream["position"];
            this._streamStartIndex[moduleName] = contentOffset + 7;
            headers = stream.readMultiByte(3,"ASCII");
            if(headers != "D2O")
            {
               throw new Error("Malformated game data file. (D2O)");
            }
         }
         var indexesPointer:int = stream.readInt();
         stream["position"] = contentOffset + indexesPointer;
         var indexesLength:int = stream.readInt();
         for(var i:uint = 0; i < indexesLength; i = i + 8)
         {
            key = stream.readInt();
            pointer = stream.readInt();
            indexes[key] = contentOffset + pointer;
            count++;
         }
         this._counter[moduleName] = count;
         var classes:Dictionary = new Dictionary();
         this._classes[moduleName] = classes;
         var classesCount:int = stream.readInt();
         for(var j:uint = 0; j < classesCount; j++)
         {
            classIdentifier = stream.readInt();
            this.readClassDefinition(classIdentifier,stream,classes);
         }
         if(stream.bytesAvailable)
         {
            this._gameDataProcessor[moduleName] = new GameDataProcess(stream);
         }
}

private function readClassDefinition(classId:int, stream:IDataInput, store:Dictionary) : void
{
         var fieldName:String = null;
         var fieldType:int = 0;
         var className:String = stream.readUTF();
         var packageName:String = stream.readUTF();
         var classDef:GameDataClassDefinition = new GameDataClassDefinition(packageName,className);
         var fieldsCount:int = stream.readInt();
         for(var i:uint = 0; i < fieldsCount; i++)
         {
            fieldName = stream.readUTF();
            classDef.addField(fieldName,stream);
         }
         store[classId] = classDef;
}

BON, maintenant les données. Non je déconne.

Vous êtes surpris ? C'est normal, il reste des données en fait. La table de recherche.

C'est quoi cette table ? Ça sert (d'après ce que j'ai compris) à aller chercher des infos dans les D2O sans pour autant extraire toutes les données du fichier et les charger en mémoire.

Pour la récupérer vous devez :

- Vérifier qu'ils reste des choses à lire dans votre fichier

- Récupérer un int comprenant la taille de la table, stockez ça dans une variable

- Définir l'indexSearchOffset qui correspond à : pointeur actuel (position du curseur) + taille de la table + 4

Itérez ensuite tant que TailleDeLaTable > 0, vous allez donc maintenant :

- Déclarer une variable size contenant le nombre de bytes disponibles à la lecture

Puis lire :

- fileldName : UTF8

- Index de départ des données : int puis ajoutez indexSearchOffset

- Type de donnée : int

- Nombre de données : int

Pour finir décrémenter fieldListSize de fieldListSize - size - nombre de bytes disponibles à la lecture

En gros ça donne ça (C#) :


public Dictionary<string, int> SearchFieldIndex { get; private set; } = new Dictionary<string, int>();
public Dictionary<string, int> SearchFieldCount { get; private set; } = new Dictionary<string, int>();
public Dictionary<string, int> SearchFieldType { get; private set; } = new Dictionary<string, int>();

public List<string> QueryableField = new List<string>();

private void ParseStream()
{
    int fieldListSize = _reader.ReadInt();
    uint indexSearchOffset = (uint)_reader.Position + (uint)fieldListSize + 4;

    while (fieldListSize > 0)
    {
        uint size = (uint)_reader.BytesAvailable;
        string fieldName = _reader.ReadUTF();

        QueryableField.Add(fieldName);

        SearchFieldIndex[fieldName] = _reader.ReadInt() + (int)indexSearchOffset;
        SearchFieldType[fieldName] = _reader.ReadInt();
        SearchFieldCount[fieldName] = _reader.ReadInt();

        fieldListSize -= (int)(size - _reader.BytesAvailable);
    }
}

Code original (com.ankamagames.jerakinel.data.GameDataProcess.cs) :


private function parseStream() : void
{
         var size:uint = 0;
         var fieldName:String = null;

         this._queryableField = new Vector.<String>();
         this._searchFieldIndex = new Dictionary();
         this._searchFieldType = new Dictionary();
         this._searchFieldCount = new Dictionary();

         var fieldListSize:int = this._stream.readInt();
         var indexSearchOffset:uint = Object(this._stream).position + fieldListSize + 4;

         while(fieldListSize)
         {
            size = this._stream.bytesAvailable;
            fieldName = this._stream.readUTF();
            this._queryableField.push(fieldName);
            this._searchFieldIndex[fieldName] = this._stream.readInt() + indexSearchOffset;
            this._searchFieldType[fieldName] = this._stream.readInt();
            this._searchFieldCount[fieldName] = this._stream.readInt();
            fieldListSize = fieldListSize - (size - this._stream.bytesAvailable);
         }
}

Voilà, maintenant pour de vrai, les données.

Maintenant que vous avez vos structures et votre table d'index, il suffit d’itérer dessus donc de vous déplacer à chaque index dans le fichier, et lire les données correspondantes.

Elles sont dans l'ordre des champs de chaque structure que vous avez récupérer.

Exception pour les listes et types spéciaux (qui se réfèrent à des classes) :

- Si votre type est une liste, vous devez lire un int indiquant la taille de la liste.

- Si votre type est une structure (id > 0) alors vous devez lire un int correspondant à l'id de la structure que vous devez suivre pour récupérer les données (attention, il peut être différent du type que vous avez eu lors de la récupération de la structure, exemple pour le fichier Items.d2o, possibleEffects est défini de type 1 (EffectInstance donc) mais lors de la lecture des données le type envoyé est 3 (EffectInstanceDice))

On a presque fini ! Il reste un dernier détail, vous vous souvenez de la search table ? Voilà donc son fonctionnement.

Avec les indexes que vous avez récupéré en la lisant (SearchFieldIndex dans mon exemple) vous pouvez naviguer à la manière de l'index table.

Comment elle se compose ?

En ayant bien analysé la search table contient en fait les index de recherche pour une valeur X. En gros, si un champ est défini comme "queryable" il sera donc présent dans cette table.

Elle se présente sous cette forme (pour chaque "queryable" field) :

- Une valeur X qui sera lu en fonction du type de la field (SearchFieldType[field] dans mon exemple)

- Une valeur int à multiplier par 0.25 qui correspond au nombres d'index dans la liste

- Une liste de valeurs int sur laquelle vous devez itéré X fois en fonction du nombre défini par ce que je viens d'expliquer juste au dessus, ces valeurs correspondent aux indexes dans l'index table)

Comment elle marche ?

Elle sert en fait à effectuer une recherche plus rapidement, elle groupe donc les données qui possèdent une même valeur pour un champ donné.

Exemple :

J'ai une structure de donnée Item composée de 2 champs : Nom et Niveau

J'ai 3 entrées dans mon fichier :

1 - Nom: Test, Niveau: 100

2 - Nom: Test 2, Niveau: 150

3 - Nom: Test 3, Niveau 100

Ma search table a le champ "Niveau" "queryable", elle sera donc :

Valeur | Nombre d'entrées (divisé par 0.25) | Liste d'indexes

100 8 1,3

150 4 2

Exemple pour la lire :


private Dictionary<string, Dictionary<dynamic, List<int>>> ExtractSearchTableData()
{
    Dictionary<string, Dictionary<dynamic, List<int>>> searchTable =
        new Dictionary<string, Dictionary<dynamic, List<int>>>();

    var queryableFieldsEnumarator = QueryableField.GetEnumerator();

    for (int i = 0; queryableFieldsEnumarator.MoveNext(); i++)
    {
        var readingFunction = D2OField.GetReadingMethod(SearchFieldType[queryableFieldsEnumarator.Current]);

        if (readingFunction == null)
        {
            continue;
        }

        var currentEntry = searchTable[queryableFieldsEnumarator.Current] = new Dictionary<dynamic, List<int>>();

        int currentPositon = (int)_binaryReader.Position;

        _binaryReader.Seek(SearchFieldIndex[queryableFieldsEnumarator.Current], SeekOrigin.Begin);

        for (int j = 0; j < SearchFieldCount[queryableFieldsEnumarator.Current]; j++)
        {
            var indexesList = currentEntry[readingFunction(_binaryReader)] = new List<int>();

            int size = (int)(_binaryReader.ReadInt() * 0.25);

            while (size > 0)
            {
                indexesList.Add(_binaryReader.ReadInt());

                size -= 1;
            }
        }

        _binaryReader.Seek(currentPositon, SeekOrigin.Begin);
    }

    return searchTable;
}

Voilà c'est tout, vous êtes maintenant un master des fichiers D2O, vous pouvez vous applaudir !

Si vous avez des questions laissez un commentaire sur ce post et j’essaierais d'y répondre :)

Le tutoriel est très bon, juste je trouve dommage que tu ne montres pas toute la partie du reverse engineering du code AS3 de Dofus pour ensuite le commenter. (Imaginons qu'une mise à jour soit faites, on ne saura pas si ton code est encore à jour)

Mais ça n'en reste pas moins un très bon tutoriel merci.

Et beh je vais remédier à ça ! Je vais ajouter les bout de codes correspondants et ou les trouver.

Bon tutoriel, merci pour ton temps ^^

Un petit soucis c'est que tu n'as pas parlé de "GameDataProcess", alors que c'est une classe d'importante qu'on ne peut negliger.

Bon presque tout les WorldEditor partagés n'implémentent pas sa logique, on trouvera bien un problème lors de la modification de par exemple Monsters.d2o, puisque le Bestiaire à besoin de ces données.

    TheWhitesmith

    Bon tutoriel, merci pour ton temps ^^

    Un petit soucis c'est que tu n'as pas parlé de "GameDataProcess", alors que c'est une classe d'importante qu'on ne peut negliger.

    Bon presque tout les WorldEditor partagés n'implémentent pas sa logique, on trouvera bien un problème lors de la modification de par exemple Monsters.d2o, puisque le Bestiaire à besoin de ces données.

    je pense que tu devrais relire le tuto la moitié parle du GameDataProcess, et il est cité ^^

    Je sais pour les worlds editors, c’est pour ça que je vais en partager un fin de semaine refait à neuf qui prend en charge les classes dans des classes et qui écrit cette fameuse search table.

    un mois plus tard

    Pas mal, le tuto.

    Ce qui me chiffonne un peu dans ton code C# c'est ceci

    img

    Je trouve ça pas très judicieux de mettre du dynamic en clé de dictionnaire tu peux plomber les perfs et même avoir des duplicatas à cause de la référence volatile de l'objet récupérée, :(

      Fallen

      Pas mal, le tuto.

      Ce qui me chiffonne un peu dans ton code C# c'est ceci

      img

      Je trouve ça pas très judicieux de mettre du dynamic en clé de dictionnaire tu peux plomber les perfs et même avoir des duplicatas à cause de la référence volatile de l'objet récupérée, :(

      Si t’as une meilleure idée hésite pas, l’objet peut être de n’importe quel type, d’où le dynamic, après j’aurais pu faire une classe avec un template qui prend tout les types possibles mais j’avais un peu la flemme pour un truc aussi peu sensible

      6 mois plus tard

      Super tuto, merci beaucoup. Je vais surement posé une question qui vous semble idiote mais comment as-tu fait pour savoir que :

      com.ankamagames.jerakine.data.GameDataFileAccessor.as

      gérer les fichier d2o

      J'utilise JPEXS free flash decompiler et j'ai bien vu qu'il y avais la ligne :

      if(headers != "D2O")

      Seulement sur ce logiciel pour décompiler je ne peut que faire ctrl+f pour la recherche dans un seul fichier et non dans tous, j'aimerais trouver directement les méthodes pour D2P et D2I aussi.

      j'ai trouvé ça dans un autre commentaire sur un autre sujet :

      d2i : https://github.com/Emudofus/Dofus/b...ankamagames/jerakine/data/I18nFileAccessor.as

      d2o :https://github.com/Emudofus/Dofus/b...magames/jerakine/data/GameDataFileAccessor.as

      d2p : https://github.com/Emudofus/Dofus/b...kine/resources/protocols/impl/PakProtocol2.as

      du coup j'ai pu en déduire les chemins :

      d2i : com.ankamagames.jerakine.data.I18nFileAccessor.as

      d2o : com.ankamagames.jerakine.data.GameDataFileAccessor.as

      d2p : com.ankamagames.jerakine.resources.protocols.impl.PakProtocol2.as

      Seulement voilà si je devais les chercher sans avoir quasiment explicitement le chemin grâce au commentaire trouvé sur un autre sujet j'aurais été incapable de le faire. Comment as tu fait exactement pour savoir où précisément chercher. Vraiment désolé si ma question peut paraitre totalement idiote j'ai jamais fais ce genre de chose (reverse engenering) par conséquent je suis totalement a la ramasse complet.

        8 jours plus tard

        kiyoshi

        Super tuto, merci beaucoup. Je vais surement posé une question qui vous semble idiote mais comment as-tu fait pour savoir que :

        com.ankamagames.jerakine.data.GameDataFileAccessor.as

        gérer les fichier d2o

        J'utilise JPEXS free flash decompiler et j'ai bien vu qu'il y avais la ligne :

        if(headers != "D2O")

        Seulement sur ce logiciel pour décompiler je ne peut que faire ctrl+f pour la recherche dans un seul fichier et non dans tous, j'aimerais trouver directement les méthodes pour D2P et D2I aussi.

        j'ai trouvé ça dans un autre commentaire sur un autre sujet :

        d2i : https://github.com/Emudofus/Dofus/b...ankamagames/jerakine/data/I18nFileAccessor.as

        d2o :https://github.com/Emudofus/Dofus/b...magames/jerakine/data/GameDataFileAccessor.as

        d2p : https://github.com/Emudofus/Dofus/b...kine/resources/protocols/impl/PakProtocol2.as

        du coup j'ai pu en déduire les chemins :

        d2i : com.ankamagames.jerakine.data.I18nFileAccessor.as

        d2o : com.ankamagames.jerakine.data.GameDataFileAccessor.as

        d2p : com.ankamagames.jerakine.resources.protocols.impl.PakProtocol2.as

        Seulement voilà si je devais les chercher sans avoir quasiment explicitement le chemin grâce au commentaire trouvé sur un autre sujet j'aurais été incapable de le faire. Comment as tu fait exactement pour savoir où précisément chercher. Vraiment désolé si ma question peut paraitre totalement idiote j'ai jamais fais ce genre de chose (reverse engenering) par conséquent je suis totalement a la ramasse complet.

        Yop, alors c'est simple pour trouver quel fichier j'ai besoin il suffit de faire l'exécution dans ta tête et de suivre les imports, une fois fait tu pourras facilement identifier quoi prendre et quoi laisser !

        7 mois plus tard

        C'est honnêtement impressionnant. En revanche, je ne sais pas ce que sont ces fichiers d2o. De par l'exemple des items j'en déduis que ce sont des fichiers ressemblant de près ou de loin (de très loin je pense) à un dump d'une bdd ? Si je voulais par exemple récupérer les cellules navigables ou non en combat, ce sera dans un d2o ?

          jokoast

          C'est honnêtement impressionnant. En revanche, je ne sais pas ce que sont ces fichiers d2o. De par l'exemple des items j'en déduis que ce sont des fichiers ressemblant de près ou de loin (de très loin je pense) à un dump d'une bdd ? Si je voulais par exemple récupérer les cellules navigables ou non en combat, ce sera dans un d2o ?

          Ces fichiers ressemblent plutôt à ce qu'on avait sur la 1.29 dans le dossier "lang", ils contiennent les infos statiques du jeu, maps, items, jobs, etc