1 /// Authors: Thibaut CHARLES (Crom) cromfr@gmail.com 2 /// License: GPL-3.0 3 /// Copyright: Copyright Thibaut CHARLES 2016 4 5 module tools.nwngff; 6 7 import std.stdio; 8 import std.conv: to, ConvException; 9 import std.traits; 10 import std.string; 11 import std.exception: enforce; 12 import std.base64: Base64; 13 import std.algorithm; 14 import std.typecons: Tuple, Nullable, tuple; 15 version(unittest) import std.exception: assertThrown, assertNotThrown; 16 17 import tools.common.getopt; 18 import nwn.gff; 19 import nwnlibd.orderedjson; 20 21 22 class ArgException : Exception{ 23 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 24 super(msg, f, l, t); 25 } 26 } 27 class GffPathException : Exception{ 28 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 29 super(msg, f, l, t); 30 } 31 } 32 class GffValueSpecException : Exception{ 33 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 34 super(msg, f, l, t); 35 } 36 } 37 38 // Hack for having a full stacktrace when unittest fails (otherwise it stops the stacktrace at main()) 39 int main(string[] args){return _main(args);} 40 int _main(string[] args){ 41 string inputPath, outputPath; 42 Format inputFormat = Format.detect, outputFormat = Format.detect; 43 string[] setValuesList; 44 string[] setLocVars; 45 string[] removeValuesList; 46 bool cleanLocale = false; 47 bool printVersion = false; 48 auto res = getopt(args, 49 "i|input", "Input file. '-' to read from stdin. Provided for compatibility with Niv GFF tool.", &inputPath, 50 "j|input-format", "Input file format ("~EnumMembers!Format.stringof[6..$-1]~")", &inputFormat, 51 "o|output", "Output file. Defaults to stdout.", &outputPath, 52 "k|output-format", "Output file format ("~EnumMembers!Format.stringof[6..$-1]~")", &outputFormat, 53 "s|set", "Set or add nodes in the GFF file. See section 'Setting nodes'.\nEx: 'DayNight.7.SkyDomeModel=my_sky_dome.tga'", &setValuesList, 54 "r|remove", "Removes a GFF node with the given node path. See section 'Node paths'.\nEx: 'DayNight.7.SkyDomeModel'", &removeValuesList, 55 "set-locvar", "Set a local variable. See section 'Setting local variables'", &setLocVars, 56 "clean-locstr", "Remove empty values from localized strings.\n", &cleanLocale, 57 "version", "Print nwn-lib-d version and exit.\n", &printVersion, 58 ); 59 if(res.helpWanted){ 60 improvedGetoptPrinter( 61 "Parsing and serialization tool for GFF files like ifo, are, bic, uti, ...", 62 res.options, 63 multilineStr!` 64 ===============| Setting nodes |=============== 65 There are 3 ways to set a GFF value: 66 67 --set <node_path>=<node_value> 68 Sets the value of an existing GFF node, without changing its type 69 70 --set <node_path>:<node_type>=<node_value> 71 Sets the value and type of a GFF node, creating it if does not exist. 72 Structs and Lists cannot be set using this technique. 73 74 --set <node_path>:json=<json_value> 75 Sets the value and type of a GFF node using a JSON object. 76 77 <node_path> Dot-separated path to the node to set. See section 'Node paths'. 78 <node_type> GFF type. Any of byte, char, word, short, dword, int, dword64, int64, float, double, cexostr, resref, cexolocstr, void, list, struct 79 <node_value> GFF value. 80 'void' values must be encoded in base64. 81 'cexolocstr' values can be either an integer (sets the resref) or a string (sets the english string). 82 You can reference another node's value using the syntax: gff@<node_path> 83 <json_value> GFF JSON value, as represented in the json output format. 84 ex: {"type": "struct","value":{"Name":{"type":"cexostr","value":"tk_item_dropped"}}} 85 86 Examples: 87 --set 'FirstName=Drizzt' 88 Set the first name to 'Drizzt' 89 --set 'Tag=gff@TemplateResRef' 90 Set the Tag to match the ResRef 91 --set 'FeatList.$:json={"type":"struct","__struct_id":1,"value":{"Feat":{"value":1337,"type":"word"}}} 92 Give the feat ID 1337 93 94 95 ===============| Setting local variables |=============== 96 You can set a local variable on the object with this syntax: 97 98 --set-locvar <var_name>:<var_type>=<value> 99 100 <var_name> The local variable name 101 <var_type> The local variable type. Only 'int', 'float', 'string' are supported. 102 <value> The local variable value to set. Values are converted to var_type when necessary. 103 104 Examples: 105 --set-locvar nAnswer:int=42 106 Set int nAnswer to 42 107 --set-locvar nPVPRules:int=gff@PlayerVsPlayer 108 Set int nPVPRules to the value of the PlayerVsPlayer GFF node 109 --set-locvar sDescription:string=gff@DescIdentified 110 Register a local var containing the object's description 111 112 113 ===============| Node paths |=============== 114 A GFF node path is a succession of path elements separated by dots. 115 116 The path elements can be: 117 - Any string: If the parent is a struct, will select the child value with a given label 118 - Any integer: If the parent is a list, will select the Nth child struct 119 - '$-42': If parent is a list, '$' is replaced by the list length, allowing to access last children of the list with '$-1' 120 - '$': Will add a child at the end of the list 121 122 Examples: 123 Tint_Hair.Tintable.Tint.1.r Red component of someone's hair color 124 DayNight.7.SkyDomeModel Default skydome 125 ItemList.$-1 Last item in the inventory 126 FeatList.$ Slot for adding a new feat with --set 127 ` 128 ); 129 return 0; 130 } 131 if(printVersion){ 132 import nwn.ver: NWN_LIB_D_VERSION; 133 writeln(NWN_LIB_D_VERSION); 134 return 0; 135 } 136 137 if(inputPath is null){ 138 enforce(args.length > 1, "No input file provided"); 139 enforce(args.length <= 2, "Too many input files provided"); 140 inputPath = args[1]; 141 } 142 143 if(inputFormat == Format.detect){ 144 if(inputPath is null) 145 inputFormat = Format.gff; 146 else 147 inputFormat = guessFormat(inputPath); 148 } 149 if(outputFormat == Format.detect){ 150 if(outputPath is null) 151 outputFormat = Format.pretty; 152 else 153 outputFormat = guessFormat(outputPath); 154 } 155 156 157 //Special cases where FastGFF can be used 158 if(inputFormat == Format.gff && outputFormat == Format.pretty && setValuesList.length == 0){ 159 import nwn.fastgff: FastGff; 160 161 File inputFile = inputPath == "-" ? stdin : File(inputPath, "r"); 162 auto gff = new FastGff(inputFile.readAll); 163 164 File outputFile = outputPath == "-" || outputPath is null ? stdout : File(outputPath, "w"); 165 outputFile.writeln(gff.toPrettyString()); 166 return 0; 167 } 168 169 170 171 //Parsing 172 Gff gff; 173 File inputFile = inputPath == "-"? stdin : File(inputPath, "r"); 174 175 switch(inputFormat){ 176 case Format.gff: 177 gff = new Gff(inputFile); 178 break; 179 case Format.json, Format.json_minified: 180 import nwnlibd.orderedjson; 181 gff = new Gff(parseJSON(cast(string)inputFile.readAll.idup)); 182 break; 183 case Format.pretty: 184 enforce(0, inputFormat.to!string~" parsing not supported"); 185 break; 186 default: 187 enforce(0, inputFormat.to!string~" parsing not implemented"); 188 } 189 inputFile.close(); 190 191 // Returns a pointer to the GFF node pointed by path 192 auto getGffNode(in string[] path, bool write){ 193 void* node = &gff.root; 194 GffType nodeType = GffType.Struct; 195 foreach(i, string nextName ; path){ 196 GffType targetType = GffType.Invalid; 197 auto col = nextName.lastIndexOf(':'); 198 if(col >= 0){ 199 targetType = compatStrToGffType(nextName[col + 1 .. $]); 200 enforce!GffPathException(targetType != GffType.Invalid, 201 format!"Unknown GFF type string: %s. Allowed types are: byte char word short dword int dword64 int64 float double cexostr resref cexolocstr void struct list json"(nextName[col + 1 .. $])); 202 nextName = nextName[0 .. col]; 203 } 204 205 if(nodeType == GffType.Struct){ 206 auto gffStruct = cast(GffStruct*)node; 207 208 if(auto next = (nextName in *gffStruct)){ 209 // Select labeled value 210 if(targetType != GffType.Invalid){ 211 // Change node type 212 enforce!GffPathException(write, format!"Node %s is not of type %s"(path[0 .. i + 1].join('.'), targetType)); 213 214 if(i + 1 == path.length){ 215 // Only allow changing the type of the last node in path 216 *next = GffValue(targetType); 217 } 218 else 219 enforce!GffPathException(next.type == targetType, 220 format!"Type mismatch for node %s of type %s versus provided type %s"(path[0 .. i + 1].join('.'), next.type, targetType) 221 ); 222 } 223 224 node = next; 225 nodeType = next.type; 226 } 227 else if(write && targetType != GffType.Invalid){ 228 // Insert new value 229 tswitch: final switch(targetType) { 230 static foreach(TYPE ; EnumMembers!GffType){ 231 case TYPE: 232 static if(TYPE != GffType.Invalid){ 233 node = &((*gffStruct)[nextName] = GffValue(targetType)).get!(gffTypeToNative!TYPE)(); 234 break tswitch; 235 } 236 else 237 assert(0); 238 } 239 } 240 //node = &((*gffStruct)[nextName] = GffValue(targetType)); 241 242 assert(nextName in *gffStruct); 243 244 nodeType = targetType; 245 } 246 else 247 throw new GffPathException(format!"Node '%s' does not exist in %s"(nextName, path[0 .. i].join('.'))); 248 } 249 else if(nodeType == GffType.List){ 250 enforce!GffPathException(targetType == GffType.Invalid || targetType == GffType.Struct, 251 format!"Node %s is a list and can only contain struct children"(path[0 .. i].join('.')) 252 ); 253 auto gffList = cast(GffList*)node; 254 255 if(nextName == "$"){ 256 // Append to list 257 (*gffList) ~= GffStruct(); 258 node = &(*gffList)[$ - 1]; 259 nodeType = GffType.Struct; 260 } 261 else{ 262 size_t index = 0; 263 try{ 264 if(nextName[0] == '$') 265 index = (gffList.length + nextName[1 .. $].to!int).to!size_t; 266 else 267 index += nextName.to!size_t; 268 } 269 catch(ConvException e){ 270 e.msg = format!"Node %s is a list, and '%s' is not a valid index: %s"(path[0 .. i].join('.'), nextName, e.msg); 271 throw e; 272 } 273 274 enforce!GffPathException(index < gffList.length, 275 format!"Node %s is a list, and index %d is out of bounds"(path[0 .. i], index) 276 ); 277 278 node = &(*gffList)[index]; 279 nodeType = GffType.Struct; 280 } 281 } 282 else 283 throw new Exception(format!"Node %s is a %s, and cannot contain any children"(path[0 .. i].join('.'), nodeType.gffTypeToCompatStr())); 284 } 285 286 return tuple(nodeType, node); 287 } 288 // Sets a GFF node value, without changing its type 289 static void setGffNodeValue(in ReturnType!getGffNode node, GffValue value){ 290 auto gffType = node[0]; 291 auto gffNode = node[1]; 292 assert(gffType == value.type); 293 294 final switch(gffType){ 295 static foreach(TYPE ; EnumMembers!GffType){ 296 case TYPE: 297 static if(TYPE == GffType.Invalid) 298 assert(0); 299 else{ 300 (*cast(gffTypeToNative!TYPE*)gffNode) = value.get!(gffTypeToNative!TYPE); 301 return; 302 } 303 } 304 } 305 } 306 T resolveValueSpec(T)(in string valueSpec) { 307 enum retGffType = nativeToGffType!T; 308 309 if(valueSpec.length >= 4 && valueSpec[0 .. 4] == "gff@"){ 310 // valueSpec is provided as a reference to a GFF node 311 const valuePath = valueSpec[4 .. $].split("."); 312 auto valueNode = getGffNode(valuePath, false); 313 auto valueGffType = valueNode[0]; 314 void* value = valueNode[1]; 315 316 if(valueGffType == retGffType) 317 return *cast(T*)value; 318 else{ 319 //Convert to destination type 320 final switch(valueGffType){ 321 static foreach(ValueGffType ; EnumMembers!GffType){ 322 case ValueGffType: 323 324 static if(ValueGffType == GffType.Invalid) 325 assert(0); 326 else{ 327 alias ValueType = gffTypeToNative!ValueGffType; 328 static if(__traits(compiles, (*cast(ValueType*)value).to!T)){ 329 //conversion using to!T 330 return (*cast(ValueType*)value).to!T; 331 } 332 else static if(is(T: GffLocString) || is(T: GffResRef)){ 333 // Build struct after conversion with to!string 334 static if(is(ValueType: GffStruct) || is(ValueType: GffList)){ 335 throw new ConvException( 336 format!"Cannot convert node %s of type %s into %s"( 337 valuePath.join("."), valueGffType.gffTypeToCompatStr(), retGffType.gffTypeToCompatStr() 338 ) 339 ); 340 } 341 else{ 342 string ret; 343 static if(is(ValueType: GffVoid)) 344 ret = Base64.encode(*cast(GffVoid*)value).idup; 345 else 346 ret = (*cast(ValueType*)value).to!string; 347 348 static if(is(T: GffLocString)) 349 return GffLocString(GffLocString.strref.max, [0: (*cast(ValueType*)value).to!string]); 350 else 351 return GffResRef((*cast(ValueType*)value).to!string); 352 } 353 } 354 else{ 355 throw new ConvException( 356 format!"Cannot convert node %s of type %s into %s"( 357 valuePath.join("."), valueGffType.gffTypeToCompatStr(), retGffType.gffTypeToCompatStr() 358 ) 359 ); 360 } 361 } 362 } 363 } 364 } 365 } 366 else{ 367 // valueSpec is a string representation of the value 368 static if(retGffType >= GffType.Byte && retGffType <= GffType.Double) 369 return valueSpec.to!T; 370 else static if(retGffType == GffType.String) 371 return valueSpec; 372 else static if(retGffType == GffType.ResRef) 373 return GffResRef(valueSpec); 374 else static if(retGffType == GffType.LocString) 375 return GffLocString(GffLocString.strref.max, [0: valueSpec]); 376 else static if(retGffType == GffType.Void) 377 return cast(GffVoid)Base64.decode(valueSpec); 378 else static if(retGffType == GffType.Struct || retGffType == GffType.List) 379 throw new Exception(format!"Use json format for setting a value of type %s"(retGffType.gffTypeToCompatStr())); 380 else 381 assert(0); 382 } 383 assert(0); 384 } 385 386 387 //Modifications 388 foreach(setValue ; setValuesList){ 389 auto eq = setValue.indexOf('='); 390 enforce(eq >= 0, "--set value must contain a '=' character"); 391 string pathWithType = setValue[0 .. eq]; 392 393 string[] path = pathWithType.split("."); 394 395 string valueSpec = setValue[eq + 1 .. $]; 396 397 try{ 398 bool isValueBuilt = false; 399 400 GffValue valueToSet; 401 402 auto col = path[$ - 1].lastIndexOf(':'); 403 if(col >= 0){ 404 // Last path element has a defined type 405 406 if(path[$ - 1][col + 1 .. $] == "json"){ 407 // valueSpec is in JSON format 408 auto json = valueSpec.parseJSON; 409 enforce!GffPathException(json.type == JSONType.object, "JSON values must be an objects"); 410 enforce!GffPathException("type" in json, "JSON object must contain a \"type\" key"); 411 412 // Just check that it's convertible. Throws an exception if not 413 json["type"].str.compatStrToGffType(); 414 415 // Store associated gff value 416 valueToSet = GffValue(json); 417 isValueBuilt = true; 418 419 // Replace the last type in path to the one stored in the JSON object 420 path[$ - 1] = path[$ - 1][0 .. col + 1] ~ json["type"].str; 421 } 422 } 423 424 auto nodeToSet = getGffNode(path, true); 425 426 if(valueToSet.type == GffType.Invalid){ 427 final switch(nodeToSet[0]) with(GffType) { 428 case Byte: valueToSet = GffValue(resolveValueSpec!GffByte (valueSpec)); break; 429 case Char: valueToSet = GffValue(resolveValueSpec!GffChar (valueSpec)); break; 430 case Word: valueToSet = GffValue(resolveValueSpec!GffWord (valueSpec)); break; 431 case Short: valueToSet = GffValue(resolveValueSpec!GffShort (valueSpec)); break; 432 case DWord: valueToSet = GffValue(resolveValueSpec!GffDWord (valueSpec)); break; 433 case Int: valueToSet = GffValue(resolveValueSpec!GffInt (valueSpec)); break; 434 case DWord64: valueToSet = GffValue(resolveValueSpec!GffDWord64 (valueSpec)); break; 435 case Int64: valueToSet = GffValue(resolveValueSpec!GffInt64 (valueSpec)); break; 436 case Float: valueToSet = GffValue(resolveValueSpec!GffFloat (valueSpec)); break; 437 case Double: valueToSet = GffValue(resolveValueSpec!GffDouble (valueSpec)); break; 438 case String: valueToSet = GffValue(resolveValueSpec!GffString (valueSpec)); break; 439 case ResRef: valueToSet = GffValue(resolveValueSpec!GffResRef (valueSpec)); break; 440 case LocString: valueToSet = GffValue(resolveValueSpec!GffLocString(valueSpec)); break; 441 case Void: valueToSet = GffValue(resolveValueSpec!GffVoid (valueSpec)); break; 442 case Struct: valueToSet = GffValue(resolveValueSpec!GffStruct (valueSpec)); break; 443 case List: valueToSet = GffValue(resolveValueSpec!GffList (valueSpec)); break; 444 case Invalid: assert(0); 445 } 446 } 447 448 setGffNodeValue(nodeToSet, valueToSet); 449 } 450 catch(Exception e){ 451 e.msg = format!"Error for GFF node '%s': %s"(pathWithType, e.msg); 452 throw e; 453 } 454 } 455 456 //Value removal 457 foreach(rmValue ; removeValuesList){ 458 string[] path = rmValue.split("."); 459 460 auto parent = getGffNode(path[0 .. $ - 1], false); 461 auto parentType = parent[0]; 462 auto parentNode = parent[1]; 463 464 string lastName = path[$ - 1]; 465 GffType lastType = GffType.Invalid; 466 467 auto col = lastName.lastIndexOf(':'); 468 if(col >= 0){ 469 lastType = lastName[col + 1 .. $].compatStrToGffType; 470 lastName = lastName[0 .. col]; 471 } 472 473 switch(parentType) with(GffType) { 474 case Struct: 475 auto gffStruct = cast(GffStruct*)parentNode; 476 enforce!GffPathException(lastName in *gffStruct, 477 format!"Node %s cannot be found in struct %s"(lastName, path[0 .. $ - 1]) 478 ); 479 if(auto val = lastName in *gffStruct){ 480 enforce!GffPathException(lastType == Invalid || lastType == val.type, 481 format!"Type mismatch: %s is of type %s, not %s"(path.join("."), val.type, lastType) 482 ); 483 gffStruct.remove(lastName); 484 } 485 else 486 throw new GffPathException(format!"Node %s does not exist"(path.join("."))); 487 break; 488 case List: 489 auto gffList = cast(GffList*)parentNode; 490 size_t index = 0; 491 try{ 492 if(lastName[0] == '$') 493 index = (gffList.length + lastName[1 .. $].to!int).to!size_t; 494 else 495 index += lastName.to!size_t; 496 } 497 catch(ConvException e){ 498 e.msg = format!"Node %s is a list, and '%s' is not a valid index: %s"(path[0 .. $ - 1].join('.'), lastName, e.msg); 499 throw e; 500 } 501 502 enforce!GffPathException(index < gffList.length, 503 format!"Node %s is a list, and index %d is out of bounds"(path[0 .. $ - 1], index) 504 ); 505 enforce!GffPathException(lastType == Invalid || lastType == Struct, 506 format!"Type mismatch: %s is of type %s, not %s"(path.join("."), GffType.Struct, lastType) 507 ); 508 509 gffList.children = gffList.children.remove(index); 510 break; 511 default: 512 throw new GffPathException(format!"Node %s of type %s cannot contain any children"(path[0 .. $ - 1].join("."), parentType)); 513 } 514 } 515 516 // Set local vars 517 foreach(ref setlocvar ; setLocVars){ 518 import nwn.nwscript.functions; 519 520 auto eq = setlocvar.indexOf('='); 521 enforce(eq >= 0, "--set-locvar value must contain a '=' character"); 522 enforce(eq + 1 < setlocvar.length, "No value provided for --set-locvar "~setlocvar); 523 const varSpec = setlocvar[0 .. eq]; 524 const valueSpec = setlocvar[eq + 1 .. $]; 525 526 auto colon = varSpec.lastIndexOf(':'); 527 enforce(colon >= 0 && colon + 1 < varSpec.length, "No variable type provided for --set-locvar "~setlocvar); 528 const varName = varSpec[0 .. colon]; 529 const varType = varSpec[colon + 1 .. $]; 530 531 switch(varType){ 532 case "int": 533 NWInt value = resolveValueSpec!NWInt(valueSpec); 534 SetLocalInt(gff.root, varName, value); 535 break; 536 case "float": 537 NWFloat value = resolveValueSpec!NWFloat(valueSpec); 538 SetLocalFloat(gff.root, varName, value); 539 break; 540 case "string": 541 NWString value = resolveValueSpec!NWString(valueSpec); 542 SetLocalString(gff.root, varName, value); 543 break; 544 default: throw new Exception(format!"Unhandled local variable type '%s'"(varType)); 545 } 546 } 547 548 549 if(cleanLocale){ 550 551 static void cleanGffLocale(T)(ref T value){ 552 static if(is(T: GffValue)){ 553 switch(value.type) with(GffType){ 554 case LocString: 555 with(value.get!GffLocString){ 556 foreach(k ; strings.keys){ 557 if(strings[k] == ""){ 558 strings.remove(k); 559 } 560 } 561 } 562 break; 563 case Struct: 564 cleanGffLocale(value.get!GffStruct); 565 break; 566 case List: 567 cleanGffLocale(value.get!GffList); 568 break; 569 default: 570 break; 571 } 572 } 573 else static if(is(T: GffStruct)){ 574 foreach(ref GffValue innerValue ; value){ 575 cleanGffLocale(innerValue); 576 } 577 } 578 else static if(is(T: GffList)){ 579 foreach(ref GffStruct innerStruct ; value){ 580 cleanGffLocale(innerStruct); 581 } 582 } 583 } 584 585 cleanGffLocale(gff.root); 586 } 587 588 589 //Serialization 590 File outputFile = outputPath is null || outputPath == "-" ? stdout : File(outputPath, "w"); 591 switch(outputFormat){ 592 case Format.gff: 593 outputFile.rawWrite(gff.serialize()); 594 break; 595 case Format.pretty: 596 outputFile.writeln(gff.toPrettyString()); 597 break; 598 case Format.json, Format.json_minified: 599 auto json = gff.toJson; 600 outputFile.writeln(outputFormat==Format.json? json.toPrettyString : json.toString); 601 break; 602 default: 603 assert(0, outputFormat.to!string~" serialization not implemented"); 604 } 605 return 0; 606 } 607 608 enum Format{ detect, gff, json, json_minified, pretty } 609 610 Format guessFormat(in string fileName){ 611 import std.path: extension; 612 import std.string: toLower; 613 assert(fileName !is null); 614 615 immutable ext = fileName.extension.toLower; 616 switch(ext){ 617 case ".gff": 618 case ".are",".gic",".git"://areas 619 case ".dlg"://dialogs 620 case ".fac",".ifo",".jrl"://module files 621 case ".cam"://campaign files 622 case ".bic"://characters 623 case ".ult",".upe",".utc",".utd",".ute",".uti",".utm",".utp",".utr",".utt",".utw",".pfb"://blueprints 624 return Format.gff; 625 626 case ".json": 627 return Format.json; 628 629 case ".txt": 630 return Format.pretty; 631 632 default: 633 throw new ArgException("Unrecognized file extension: '"~ext~"'"); 634 } 635 636 } 637 638 ubyte[] readAll(File stream){ 639 ubyte[] data; 640 ubyte[500] buf; 641 642 size_t prevLength; 643 do{ 644 prevLength = data.length; 645 data ~= stream.rawRead(buf); 646 }while(data.length != prevLength); 647 648 return data; 649 } 650 651 652 653 unittest{ 654 import std.file; 655 import std.path; 656 657 658 auto stdout_ = stdout; 659 auto tmpOut = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".out"); 660 stdout = File(tmpOut, "w"); 661 scope(success) std.file.remove(tmpOut); 662 scope(exit) stdout = stdout_; 663 664 auto krogarData = cast(ubyte[])import("krogar.bic"); 665 auto krogarFilePath = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".krogar.bic"); 666 scope(success) std.file.remove(krogarFilePath); 667 std.file.write(krogarFilePath, krogarData); 668 669 assertThrown(_main(["nwn-gff"])); 670 assert(_main(["nwn-gff","--help"])==0); 671 assert(_main(["nwn-gff","--version"])==0); 672 673 // binary perfect read / serialization 674 immutable krogarFilePathDup = krogarFilePath~".dup.bic"; 675 scope(success) std.file.remove(krogarFilePathDup); 676 assert(_main(["nwn-gff",krogarFilePath,"-o",krogarFilePathDup])==0); 677 assert(krogarFilePath.read == krogarFilePathDup.read); 678 679 stdout.reopen(null, "w"); 680 assert(_main(["nwn-gff",krogarFilePath,"-o",krogarFilePathDup, "-k","pretty"])==0); 681 stdout.flush(); 682 assert(krogarFilePathDup.readText.splitLines.length == 23067); 683 assertThrown(_main(["nwn-gff",krogarFilePath, "-j","pretty"])); 684 685 686 auto dogeData = cast(ubyte[])import("doge.utc"); 687 immutable dogePath = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".doge.utc"); 688 scope(success) std.file.remove(dogePath); 689 std.file.write(dogePath, dogeData); 690 691 immutable dogePathJson = dogePath~".json"; 692 immutable dogePathDup = dogePath~".dup.utc"; 693 694 695 assert(_main(["nwn-gff","-i",dogePath,"-o",dogePathJson])==0); 696 assert(_main(["nwn-gff","-i",dogePathJson,"-o",dogePathDup])==0); 697 assert(_main(["nwn-gff",dogePath,"-o",dogePathJson])==0); 698 699 700 // Simple modifications 701 assert(_main(["nwn-gff","-i",dogePath,"-o",dogePath~"modified.gff", 702 "--set","Subrace=1", 703 "--set","ACRtHip.Tintable.Tint.3.a=42", 704 "--set","SkillList.0.Rank=10", 705 "--set","Tag=tag_hello", // set GffString 706 "--set","FirstName=Hello", // set GffLocString 707 "--set","TemplateResRef=gff@Tag", // Convert GffString to GffResRef 708 "--set","ScriptAttacked=gff@ModelScale.z", // Convert float to string 709 "--set","ACLtShoulder:list=gff@SkillList", // Copy list and change dest type 710 "--remove","LastName", 711 "--remove","FeatList.0", 712 "--set-locvar","nAnswer:int=42", 713 "--set-locvar","nSpawnHP:int=gff@CurrentHitPoints", 714 "--set-locvar","sFirstName:string=gff@FirstName", 715 "--set-locvar","sDescription:string=gff@Description", 716 "--set-locvar","nCR:int=gff@ChallengeRating", 717 "--set-locvar","fNaturalAC:float=gff@NaturalAC", 718 ])==0); 719 auto gff = new Gff(dogePath~"modified.gff"); 720 assert(gff["Subrace"].to!int == 1); 721 assert(gff["ACRtHip"]["Tintable"]["Tint"]["3"]["a"].to!int == 42); 722 assert(gff["SkillList"][0]["Rank"].to!int == 10); 723 assert(gff["FirstName"].to!string == "Hello"); 724 assert(gff["Tag"].to!string == "tag_hello"); 725 assert(gff["TemplateResRef"].get!GffResRef == "tag_hello"); 726 assert(gff["ScriptAttacked"].get!GffResRef == "0.8"); 727 assert(gff["ACLtShoulder"] == gff["SkillList"]); 728 assert("LastName" !in gff); 729 assert(gff["FeatList"][0]["Feat"].get!GffWord == 354); 730 731 import nwn.nwscript.functions; 732 assert(GetLocalInt(gff.root, "nAnswer") == 42); 733 assert(GetLocalInt(gff.root, "nSpawnHP") == 13); 734 assert(GetLocalString(gff.root, "sDescription") == "Une indicible intelligence pétille dans ses yeux fous...\r\nWow..."); 735 assert(GetLocalInt(gff.root, "nCR") == 100); 736 assert(GetLocalFloat(gff.root, "fNaturalAC") == 2f); 737 assert(GetLocalString(gff.root, "sFirstName") == "Hello"); 738 739 // Type conv / path issues 740 assertThrown!ConvException(_main(["nwn-gff","-i",dogePath,"-o",dogePath~"modified.gff", "--set","TemplateResRef=gff@UVScroll"])); 741 assertThrown!GffPathException(_main(["nwn-gff","-i",dogePath,"-o",dogePath~"modified.gff", "--set","TemplateResRef=gff@DoesntExist"])); 742 743 744 // Type mismatch 745 assertThrown!GffPathException(_main(["nwn-gff","-i",dogePath,"-o",dogePath~"modified.gff", 746 "--set","SkillList:struct.0.Rank=10"])); 747 748 // Cannot create node without type 749 assertThrown!GffPathException(_main(["nwn-gff","-i",dogePath,"-o",dogePath~"modified.gff", 750 "--set","NewNode=hello"])); 751 752 assertThrown!ArgException(_main(["nwn-gff","-i","nothing.yolo","-o","something.gff"])); 753 assert(!"nothing.yolo".exists); 754 assert(!"something.gff".exists); 755 756 757 assert(dogePath.read == dogePathDup.read); 758 759 760 // set struct / lists operations 761 assertThrown!GffPathException(_main(["nwn-gff","-i",dogePath, "--set", `VarTable.$:notvalidtype=5`])); 762 std.file.write(dogePath, dogeData); 763 assert(_main([ 764 "nwn-gff","-i",dogePath, "-o",dogePathDup, 765 "--set", `VarTable.$:json={"type": "struct","value":{"Name":{"type":"cexostr","value":"tk_item_dropped"},"Type":{"type":"dword","value":1},"Value":{"type":"int","value":1}}}`, 766 "--set", `ModelScale.Yolo:int=42`, 767 "--set", `DirtyLocStr:json={"type": "cexolocstr", "str_ref": 0, "value": {"0": "", "2": "hello", "3": ""}}`, 768 "--clean-locstr" 769 ])==0); 770 771 gff = new Gff(dogePathDup); 772 assert(gff["VarTable"].get!GffList.length == 1); 773 assert(gff["VarTable"][0]["Name"].get!GffString == "tk_item_dropped"); 774 assert(gff["VarTable"][0]["Type"].get!GffDWord == 1); 775 assert(gff["ModelScale"]["Yolo"].get!GffInt == 42); 776 assert(gff["DirtyLocStr"].get!GffLocString == GffLocString(0, [2: "hello"])); 777 } 778