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 }