1 /// Authors: Thibaut CHARLES (Crom) cromfr@gmail.com 2 /// License: GPL-3.0 3 /// Copyright: Copyright Thibaut CHARLES 2016 4 5 module tools.nwnbdb; 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.file; 13 import std.exception; 14 import std.algorithm; 15 import std.math: log10; 16 import std.typecons: Tuple, Nullable; 17 import std.json; 18 import std.base64: Base64; 19 import std.regex; 20 version(unittest) import std.exception: assertThrown, assertNotThrown; 21 22 import tools.common.getopt; 23 import nwn.biowaredb; 24 25 26 class ArgException : Exception{ 27 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 28 super(msg, f, l, t); 29 } 30 } 31 32 int main(string[] args){ 33 if(args.length >= 2 && (args[1] == "-h" || args[1] == "--help")){ 34 writeln("Bioware database (foxpro .dbf) tool"); 35 writefln("Usage: %s (search)", args[0].baseName); 36 return args.length <= 1; 37 } 38 if(args.any!(a => a == "--version")){ 39 import nwn.ver: NWN_LIB_D_VERSION; 40 writeln(NWN_LIB_D_VERSION); 41 return 0; 42 } 43 enforce(args.length >= 2, "No subcommand provided"); 44 45 immutable command = args[1]; 46 args = args[0] ~ args[2..$]; 47 48 switch(command){ 49 default: 50 writefln("Unknown sub-command '%s'. See --help", command); 51 return 1; 52 case "search":{ 53 string playerid; 54 bool moduleOnly = false; 55 string name = r".*"; 56 enum DeletedFlag: int { no = 0, yes = 1, any } 57 DeletedFlag deleted = DeletedFlag.any; 58 string varTypeStr; 59 bool noHeader; 60 enum OutputFormat { text, json } 61 OutputFormat outputFormat; 62 63 64 auto res = getopt(args, 65 "module", "Search only for module variables (vars not linked to a player character)", &moduleOnly, 66 "playerid", "Player ID regex. Player ID is build by appending AccountName + CharacterName, truncated to 32 characters.", &playerid, 67 "type", "variable type. Valid values are: Int, Float, String, Vector, Location, Object.", &varTypeStr, 68 "name", "Variable name regex", &name, 69 "deleted", "Search for either deleted variables or not. Valid values are: any (default), yes, no", &deleted, 70 "no-header", "Do not show column header", &noHeader, 71 "k|output-format", "Output format. Defaults to text. Valid values are: text, json", &outputFormat, 72 ); 73 if(res.helpWanted || args.length != 2){ 74 improvedGetoptPrinter( 75 "Search specific variables inside a database file\n" 76 ~"Example: " ~ args[0].baseName ~ " search foxpro-file-basename --name='^VAR_NAME_.*'", 77 res.options); 78 return 1; 79 } 80 81 if(moduleOnly) 82 enforce(playerid is null, "--module cannot be used with --playerid"); 83 84 if(playerid is null) 85 playerid = r".*"; 86 87 Nullable!(BiowareDB.VarType) varType; 88 if(varTypeStr !is null) 89 varType = varTypeStr.to!(BiowareDB.VarType); 90 91 92 auto dbName = args[1]; 93 if(dbName.extension.toLower == ".dbf") 94 dbName = dbName.stripExtension; 95 96 const db = new BiowareDB(dbName, false); 97 auto dbLen = db.length; 98 int indexLength = cast(int)log10(dbLen) + 1; 99 if(indexLength < 3) 100 indexLength = 3; 101 102 auto rgxPlayerID = regex(playerid); 103 auto rgxName = regex(name); 104 105 if(!noHeader && outputFormat == OutputFormat.text){ 106 writefln("%s %s %-20s %-32s %-8s %32s %s", 107 "D", 108 "Idx".leftJustify(indexLength), 109 "Timestamp", 110 "Character ID", 111 "Type", 112 "Variable name", 113 "Variable Value", 114 ); 115 writeln("------------------------------------------------------------------------------------------------------------------------"); 116 } 117 auto jsonValue = JSONValue(cast(JSONValue[])null); 118 119 foreach(var ; db){ 120 if(deleted != DeletedFlag.any && var.deleted != deleted) 121 continue; 122 if(!varType.isNull && varType.get != var.type) 123 continue; 124 125 if(moduleOnly){ 126 if(var.playerid.toString != "") 127 continue; 128 } 129 else{ 130 auto playerIDCap = var.playerid.toString.matchFirst(rgxPlayerID); 131 if(playerIDCap.empty) 132 continue; 133 } 134 135 auto nameCap = var.name.matchFirst(rgxName); 136 if(nameCap.empty) 137 continue; 138 139 140 if(outputFormat == OutputFormat.text){ 141 string value; 142 final switch(var.type) with(BiowareDB.VarType){ 143 case Int: value = db.getVariableValue!NWInt(var.index).to!string; break; 144 case Float: value = db.getVariableValue!NWFloat(var.index).to!string; break; 145 case String: value = db.getVariableValue!NWString(var.index).to!string; break; 146 case Vector: value = db.getVariableValue!NWVector(var.index).to!string; break; 147 case Location: value = db.getVariableValue!NWLocation(var.index).to!string; break; 148 case Object: value = Base64.encode(db.getVariableValue!(nwn.biowaredb.BinaryObject)(var.index)); break; 149 } 150 151 writefln("%s %s %20s %-32s %-8s %32s = %s", 152 var.deleted ? "D" : " ", 153 var.index.to!string.leftJustify(indexLength), 154 var.timestamp.toSimpleString, 155 var.playerid.to!string, 156 var.type, 157 var.name, 158 value, 159 ); 160 } 161 else if(outputFormat == OutputFormat.json){ 162 JSONValue varValue; 163 varValue["deleted"] = var.deleted; 164 varValue["index"] = var.index; 165 varValue["timestamp"] = var.timestamp.toISOString; 166 varValue["pcid"] = var.playerid.to!string; 167 varValue["type"] = var.type; 168 varValue["name"] = var.name; 169 final switch(var.type) with(BiowareDB.VarType){ 170 case Int: varValue["value"] = db.getVariableValue!NWInt(var.index); break; 171 case Float: varValue["value"] = db.getVariableValue!NWFloat(var.index); break; 172 case String: varValue["value"] = db.getVariableValue!NWString(var.index); break; 173 case Vector: varValue["value"] = db.getVariableValue!NWVector(var.index).value; break; 174 case Location: varValue["value"] = db.getVariableValue!NWLocation(var.index).to!string; break; 175 case Object: varValue["value"] = Base64.encode(db.getVariableValue!(nwn.biowaredb.BinaryObject)(var.index)); break; 176 } 177 jsonValue.array ~= varValue; 178 } 179 } 180 181 if(outputFormat == OutputFormat.json){ 182 writeln(jsonValue.toPrettyString); 183 } 184 } 185 break; 186 } 187 return 0; 188 } 189 190 191 unittest { 192 auto stdout_ = stdout; 193 auto tmpOut = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__); 194 stdout = File(tmpOut, "w"); 195 scope(success) tmpOut.remove(); 196 scope(exit) stdout = stdout_; 197 198 199 assertThrown(main(["nwn-bdb"]) != 0); 200 assert(main(["nwn-bdb","--help"]) == 0); 201 assert(main(["nwn-bdb","--version"]) == 0); 202 203 // List everything 204 stdout.reopen(null, "w"); 205 assert(main(["nwn-bdb", "search", "../../unittest/testcampaign", "--no-header"]) == 0); 206 stdout.flush(); 207 assert(tmpOut.readText.splitLines.length == 8); 208 209 // List module vars 210 stdout.reopen(null, "w"); 211 assert(main(["nwn-bdb", "search", "../../unittest/testcampaign", "--no-header", "--module"]) == 0); 212 stdout.flush(); 213 assert(tmpOut.readText.splitLines.length == 6); 214 215 // Search player vars 216 stdout.reopen(null, "w"); 217 assert(main(["nwn-bdb", "search", "../../unittest/testcampaign", "--no-header", "--playerid", "Crom 2"]) == 0); 218 stdout.flush(); 219 assert(tmpOut.readText.splitLines.length == 2); 220 221 // Search var names 222 stdout.reopen(null, "w"); 223 assert(main(["nwn-bdb", "search", "../../unittest/testcampaign", "--no-header", "--name", "ThisIs"]) == 0); 224 stdout.flush(); 225 assert(tmpOut.readText.splitLines.length == 5); 226 227 // Output json 228 stdout.reopen(null, "w"); 229 assert(main(["nwn-bdb", "search", "../../unittest/testcampaign", "--output-format", "json"]) == 0); 230 stdout.flush(); 231 assert(tmpOut.readText.parseJSON.array.length == 8); 232 }