1 /// Faster and more memory efficient GFF file reading without writing support
2 module nwn.fastgff;
3 
4 import std.exception: enforce, assertNotThrown;
5 import std.stdint;
6 import std.stdio: writeln;
7 import std.conv: to;
8 import std.traits: EnumMembers;
9 import std.string;
10 import std.range.primitives;
11 import std.base64: Base64;
12 debug import std.stdio;
13 
14 /// Parsing exception
15 class GffParseException : Exception{
16 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
17 		super(msg, f, l, t);
18 	}
19 }
20 /// Type mismatch exception
21 class GffTypeException : Exception{
22 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
23 		super(msg, f, l, t);
24 	}
25 }
26 
27 
28 /// Type of data owned by a $(D GffField)
29 /// See_Also: $(D ToNative)
30 enum GffType: uint32_t{
31 	Invalid   = -1, /// Init value
32 	Byte      = 0,  /// Signed 8-bit int
33 	Char      = 1,  /// Unsigned 8-bit int
34 	Word      = 2,  /// Signed 16-bit int
35 	Short     = 3,  /// Unsigned 16-bit int
36 	DWord     = 4,  /// Signed 32-bit int
37 	Int       = 5,  /// Unsigned 32-bit int
38 	DWord64   = 6,  /// Signed 64-bit int
39 	Int64     = 7,  /// Unsigned 64-bit int
40 	Float     = 8,  /// 32-bit float
41 	Double    = 9,  /// 64-bit float
42 	String    = 10, /// String
43 	ResRef    = 11, /// String with width <= 16 (32 for NWN2)
44 	LocString = 12, /// Localized string
45 	Void      = 13, /// Binary data
46 	Struct    = 14, /// Map of other $(D GffField)
47 	List      = 15  /// Array of other $(D GffField)
48 }
49 /// A simple type can be stored inside the GffField value slot (32-bit or less)
50 bool isSimpleType(GffType type){
51 	with(GffType){
52 		return type <= Int || type == Float;
53 	}
54 }
55 /// Maps $(D GffType) to native D type
56 template ToNative(GffType t){
57 	     static if(t==GffType.Invalid)         static assert(0, "No native type for GffType.Invalid");
58 	else static if(t==GffType.Byte)            alias ToNative = GffByte;
59 	else static if(t==GffType.Char)            alias ToNative = GffChar;
60 	else static if(t==GffType.Word)            alias ToNative = GffWord;
61 	else static if(t==GffType.Short)           alias ToNative = GffShort;
62 	else static if(t==GffType.DWord)           alias ToNative = GffDWord;
63 	else static if(t==GffType.Int)             alias ToNative = GffInt;
64 	else static if(t==GffType.DWord64)         alias ToNative = GffDWord64;
65 	else static if(t==GffType.Int64)           alias ToNative = GffInt64;
66 	else static if(t==GffType.Float)           alias ToNative = GffFloat;
67 	else static if(t==GffType.Double)          alias ToNative = GffDouble;
68 	else static if(t==GffType.String)          alias ToNative = GffString;
69 	else static if(t==GffType.ResRef)          alias ToNative = GffResRef;
70 	else static if(t==GffType.LocString) alias ToNative = GffLocString;
71 	else static if(t==GffType.Void)            alias ToNative = GffVoid;
72 	else static if(t==GffType.Struct)          alias ToNative = GffStruct;
73 	else static if(t==GffType.List)            alias ToNative = GffList;
74 	else static assert(0);
75 }
76 
77 /// Basic GFF value type
78 alias GffByte = uint8_t;
79 /// Basic GFF value type
80 alias GffChar = int8_t;
81 /// Basic GFF value type
82 alias GffWord = uint16_t;
83 /// Basic GFF value type
84 alias GffShort = int16_t;
85 /// Basic GFF value type
86 alias GffDWord = uint32_t;
87 /// Basic GFF value type
88 alias GffInt = int32_t;
89 /// Basic GFF value type
90 alias GffDWord64 = uint64_t;
91 /// Basic GFF value type
92 alias GffInt64 = int64_t;
93 /// Basic GFF value type
94 alias GffFloat = float;
95 /// Basic GFF value type
96 alias GffDouble = double;
97 /// Basic GFF value type
98 alias GffString = string;
99 
100 /// GFF Resref value type (32 character string)
101 struct GffResRef{
102 	import nwnlibd.parseutils: stringToCharArray, charArrayToString;
103 
104 	///
105 	this(in char[] value){
106 		assert(value.length <= 32, "Resref cannot be longer than 32 characters");
107 		data[0 .. value.length] = value;
108 		if(value.length < data.length)
109 			data[value.length .. $] = 0;
110 	}
111 
112 	alias toString this;
113 
114 
115 	version(FastGffWrite)
116 	void opAssign(in string str){
117 		assert(str.length <= 32, "Value is too long");
118 		data = str.stringToCharArray!(char[32]);
119 	}
120 
121 	/// Converts `this.data` into a usable string (trim trailing NULL chars)
122 	string toString() const {
123 		return charArrayToString(data);
124 	}
125 
126 package:
127 	char[32] data;
128 }
129 
130 /// GFF Localized string value type (strref with eventually a `nwn.constants.Language` => `string` map)
131 struct GffLocString{
132 	uint32_t strref;
133 	string[int32_t] strings;
134 
135 	/// Get the string value without attempting to resolve strref using TLKs
136 	string toString() const {
137 		return format!"{%d, %s}"(strref == strref.max ? -1 : cast(long)strref, strings);
138 	}
139 
140 	import nwn.tlk: StrRefResolver, LanguageGender, TlkOutOfBoundsException;
141 	/// Get the string value using TLK tables if needed
142 	string resolve(in StrRefResolver resolver) const{
143 		if(strings.length > 0){
144 			immutable preferedLang = resolver.standartTable.language * 2;
145 			if(auto str = preferedLang in strings)
146 				return *str;
147 			if(auto str = preferedLang + 1 in strings)
148 				return *str;
149 			if(auto str = -2 in strings) // SetFirstName sets the -2 language
150 				return *str;
151 
152 			foreach(lang ; EnumMembers!LanguageGender){
153 				if(auto str = lang in strings)
154 					return *str;
155 			}
156 		}
157 
158 		if(strref != strref.max){
159 			try return resolver[strref];
160 			catch(TlkOutOfBoundsException){
161 				return "invalid_strref";
162 			}
163 		}
164 
165 		return "";
166 	}
167 }
168 /// Basic GFF value type
169 alias GffVoid = ubyte[];
170 
171 /// GFF structure value type (`string` => `GffField` map)
172 struct GffStruct{
173 	import std.typecons: Nullable;
174 
175 	@property{
176 		/// Struct subtype ID
177 		uint32_t id() const{
178 			return internal.id;
179 		}
180 	}
181 
182 	/// Get child GffField
183 	const(GffField) opIndex(in string label) const{
184 		assert(gff !is null, "GffStruct has no data");
185 		auto fieldIndex = index[label];
186 		return const(GffField)(gff, gff.getField(fieldIndex));
187 	}
188 
189 	/// Allows `foreach(gffField ; this)`
190 	int opApply(scope int delegate(in GffField child) dlg) const{
191 		return _opApply(delegate(uint32_t fieldIndex){
192 			auto field = gff.getField(fieldIndex);
193 			return dlg(const(GffField)(gff, field));
194 		});
195 	}
196 
197 	/// Return type for `"key" in gffStruct`. Behaves like a pointer.
198 	static struct NullableField {
199 		bool isNull = true;
200 		GffField value;
201 
202 		//alias isNull this;
203 		bool opCast(T: bool)() const {
204 			return !isNull;
205 		}
206 
207 		alias value this;
208 		GffField opUnary(string op: "*")() const {
209 			assert(!isNull, "NullableField is null");
210 			return value;
211 		}
212 	}
213 
214 	/// Allows `"value" in this`
215 	const(NullableField) opBinaryRight(string op : "in")(string label) const
216 	{
217 		if(gff is null)
218 			return NullableField();
219 
220 		if(auto fieldIndex = label in index)
221 			return const(NullableField)(false, const(GffField)(gff, gff.getField(*fieldIndex)));
222 
223 		return NullableField();
224 	}
225 
226 
227 	/// Serialize the struct and its children in a human-readable format
228 	string toPrettyString(string tabs = null) const {
229 		string ret = format!"%s(Struct %s)"(tabs, id == id.max ? "-1" : id.to!string);
230 		foreach(field ; this){
231 			const innerTabs = tabs ~ "|  ";
232 			const type = (field.type != GffType.Struct && field.type != GffType.List) ? " (" ~ field.type.to!string ~ ")" : null;
233 
234 			ret ~= format!"\n%s├╴ %-16s = %s%s"(
235 				tabs,
236 				field.label, field.toPrettyString(innerTabs)[innerTabs.length .. $], type
237 			);
238 		}
239 		return ret;
240 	}
241 
242 package:
243 	this(inout(FastGff) gff, inout(FastGff.Struct)* internal) inout{
244 		this.gff = gff;
245 		this.internal = internal;
246 
247 		uint32_t[string] index;
248 		_opApply(delegate(uint32_t fieldIndex){
249 			auto field = gff.getField(fieldIndex);
250 			index[gff.getLabel(field.label_index).toString] = fieldIndex;
251 			return 0;
252 		});
253 		this.index = cast(inout)index;
254 	}
255 
256 private:
257 	FastGff gff = null;
258 	FastGff.Struct* internal = null;
259 	uint32_t[string] index;
260 
261 	int _opApply(scope int delegate(uint32_t fieldIndex) dlg) const{
262 		if(gff is null)
263 			return 0;
264 
265 		if(internal.field_count == 1){
266 			auto fieldIndex = internal.data_or_data_offset;
267 			return dlg(fieldIndex);
268 		}
269 		else if(internal.field_count > 1){
270 			if(internal.data_or_data_offset != uint32_t.max){
271 				auto fieldList = gff.getFieldList(internal.data_or_data_offset);
272 				foreach(i ; 0 .. internal.field_count){
273 					auto fieldIndex = fieldList[i];
274 					int res = dlg(fieldIndex);
275 					if(res != 0)
276 						return res;
277 				}
278 			}
279 		}
280 		return 0;
281 	}
282 
283 }
284 /// GFF list value type (Array of GffStruct)
285 struct GffList{
286 	/// Get nth child GffStruct
287 	const(GffStruct) opIndex(size_t index) const{
288 		assert(gff !is null && index < length, "Out of bound");
289 		return const(GffStruct)(gff, gff.getStruct(structIndexList[index]));
290 	}
291 
292 	/// Number of children elements
293 	@property
294 	size_t length() const{
295 		return listLength;
296 	}
297 
298 	///
299 	@property
300 	bool empty() const{
301 		return listLength == 0;
302 	}
303 
304 	/// Converts the GffLIst into a list of GffStruct
305 	@property const(GffStruct)[] children() const {
306 		//auto ret = new GffStruct[listLength];
307 		const(GffStruct)[] ret;
308 		foreach(i ; 0 .. listLength){
309 			ret ~= const(GffStruct)(gff, gff.getStruct(structIndexList[i]));
310 			//ret[i] = const(GffStruct)(gff, gff.getStruct(structIndexList[i]));
311 		}
312 		return ret;
313 	}
314 	///
315 	alias children this;
316 
317 
318 	/// Serialize the list and its children in a human-readable format
319 	string toPrettyString(string tabs = null) const {
320 		string ret = format!"%s(List)"(tabs);
321 		foreach(child ; this){
322 			auto innerTabs = tabs ~ "|  ";
323 			ret ~= format!"\n%s├╴ %s"(
324 				tabs,
325 				child.toPrettyString(innerTabs)[innerTabs.length .. $]
326 			);
327 		}
328 		return ret;
329 	}
330 
331 package:
332 	this(inout(FastGff) gff, uint32_t listOffset) inout{
333 		this.gff = gff;
334 
335 		auto list = gff.getStructList(listOffset);
336 		this.listLength = list[0];
337 		this.structIndexList = &(list[1]);
338 	}
339 
340 
341 private:
342 	FastGff gff = null;
343 	uint32_t* structIndexList;
344 	uint32_t listLength;
345 }
346 
347 
348 /// GFF generic value (used for `GffStruct` children). This can be used as a Variant.
349 struct GffField{
350 	import std.variant: VariantN;
351 	alias Value = VariantN!(32,
352 		GffByte, GffChar,
353 		GffWord, GffShort,
354 		GffDWord, GffInt,
355 		GffDWord64, GffInt64,
356 		GffFloat, GffDouble,
357 		GffString, GffResRef, GffLocString, GffVoid,
358 		GffStruct, GffList);
359 
360 	alias value this;
361 
362 	@property{
363 		/// Value type
364 		GffType type() const{
365 			return internal.type;
366 		}
367 
368 		/// Get the value as a Variant
369 		Value value() const{
370 
371 			final switch(type) with(GffType){
372 				foreach(Type ; EnumMembers!GffType){
373 					case Type:
374 					static if(Type == Invalid)
375 						assert(0, "Invalid type");
376 					else static if(isSimpleType(Type)){
377 						return Value(*cast(ToNative!Type*)&internal.data_or_data_offset);
378 					}
379 					else static if(Type == Struct){
380 						return const(Value)(cast(GffStruct)const(GffStruct)(gff, gff.getStruct(internal.data_or_data_offset)));
381 					}
382 					else static if(Type == List){
383 						return const(Value)(cast(GffList)const(GffList)(gff, internal.data_or_data_offset));
384 					}
385 					else{
386 						auto fieldData = gff.getFieldData(internal.data_or_data_offset);
387 						static if(Type == DWord64 || Type == Int64 || Type == Double){
388 							return Value(*cast(ToNative!Type*)fieldData);
389 						}
390 						else static if(Type == String){
391 							auto length = *cast(uint32_t*)fieldData;
392 							return Value(cast(GffString)fieldData[4 .. 4 + length].idup);
393 						}
394 						else static if(Type == ResRef){
395 							auto length = *cast(uint8_t*)fieldData;
396 							return Value(GffResRef(cast(char[])fieldData[1 .. 1 + length]));
397 						}
398 						else static if(Type == LocString){
399 							auto blockLength = *cast(uint32_t*)fieldData;
400 							import nwnlibd.parseutils: ChunkReader;
401 							auto reader = ChunkReader(fieldData[4 .. 4 + blockLength]);
402 
403 							GffLocString ret;
404 							ret.strref = reader.read!uint32_t;
405 
406 							auto count = reader.read!uint32_t;
407 							foreach(i ; 0 .. count){
408 								auto id = reader.read!uint32_t;
409 								auto strlen = reader.read!uint32_t;
410 
411 								ret.strings[id] = reader.readArray!char(strlen).idup;
412 							}
413 							return Value(ret);
414 						}
415 						else static if(Type == Void){
416 							auto length = *cast(uint32_t*)fieldData;
417 							return Value(fieldData[4 .. 4 + length].dup);
418 						}
419 					}
420 				}
421 			}
422 		}
423 
424 		/// Get the label of this field
425 		string label() const{
426 			return gff.getLabel(internal.label_index).toString;
427 		}
428 	}
429 
430 	/// Shorthand for getting child field assuming this field is a `GffStruct`
431 	const(GffField) opIndex(in string label) const{
432 		return value.get!GffStruct[label];
433 	}
434 
435 	/// Shorthand for getting child field assuming this field is a `GffList`
436 	const(GffStruct) opIndex(in size_t index) const{
437 		return value.get!GffList[index];
438 	}
439 
440 	/// Serialize the field in a human-readable format. Does not represents struct or list children.
441 	string toString() const{
442 		typeswitch:
443 		final switch(type) with(GffType){
444 			foreach(Type ; EnumMembers!GffType){
445 				case Type:
446 				static if(Type == Invalid)
447 					assert(0, "Invalid type");
448 				else static if(Type == Void){
449 					return Base64.encode(value.get!(ToNative!Type));
450 				}
451 				else static if(Type == Struct){
452 					return "{Struct}";
453 				}
454 				else static if(Type == List){
455 					return "[List]";
456 				}
457 				else{
458 					return value.get!(ToNative!Type).to!string;
459 				}
460 			}
461 		}
462 	}
463 
464 	/// Serialize the field and its children in a human-readable format
465 	string toPrettyString(in string tabs = null) const{
466 		import std.string;
467 		string ret = tabs;
468 
469 		typeswitch:
470 		final switch(type) with(GffType){
471 			foreach(Type ; EnumMembers!GffType){
472 				case Type:
473 				static if(Type == Invalid)
474 					assert(0, "Invalid type");
475 				else static if(Type == Void){
476 					return ret ~ Base64.encode(value.get!(ToNative!Type)).to!string;
477 				}
478 				else static if(Type == Struct || Type == List){
479 					return value.get!(ToNative!Type).toPrettyString(tabs);
480 				}
481 				else{
482 					return ret ~ value.get!(ToNative!Type).to!string;
483 				}
484 			}
485 		}
486 	}
487 
488 	T to(T)() const {
489 		final switch(type) with(GffType) {
490 			case Byte:      static if(__traits(compiles, value.get!GffByte.to!T))      return value.get!GffByte.to!T;      else break;
491 			case Char:      static if(__traits(compiles, value.get!GffChar.to!T))      return value.get!GffChar.to!T;      else break;
492 			case Word:      static if(__traits(compiles, value.get!GffWord.to!T))      return value.get!GffWord.to!T;      else break;
493 			case Short:     static if(__traits(compiles, value.get!GffShort.to!T))     return value.get!GffShort.to!T;     else break;
494 			case DWord:     static if(__traits(compiles, value.get!GffDWord.to!T))     return value.get!GffDWord.to!T;     else break;
495 			case Int:       static if(__traits(compiles, value.get!GffInt.to!T))       return value.get!GffInt.to!T;       else break;
496 			case DWord64:   static if(__traits(compiles, value.get!GffDWord64.to!T))   return value.get!GffDWord64.to!T;   else break;
497 			case Int64:     static if(__traits(compiles, value.get!GffInt64.to!T))     return value.get!GffInt64.to!T;     else break;
498 			case Float:     static if(__traits(compiles, value.get!GffFloat.to!T))     return value.get!GffFloat.to!T;     else break;
499 			case Double:    static if(__traits(compiles, value.get!GffDouble.to!T))    return value.get!GffDouble.to!T;    else break;
500 			case String:    static if(__traits(compiles, value.get!GffString.to!T))    return value.get!GffString.to!T;    else break;
501 			case ResRef:    static if(__traits(compiles, value.get!GffResRef.to!T))    return value.get!GffResRef.to!T;    else break;
502 			case LocString: static if(__traits(compiles, value.get!GffLocString.to!T)) return value.get!GffLocString.to!T; else break;
503 			case Void:      static if(__traits(compiles, value.get!GffVoid.to!T))      return value.get!GffVoid.to!T;      else break;
504 			case Struct:    static if(__traits(compiles, value.get!GffStruct.to!T))    return value.get!GffStruct.to!T;    else break;
505 			case List:      static if(__traits(compiles, value.get!GffList.to!T))      return value.get!GffList.to!T;      else break;
506 			case Invalid:   assert(0, "No type set");
507 		}
508 		assert(0, format!"Cannot convert GFFType %s to %s"(type, T.stringof));
509 	}
510 
511 
512 package:
513 	this(inout(FastGff) gff, inout(FastGff.Field)* field) inout{
514 		this.gff = gff;
515 		internal = field;
516 	}
517 private:
518 	FastGff gff = null;
519 	FastGff.Field* internal = null;
520 
521 }
522 
523 
524 /// GFF file parser
525 class FastGff{
526 
527 	/// Parse GFF file
528 	this(in string filePath){
529 		import std.file: read;
530 		this(cast(ubyte[])filePath.read());
531 	}
532 
533 	/// Parse GFF raw data
534 	this(in ubyte[] rawData){
535 		enforce!GffParseException(rawData.length >= header.sizeof,
536 			"rawData length is too small");
537 
538 		header       = *cast(Header*)rawData.ptr;
539 		structs      = cast(Struct[])   rawData[header.struct_offset        .. header.struct_offset        + Struct.sizeof * header.struct_count    ].dup;
540 		fields       = cast(Field[])    rawData[header.field_offset         .. header.field_offset         + Field.sizeof  * header.field_count     ].dup;
541 		labels       = cast(Label[])    rawData[header.label_offset         .. header.label_offset         + Label.sizeof  * header.label_count     ].dup;
542 		fieldData    = cast(ubyte[])    rawData[header.field_data_offset    .. header.field_data_offset    + ubyte.sizeof  * header.field_data_count].dup;
543 		fieldIndices = cast(uint32_t[]) rawData[header.field_indices_offset .. header.field_indices_offset + header.field_indices_count             ].dup;
544 		listIndices  = cast(uint32_t[]) rawData[header.list_indices_offset  .. header.list_indices_offset  + header.list_indices_count              ].dup;
545 
546 	}
547 
548 	alias root this;
549 
550 	@property{
551 		/// Get root node (accessible with alias this)
552 		const const(GffStruct) root(){
553 			return const(GffStruct)(this, getStruct(0));
554 		}
555 
556 		/// GFF file type string
557 		const string fileType(){
558 			return header.file_type.idup().stripRight;
559 		}
560 
561 		/// GFF file version string
562 		const string fileVersion(){
563 			return header.file_version.idup().stripRight;
564 		}
565 	}
566 
567 	/// Serialize the entire file in a human-readable format
568 	string toPrettyString() const{
569 		import std.string: stripRight;
570 		return "========== GFF-"~header.file_type.idup.stripRight~"-"~header.file_version.idup.stripRight~" ==========\n"
571 				~ root.toPrettyString;
572 	}
573 
574 
575 private:
576 	Header header;
577 	Struct[] structs;
578 	Field[] fields;
579 	Label[] labels;
580 
581 	ubyte[] fieldData;
582 	uint32_t[] fieldIndices;
583 	uint32_t[] listIndices;
584 
585 
586 
587 	inout(Struct)* getStruct(in size_t id) inout {
588 		return &structs[id];
589 	}
590 	inout(Field)* getField(in size_t id) inout {
591 		return &fields[id];
592 	}
593 	inout(Label)* getLabel(in size_t id) inout {
594 		return &labels[id];
595 	}
596 
597 	inout(ubyte)* getFieldData(in size_t offset) inout {
598 		return &fieldData[offset];
599 	}
600 	inout(uint32_t)* getFieldList(in size_t offset) inout {
601 		return cast(inout(uint32_t)*)
602 		       &(cast(ubyte[])fieldIndices)[offset];
603 	}
604 	inout(uint32_t)* getStructList(in size_t offset) inout {
605 		return cast(inout(uint32_t)*)
606 		       &(cast(ubyte[])listIndices)[offset];
607 	}
608 
609 
610 
611 
612 	static align(1) struct Header{
613 		static assert(this.sizeof == 56);
614 		align(1):
615 		char[4]  file_type;
616 		char[4]  file_version;
617 		uint32_t struct_offset;
618 		uint32_t struct_count;
619 		uint32_t field_offset;
620 		uint32_t field_count;
621 		uint32_t label_offset;
622 		uint32_t label_count;
623 		uint32_t field_data_offset;
624 		uint32_t field_data_count;
625 		uint32_t field_indices_offset;
626 		uint32_t field_indices_count;
627 		uint32_t list_indices_offset;
628 		uint32_t list_indices_count;
629 
630 		string toString() const{
631 			return "Header: |"~file_type.to!string~"|"~file_version.to!string~"|\n"
632 			      ~"  struct: "~struct_offset.to!string~" ("~struct_count.to!string~")\n"
633 			      ~"  field: "~field_offset.to!string~" ("~field_count.to!string~")\n"
634 			      ~"  label: "~label_offset.to!string~" ("~label_count.to!string~")\n"
635 			      ~"  field_data: "~field_data_offset.to!string~" ("~field_data_count.to!string~")\n"
636 			      ~"  field_indices: "~field_indices_offset.to!string~" ("~field_indices_count.to!string~")\n"
637 			      ~"  list_indices: "~list_indices_offset.to!string~" ("~list_indices_count.to!string~")\n";
638 		}
639 	}
640 	static align(1) struct Struct{
641 		static assert(this.sizeof == 12);
642 		align(1):
643 		uint32_t id;
644 		uint32_t data_or_data_offset;
645 		uint32_t field_count;
646 		debug string toString() const {
647 			return format!"Struct(id=%d dodo=%d fc=%d)"(id, data_or_data_offset, field_count);
648 		}
649 	}
650 	static align(1) struct Field{
651 		static assert(this.sizeof == 12);
652 		align(1):
653 		GffType type;
654 		uint32_t label_index;
655 		uint32_t data_or_data_offset;
656 		debug string toString() const {
657 			return format!"Field(t=%s lblidx=%d dodo=%d)"(type.to!GffType, label_index, data_or_data_offset);
658 		}
659 	}
660 	static align(1) struct Label{
661 		static assert(this.sizeof == 16);
662 		align(1):
663 		char[16] value;
664 		string toString() const{
665 			import nwnlibd.parseutils: charArrayToString;
666 			return value.charArrayToString;
667 		}
668 	}
669 
670 
671 }
672 
673 
674 
675 unittest{
676 	import std.file : read;
677 	with(GffType){
678 		immutable krogarDataOrig = cast(immutable ubyte[])import("krogar.bic");
679 		auto gff = new FastGff(krogarDataOrig);
680 
681 		//Parsing checks
682 		assert(gff.fileType == "BIC");
683 		assert(gff.fileVersion == "V3.2");
684 
685 		assert(gff["IsPC"].get!GffByte == true);
686 		assert(gff["RefSaveThrow"].get!GffChar == 13);
687 		assert(gff["SoundSetFile"].get!GffWord == 363);
688 		assert(gff["HitPoints"].get!GffShort == 320);
689 		assert(gff["Gold"].get!GffDWord == 6400);
690 		assert(gff["Age"].get!GffInt == 50);
691 		//assert(gff[""].get!GffDWord64 == );
692 		//assert(gff[""].get!GffInt64 == );
693 		assert(gff["XpMod"].get!GffFloat == 1);
694 		//assert(gff[""].get!GffDouble == );
695 		assert(gff["Deity"].get!GffString == "Gorm Gulthyn");
696 		assert(gff["ScriptHeartbeat"].get!GffResRef == "gb_player_heart");
697 		assert(gff["FirstName"].get!GffLocString.strref == -1);
698 		assert(gff["FirstName"].get!GffLocString.strings[0] == "Krogar");
699 		//assert(gff[""].get!GffVoid == );
700 		assert(gff["Tint_Head"]["Tintable"]["Tint"]["1"]["b"].get!GffByte == 109);
701 		assert(gff["ClassList"][0]["Class"].get!GffInt == 4);
702 
703 		// Tintable appears two times in the gff
704 		assert(gff["Tintable"]["Tint"]["1"]["r"].get!GffByte == 253);
705 
706 		assertNotThrown(gff.toPrettyString());
707 
708 
709 		// parity with nwn.gff.Gff
710 		static import nwn.gff;
711 		assert(new FastGff(krogarDataOrig).toPrettyString == new nwn.gff.Gff(krogarDataOrig).toPrettyString);
712 		//assert(new FastGff(krogarDataOrig).toJson.toString == new nwn.gff.Gff(krogarDataOrig).toJson.toString);
713 	}
714 
715 }