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 :)