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