1 /// Authors: Thibaut CHARLES (Crom) cromfr@gmail.com
2 /// License: GPL-3.0
3 /// Copyright: Copyright Thibaut CHARLES 2016
4 
5 module tools.nwntlk;
6 
7 import std.stdio;
8 import std.conv: to, ConvException;
9 import std.traits;
10 import std.string;
11 import std.path;
12 import std.exception;
13 import std.typecons: Tuple, Nullable;
14 import std.json;
15 version(unittest) import std.exception: assertThrown, assertNotThrown;
16 
17 import tools.common.getopt;
18 import nwn.tlk;
19 
20 class ArgException : Exception{
21 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
22 		super(msg, f, l, t);
23 	}
24 }
25 
26 int main(string[] args){
27 	string inputPath, outputPath;
28 	Format inputFormat = Format.detect, outputFormat = Format.detect;
29 	bool baseIndices, userIndices;
30 	bool printVersion;
31 	auto res = getopt(args,
32 		"i|input", "Explicit input file (the extra argument is passed as input file)", &inputPath,
33 		"j|input-format", "Input file format ("~EnumMembers!Format.stringof[6..$-1]~")", &inputFormat,
34 		"o|output", "<file> Output file", &outputPath,
35 		"k|output-format", "Output file format ("~EnumMembers!Format.stringof[6..$-1]~")", &outputFormat,
36 		"b|base", "Display TLK base indices (starting from 0)", &baseIndices,
37 		"u|user", "Display TLK user indices (starting from 16777216)", &userIndices,
38 		"version", "Print nwn-lib-d version and exit", &printVersion,
39 		);
40 	if(res.helpWanted){
41 		improvedGetoptPrinter(
42 			"Parsing and serialization tool for TLK files",
43 			res.options);
44 		return 0;
45 	}
46 	if(printVersion){
47 		import nwn.ver: NWN_LIB_D_VERSION;
48 		writeln(NWN_LIB_D_VERSION);
49 		return 0;
50 	}
51 
52 	if(inputPath is null){
53 		enforce(args.length > 1, "No input file provided");
54 		enforce(args.length <= 2, "Too many input files provided");
55 		inputPath = args[1];
56 	}
57 
58 
59 	size_t offset = 0;
60 	if(baseIndices || userIndices){
61 		enforce(baseIndices ^ userIndices,
62 			"You cannot provide both --base and --user");
63 		if(userIndices)
64 			offset = 16777216;
65 	}
66 	else{
67 		if(!inputPath.baseName.stripExtension.toLower.startsWith("dialog"))
68 			offset = 16777216;
69 	}
70 
71 	if(inputFormat == Format.detect){
72 		if(inputPath is null)
73 			inputFormat = Format.tlk;
74 		else
75 			inputFormat = guessFormat(inputPath);
76 	}
77 	if(outputFormat == Format.detect){
78 		if(outputPath is null)
79 			outputFormat = Format.text;
80 		else
81 			outputFormat = guessFormat(outputPath);
82 	}
83 
84 	//Parsing
85 	Tlk tlk;
86 	File inputFile = inputPath is null? stdin : File(inputPath, "r");
87 
88 	switch(inputFormat){
89 		case Format.tlk:
90 			tlk = new Tlk(inputFile.readAll);
91 			break;
92 		default:
93 			assert(0, inputFormat.to!string~" parsing not implemented");
94 	}
95 	inputFile.close();
96 
97 	//Serialization
98 	File outputFile = outputPath is null? stdout : File(outputPath, "w");
99 	switch(outputFormat){
100 		case Format.text:
101 			import std.math: log10;
102 			int idColLength = cast(int)log10(offset + tlk.length) + 1;
103 			foreach(strref, str ; tlk){
104 				import std.typecons: Yes;
105 				foreach(i, ref line ; str.splitLines(Yes.keepTerminator)){
106 					outputFile.write((i == 0? (offset + strref).to!string : null).rightJustify(idColLength), "|", line);
107 				}
108 				outputFile.writeln();
109 			}
110 			break;
111 
112 		default:
113 			assert(0, outputFormat.to!string~" serialization not implemented");
114 	}
115 	return 0;
116 }
117 
118 enum Format{ detect, tlk, text }
119 
120 Format guessFormat(in string fileName){
121 	import std.path: extension;
122 	import std.string: toLower;
123 	assert(fileName !is null);
124 
125 	immutable ext = fileName.extension.toLower;
126 	switch(ext){
127 		case ".tlk":
128 			return Format.tlk;
129 		case ".txt":
130 			return Format.text;
131 		default:
132 			throw new ArgException("Unrecognized file extension: '"~ext~"'");
133 	}
134 
135 }
136 
137 ubyte[] readAll(File stream){
138 	ubyte[] data;
139 	ubyte[500] buf;
140 	ubyte[] dataRead;
141 
142 	do{
143 		dataRead = stream.rawRead(buf);
144 		data ~= dataRead;
145 	}while(dataRead.length>0);
146 
147 	return data;
148 }
149 
150 
151 unittest {
152 	import std.file;
153 	import std.path;
154 
155 	auto stdout_ = stdout;
156 	auto tmpOut = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".out");
157 	stdout = File(tmpOut, "w");
158 	scope(success) std.file.remove(tmpOut);
159 	scope(exit) stdout = stdout_;
160 
161 	assertThrown(main(["nwn-tlk"]));
162 	assert(main(["nwn-tlk","--help"]) == 0);
163 	assert(main(["nwn-tlk","--version"]) == 0);
164 
165 
166 	// Parsing => pretty
167 	stdout.reopen(null, "w");
168 	assert(main(["nwn-tlk","../../unittest/dialog.tlk"]) == 0);
169 	stdout.flush();
170 	assert(tmpOut.readText.splitLines.length == 76);
171 	assert(tmpOut.readText.splitLines[0] == " 0|Bad Strref");
172 
173 	stdout.reopen(null, "w");
174 	assert(main(["nwn-tlk","../../unittest/user.tlk"]) == 0);
175 	stdout.flush();
176 	assert(tmpOut.readText.splitLines.length == 2);
177 	assert(tmpOut.readText.splitLines[0] == "16777216|Hello world");
178 
179 }