diff --git a/aowow b/aowow
index 428d109f..3e1adb62 100755
--- a/aowow
+++ b/aowow
@@ -9,6 +9,9 @@ if (PHP_SAPI === 'cli' && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__)
require_once 'includes/kernel.php';
require_once 'includes/setup/cli.class.php';
require_once 'includes/setup/timer.class.php';
+require_once 'includes/setup/datatypes/primitives.php';
+require_once 'includes/setup/files/binaryfile.class.php';
+require_once 'includes/setup/files/dbcfile.class.php';
require_once 'setup/setup.php';
diff --git a/includes/setup/datatypes/primitives.php b/includes/setup/datatypes/primitives.php
new file mode 100644
index 00000000..2bf355bc
--- /dev/null
+++ b/includes/setup/datatypes/primitives.php
@@ -0,0 +1,104 @@
+data = $data;
+ else
+ $this->data = $data->read(static::SIZE);
+ }
+
+ public function pack() : string
+ {
+ return $this->data;
+ }
+
+ public function unpack() : mixed
+ {
+ return current(unpack(static::PACK_FMT, $this->data));
+ }
+
+ public function __debugInfo() : array
+ {
+ return [$this->unpack()];
+ }
+}
+
+class Char extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+
+ public function unpack() : string
+ {
+ return chr(parent::unpack());
+ }
+}
+
+class Boolean extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+
+ public function unpack() : string
+ {
+ return !!(parent::unpack());
+ }
+}
+
+class UInt8 extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+}
+
+class Int8 extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'c';
+}
+
+class UInt16 extends Primitive
+{
+ public const /* int */ SIZE = 2;
+ public const /* string */ PACK_FMT = 'v';
+}
+
+class Int16 extends Primitive
+{
+ public const /* int */ SIZE = 2;
+ public const /* string */ PACK_FMT = 's';
+}
+
+class UInt32 extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'V';
+}
+
+class Int32 extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'l';
+}
+
+class Double extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'f';
+}
+
+?>
diff --git a/includes/setup/files/binaryfile.class.php b/includes/setup/files/binaryfile.class.php
new file mode 100644
index 00000000..c34dba3d
--- /dev/null
+++ b/includes/setup/files/binaryfile.class.php
@@ -0,0 +1,202 @@
+error = 'file '.$file.' not found';
+ return;
+ }
+
+ if (!$this->handle = fopen($file, 'rb'))
+ {
+ $this->error = 'failed to open file '.$file;
+ return;
+ }
+
+ $this->filesize = filesize($file);
+
+ if ($inRAM)
+ $this->data = file_get_contents($file);
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+
+ /**********************/
+ /* direct file access */
+ /**********************/
+
+ public function read(int $bytes) : ?string
+ {
+ if ($this->error || !is_resource($this->handle) || $bytes < 0)
+ return null;
+
+ $start = $this->pos;
+ $this->pos += $bytes;
+
+ if ($this->inRAM)
+ return substr($this->data, $start, $bytes);
+ else
+ return fread($this->handle, $bytes);
+ }
+
+ public function readOffset(int $bytes, int $offset, bool $jumpBack = true) : ?string
+ {
+ if ($this->error || !is_resource($this->handle))
+ return null;
+
+ if ($jumpBack)
+ $curPos = $this->inRAM ? $this->pos : ftell($this->handle);
+
+ $this->seek($offset);
+
+ $str = $this->read($bytes);
+
+ if ($jumpBack)
+ $this->seek($curPos);
+
+ return $str;
+ }
+
+ public function seek(int $pos) : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ if ($pos < 0)
+ $pos = 0;
+ if ($pos > $this->filesize)
+ $pos = $this->filesize;
+
+ $this->pos = $pos;
+
+ if (!$this->inRAM)
+ fseek($this->handle, $pos, SEEK_SET);
+
+ return $pos;
+ }
+
+ public function ffwd(int $bytes) : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ $curPos = $this->inRAM ? $this->pos : ftell($this->handle);
+
+ if ($curPos + $bytes < 0)
+ $bytes -= $curPos;
+ if ($curPos + $bytes > $this->filesize)
+ $bytes -= $this->filesize;
+
+ $this->pos += $bytes;
+
+ if ($this->inRAM)
+ return $this->pos;
+
+ fseek($this->handle, $bytes, SEEK_CUR);
+ return ftell($this->handle);
+ }
+
+ public function close() : void
+ {
+ if (is_resource($this->handle))
+ fclose($this->handle);
+ }
+
+ public function tell() : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ return $this->inRAM ? $this->pos : ftell($this->handle);
+ }
+
+ /******************/
+ /* read Primitive */
+ /******************/
+
+ public function readInt8() : ?Int8
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int8($this);
+ }
+
+ public function readInt16() : ?Int16
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int16($this);
+ }
+
+ public function readInt32() : ?Int32
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int32($this);
+ }
+
+ public function readUInt8() : ?UInt8
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt8($this);
+ }
+
+ public function readUInt16() : ?UInt16
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt16($this);
+ }
+
+ public function readUInt32() : ?UInt32
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt32($this);
+ }
+
+ public function readFloat() : ?Double
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Double($this);
+ }
+
+ public function readChar() : ?Char
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Char($this);
+ }
+
+ public function readBool() : ?Boolean
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Boolean($this);
+ }
+}
+
+?>
diff --git a/includes/setup/files/dbcfile.class.php b/includes/setup/files/dbcfile.class.php
new file mode 100644
index 00000000..25d9d170
--- /dev/null
+++ b/includes/setup/files/dbcfile.class.php
@@ -0,0 +1,83 @@
+filesize < strlen(self::MAGIC) + self::HEADER_SIZE)
+ {
+ $this->error = 'file '.$file.' too small for a dbc';
+ $this->close();
+ return;
+ }
+
+ if ($this->read(4) != self::MAGIC)
+ {
+ $this->error = 'file '.$file.' has incorrect magic bytes';
+ $this->close();
+ return;
+ }
+
+ [, $this->nRows, $this->nCols, $this->recordSize, $this->stringSize] = unpack(UInt32::PACK_FMT.'4', $this->read(self::HEADER_SIZE));
+ $this->stringOffset = strlen(self::MAGIC) + self::HEADER_SIZE + $this->recordSize * $this->nRows;
+
+ if ($this->stringOffset + $this->stringSize != $this->filesize)
+ {
+ $this->error = 'file '.$file.' has unexpected size - expected: '.($this->stringOffset + $this->stringSize).' has: '.$this->filesize;
+ $this->close();
+ return;
+ }
+ }
+
+ public function readRecord(string $colFmt = "V*") : array
+ {
+ return unpack($colFmt, $this->read($this->recordSize));
+ }
+
+ public function readString() : ?string
+ {
+ $x = $this->readUInt32();
+ if (is_null($x))
+ return null;
+
+ return $this->getStringFromBlock($x->unpack());
+ }
+
+ public function getStringFromBlock(int $offset) : ?string
+ {
+ $curPos = $this->tell();
+
+ $this->seek($this->stringOffset + $offset);
+
+ // apparently it is more efficient to read more than one byte at once..?
+ $str = '';
+ while (($pos = strpos($str, "\0")) === false)
+ $str .= $this->read(255);
+
+ $str = substr($str, 0, $pos);
+
+ $this->seek($curPos);
+
+ return $pos ? $str : null;
+ }
+}
+
+?>
diff --git a/setup/setup.php b/setup/setup.php
index 8d59647b..029eb864 100644
--- a/setup/setup.php
+++ b/setup/setup.php
@@ -12,7 +12,7 @@ if (!CLI)
require_once 'setup/tools/setupScript.class.php';
require_once 'setup/tools/utilityScript.class.php';
require_once 'setup/tools/CLISetup.class.php';
-require_once 'setup/tools/dbc.class.php';
+require_once 'setup/tools/dbcreader.class.php';
require_once 'setup/tools/imagecreatefromblp.func.php';
CLISetup::init();
diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php
index 8ea9efd2..cab2a554 100644
--- a/setup/tools/CLISetup.class.php
+++ b/setup/tools/CLISetup.class.php
@@ -654,7 +654,7 @@ class CLISetup
if (DB::Aowow()->selectCell('SHOW TABLES LIKE %s', 'dbc_'.$name) && DB::Aowow()->selectCell('SELECT count(1) FROM %n', 'dbc_'.$name))
return true;
- $dbc = new DBC($name, ['temporary' => self::getOpt('delete')]);
+ $dbc = new DBCReader($name, ['temporary' => self::getOpt('delete')]);
if ($dbc->error)
{
CLI::write('CLISetup::loadDBC() - required DBC '.$name.'.dbc not found!', CLI::LOG_ERROR);
diff --git a/setup/tools/clisetup/dbc.us.php b/setup/tools/clisetup/dbc.us.php
index 0ae2500c..de910e6d 100644
--- a/setup/tools/clisetup/dbc.us.php
+++ b/setup/tools/clisetup/dbc.us.php
@@ -40,7 +40,7 @@ CLISetup::registerUtility(new class extends UtilityScript
if ($args[0])
$opts['tableName'] = $args[0];
- $dbc = new DBC(strtolower($n), $opts, $args[1] ?: DBC::DEFAULT_WOW_BUILD);
+ $dbc = new DBCReader(strtolower($n), $opts, $args[1] ?: DBCReader::DEFAULT_WOW_BUILD);
if ($dbc->error)
{
CLI::write('[dbc] required DBC '.CLI::bold($n).'.dbc not found!', CLI::LOG_ERROR);
@@ -84,7 +84,7 @@ CLISetup::registerUtility(new class extends UtilityScript
CLI::write();
CLI::write(' Known DBC files:', -1, false);
- $defs = DBC::getDefinitions();
+ $defs = DBCReader::getDefinitions();
$letter = '';
$buff = [];
diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php
deleted file mode 100644
index c4d4fad1..00000000
--- a/setup/tools/dbc.class.php
+++ /dev/null
@@ -1,512 +0,0 @@
-
-
- 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
- 'I' => 'l', // I - signed int, 4 bytes, sql index
- 'u' => 'V', // u - unsigned int, 4 bytes
- 'U' => 'V', // U - unsigned int, 4 bytes, sql index
- 'b' => 'C', // b - unsigned char, 1 byte
- 'd' => 'x4', // d - ordered by this field, not included in array
- 'n' => 'V' // n - unsigned int, 4 bytes, sql primary key
- );
-
- 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 (Locale::cases() as $loc)
- {
- if (!in_array($loc, CLISetup::$locales))
- continue;
-
- if ($foundMask & (1 << $loc->value))
- continue;
-
- foreach ($loc->gameDirs() as $dir)
- {
- $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $dir, 'DBFilesClient');
- if (!CLISetup::fileExists($fullPath))
- continue;
-
- $this->curFile = $fullPath;
- if ($this->validateFile($loc))
- {
- $foundMask |= (1 << $loc->value);
- break;
- }
- }
- }
-
- 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 different 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_keys($this->fileRefs), callback: fn($x) => CLI::bold(Locale::from($x)->name)));
- 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) : array
- {
- if (!is_resource($handle))
- $handle = fopen($this->curFile, 'rb');
-
- if (!$handle)
- return [];
-
- if (fread($handle, 4) != 'WDBC')
- {
- CLI::write('file '.$this->curFile.' has incorrect magic bytes', CLI::LOG_ERROR);
- fclose($handle);
- return [];
- }
-
- return unpack('VrecordCount/VfieldCount/VrecordSize/VstringSize', fread($handle, 16));
- }
-
- private function validateFile(Locale $loc) : bool
- {
- $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[$loc->value] = [$handle, $this->curFile, $header];
-
- return true;
- }
-
- private function createTable()
- {
- if ($this->error)
- return;
-
- $pKey = '';
- $query = 'CREATE '.($this->tempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` (';
- $indizes = [];
-
- 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':
- $indizes[] = $name;
- case 'i':
- case 'n':
- $query .= '`'.$name.'` INT SIGNED NOT NULL, ';
- break;
- case 'U':
- $indizes[] = $name;
- case 'u':
- $query .= '`'.$name.'` INT UNSIGNED NOT NULL, ';
- break;
- default: // 'x', 'X', 'd'
- continue 2;
- }
-
- if ($type == 'n')
- $pKey = $name;
- }
-
- foreach ($indizes as $i)
- $query .= 'KEY `idx_'.$i.'` (`'.$i.'`), ';
-
- if ($pKey)
- $query .= 'PRIMARY KEY (`'.$pKey.'`) ';
- else
- $query = substr($query, 0, -2);
-
- $query .= ') COLLATE=\'utf8mb4_unicode_ci\' ENGINE=InnoDB';
-
- DB::Aowow()->qry('DROP TABLE IF EXISTS %n', $this->tableName);
- DB::Aowow()->qry($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');
-
- foreach ($this->dataBuffer as $row)
- DB::Aowow()->qry('INSERT INTO %n %v', $this->tableName, array_combine($cols, $row));
-
- $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;
- }
-}
-
-?>
diff --git a/setup/tools/dbcreader.class.php b/setup/tools/dbcreader.class.php
new file mode 100644
index 00000000..3138f623
--- /dev/null
+++ b/setup/tools/dbcreader.class.php
@@ -0,0 +1,362 @@
+
+
+ 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 DBCReader
+{
+ private const /* string */ INI_FILE_PATH = 'setup/tools/dbc/%s.ini';
+ private const /* int */ MAX_INSERT_ROWS = 500;
+
+ public const /* string */ DEFAULT_WOW_BUILD = '12340';
+
+ private bool $isGameTable = false;
+ private bool $isLocalized = false;
+ private bool $isTempTable = true;
+ private string $tableName = '';
+ private array $dataBuffer = [];
+ private array $fileRefs = [];
+ private array $format = [];
+ private string $recordFmt = '';
+ private array $macro = array(
+ 'LOC' => 'sxsssxsxsxxxxxxxx', // pre 4.x locale block (in use)
+ 'X_LOC' => 'xxxxxxxxxxxxxxxxx' // pre 4.x locale block (unused)
+ );
+ private array $unpackFmt = array( // Supported format characters:
+ 'x' => Primitive::PACK_FMT.'4', // x - not used/unknown, 4 bytes
+ 'X' => Primitive::PACK_FMT, // X - not used/unknown, 1 byte
+ 's' => UInt32::PACK_FMT, // s - string block index, 4 bytes
+ 'S' => UInt32::PACK_FMT, // S - string block index, 4 bytes - localized; autofill
+ 'f' => Double::PACK_FMT, // f - float, 4 bytes (rounded to 4 digits after comma)
+ 'i' => Int32::PACK_FMT, // i - signed int, 4 bytes
+ 'I' => Int32::PACK_FMT, // I - signed int, 4 bytes, sql index
+ 'u' => UInt32::PACK_FMT, // u - unsigned int, 4 bytes
+ 'U' => UInt32::PACK_FMT, // U - unsigned int, 4 bytes, sql index
+ 'b' => UInt8::PACK_FMT, // b - unsigned char, 1 byte
+ 'd' => Primitive::PACK_FMT.'4', // d - ordered by this field, not included in array
+ 'n' => UInt32::PACK_FMT // n - unsigned int, 4 bytes, sql primary key
+ );
+
+ private static array $structs = [];
+
+ public bool $error = true;
+
+ public function __construct(public string $file, array $opts = [], string $wowBuild = self::DEFAULT_WOW_BUILD)
+ {
+ self::loadStructs($wowBuild);
+
+ $this->file = strtolower($this->file);
+ if (empty(self::$structs[$this->file]))
+ {
+ CLI::write('no structure known for '.$this->file.'.dbc, build '.$wowBuild, CLI::LOG_ERROR);
+ return;
+ }
+
+ foreach (self::$structs[$this->file] as $name => $type)
+ {
+ // resolove locale macro
+ if (isset($this->macro[$type]))
+ {
+ $this->isLocalized = true;
+ for ($i = 0; $i < strlen($this->macro[$type]); $i++)
+ {
+ $this->format[$name.'_loc'.$i] = $this->macro[$type][$i];
+ $this->recordFmt .= '/'.$this->unpackFmt[$this->macro[$type][$i]].$name.'_loc'.$i;
+ }
+ }
+ else if (!isset($this->unpackFmt[$type]))
+ {
+ CLI::write('unknown format parameter '.CLI::bold($type).' at for field '.CLI::bold($name).' in format string', CLI::LOG_ERROR);
+ return;
+ }
+ else
+ {
+ $this->format[$name] = $type;
+ $this->recordFmt .= '/'.$this->unpackFmt[$type];
+ if ($type !== 'x' && $type !== 'X')
+ $this->recordFmt .= $name;
+
+ if ($type === 'S')
+ $this->isLocalized = true;
+ }
+ }
+
+ // Optimizing unpack string: 'x/x/x/x/x/x' => 'x6'
+ $this->recordFmt = preg_replace_callback('/x(\/x)+/i', fn($m) => 'x'.((strlen($m[0]) + 1) / 2), substr($this->recordFmt, 1));
+
+ if (is_bool($opts['temporary']))
+ $this->isTempTable = $opts['temporary'];
+
+ if (!empty($opts['tableName']))
+ $this->tableName = $opts['tableName'];
+ else
+ $this->tableName = 'dbc_'.$this->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($this->file, 0, 2) == 'gt';
+
+ $foundMask = 0x0;
+ foreach (Locale::cases() as $loc)
+ {
+ if (!in_array($loc, CLISetup::$locales))
+ continue;
+
+ if ($foundMask & (1 << $loc->value))
+ continue;
+
+ foreach ($loc->gameDirs() as $dir)
+ {
+ $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $dir, 'DBFilesClient');
+ if (!CLISetup::fileExists($fullPath))
+ continue;
+
+ $dbcFile = new DBCFile($fullPath);
+ if ($dbcFile->error)
+ {
+ CLI::write($dbcFile->error, CLI::LOG_ERROR);
+ unset($dbcFile);
+ continue;
+ }
+
+ if ($dbcFile->nCols != count($this->format))
+ {
+ CLI::write('incorrect format specified for file '.$this->file.' - expected fields: '.count($this->format).' read fields: '.$dbcFile->nCols, CLI::LOG_ERROR);
+ unset($dbcFile);
+ continue;
+ }
+
+ $recSize = 0;
+ foreach ($this->format as $ch)
+ $recSize += ($ch == 'X' || $ch == 'b') ? 1 : 4;
+
+ if ($recSize != $dbcFile->recordSize)
+ {
+ CLI::write('format string size ('.$recSize.') for file '.$this->file.' does not match actual size ('.$dbcFile->recordSize.')', CLI::LOG_ERROR);
+ unset($dbcFile);
+ continue;
+ }
+
+ $this->fileRefs[$loc->value] = $dbcFile;
+ $foundMask |= (1 << $loc->value);
+ }
+ }
+
+ if (!$this->fileRefs)
+ {
+ CLI::write('no suitable files found for '.$this->file.'.dbc, aborting.', CLI::LOG_ERROR);
+ return;
+ }
+
+ // check if DBCs are identical
+
+ $tests = ['nRows' => null, 'nCols' => null, 'recordSize' => null];
+ foreach ($this->fileRefs as $fileRef)
+ {
+ foreach ($tests as $field => $val)
+ {
+ if ($val === null)
+ $tests[$field] = $fileRef->{$field};
+ else if ($val != $fileRef->{$field})
+ {
+ CLI::write('some DBCs have different '.$field.': '.CLI::bold($val).' <> '.CLI::bold($fileRef->{$field}).' respectively. cannot merge!', CLI::LOG_ERROR);
+ return;
+ }
+ }
+ }
+
+ $this->error = false;
+ }
+
+ public function readFile() : bool
+ {
+ if (!$this->file || $this->error)
+ return false;
+
+ $this->createTable();
+
+ if ($this->isLocalized)
+ CLI::write(' - DBC: reading and merging '.$this->file.'.dbc for locales '.Lang::concat(array_keys($this->fileRefs), callback: fn($x) => CLI::bold(Locale::from($x)->name)));
+ else
+ CLI::write(' - DBC: reading '.$this->file.'.dbc');
+
+ $this->read();
+
+ 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() : void
+ {
+ unset($this->fileRefs, $this->dataBuffer);
+ }
+
+ private function createTable() : void
+ {
+ if ($this->error)
+ return;
+
+ $pKey = '';
+ $query = 'CREATE '.($this->isTempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` (';
+ $indizes = [];
+
+ if ($this->isGameTable)
+ {
+ $query .= '`idx` INT SIGNED NOT NULL, ';
+ $pKey = 'idx';
+ }
+
+ foreach ($this->format as $name => $type)
+ {
+ $query .= match($type)
+ {
+ 'f' => '`'.$name.'` FLOAT NOT NULL, ',
+ 's' => '`'.$name.'` TEXT NULL, ',
+ 'b' => '`'.$name.'` TINYINT UNSIGNED NOT NULL, ',
+ 'i', 'I', 'n' => '`'.$name.'` INT SIGNED NOT NULL, ',
+ 'u', 'U' => '`'.$name.'` INT SIGNED NOT NULL, ',
+ 'S' => (function ($n) {
+ $buf = '';
+ for ($l = 0; $l < strlen($this->macro['LOC']); $l++)
+ if ($this->macro['LOC'][$l] == 's')
+ $buf .= '`'.$n.'_loc'.$l.'` TEXT NULL, ';
+ return $buf;
+ })($name),
+ default => '' // 'x', 'X', 'd'
+ };
+
+ if ($this->isGameTable)
+ continue;
+
+ if ($type == 'I' || $type == 'U')
+ $indizes[] = $name;
+ if ($type == 'n')
+ $pKey = $name;
+ }
+
+ foreach ($indizes as $i)
+ $query .= 'KEY `idx_'.$i.'` (`'.$i.'`), ';
+
+ if ($pKey)
+ $query .= 'PRIMARY KEY (`'.$pKey.'`) ';
+ else
+ $query = substr($query, 0, -2);
+
+ $query .= ') COLLATE=\'utf8mb4_unicode_ci\' ENGINE=InnoDB';
+
+ DB::Aowow()->qry('DROP TABLE IF EXISTS %n', $this->tableName);
+ DB::Aowow()->qry($query);
+ }
+
+ private function writeToDB() : void
+ {
+ if (!$this->dataBuffer || $this->error)
+ return;
+
+ DB::Aowow()->qry('INSERT INTO %n %m', $this->tableName, $this->dataBuffer);
+
+ $this->dataBuffer = [];
+ }
+
+ private function read() : void
+ {
+ $nRows = reset($this->fileRefs)->nRows; // set to actual value once we have a file handle
+
+ for ($i = 0; $i < $nRows; $i++)
+ {
+ // add 'virtual' enumerator for gt*-dbcs
+ if ($this->isGameTable)
+ $this->dataBuffer['idx'][$i] = $i;
+
+ foreach ($this->fileRefs as $locId => $dbcFile)
+ {
+ // note that the file pointer is already on the first record as the DBCFile reads its own header
+ $row = $dbcFile->readRecord($this->recordFmt);
+
+ foreach ($row as $name => $value)
+ {
+ $type = $this->format[$name];
+
+ // handle locale fields for post 3.3.5a DBCs
+ if ($type === 'S')
+ {
+ for ($k = 0; $k < strlen($this->macro['LOC']); $k++)
+ if ($this->macro['LOC'][$k] === 's')
+ $this->dataBuffer[$name.'_loc'.$k][$i] ??= null;
+
+ $this->dataBuffer[$name.'_loc'.$locId][$i] ??= $dbcFile->getStringFromBlock($value);
+ }
+ if (empty($this->dataBuffer[$name][$i]))
+ {
+ if ($type == 's')
+ $this->dataBuffer[$name][$i] ??= $dbcFile->getStringFromBlock($value);
+ else
+ $this->dataBuffer[$name][$i] = $value;
+ }
+ }
+
+ if (!$this->isLocalized) // one match is enough
+ break;
+ }
+
+ if (count(current($this->dataBuffer)) >= self::MAX_INSERT_ROWS)
+ $this->writeToDB();
+ }
+
+ $this->writeToDB();
+
+ $this->endClean();
+ }
+}
+
+?>
diff --git a/setup/tools/setupScript.class.php b/setup/tools/setupScript.class.php
index 608c80e1..048c0926 100644
--- a/setup/tools/setupScript.class.php
+++ b/setup/tools/setupScript.class.php
@@ -30,7 +30,7 @@ trait TrDBCcopy
CLI::write('[sql] copying '.$this->dbcSourceFiles[0].'.dbc into aowow_'.$this->command);
- $dbc = new DBC($this->dbcSourceFiles[0], ['temporary' => false, 'tableName' => 'aowow_'.$this->command]);
+ $dbc = new DBCReader($this->dbcSourceFiles[0], ['temporary' => false, 'tableName' => 'aowow_'.$this->command]);
if ($dbc->error)
return false;
diff --git a/setup/tools/sqlgen/sounds.ss.php b/setup/tools/sqlgen/sounds.ss.php
index c7be95e7..8b201393 100644
--- a/setup/tools/sqlgen/sounds.ss.php
+++ b/setup/tools/sqlgen/sounds.ss.php
@@ -62,10 +62,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript
// .mp3 => audio/mpeg
$query =
- 'SELECT `id` AS `id`, `type` AS `cat`, `name`, 0 AS `cuFlags`,
- `file1` AS `soundFile1`, `file2` AS `soundFile2`, `file3` AS `soundFile3`, `file4` AS `soundFile4`, `file5` AS `soundFile5`,
- `file6` AS `soundFile6`, `file7` AS `soundFile7`, `file8` AS `soundFile8`, `file9` AS `soundFile9`, `file10` AS `soundFile10`,
- `path`, `flags`
+ 'SELECT `id` AS "id", `type` AS "cat", `name`, 0 AS "cuFlags",
+ `file1` AS "soundFile1", `file2` AS "soundFile2", `file3` AS "soundFile3", `file4` AS "soundFile4", `file5` AS "soundFile5",
+ `file6` AS "soundFile6", `file7` AS "soundFile7", `file8` AS "soundFile8", `file9` AS "soundFile9", `file10` AS "soundFile10",
+ IFNULL(`path`, "") AS "path", `flags`
FROM dbc_soundentries
LIMIT %i, %i';
@@ -95,6 +95,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript
$hasDupes = false;
for ($i = 1; $i < 11; $i++)
{
+ if (!$s['soundFile'.$i])
+ continue;
+
$nicePath = CLI::nicePath($s['soundFile'.$i], $s['path']);
if ($s['soundFile'.$i] && array_key_exists($nicePath, $soundIndex))
{
@@ -134,9 +137,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[sound] Group '.str_pad('['.$s['id'].']', 7).' '.CLI::bold($s['name']).' has invalid sound file '.CLI::bold($s['soundFile'.$i]).' on index '.$i.'! Skipping...', CLI::LOG_WARN);
$s['soundFile'.$i] = null;
}
- // empty case
- else
- $s['soundFile'.$i] = null;
}
if (!$fileSets && !$hasDupes)