From e51e40175edba33076a7db52824e96f4c96b67f6 Mon Sep 17 00:00:00 2001 From: Hash Borgir Date: Wed, 29 Jun 2022 01:53:27 -0600 Subject: [PATCH] D2S Parsing almost done. Todo: full item parsing, Editor GUI, NPC Intro Data, refactor code, writeQuest/writeStat function etc. --- CharEditor.php | 49 +++++++++++++----- src/D2BitReader.php | 18 +++++-- src/D2ByteReader.php | 15 ++++-- src/D2Char.php | 119 ++++++++++++++++++++++++++++++------------- src/tabs/Chars.php | 4 +- 5 files changed, 146 insertions(+), 59 deletions(-) diff --git a/CharEditor.php b/CharEditor.php index 77183ce..058d0d8 100644 --- a/CharEditor.php +++ b/CharEditor.php @@ -66,25 +66,48 @@ $stats->setBits($cleanbits); //} + $stats->rewind(); for($i=0; $i <= strlen($bits); $i++) { $id = hexdec($ByteReader->toBytesR($stats->readb(9))); - if (!empty($ISC[$id])){ - $val = $stats->readb($ISC[$id]['CSvBits']); - $stat = $ISC[$id]['Stat']; - $values[$stat] = hexdec($ByteReader->toBytesR($val)); - } + $stats->skip($ISC[$id]['CSvBits']); + $ids[$id] = $id; } -$values['hitpoints'] = (int) round($values['hitpoints'] / 2048); -$values['maxhp'] = (int) round($values['maxhp'] / 2048); -$values['mana'] = (int) round($values['mana'] / 2048); -$values['maxmana'] = (int) round($values['maxmana'] / 2048); -$values['stamina'] = (int) round($values['stamina'] / 2048); -$values['maxstamina'] = (int) round($values['maxstamina'] / 2048); -$values['killcounter'] = (int) round($values['killcounter'] / 2); - +$stats->rewind(); +foreach($ids as $id){ + $stats->skip(9); + $val = $stats->readb($ISC[$id]['CSvBits']); + $stat = $ISC[$id]['Stat']; + $values[$stat] = hexdec($ByteReader->toBytesR($val)); +} +$values['hitpoints'] = (int) round($values['hitpoints'] >> 11); +$values['maxhp'] = (int) round($values['maxhp'] >> 11); +$values['mana'] = (int) round($values['mana'] >> 11); +$values['maxmana'] = (int) round($values['maxmana'] >> 11); +$values['stamina'] = (int) round($values['stamina'] >> 11); +$values['maxstamina'] = (int) round($values['maxstamina'] >> 11); +$values['killcounter'] = (int) round($values['killcounter'] >> 1); var_dump($values); +//$stats->rewind(); +//for($i=0; $i <= strlen($bits); $i++) { +// $id = hexdec($ByteReader->toBytesR($stats->readb(9))); +// if (!empty($ISC[$id])){ +// $val = $stats->readb($ISC[$id]['CSvBits']); +// $stat = $ISC[$id]['Stat']; +// $values[$stat] = hexdec($ByteReader->toBytesR($val)); +// } +//} +//$values['hitpoints'] = (int) round($values['hitpoints'] / 2048); +//$values['maxhp'] = (int) round($values['maxhp'] / 2048); +//$values['mana'] = (int) round($values['mana'] / 2048); +//$values['maxmana'] = (int) round($values['maxmana'] / 2048); +//$values['stamina'] = (int) round($values['stamina'] / 2048); +//$values['maxstamina'] = (int) round($values['maxstamina'] / 2048); +//$values['killcounter'] = (int) round($values['killcounter'] / 2); +// +//var_dump($values); + //array_pop($ids); // diff --git a/src/D2BitReader.php b/src/D2BitReader.php index 9c7784e..a746f0a 100644 --- a/src/D2BitReader.php +++ b/src/D2BitReader.php @@ -2,7 +2,7 @@ class D2BitReader { - private string $bits; + private string $bits = ''; private int $offset = 0; public function __construct(string $bits = '') { @@ -17,7 +17,7 @@ class D2BitReader { /* read X number of bits, like fread */ - public function read(int $numBits = 0, bool $str = true) { + public function read(int $numBits = 0, bool $str = true) : string { $bits = null; for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { $str ? $bits .= $this->bits[$i] : $bits[] = $this->bits[$i]; @@ -26,7 +26,16 @@ class D2BitReader { return $bits; } - public function readr(int $numBits = 0) { + public function readb(int $numBits = 0, bool $str = true) : string { + $bits = null; + for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { + $str ? $bits .= $this->bits[$i] : $bits[] = $this->bits[$i]; + } + $this->offset += $numBits; + return strrev(str_pad($bits, 16, 0, STR_PAD_RIGHT)); + } + + public function readr(int $numBits = 0) : string { $bits = null; for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { $bits .= $this->bits[$i]; @@ -74,6 +83,9 @@ class D2BitReader { public function getBits(): string { return $this->bits; } + public function setBits(string $bits) { + $this->bits = $bits; + } public function getOffset(): int { return $this->offset; diff --git a/src/D2ByteReader.php b/src/D2ByteReader.php index 26afbd9..b1e13ce 100644 --- a/src/D2ByteReader.php +++ b/src/D2ByteReader.php @@ -5,7 +5,7 @@ require_once './src/D2BitReader.php'; class D2ByteReader { - private string $data; + private string $data = ''; private int $offset = 0; public function __construct(string $data) { @@ -102,13 +102,18 @@ class D2ByteReader { } } - public function toBytes(string $bits) { + public function toBytesR(string $bits) : string { foreach (str_split($bits, 8) as $byteString) { - $bytes[] = (bindec(strrev($byteString))); + $bytes .= strtoupper(str_pad(dechex(bindec(($byteString))), 2, 0, STR_PAD_LEFT)); } - foreach ($bytes as $byte) { - dump($byte); + return $bytes; + } + + public function toBytes(string $bits) : string { + foreach (str_split($bits, 8) as $byteString) { + $bytes .= strtoupper(str_pad(dechex(bindec(strrev($byteString))), 2, 0, STR_PAD_LEFT)); } + return $bytes; } public function bitsToHexString(string $bits): string { diff --git a/src/D2Char.php b/src/D2Char.php index 16ee5cc..6cbbe06 100644 --- a/src/D2Char.php +++ b/src/D2Char.php @@ -15,11 +15,23 @@ class D2Char { private $bData = null; // char binary data from d2s private $filePath = null; // .d2s file path private $fp = null; // file pointer + private $data = null; // full d2s file loaded in $data + private $ByteReader = null; // put $data into bytereader + private $ISC = null; public function __construct($file) { $this->sData = new D2CharStructureData(); $this->filePath = $_SESSION['savepath'] . $file; $this->fp = fopen($this->filePath, "r+b"); + $this->data = file_get_contents($this->filePath); + $this->ByteReader = new D2ByteReader($this->data); + + $sql = "SELECT ID,Stat,CSvBits FROM itemstatcost WHERE Saved=1"; + $ISCData = PDO_FetchAll($sql); + + foreach ($ISCData as $k => $v) { + $this->ISC[$v['ID']] = $v; + } // read offsets here from sData and put into $this->bData // which will be used for cData output @@ -28,40 +40,34 @@ class D2Char { $this->bData[$k] = fread($this->fp, $v); } - return $this->parseChar(); // end of parseChar() calls parseItems() + return $this->parseChar(); // end of parseChar() calls parseItems(), parseStats, etc. } public function parseItems() { - $data = file_get_contents($this->filePath); - $ByteReader = new D2ByteReader($data); - - $i_TotalOffset = strpos($data, "JM"); + $i_TotalOffset = strpos($this->data, "JM"); fseek($this->fp, $i_TotalOffset + 2); $i_Total = unpack('S*', (fread($this->fp, 2)))[1]; $i_Offsets = []; for ($i = 0; $i <= $i_Total; $i++) { - $i_Offsets[] = strposX($data, "JM", $i + 2); + $i_Offsets[] = strposX($this->data, "JM", $i + 2); } foreach ($i_Offsets as $k => $v) { $itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k]; } array_pop($itemOffsets); - $_items=[]; + $_items = []; foreach ($itemOffsets as $offset => $bytes) { - $this->items[] = new D2Item($ByteReader->toBits($ByteReader->readh($offset, $bytes))); - } + $this->items[] = new D2Item($this->ByteReader->toBits($this->ByteReader->readh($offset, $bytes))); + } } public function parseChar() { - - // dump(unpack('l', $this->bData[4])[1]); - $cData = null; $cData['Identifier'] = bin2hex($this->bData[0]); - // 96 is v1.10+ - checks out + // 96 is v1.10+ - checks out $cData['VersionID'] = ($this->sData->version[unpack('l', $this->bData[4])[1]]); - + // 1.41 KB (1,447 bytes) - checks out $cData['Filesize'] = round(unpack('l', $this->bData[8])[1] / 1024, 2) . " KB"; $cData['Checksum'] = bin2hex($this->bData[12]); @@ -76,38 +82,32 @@ class D2Char { $cData['CharacterClass'] = $this->sData->class[unpack('C', $this->bData[40])[1]]; $cData['CharacterLevel'] = unpack('C', $this->bData[43])[1]; $cData['Lastplayed'] = gmdate("Y-m-d\TH:i:s\Z", unpack('I', $this->bData[48])[0]); - + $skills = (unpack('l16', $this->bData[56])); - foreach($skills as $skill){ + foreach ($skills as $skill) { $cData['Assignedskills'][] = $this->sData->skills[$skill]; - } - - //ddump($this->bData); - //ddump($cData); - + } + $cData['LeftmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[120])[1]]; $cData['RightmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[124])[1]]; $cData['LeftswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[128])[1]]; $cData['RightswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[132])[1]]; - + // Char menu appearance not needed // $cData['Charactermenuappearance'] = unpack('i', $this->bData[136]); - - // todo: refactor to use D2BitstreamReader here $x = str_split(strtobits($this->bData[168]), 8); //$x[0][0] ? $diff = 'Normal' : ($x[1][0] ? $diff = 'Nitemare' : $diff = 'Hell'); - $onDifficulty['NM'] = $x[1][0]; + $onDifficulty['Normal'] = $x[0][0]; $onDifficulty['NM'] = $x[1][0]; $onDifficulty['Hell'] = $x[2][0]; - $cData['Difficulty'] = array_filter($onDifficulty); // $diff; - - // Map ID. This value looks like a random number, but it corresponds with one of the longwords + $cData['Difficulty'] = ($onDifficulty); // $diff; + // Map ID. This value looks like a random number, but it corresponds with one of the longwords // found in the character.map file, according to the difficulty being played. Not needed //$cData['MapID'] = $this->bData[171]; - + $cData['MercenaryDead'] = unpack('i', $this->bData[177])[1]; - // This looks like a random ID for your mercenary. + // This looks like a random ID for your mercenary. // $cData['MercenaryID'] = unpack('H*', $this->bData[179]); $cData['MercenaryNameID'] = unpack('S', $this->bData[183])[1]; $cData['MercenaryType'] = unpack('S', $this->bData[185])[1]; @@ -116,20 +116,66 @@ class D2Char { $cData['Waypoints'] = $this->getWaypointsData($file); $cData['NPCIntroductions'] = $this->bData[714]; $cData['filePath'] = $this->filePath; - - // returns an array of items, + + // returns an array of items, // each item is an array of item details $this->parseItems(); // parse items will populate $this->items $cData['items'] = $this->items; // cData[items] will be $this->items $this->cData = $cData; + // parse stats + $this->parseStats(); + unset($this->items); + unset($this->bData); + unset($this->sData); + unset($this->ByteReader); + unset($this->data); + unset($this->fp); + unset($this->ISC); return $this->cData; } - - + public function parseStats() { + $gf = strposX($this->data, 'gf', 1) + 2; // find gf and skip it + $if = strposX($this->data, 'if', 1); + $len = $if - $gf; + $stats = new D2BitReader($this->ByteReader->toBits($this->ByteReader->readh($gf, $len))); + + $bits = $stats->getBits(); + $cleanbits = substr($bits, 0, -11); + $stats->setBits($cleanbits); + $bits = $stats->getBits(); + + $stats->rewind(); + for($i=0; $i <= strlen($bits); $i++) { + $id = hexdec($this->ByteReader->toBytesR($stats->readb(9))); + if($this->ISC[$id]['CSvBits'] !== NULL){ + $stats->skip($this->ISC[$id]['CSvBits']); + } + $ids[$id] = $id; + } + $stats->rewind(); + foreach($ids as $id){ + $stats->skip(9); + if($this->ISC[$id]['CSvBits'] !== NULL){ + $val = $stats->readb($this->ISC[$id]['CSvBits']); + } + $stat = $this->ISC[$id]['Stat']; + $values[$stat] = hexdec($this->ByteReader->toBytesR($val)); + } + $values['hitpoints'] = (int) round($values['hitpoints'] >> 11); + $values['maxhp'] = (int) round($values['maxhp'] >> 11); + $values['mana'] = (int) round($values['mana'] >> 11); + $values['maxmana'] = (int) round($values['maxmana'] >> 11); + $values['stamina'] = (int) round($values['stamina'] >> 11); + $values['maxstamina'] = (int) round($values['maxstamina'] >> 11); + $values['killcounter'] = (int) round($values['killcounter'] >> 1); + + $this->cData['stats'] = $values; + } + public function getQuestData($file) { $questsNorm = null; $questsNM = null; @@ -181,7 +227,7 @@ class D2Char { fseek($this->fp, $this->sData->wpOffsetsNorm + 4); $a5 = strrev(strtobits(fread($this->fp, 1))); $wp['Norm'] = str_split($a1 . $a2 . $a3 . $a4 . $a5); - + // ddump($wp['Norm']); fseek($this->fp, $this->sData->wpOffsetsNM); @@ -224,5 +270,6 @@ class D2Char { // } } return $waypoints; - } + } + } diff --git a/src/tabs/Chars.php b/src/tabs/Chars.php index 4e097b4..503abcd 100644 --- a/src/tabs/Chars.php +++ b/src/tabs/Chars.php @@ -42,7 +42,7 @@ $form = new Formr\Formr(); SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - ddump($charData); +//dump($charData); ?> @@ -215,7 +215,7 @@ EOT; -
+ Level:
$radio

Quests