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 }