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 }