Passed
Push — master ( 931c46...651cd8 )
by smiley
01:22
created

DTBLReader::read()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class DTBLReader
4
 *
5
 * @link         https://arctium.io/wiki/index.php?title=WildStar_Client_Database_(.tbl)
6
 * @link         https://bitbucket.org/mugadr_m/wildstar-studio/src/973583416d4436e4980de840c2c91cfc5972fb2a/WildstarStudio/DataTable.h
7
 *
8
 * @filesource   DTBLReader.php
9
 * @created      04.01.2019
10
 * @package      codemasher\WildstarDB
11
 * @author       smiley <[email protected]>
12
 * @copyright    2019 smiley
13
 * @license      MIT
14
 */
15
16
namespace codemasher\WildstarDB;
17
18
class DTBLReader extends ReaderAbstract{
19
20
	/**
21
	 * @var string
22
	 * @internal
23
	 */
24
	protected $FORMAT_HEADER = 'a4Signature/LVersion/QTableNameLength/x8/QRecordSize/QFieldCount/QDescriptionOffset/QRecordCount/QFullRecordSize/QEntryOffset/QNextId/QIDLookupOffset/x8';
25
26
	/**
27
	 * @var bool
28
	 * @internal
29
	 */
30
	protected $skip;
31
32
	/**
33
	 * @var int
34
	 * @internal
35
	 */
36
	protected $pos;
37
38
	/**
39
	 * @var string
40
	 * @internal
41
	 */
42
	protected $rowdata;
43
44
	/**
45
	 * @param string $filename
46
	 *
47
	 * @return \codemasher\WildstarDB\ReaderInterface
48
	 * @throws \codemasher\WildstarDB\WSDBException
49
	 */
50
	public function read(string $filename):ReaderInterface{
51
		$this->loadFile($filename);
52
53
		if($this->header['Signature'] !== "\x4c\x42\x54\x44"){ // LBTD
54
			throw new WSDBException('invalid DTBL');
55
		}
56
57
		$this->readColumnHeaders();
58
		$this->readData();
59
60
		if(\count($this->data) !== $this->header['RecordCount']){
61
			throw new WSDBException('invalid row count');
62
		}
63
64
		return $this;
65
	}
66
67
	/**
68
	 * @return void
69
	 */
70
	protected function readColumnHeaders():void{
71
72
		// table name (UTF-16LE: length *2)
73
		$this->name = $this->decodeString(\fread($this->fh, $this->header['TableNameLength'] * 2));
74
75
		// skip forward
76
		\fseek($this->fh, $this->header['DescriptionOffset'] + $this->headerSize);
77
78
		// read the column headers (4+4+8+2+2+4 = 24 bytes)
79
		for($i = 0; $i < $this->header['FieldCount']; $i++){
80
			$this->cols[$i]['header'] = \unpack('LNameLength/x4/QNameOffset/SDataType/x2/x4', \fread($this->fh, 24));
81
		}
82
83
		$offset = $this->header['FieldCount'] * 24 + $this->header['DescriptionOffset'] + $this->headerSize;
84
85
		if($this->header['FieldCount'] % 2){
86
			$offset += 8;
87
		}
88
89
		// read the column names
90
		foreach($this->cols as $i => $col){
91
			\fseek($this->fh, $offset + $col['header']['NameOffset']);
92
93
			// column name (UTF-16LE: length *2)
94
			$this->cols[$i]['name'] = $this->decodeString(\fread($this->fh, $col['header']['NameLength'] * 2));
95
		}
96
97
		$this->logger->info($this->name.', fields: '.$this->header['FieldCount'].', rows: '.$this->header['RecordCount']);
98
	}
99
100
	/**
101
	 * @return void
102
	 * @throws \codemasher\WildstarDB\WSDBException
103
	 */
104
	protected function readData():void{
105
		\fseek($this->fh, $this->header['EntryOffset'] + $this->headerSize);
106
107
		$this->data = array_fill(0, $this->header['RecordCount'], null);
108
109
		// read a row
110
		foreach($this->data as $i => $_){
111
			$this->rowdata = \fread($this->fh, $this->header['RecordSize']);
112
			$this->pos     = 0;
113
			$this->skip    = false;
114
115
			// loop through the columns
116
			foreach($this->cols as $c => $col){
117
118
				// skip 4 bytes if the string offset is 0 (determined by $skip), the current type is string and the next isn't
119
				if($this->skip === true && ($c > 0 && $this->cols[$c - 1]['header']['DataType'] === 130) && $col['header']['DataType'] !== 130){
120
					$this->pos += 4;
121
				}
122
123
				$this->data[$i][$col['name']] = $this->getValue($col['header']['DataType']);
124
			}
125
126
			// if we run into this, a horrible thing happened
127
			if(\count($this->data[$i]) !== $this->header['FieldCount']){
128
				throw new WSDBException('invalid field count');
129
			}
130
131
		}
132
133
	}
134
135
	/**
136
	 * @param int $datatype
137
	 *
138
	 * @return int|float|string|null
139
	 */
140
	protected function getValue(int $datatype){
141
142
		switch($datatype){
143
			case 3:  // uint32
144
			case 11: // booleans (stored as uint32 0/1)
145
				$v = uint32(\substr($this->rowdata, $this->pos, 4));
146
				$this->pos += 4; break;
147
			case 4:  // float
148
				$v = \round(float(\substr($this->rowdata, $this->pos, 4)), 3); // @todo: determine round precision
149
				$this->pos += 4; break;
150
			case 20: // uint64
151
				$v = uint64(\substr($this->rowdata, $this->pos, 8));
152
				$this->pos += 8; break;
153
			case 130: // string (UTF-16LE)
154
				$v = $this->readString();
155
				$this->pos += 8; break;
156
157
			default: $v = null;
158
		}
159
160
		return $v;
161
	}
162
163
	/**
164
	 * @return string
165
	 */
166
	protected function readString():string{
167
		$o    = uint32(\substr($this->rowdata, $this->pos, 4));
168
		$p    = \ftell($this->fh);
169
		$this->skip = $o === 0;
170
171
		\fseek($this->fh, $this->header['EntryOffset'] + $this->headerSize + ($o > 0 ? $o : uint32(\substr($this->rowdata, $this->pos + 4, 4))));
172
173
		$v = '';
174
		// loop through the string until we hit 2 nul bytes or the void
175
		do{
176
			$s = \fread($this->fh, 2);
177
			$v .= $s;
178
		}
179
		while($s !== "\x00\x00" && $s !== '');
180
181
		\fseek($this->fh, $p);
182
183
		return $this->decodeString($v);
184
	}
185
186
}
187