1 /** Bioware campaign database (Foxpro db) 2 * Macros: 3 * INDEX = Index of the variable in the table 4 * VARNAME = Campaign variable name 5 * TYPE = Type of the variable. Must match the stored variable type. 6 * PCID = player character identifier<br/> 7 * Should be his account name concatenated with his character name<br/> 8 * $(D null) for module variables 9 * 10 * ACCOUNT = Player account name<br/> 11 * $(D null) for module variables 12 * CHARACTER = Character name.<br/> 13 * $(D null) for module variables 14 * 15 * Examples: 16 * -------------------- 17 * // Open ./yourdatabasename.dbf, ./yourdatabasename.cdx, ./yourdatabasename.fpt 18 * auto db = new BiowareDB("./yourdatabasename"); 19 * 20 * // Set a campaign variable associated to a character 21 * db.setVariableValue("YourAccount", "YourCharName", "TestFloat", 42.0f); 22 * 23 * // Set a campaign variable associated with the module 24 * db.setVariableValue(null, null, "TestVector", NWVector([1.0f, 2.0f, 3.0f])); 25 * 26 * // Retrieve variable information 27 * auto var = db.getVariable("YourAccount", "YourCharName", "TestFloat").get(); 28 * 29 * // Retrieve variable value using its index (fast) 30 * float f = db.getVariableValue!NWFloat(var.index); 31 * 32 * // Retrieve variable value by searching it 33 * NWVector v = db.getVariableValue!NWVector(null, null, "TestVector").get(); 34 * 35 * // Iterate over all variables (using variable info) 36 * foreach(varinfo ; db){ 37 * if(varinfo.deleted == false) 38 * // Variable exists 39 * } 40 * else{ 41 * // Variable has been deleted, skip it 42 * continue; 43 * } 44 * } 45 * 46 * // Save changes 47 * auto serialized = db.serialize(); 48 * std.file.write("./yourdatabasename.dbf", serialized.dbf); 49 * std.file.write("./yourdatabasename.fpt", serialized.fpt); 50 * 51 * -------------------- 52 */ 53 module nwn.biowaredb; 54 55 public import nwn.types; 56 57 import std.stdio: File, stderr; 58 import std.stdint; 59 import std.conv: to; 60 import std.datetime: Clock, DateTime; 61 import std.typecons: Tuple, Nullable; 62 import std.string; 63 import std.exception: enforce; 64 import std.json; 65 import nwnlibd.parseutils; 66 67 debug import std.stdio: writeln; 68 version(unittest) import std.exception: assertThrown, assertNotThrown; 69 70 71 /// Type of the GFF raw data stored when using StoreCampaignObject 72 alias BinaryObject = ubyte[]; 73 74 75 /// 76 class BiowareDBException : Exception{ 77 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 78 super(msg, f, l, t); 79 } 80 } 81 82 /// ID used by Bioware Database to 'uniquely' identify a specific character. Can be used as a char[32]. 83 struct PCID { 84 /// Standard way to create a PCID. 85 /// 86 /// Will create a char[32] containing at most 16 chars from the account and 16 chars from the char name, right-filled with spaces. 87 this(in string accountName, in string charName){ 88 string pcidTmp; 89 pcidTmp ~= accountName[0 .. accountName.length <= 16? $ : 16]; 90 pcidTmp ~= charName[0 .. charName.length <= 16? $ : 16]; 91 92 if(pcidTmp.length < 32){ 93 pcid[0 .. pcidTmp.length] = pcidTmp; 94 pcid[pcidTmp.length .. $] = ' '; 95 } 96 else 97 pcid = pcidTmp[0 .. 32]; 98 } 99 100 /// Create with existing PCID 101 this(in char[32] pcid){ 102 this.pcid = pcid; 103 } 104 105 106 /// Easily readable PCID, with spaces stripped 107 string toString() const { 108 import std.string: stripRight; 109 return pcid.stripRight.idup(); 110 } 111 112 alias pcid this; 113 char[32] pcid = " "; 114 } 115 116 117 /// Bioware database (in FoxPro format, ie dbf, cdx and ftp files) 118 class BiowareDB{ 119 120 /// Constructor with raw data 121 /// Note: data will be copied inside the class 122 this(in ubyte[] dbfData, in ubyte[] cdxData, in ubyte[] fptData, bool buildIndex = true){ 123 table.data = dbfData.dup(); 124 //index.data = null;//Not used 125 memo.data = fptData.dup(); 126 127 if(buildIndex) 128 buildTableIndex(); 129 } 130 131 /// Constructor with file paths 132 this(in string dbfPath, in string cdxPath, in string fptPath, bool buildIndex = true){ 133 import std.stdio: File; 134 135 auto dbf = File(dbfPath, "r"); 136 table.data.length = dbf.size.to!size_t; 137 table.data = dbf.rawRead(table.data); 138 139 auto fpt = File(fptPath, "r"); 140 memo.data.length = fpt.size.to!size_t; 141 memo.data = fpt.rawRead(memo.data); 142 143 if(buildIndex) 144 buildTableIndex(); 145 } 146 147 /// Constructor with file path without its extension. It will try to open the dbf and ftp files. 148 this(in string dbFilesPath, bool buildIndex = true){ 149 this( 150 dbFilesPath~".dbf", 151 null,//Not used 152 dbFilesPath~".fpt", 153 buildIndex 154 ); 155 } 156 157 /// Returns a tuple with dbf and fpt raw data (accessible with .dbf and .fpt) 158 /// Warning: Does not serialize cdx file 159 auto serialize(){ 160 //TODO: check if serialization does not break nwn2 since CDX isn't generated 161 return Tuple!(const ubyte[], "dbf", const ubyte[], "fpt")(table.data, memo.data); 162 } 163 164 165 /// Type of a stored variable 166 enum VarType : char{ 167 Int = 'I', 168 Float = 'F', 169 String = 'S', 170 Vector = 'V', 171 Location = 'L', 172 Object = 'O', 173 } 174 /// Convert a BiowareDB.VarType into the associated native type 175 template toVarType(T){ 176 static if(is(T == NWInt)) alias toVarType = VarType.Int; 177 else static if(is(T == NWFloat)) alias toVarType = VarType.Float; 178 else static if(is(T == NWString)) alias toVarType = VarType.String; 179 else static if(is(T == NWVector)) alias toVarType = VarType.Vector; 180 else static if(is(T == NWLocation)) alias toVarType = VarType.Location; 181 else static if(is(T == BinaryObject)) alias toVarType = VarType.Object; 182 else static assert(0); 183 } 184 /// Representation of a stored variable 185 static struct Variable{ 186 size_t index; 187 bool deleted; 188 189 string name; 190 PCID playerid; 191 DateTime timestamp; 192 193 VarType type; 194 } 195 196 /// Search and return the index of a variable 197 /// 198 /// Expected O(1). 199 /// Params: 200 /// pcid = $(PCID) 201 /// varName = $(VARNAME) 202 /// Returns: `null` if not found 203 Nullable!size_t getVariableIndex(in PCID pcid, in string varName) const{ 204 if(auto i = Key(pcid, varName) in index) 205 return Nullable!size_t(*i); 206 return Nullable!size_t(); 207 } 208 209 /// Search and return the index of a variable 210 /// 211 /// Expected O(1). 212 /// Params: 213 /// account = $(ACCOUNT) 214 /// character = $(CHARACTER) 215 /// varName = $(VARNAME) 216 /// Returns: `null` if not found 217 Nullable!size_t getVariableIndex(in string account, in string character, in string varName) const{ 218 return getVariableIndex(PCID(account, character), varName); 219 } 220 221 /// Get the variable value at `index` 222 /// Note: Can be used to retrieve deleted variable values. 223 /// Params: 224 /// T = $(TYPE) 225 /// index = $(INDEX) 226 /// Returns: the variable value 227 /// Throws: BiowareDBException if stored type != requested type 228 const(T) getVariableValue(T)(size_t index) const 229 if(is(T == NWInt) || is(T == NWFloat) || is(T == NWString) || is(T == NWVector) || is(T == NWLocation) || is(T == BinaryObject)) 230 { 231 auto record = table.getRecord(index); 232 char type = record[RecOffset.VarType]; 233 234 enforce!BiowareDBException(type == toVarType!T, 235 "Variable is not a "~T.stringof); 236 237 static if(is(T == NWInt)){ 238 return (cast(const char[])record[RecOffset.Int .. RecOffset.IntEnd]).strip().to!T; 239 } 240 else static if(is(T == NWFloat)){ 241 return (cast(const char[])record[RecOffset.DBL1 .. RecOffset.DBL1End]).strip().to!T; 242 } 243 else static if(is(T == NWString)){ 244 auto memoIndexStr = (cast(const char[])record[RecOffset.Memo .. RecOffset.MemoEnd]).strip(); 245 if(memoIndexStr.length == 0) 246 return null; 247 return (cast(const char[])memo.getBlockContent(memoIndexStr.to!size_t)).to!string; 248 } 249 else static if(is(T == NWVector)){ 250 return NWVector([ 251 (cast(const char[])record[RecOffset.DBL1 .. RecOffset.DBL1End]).strip().to!NWFloat, 252 (cast(const char[])record[RecOffset.DBL2 .. RecOffset.DBL2End]).strip().to!NWFloat, 253 (cast(const char[])record[RecOffset.DBL3 .. RecOffset.DBL3End]).strip().to!NWFloat, 254 ]); 255 } 256 else static if(is(T == NWLocation)){ 257 import std.math: atan2, PI; 258 auto facing = atan2( 259 (cast(const char[])record[RecOffset.DBL5 .. RecOffset.DBL5End]).strip().to!double, 260 (cast(const char[])record[RecOffset.DBL4 .. RecOffset.DBL4End]).strip().to!double); 261 262 return NWLocation( 263 (cast(const char[])record[RecOffset.Int .. RecOffset.IntEnd]).strip().to!NWObject, 264 NWVector([ 265 (cast(const char[])record[RecOffset.DBL1 .. RecOffset.DBL1End]).strip().to!NWFloat, 266 (cast(const char[])record[RecOffset.DBL2 .. RecOffset.DBL2End]).strip().to!NWFloat, 267 (cast(const char[])record[RecOffset.DBL3 .. RecOffset.DBL3End]).strip().to!NWFloat, 268 ]), 269 facing * 180.0 / PI 270 ); 271 } 272 else static if(is(T == BinaryObject)){ 273 auto memoIndexStr = (cast(const char[])record[RecOffset.Memo .. RecOffset.MemoEnd]).strip(); 274 if(memoIndexStr.length == 0) 275 return null; 276 return memo.getBlockContent(memoIndexStr.to!size_t); 277 } 278 else static assert(0); 279 } 280 281 const(string) getVariableValueString(size_t index) const{ 282 const record = table.getRecord(index); 283 const type = record[RecOffset.VarType].to!VarType; 284 285 final switch(type) with(VarType){ 286 case Int: 287 return getVariableValue!NWInt(index).to!string; 288 case Float: 289 return getVariableValue!NWFloat(index).to!string; 290 case String: 291 return getVariableValue!NWString(index).to!string; 292 case Vector: 293 return getVariableValue!NWVector(index).toString; 294 case Location: 295 return getVariableValue!NWLocation(index).toString; 296 case Object: 297 import std.base64: Base64; 298 return Base64.encode(getVariableValue!BinaryObject(index)); 299 } 300 } 301 302 JSONValue getVariableValueJSON(size_t index) const{ 303 JSONValue ret = cast(JSONValue[string])null; 304 305 const record = table.getRecord(index); 306 const type = record[RecOffset.VarType].to!VarType; 307 308 final switch(type) with(VarType){ 309 case Int: 310 return JSONValue(getVariableValue!NWInt(index)); 311 case Float: 312 return JSONValue(getVariableValue!NWFloat(index)); 313 case String: 314 return JSONValue(getVariableValue!NWString(index)); 315 case Vector: 316 const v = getVariableValue!NWVector(index); 317 return JSONValue(v.value); 318 case Location: 319 const l = getVariableValue!NWLocation(index); 320 return JSONValue([ 321 "area": JSONValue(l.area), 322 "position": JSONValue(l.position.value), 323 "facing": JSONValue(l.facing), 324 ]); 325 case Object: 326 import std.base64: Base64; 327 return JSONValue(Base64.encode(getVariableValue!BinaryObject(index))); 328 } 329 } 330 331 332 /// Search and return the value of a variable 333 /// 334 /// Expected O(1). 335 /// Params: 336 /// T = $(TYPE) 337 /// pcid = $(PCID) 338 /// varName = $(VARNAME) 339 /// Returns: the variable value, or null if not found 340 Nullable!(const(T)) getVariableValue(T)(in PCID pcid, in string varName) const 341 if(is(T == NWInt) || is(T == NWFloat) || is(T == NWString) || is(T == NWVector) || is(T == NWLocation) || is(T == BinaryObject)) 342 { 343 auto idx = getVariableIndex(pcid, varName); 344 if(idx.isNull == false) 345 return Nullable!(const(T))(getVariableValue!T(idx.get)); 346 return Nullable!(const(T))(); 347 } 348 349 /// Search and return the value of a variable 350 /// 351 /// Expected O(1). 352 /// Params: 353 /// T = $(TYPE) 354 /// account = $(ACCOUNT) 355 /// character = $(CHARACTER) 356 /// varName = $(VARNAME) 357 /// Returns: the variable value, or null if not found 358 Nullable!(const(T)) getVariableValue(T)(in string account, in string character, in string varName) const 359 if(is(T == NWInt) || is(T == NWFloat) || is(T == NWString) || is(T == NWVector) || is(T == NWLocation) || is(T == BinaryObject)) 360 { 361 return getVariableValue(PCID(account, character), varName); 362 } 363 364 /// Get variable information using its index 365 /// 366 /// Note: Be sure to check `Variable.deleted` value 367 /// Params: 368 /// index = $(INDEX) 369 /// Returns: the variable information 370 Variable getVariable(size_t index) const{ 371 auto record = table.getRecord(index); 372 auto ts = cast(const char[])record[RecOffset.Timestamp .. RecOffset.TimestampEnd]; 373 374 return Variable( 375 index, 376 record[0] == Table.DeletedFlag.True, 377 (cast(const char[])record[RecOffset.VarName .. RecOffset.VarNameEnd]).strip().to!string, 378 PCID(cast(char[32])record[RecOffset.PlayerID .. RecOffset.PlayerIDEnd]), 379 DateTime( 380 ts[6..8].to!int + 2000, 381 ts[0..2].to!int, 382 ts[3..5].to!int, 383 ts[8..10].to!int, 384 ts[11..13].to!int, 385 ts[14..16].to!int), 386 record[RecOffset.VarType].to!VarType, 387 ); 388 } 389 390 /// Search and return variable information 391 /// 392 /// Expected O(1). 393 /// Params: 394 /// pcid = $(PCID) 395 /// varName = $(VARNAME) 396 /// Returns: the variable information, or null if not found 397 Nullable!Variable getVariable(in PCID pcid, in string varName) const{ 398 auto idx = getVariableIndex(pcid, varName); 399 if(idx.isNull == false) 400 return Nullable!Variable(getVariable(idx.get)); 401 return Nullable!Variable(); 402 } 403 404 /// Search and return variable information 405 /// 406 /// Expected O(1). 407 /// Params: 408 /// account = $(ACCOUNT) 409 /// character = $(CHARACTER) 410 /// varName = $(VARNAME) 411 /// Returns: the variable information, or null if not found 412 Nullable!Variable getVariable(in string account, in string character, in string varName) const{ 413 return getVariable(PCID(account, character), varName); 414 } 415 416 417 /// Set the value of an existing variable using its index. 418 /// 419 /// Params: 420 /// T = $(TYPE) 421 /// index = $(INDEX) 422 /// value = value to set 423 /// updateTimestamp = true to change the variable modified date, false to keep current value 424 void setVariableValue(T)(size_t index, in T value, bool updateTimestamp = true) 425 if(is(T == NWInt) || is(T == NWFloat) || is(T == NWString) || is(T == NWVector) || is(T == NWLocation) || is(T == BinaryObject)) 426 { 427 auto record = table.getRecord(index); 428 char type = record[RecOffset.VarType]; 429 430 enforce!BiowareDBException(type == toVarType!T, 431 "Variable is not a "~T.stringof); 432 433 import std.string: leftJustify, format; 434 435 with(RecOffset){ 436 static if(is(T == NWInt)){ 437 record[Int .. IntEnd] = 438 cast(ubyte[])value.to!string.leftJustify(IntEnd - Int); 439 } 440 else static if(is(T == NWFloat)){ 441 record[DBL1 .. DBL1End] = 442 cast(ubyte[])value.to!string.leftJustify(DBL1End - DBL1); 443 } 444 else static if(is(T == NWString) || is(T == BinaryObject)){ 445 auto oldMemoIndexStr = (cast(const char[])record[Memo .. MemoEnd]).strip(); 446 auto oldMemoIndex = oldMemoIndexStr != ""? oldMemoIndexStr.to!size_t : 0; 447 448 auto memoIndex = memo.setBlockValue(cast(const ubyte[])value, oldMemoIndex); 449 450 record[Memo .. MemoEnd] = 451 cast(ubyte[])memoIndex.to!string.leftJustify(MemoEnd - Memo); 452 } 453 else static if(is(T == NWVector)){ 454 record[DBL1 .. DBL1End] = 455 cast(ubyte[])value[0].to!string.leftJustify(DBL1End - DBL1); 456 record[DBL2 .. DBL2End] = 457 cast(ubyte[])value[1].to!string.leftJustify(DBL2End - DBL2); 458 record[DBL3 .. DBL3End] = 459 cast(ubyte[])value[2].to!string.leftJustify(DBL3End - DBL3); 460 } 461 else static if(is(T == NWLocation)){ 462 import std.math: cos, sin, PI; 463 464 record[Int .. IntEnd] = 465 cast(ubyte[])value.area.to!string.leftJustify(IntEnd - Int); 466 467 record[DBL1 .. DBL1End] = 468 cast(ubyte[])value.position[0].to!string.leftJustify(DBL1End - DBL1); 469 record[DBL2 .. DBL2End] = 470 cast(ubyte[])value.position[1].to!string.leftJustify(DBL2End - DBL2); 471 record[DBL3 .. DBL3End] = 472 cast(ubyte[])value.position[2].to!string.leftJustify(DBL3End - DBL3); 473 474 immutable float facingx = cos(value.facing * PI / 180.0) * 180.0 / PI; 475 immutable float facingy = sin(value.facing * PI / 180.0) * 180.0 / PI; 476 477 record[DBL4 .. DBL4End] = 478 cast(ubyte[])facingx.to!string.leftJustify(DBL4End - DBL4); 479 record[DBL5 .. DBL5End] = 480 cast(ubyte[])facingy.to!string.leftJustify(DBL5End - DBL5); 481 record[DBL6 .. DBL6End] = 482 cast(ubyte[])"0.0".leftJustify(DBL6End - DBL6); 483 } 484 else static assert(0); 485 486 //Update timestamp 487 if(updateTimestamp){ 488 auto now = cast(DateTime)Clock.currTime; 489 immutable ts = format("%02d/%02d/%02d%02d:%02d:%02d", 490 now.month, 491 now.day, 492 now.year-2000, 493 now.hour, 494 now.minute, 495 now.second); 496 record[Timestamp .. TimestampEnd] = 497 cast(ubyte[])ts.leftJustify(TimestampEnd - Timestamp); 498 } 499 500 } 501 } 502 503 /// Set / create a variable with its value 504 /// 505 /// Params: 506 /// T = $(TYPE) 507 /// pcid = $(PCID) 508 /// varName = $(VARNAME) 509 /// value = value to set 510 /// updateTimestamp = true to change the variable modified date, false to keep current value 511 void setVariableValue(T)(in PCID pcid, in string varName, in T value, bool updateTimestamp = true) 512 if(is(T == NWInt) || is(T == NWFloat) || is(T == NWString) || is(T == NWVector) || is(T == NWLocation) || is(T == BinaryObject)) 513 { 514 auto existingIndex = getVariableIndex(pcid, varName); 515 if(existingIndex.isNull == false){ 516 //Reuse existing var 517 setVariableValue(existingIndex.get, value, updateTimestamp); 518 } 519 else{ 520 //new var 521 auto index = table.addRecord(); 522 auto record = table.getRecord(index); 523 record[0..$][] = ' '; 524 525 with(RecOffset){ 526 record[VarName .. VarName + varName.length] = cast(const ubyte[])varName; 527 record[PlayerID .. PlayerID + 32] = cast(const ubyte[])pcid; 528 record[VarType] = toVarType!T; 529 530 setVariableValue(index, value, true); 531 } 532 533 this.index[Key(pcid, varName)] = index; 534 } 535 536 } 537 538 539 /// Remove a variable 540 /// 541 /// Note: Only marks the variable as deleted. Data can still be accessed using the variable index. 542 /// Params: 543 /// index = $(INDEX) 544 void deleteVariable(size_t index){ 545 auto var = this[index]; 546 547 this.index.remove(Key(var.playerid, var.name)); 548 table.getRecord(index)[0] = '*'; 549 } 550 551 /// Remove a variable 552 /// 553 /// Note: Only marks the variable as deleted. Data can still be accessed using the variable index. 554 /// Params: 555 /// pcid = $(PCID) 556 /// varName = $(VARNAME) 557 void deleteVariable(in PCID pcid, in string varName){ 558 auto var = this[pcid, varName]; 559 560 enforce!BiowareDBException(var.isNull == false, 561 "Variable not found"); 562 563 this.index.remove(Key(var.get.playerid, var.get.name)); 564 table.getRecord(var.get.index)[0] = '*'; 565 } 566 567 568 /// Alias for `getVariable` 569 alias opIndex = getVariable; 570 571 /// Number of variables (both active an deleted) stored in the database 572 @property size_t length() const{ 573 return table.header.records_count; 574 } 575 576 /// Iterate over all variables (both active and deleted) 577 /// Note: You need to check `Variable.deleted` value. 578 int opApply(scope int delegate(in Variable) dlg) const{ 579 int res = 0; 580 foreach(i ; 0 .. length){ 581 res = dlg(getVariable(i)); 582 if(res != 0) break; 583 } 584 return res; 585 } 586 /// ditto 587 int opApply(scope int delegate(size_t, in Variable) dlg) const{ 588 int res = 0; 589 foreach(i ; 0 .. length){ 590 res = dlg(i, getVariable(i)); 591 if(res != 0) break; 592 } 593 return res; 594 } 595 596 597 private: 598 Table table;//dbf 599 //Index index;//cdx 600 Memo memo;//fpt 601 602 struct Key{ 603 this(in PCID pcid, in string var){ 604 this.pcid = pcid; 605 606 if(var.length <= 32){ 607 this.var[0 .. var.length] = var; 608 this.var[var.length .. $] = ' '; 609 } 610 else 611 this.var = var[0 .. 32]; 612 } 613 char[32] pcid; 614 char[32] var; 615 } 616 size_t[Key] index = null; 617 void buildTableIndex(){ 618 foreach(i ; 0..table.header.records_count){ 619 auto record = table.getRecord(i); 620 621 if(record[0] == Table.DeletedFlag.False){ 622 //Not deleted 623 index[Key( 624 PCID(cast(char[32])record[RecOffset.PlayerID .. RecOffset.PlayerIDEnd]), 625 (cast(char[])record[RecOffset.VarName .. RecOffset.VarNameEnd]).to!string, 626 )] = i; 627 } 628 } 629 index.rehash(); 630 } 631 632 633 enum BDBColumn { 634 VarName, 635 PlayerID, 636 Timestamp, 637 VarType, 638 Int, 639 DBL1, 640 DBL2, 641 DBL3, 642 DBL4, 643 DBL5, 644 DBL6, 645 Memo, 646 } 647 enum RecOffset{ 648 VarName = 1, 649 VarNameEnd = PlayerID, 650 PlayerID = 1 + 32, 651 PlayerIDEnd = Timestamp, 652 Timestamp = 1 + 32 + 32, 653 TimestampEnd = VarType, 654 VarType = 1 + 32 + 32 + 16, 655 VarTypeEnd = Int, 656 Int = 1 + 32 + 32 + 16 + 1, 657 IntEnd = DBL1, 658 DBL1 = 1 + 32 + 32 + 16 + 1 + 10, 659 DBL1End = DBL2, 660 DBL2 = 1 + 32 + 32 + 16 + 1 + 10 + 20, 661 DBL2End = DBL3, 662 DBL3 = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20, 663 DBL3End = DBL4, 664 DBL4 = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20 + 20, 665 DBL4End = DBL5, 666 DBL5 = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20 + 20 + 20, 667 DBL5End = DBL6, 668 DBL6 = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20 + 20 + 20 + 20, 669 DBL6End = Memo, 670 Memo = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20 + 20 + 20 + 20 + 20, 671 MemoEnd = 1 + 32 + 32 + 16 + 1 + 10 + 20 + 20 + 20 + 20 + 20 + 20 + 10, 672 } 673 674 675 static struct Table{ 676 ubyte[] data; 677 678 enum DeletedFlag: char{ 679 False = ' ', 680 True = '*', 681 } 682 683 align(1) static struct Header{ 684 align(1): 685 static assert(this.sizeof == 32); 686 687 uint8_t file_type; 688 uint8_t[3] last_update;// Year+2000, month, day 689 uint32_t records_count;//Lines 690 uint16_t records_offset; 691 uint16_t record_size; 692 693 uint8_t[16] reserved0; 694 695 enum TableFlags: uint8_t{ 696 HasCDX = 0x01, 697 HasMemo = 0x02, 698 IsDBC = 0x04, 699 } 700 TableFlags table_flags; 701 uint8_t code_page_mark; 702 703 uint8_t[2] reserved1; 704 } 705 align(1) static struct FieldSubrecord{ 706 align(1): 707 static assert(this.sizeof == 32); 708 char[11] name; 709 enum SubrecordType: char{ 710 Character = 'C', 711 Currency = 'Y', 712 Numeric = 'N', 713 Float = 'F', 714 Date = 'D', 715 DateTime = 'T', 716 Double = 'B', 717 Integer = 'I', 718 Logical = 'L', 719 General = 'G', 720 Memo = 'M', 721 Picture = 'P', 722 } 723 SubrecordType field_type; 724 uint32_t field_offset; 725 uint8_t field_size; 726 uint8_t decimal_places; 727 enum SubrecordFlags: uint8_t{ 728 System = 0x01, 729 CanStoreNull = 0x02, 730 Binary = 0x04, 731 AutoIncrement = 0x0C, 732 } 733 SubrecordFlags field_flags; 734 uint32_t autoincrement_next; 735 uint8_t autoincrement_step; 736 uint8_t[8] reserved0; 737 } 738 739 @property{ 740 inout(Header)* header() inout{ 741 return cast(inout(Header)*)data.ptr; 742 } 743 version(none)//Unused: we assume fields follow BDB format 744 inout(FieldSubrecord[]) fieldSubrecords() inout{ 745 746 auto subrecordStart = cast(FieldSubrecord*)(data.ptr + Header.sizeof); 747 auto subrecord = subrecordStart; 748 749 size_t subrecordCount = 0; 750 while((cast(uint8_t*)subrecord)[0] != 0x0D){ 751 subrecordCount++; 752 subrecord++; 753 } 754 return cast(inout)subrecordStart[0..subrecordCount]; 755 } 756 inout(ubyte*) records() inout{ 757 return cast(inout)(data.ptr + header.records_offset); 758 } 759 } 760 inout(ubyte[]) getRecord(size_t i) inout{ 761 assert(i < header.records_count, "Out of bound"); 762 763 auto record = records + (i * header.record_size); 764 return cast(inout)record[0 .. header.record_size]; 765 } 766 size_t addRecord(){ 767 auto recordIndex = header.records_count; 768 data.length += header.record_size; 769 header.records_count++; 770 return recordIndex; 771 } 772 } 773 774 version(none) 775 static struct Index{ 776 ubyte[] data; 777 778 enum blockSize = 512; 779 780 align(1) static struct Header{ 781 align(1): 782 static assert(this.sizeof == blockSize); 783 784 uint32_t root_node_index; 785 uint32_t free_node_list_index; 786 uint32_t node_count_bigendian; 787 788 uint16_t key_length; 789 enum IndexFlag: uint8_t{ 790 UniqueIndex = 1, 791 HasForClause = 8, 792 } 793 IndexFlag index_flags; 794 uint8_t signature; 795 char[220] key_expression; 796 char[220] for_expression; 797 798 ubyte[56] unused0; 799 } 800 align(1) static struct Node{ 801 align(1): 802 static assert(this.sizeof == blockSize); 803 enum Attribute: uint16_t{ 804 Index = 0, 805 Root = 1, 806 Leaf = 2, 807 } 808 Attribute attributes; 809 uint16_t key_count; 810 uint32_t left_index_bigendian; 811 uint32_t right_index_bigendian; 812 char[500] key; 813 } 814 @property{ 815 Header* header(){ 816 return cast(Header*)data.ptr; 817 } 818 Node* nodes(){ 819 return cast(Node*)(data.ptr + blockSize); 820 } 821 Node* rootNode(){ 822 return &nodes[header.root_node_index]; 823 } 824 Node* freeNodeList(){ 825 return &nodes[header.free_node_list_index]; 826 } 827 828 } 829 Node* getLeft(in Node* node){ 830 return node.left_index_bigendian != uint32_t.max? 831 &nodes[node.left_index_bigendian.bigEndianToNative] : null; 832 } 833 Node* getRight(in Node* node){ 834 return node.right_index_bigendian != uint32_t.max? 835 &nodes[node.right_index_bigendian.bigEndianToNative] : null; 836 } 837 Node* findNode(in char[500] key){ 838 return null; 839 } 840 841 void showNode(Index.Node* node, in string name = null){ 842 import std.string; 843 writeln( 844 name,":", 845 " attr=", leftJustify(node.attributes.to!string, 20), 846 //" attr=", cast(Index.Node.Attribute)(cast(uint16_t)node.attributes).bigEndianToNative, 847 " nok=", leftJustify(node.key_count.to!string, 4), 848 " left=", node.left_index_bigendian.bigEndianToNative, " right=", node.right_index_bigendian.bigEndianToNative, 849 " key=", cast(ubyte[])node.key[0..10], " ... ", cast(ubyte[])node.key[$-10 .. $], 850 ); 851 } 852 } 853 854 static struct Memo{ 855 ubyte[] data; 856 857 align(1) static struct Header{ 858 align(1): 859 static assert(this.sizeof == 512); 860 861 uint32_t next_free_block_bigendian; 862 uint16_t unused0; 863 uint16_t block_size_bigendian; 864 uint8_t[504] unused1; 865 } 866 align(1) static struct Block{ 867 align(1): 868 static assert(this.sizeof == 8); 869 870 uint32_t signature_bigendian;//bit field with 0: picture, 1: text 871 uint32_t size_bigendian; 872 ubyte[0] data; 873 } 874 @property{ 875 inout(Header)* header() inout{ 876 return cast(inout(Header)*)data.ptr; 877 } 878 size_t blockCount() const{ 879 return (data.length - Header.sizeof) / header.block_size_bigendian.bigEndianToNative; 880 } 881 } 882 883 inout(Block)* getBlock(size_t i) inout{ 884 assert(i >= 1, "Memo indices starts at 1"); 885 assert(i < blockCount+1, "Out of bound"); 886 887 return cast(inout(Block)*) 888 (data.ptr + Header.sizeof 889 + (i-1) * header.block_size_bigendian.bigEndianToNative); 890 } 891 inout(ubyte[]) getBlockContent(size_t i) inout{ 892 auto block = getBlock(i); 893 return cast(inout)block.data.ptr[0 .. block.size_bigendian.bigEndianToNative]; 894 } 895 896 ///Return new index if it has been reallocated 897 size_t setBlockValue(in ubyte[] content, size_t previousIndex = 0){ 898 immutable blockSize = header.block_size_bigendian.bigEndianToNative; 899 900 //size_t requiredBlocks = content.length / blockSize + 1; 901 902 Block* block = null; 903 size_t blockIndex = 0; 904 905 if(previousIndex > 0){ 906 auto previousMemoBlock = getBlock(previousIndex); 907 auto previousBlocksWidth = previousMemoBlock.size_bigendian.bigEndianToNative / blockSize + 1; 908 909 if(content.length <= previousBlocksWidth * blockSize){ 910 //Available room in this block 911 block = previousMemoBlock; 912 blockIndex = previousIndex; 913 } 914 } 915 916 if(block is null){ 917 //New block needs to be allocated 918 auto requiredBlocks = content.length / blockSize + 1; 919 920 //Resize data 921 data.length += requiredBlocks * blockSize; 922 923 //Update header 924 immutable freeBlockIndex = header.next_free_block_bigendian.bigEndianToNative; 925 header.next_free_block_bigendian = (freeBlockIndex + requiredBlocks).to!uint32_t.nativeToBigEndian; 926 927 //Set pointer to new block 928 block = getBlock(freeBlockIndex); 929 blockIndex = freeBlockIndex; 930 } 931 932 block.signature_bigendian = 1.nativeToBigEndian;//BDB only store Text blocks 933 block.size_bigendian = content.length.to!uint32_t.nativeToBigEndian; 934 block.data.ptr[0 .. content.length] = content; 935 936 return blockIndex; 937 } 938 } 939 940 941 942 } 943 944 945 unittest{ 946 import std.range.primitives; 947 import std.math: fabs, approxEqual; 948 949 auto db = new BiowareDB( 950 cast(immutable ubyte[])import("testcampaign.dbf"), 951 cast(immutable ubyte[])import("testcampaign.cdx"), 952 cast(immutable ubyte[])import("testcampaign.fpt"), 953 ); 954 955 956 //Read checks 957 auto var = db[0]; 958 assert(var.deleted == false); 959 assert(var.name == "ThisIsAFloat"); 960 assert(var.playerid == PCID()); 961 assert(var.timestamp == DateTime(2017,06,25, 23,19,26)); 962 assert(var.type == 'F'); 963 assert(db.getVariableValue!NWFloat(var.index).approxEqual(13.37f)); 964 965 var = db[1]; 966 assert(var.deleted == false); 967 assert(var.name == "ThisIsAnInt"); 968 assert(var.playerid == PCID()); 969 assert(var.timestamp == DateTime(2017,06,25, 23,19,27)); 970 assert(var.type == 'I'); 971 assert(db.getVariableValue!NWInt(var.index) == 42); 972 973 var = db[2]; 974 assert(var.deleted == false); 975 assert(var.name == "ThisIsAVector"); 976 assert(var.playerid == PCID()); 977 assert(var.timestamp == DateTime(2017,06,25, 23,19,28)); 978 assert(var.type == 'V'); 979 assert(db.getVariableValue!NWVector(var.index) == [1.1f, 2.2f, 3.3f]); 980 981 var = db[3]; 982 assert(var.deleted == false); 983 assert(var.name == "ThisIsALocation"); 984 assert(var.playerid == PCID()); 985 assert(var.timestamp == DateTime(2017,06,25, 23,19,29)); 986 assert(var.type == 'L'); 987 auto loc = db.getVariableValue!NWLocation(var.index); 988 assert(loc.area == 61031); 989 assert(fabs(loc.position[0] - 103.060) <= 0.001); 990 assert(fabs(loc.position[1] - 104.923) <= 0.001); 991 assert(fabs(loc.position[2] - 40.080) <= 0.001); 992 assert(fabs(loc.facing - 62.314) <= 0.001); 993 994 var = db[4]; 995 assert(var.deleted == false); 996 assert(var.name == "ThisIsAString"); 997 assert(var.playerid == PCID()); 998 assert(var.timestamp == DateTime(2017,06,25, 23,19,30)); 999 assert(var.type == 'S'); 1000 assert(db.getVariableValue!NWString(var.index) == "Hello World"); 1001 1002 var = db[5]; 1003 assert(var.deleted == false); 1004 assert(var.name == "StoredObjectName"); 1005 assert(var.type == 'S'); 1006 assert(var.playerid == PCID("Crom 29", "Adaur Harbor")); 1007 1008 var = db[6]; 1009 assert(var.deleted == false); 1010 assert(var.name == "StoredObject"); 1011 assert(var.type == 'O'); 1012 import nwn.gff; 1013 auto gff = new Gff(db.getVariableValue!BinaryObject(var.index)); 1014 assert(gff["LocalizedName"].get!GffLocString.strref == 162153); 1015 1016 var = db[7]; 1017 assert(var.deleted == true); 1018 assert(var.name == "DeletedVarExample"); 1019 1020 1021 //Variable searching 1022 auto var2 = db.getVariable(null, null, "ThisIsAString").get; 1023 assert(var2.name == "ThisIsAString"); 1024 assert(var2.index == 4); 1025 assert(db.getVariableIndex(null, null, "ThisIsAString") == 4); 1026 1027 var2 = db.getVariable("Crom 29", "Adaur Harbor", "StoredObjectName").get; 1028 assert(var2.name == "StoredObjectName"); 1029 assert(var2.index == 5); 1030 1031 var2 = db["Crom 29", "Adaur Harbor", "StoredObjectName"].get; 1032 assert(var2.name == "StoredObjectName"); 1033 assert(var2.index == 5); 1034 1035 var2 = db[PCID("Crom 29", "Adaur Harbor"), "StoredObject"].get; 1036 assert(var2.name == "StoredObject"); 1037 assert(var2.index == 6); 1038 1039 assert(db["Crom 29", "Adaur Harb", "StoredObject"].isNull); 1040 assert(db.getVariableValue!BinaryObject(PCID(), "StoredObject").isNull); 1041 1042 assertThrown!BiowareDBException(db.getVariableValue!BinaryObject(0));//var type mismatch 1043 1044 1045 //Iteration 1046 foreach(var ; db){} 1047 foreach(i, var ; db){} 1048 1049 1050 1051 //Value set 1052 assertThrown!BiowareDBException(db.setVariableValue(0, 88)); 1053 1054 db.setVariableValue(0, 42.42f); 1055 var = db[0]; 1056 assert(var.timestamp != DateTime(2017,06,25, 23,19,26)); 1057 assert(var.type == 'F'); 1058 assert(db.getVariableValue!NWFloat(var.index).approxEqual(42.42f)); 1059 1060 db.setVariableValue(1, 12); 1061 var = db[1]; 1062 assert(var.timestamp != DateTime(2017,06,25, 23,19,27)); 1063 assert(var.type == 'I'); 1064 assert(db.getVariableValue!NWInt(var.index) == 12); 1065 1066 db.setVariableValue(2, NWVector([10.0f, 20.0f, 30.0f])); 1067 var = db[2]; 1068 assert(var.timestamp != DateTime(2017,06,25, 23,19,28)); 1069 assert(var.type == 'V'); 1070 assert(db.getVariableValue!NWVector(var.index) == [10.0f, 20.0f, 30.0f]); 1071 1072 db.setVariableValue(3, NWLocation(100, NWVector([10.0f, 20.0f, 30.0f]), 60.0f)); 1073 var = db[3]; 1074 assert(var.timestamp != DateTime(2017,06,25, 23,19,29)); 1075 assert(var.type == 'L'); 1076 with(db.getVariableValue!NWLocation(var.index)){ 1077 assert(area == 100); 1078 assert(position == NWVector([10.0f, 20.0f, 30.0f])); 1079 assert(fabs(facing - 60.0f) <= 0.001); 1080 } 1081 1082 1083 // Memo reallocations 1084 size_t getMemoIndex(size_t varIndex){ 1085 auto record = db.table.getRecord(varIndex); 1086 return (cast(const char[])record[BiowareDB.RecOffset.Memo .. BiowareDB.RecOffset.MemoEnd]).strip().to!size_t; 1087 } 1088 1089 size_t oldMemoIndex; 1090 1091 oldMemoIndex = getMemoIndex(4); 1092 db.setVariableValue(4, "small");//Can fit in the same memo block 1093 assert(getMemoIndex(4) == oldMemoIndex); 1094 assert(db.getVariableValue!NWString(4) == "small"); 1095 1096 import std.array: replicate, array, join; 1097 string veryLongValue = replicate(["ten chars!"], 52).array.join;//520 chars 1098 db.setVariableValue(4, veryLongValue); 1099 assert(getMemoIndex(4) == 35);//Should reallocate 1100 assert(db.memo.header.next_free_block_bigendian.bigEndianToNative == 37); 1101 1102 1103 oldMemoIndex = getMemoIndex(6); 1104 db.setVariableValue(6, cast(BinaryObject)[0, 1, 2, 3, 4, 5]); 1105 assert(getMemoIndex(6) == oldMemoIndex); 1106 assert(db.getVariableValue!BinaryObject(6) == [0, 1, 2, 3, 4, 5]); 1107 1108 db.setVariableValue(PCID(), "ThisIsAString", "yolo string"); 1109 assert(db.getVariableValue!NWString(4) == "yolo string"); 1110 1111 // Variable creation 1112 db.setVariableValue(PCID("player", "id"), "varname", "Hello string :)"); 1113 assert(db.getVariableValue!NWString(PCID("player", "id"), "varname") == "Hello string :)"); 1114 1115 1116 //Variable deleting 1117 var = db.getVariable(PCID("player", "id"), "varname").get(); 1118 assert(var.deleted == false); 1119 db.deleteVariable(PCID("player", "id"), "varname"); 1120 assert(db.getVariable(PCID("player", "id"), "varname").isNull); 1121 var = db.getVariable(var.index); 1122 assert(var.deleted == true); 1123 1124 assertThrown!BiowareDBException(db.deleteVariable(PCID("player", "id"), "varname")); 1125 assertNotThrown(db.deleteVariable(var.index)); 1126 1127 } 1128 1129 1130 1131 1132 private T bigEndianToNative(T)(inout T i){ 1133 import std.bitmanip: bigEndianToNative; 1134 return bigEndianToNative!T(cast(inout ubyte[T.sizeof])(&i)[0 .. 1]); 1135 } 1136 private T nativeToBigEndian(T)(inout T i){ 1137 import std.bitmanip: nativeToBigEndian; 1138 return *(cast(T*)nativeToBigEndian(i).ptr); 1139 }