This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ if (!defined('AOWOW_REVISION')) die('illegal access'); if (!CLI) die('not in cli mode'); class DBC { private $isGameTable = false; private $localized = false; private $tempTable = true; private $tableName = ''; private $dataBuffer = []; private $bufferSize = 500; private static $structs = []; private $fileRefs = []; private $curFile = ''; public $error = true; public $fields = []; public $format = []; public $file = ''; private $macro = array( 'LOC' => 'sxsssxsxsxxxxxxxx', // pre 4.x locale block (in use) 'X_LOC' => 'xxxxxxxxxxxxxxxxx' // pre 4.x locale block (unused) ); private $unpackFmt = array( // Supported format characters: 'x' => 'x/x/x/x', // x - not used/unknown, 4 bytes 'X' => 'x', // X - not used/unknown, 1 byte 's' => 'V', // s - string block index, 4 bytes 'S' => 'V', // S - string block index, 4 bytes - localized; autofill 'f' => 'f', // f - float, 4 bytes (rounded to 4 digits after comma) 'i' => 'l', // i - signed int, 4 bytes 'u' => 'V', // u - unsigned int, 4 bytes 'b' => 'C', // b - unsigned char, 1 byte 'd' => 'x4', // d - ordered by this field, not included in array 'n' => 'V' // n - int, 4 bytes, ordered by this field ); public const DEFAULT_WOW_BUILD = '12340'; private const INI_FILE_PATH = 'setup/tools/dbc/%s.ini'; public function __construct($file, $opts = [], string $wowBuild = self::DEFAULT_WOW_BUILD) { self::loadStructs($wowBuild); $file = strtolower($file); if (empty(self::$structs[$file])) { CLI::write('no structure known for '.$file.'.dbc, build '.$wowBuild, CLI::LOG_ERROR); return; } foreach (self::$structs[$file] as $name => $type) { // resolove locale macro if (isset($this->macro[$type])) { $this->localized = true; for ($i = 0; $i < strlen($this->macro[$type]); $i++) $this->format[$name.'_loc'.$i] = $this->macro[$type][$i]; } else { $this->format[$name] = $type; if ($type == 'S') $this->localized = true; } } $this->file = $file; if (is_bool($opts['temporary'])) $this->tempTable = $opts['temporary']; if (!empty($opts['tableName'])) $this->tableName = $opts['tableName']; else $this->tableName = 'dbc_'.$file; // gameTable-DBCs don't have an index and are accessed through value order // allas, you cannot do this with mysql, so we add a 'virtual' index $this->isGameTable = array_values($this->format) == ['f'] && substr($file, 0, 2) == 'gt'; $foundMask = 0x0; foreach (CLISetup::$expectedPaths as $locStr => $locId) { if (!in_array($locId, CLISetup::$localeIds)) continue; if ($foundMask & (1 << $locId)) continue; $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $locStr, 'DBFilesClient'); if (!CLISetup::fileExists($fullPath)) continue; $this->curFile = $fullPath; if ($this->validateFile($locId)) $foundMask |= (1 << $locId); } if (!$this->fileRefs) { CLI::write('no suitable files found for '.$file.'.dbc, aborting.', CLI::LOG_ERROR); return; } // check if DBCs are identical $headers = array_column($this->fileRefs, 2); $x = array_unique(array_column($headers, 'recordCount')); if (count($x) != 1) { CLI::write('some DBCs have differenct record counts ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); return; } $x = array_unique(array_column($headers, 'fieldCount')); if (count($x) != 1) { CLI::write('some DBCs have differenct field counts ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); return; } $x = array_unique(array_column($headers, 'recordSize')); if (count($x) != 1) { CLI::write('some DBCs have differenct record sizes ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); return; } $this->error = false; } public function readFile() { if (!$this->file || $this->error) return []; $this->createTable(); if ($this->localized) CLI::write(' - DBC: reading and merging '.$this->file.'.dbc for locales '.Lang::concat(array_intersect_key(Util::$localeStrings, $this->fileRefs), true, 'CLI::bold')); else CLI::write(' - DBC: reading '.$this->file.'.dbc'); if (!$this->read()) { CLI::write(' - DBC::read() returned with error', CLI::LOG_ERROR); return false; } return true; } public function getTableName() : string { return $this->tableName; } public static function getDefinitions() : array { if (empty(self::$structs)) self::loadStructs(); return array_keys(self::$structs); } private static function loadStructs(string $wowBuild = self::DEFAULT_WOW_BUILD) : void { $structFile = sprintf(self::INI_FILE_PATH, $wowBuild); if (!file_exists($structFile)) { CLI::write('no structure file found for wow build '.$wowBuild, CLI::LOG_ERROR); return; } self::$structs = parse_ini_file($structFile, true); } private function endClean() { foreach ($this->fileRefs as &$ref) fclose($ref[0]); $this->dataBuffer = null; } private function readHeader(&$handle = null) { if (!is_resource($handle)) $handle = fopen($this->curFile, 'rb'); if (!$handle) return false; if (fread($handle, 4) != 'WDBC') { CLI::write('file '.$this->curFile.' has incorrect magic bytes', CLI::LOG_ERROR); fclose($handle); return false; } return unpack('VrecordCount/VfieldCount/VrecordSize/VstringSize', fread($handle, 16)); } private function validateFile($locId) { $filesize = filesize($this->curFile); if ($filesize < 20) { CLI::write('file '.$this->curFile.' is too small for a DBC file', CLI::LOG_ERROR); return false; } $header = $this->readHeader($handle); if (!$header) { CLI::write('cannot open file '.$this->curFile, CLI::LOG_ERROR); return false; } // Different debug checks to be sure, that file was opened correctly $debugStr = '(recordCount='.$header['recordCount']. ' fieldCount=' .$header['fieldCount'] . ' recordSize=' .$header['recordSize'] . ' stringSize=' .$header['stringSize'] .')'; if ($header['recordCount'] * $header['recordSize'] + $header['stringSize'] + 20 != $filesize) { CLI::write('file '.$this->curFile.' has incorrect size '.$filesize.': '.$debugStr, CLI::LOG_ERROR); fclose($handle); return false; } if ($header['fieldCount'] != count($this->format)) { CLI::write('incorrect format ('.implode('', $this->format).') specified for file '.$this->curFile.' fieldCount='.$header['fieldCount'], CLI::LOG_ERROR); fclose($handle); return false; } $this->fileRefs[$locId] = [$handle, $this->curFile, $header]; return true; } private function createTable() { if ($this->error) return; $pKey = ''; $query = 'CREATE '.($this->tempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` ('; if ($this->isGameTable) { $query .= '`idx` INT SIGNED NOT NULL, '; $pKey = 'idx'; } foreach ($this->format as $name => $type) { switch ($type) { case 'f': $query .= '`'.$name.'` FLOAT NOT NULL, '; break; case 'S': for ($l = 0; $l < strlen($this->macro['LOC']); $l++) if ($this->macro['LOC'][$l] == 's') $query .= '`'.$name.'_loc'.$l.'` TEXT NULL, '; break; case 's': $query .= '`'.$name.'` TEXT NULL, '; break; case 'b': $query .= '`'.$name.'` TINYINT UNSIGNED NOT NULL, '; break; case 'i': case 'n': $query .= '`'.$name.'` INT SIGNED NOT NULL, '; break; case 'u': $query .= '`'.$name.'` INT UNSIGNED NOT NULL, '; break; default: // 'x', 'X', 'd' continue 2; } if ($type == 'n') $pKey = $name; } if ($pKey) $query .= 'PRIMARY KEY (`'.$pKey.'`) '; else $query = substr($query, 0, -2); $query .= ') COLLATE=\'utf8mb4_unicode_ci\' ENGINE=MyISAM'; DB::Aowow()->query('DROP TABLE IF EXISTS ?#', $this->tableName); DB::Aowow()->query($query); } private function writeToDB() { if (!$this->dataBuffer || $this->error) return; $cols = []; foreach ($this->format as $n => $type) { switch ($type) { case 'x': case 'X': case 'd': continue 2; case 'S': for ($l = 0; $l < strlen($this->macro['LOC']); $l++) if ($this->macro['LOC'][$l] == 's') $cols[] = $n.'_loc'.$l; break; default: $cols[] = $n; } } if ($this->isGameTable) array_unshift($cols, 'idx'); DB::Aowow()->query('INSERT INTO ?# (?#) VALUES (?a)', $this->tableName, $cols, $this->dataBuffer); $this->dataBuffer = []; } private function read() { // Check that record size also matches $itr = 0; $recSize = 0; $unpackStr = ''; foreach ($this->format as $ch) { if ($ch == 'X' || $ch == 'b') $recSize += 1; else $recSize += 4; if (!isset($this->unpackFmt[$ch])) { CLI::write('unknown format parameter \''.$ch.'\' in format string', CLI::LOG_ERROR); return false; } $unpackStr .= '/'.$this->unpackFmt[$ch]; if ($ch != 'X' && $ch != 'x') $unpackStr .= 'f'.$itr; // output can't have numeric key as it gets interpreted as repeat factor here $itr++; } $unpackStr = substr($unpackStr, 1); // Optimizing unpack string: 'x/x/x/x/x/x' => 'x6' while (preg_match('/(x\/)+x/', $unpackStr, $r)) $unpackStr = substr_replace($unpackStr, 'x'.((strlen($r[0]) + 1) / 2), strpos($unpackStr, $r[0]), strlen($r[0])); // we asserted all DBCs to be identical in structure. pick first header for checks $header = reset($this->fileRefs)[2]; if ($recSize != $header['recordSize']) { CLI::write('format string size ('.$recSize.') for file '.$this->file.' does not match actual size ('.$header['recordSize'].')', CLI::LOG_ERROR); return false; } // And, finally, extract the records $strBlock = 4 + 16 + $header['recordSize'] * $header['recordCount']; for ($i = 0; $i < $header['recordCount']; $i++) { $row = []; $idx = $i; // add 'virtual' enumerator for gt*-dbcs if ($this->isGameTable) $row[-1] = $i; foreach ($this->fileRefs as $locId => [$handle, $fullPath, $header]) { $rec = unpack($unpackStr, fread($handle, $header['recordSize'])); $offset = 0; foreach (array_values($this->format) as $j => $type) { if (!isset($rec['f'.$j])) continue; $outIdx = $j + $offset; if (!empty($row[$outIdx]) && $type != 'S') continue; switch ($type) { case 'S': // localized String - autofill $offset = substr_count($this->macro['LOC'], 's'); for ($k = 0; $k < strlen($this->macro['LOC']); $k++) { if ($this->macro['LOC'][$k] != 's') continue; if (!isset($row[$j + $k])) // prep locale fields $row[$j + $k] = null; } // provide outIdx for passthrough $outIdx = $j + $locId; case 's': $curPos = ftell($handle); fseek($handle, $strBlock + $rec['f'.$j]); $str = $chr = ''; do { $str .= $chr; $chr = fread($handle, 1); } while ($chr != "\000"); fseek($handle, $curPos); $row[$outIdx] = $str; break; case 'f': $row[$outIdx] = round($rec['f'.$j], 8); break; case 'n': // DO NOT BREAK! $idx = $rec['f'.$j]; default: // nothing special .. 'i', 'u' and the likes $row[$outIdx] = $rec['f'.$j]; } } if (!$this->localized) // one match is enough break; } $this->dataBuffer[$idx] = array_values($row); if (count($this->dataBuffer) >= $this->bufferSize) $this->writeToDB(); } $this->writeToDB(); $this->endClean(); return true; } } ?>