diff --git a/CharEditor.php b/CharEditor.php index 6fa1109..a7b5868 100644 --- a/CharEditor.php +++ b/CharEditor.php @@ -18,23 +18,19 @@ require_once './src/D2BitReader.php'; define('DB_FILE', $_SESSION['modname'] . ".db"); PDO_Connect("sqlite:" . DB_FILE); - $sql = "SELECT * FROM strings"; $strings = PDO_FetchAssoc($sql); - -$sql = "SELECT code,namestr - FROM armor +$sql = "SELECT code,namestr + FROM armor UNION SELECT code,namestr FROM misc UNION SELECT code,namestr - FROM weapons + FROM weapons "; - $namestr = PDO_FetchAssoc($sql); - +$namestr = PDO_FetchAssoc($sql); //ddump($namestr); - //$filePath = "D:\Diablo II\MODS\ironman-dev\save\Aldur.d2s"; $filePath = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s"; //$filePath = "D:\Diablo II\MODS\MedianXL2012\save\Lok.d2s"; @@ -43,6 +39,8 @@ $filePath = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s"; $fp = fopen($filePath, 'r+b'); $data = file_get_contents($filePath); + + $i_offset = strpos($data, "JM"); fseek($fp, $i_offset + 2); @@ -82,28 +80,36 @@ foreach ($items as $_item) { $b = new D2BitReader($_item); - //$b->bseek(27); - //dump($b->bread(1)); - - - $b->seek(76); - // dump($b->bread(32)); - $codeBits = str_split($b->read(32), 8); - - $itemCode = ''; - foreach ($codeBits as $byte) { - - // dump(strrev($byte)); - //dump(sprintf('%c', bindec($byte))); - - $itemCode .= chr(bindec(strrev($byte))); + $b->seek(58); + $parent = bindec(strrev($b->read(3))); + if ($parent == 0) { + $b->seek(73); + $_stored = bindec(strrev($b->read(3))); + switch ($_stored) { + case 0: + $stored = ''; // item is not stored, check bit 58 + break; + case 1: + $stored = 'Inventory'; + break; + case 4: + $stored = 'Horadric Cube'; + break; + case 5: + $stored = 'Stash'; + break; + } + dump($stored); } + $b->seek(76); + $codeBits = str_split($b->read(32), 8); + $itemCode = ''; + foreach ($codeBits as $byte) { + $itemCode .= chr(bindec(strrev($byte))); + } $itemCode = trim($itemCode); - dump($namestr[$itemCode]); dump($strings[$namestr[$itemCode]]); - - // dump($strings[trim($itemCode)]); } diff --git a/nbproject/project.properties b/nbproject/project.properties index 50a6cf9..61c76b3 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -4,5 +4,5 @@ php.version=PHP_81 source.encoding=UTF-8 src.dir=. tags.asp=false -tags.short=false +tags.short=true web.root=. diff --git a/src/D2BitReader.php b/src/D2BitReader.php index 0173271..da16515 100644 --- a/src/D2BitReader.php +++ b/src/D2BitReader.php @@ -26,6 +26,15 @@ class D2BitReader { return $bits; } + public function readr(int $numBits = 0) { + $bits = null; + for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { + $bits .= $this->bits[$i]; + } + $this->offset += $numBits; + return strrev($bits); + } + /* seek to offset (like fseek) */ public function seek(int $pos): bool { @@ -63,4 +72,19 @@ class D2BitReader { return true; } + public function getBits(): string { + return $this->bits; + } + + public function getOffset(): int { + return $this->offset; + } + + public function setOffset(int $offset): bool { + if ($offset < 0 || $offset > strlen($this->bits)) + return false; + $this->offset = $offset; + return true; + } + } diff --git a/src/D2Char.php b/src/D2Char.php index d89025a..122d7e4 100644 --- a/src/D2Char.php +++ b/src/D2Char.php @@ -4,20 +4,21 @@ require_once 'D2CharStructureData.php'; require_once 'D2Files.php'; require_once 'D2BitReader.php'; require_once 'D2Strings.php'; +require_once 'D2Item.php'; class D2Char { - public $cData; // char data output - public $items; // char item data - private $sData; // char file structure data - private $bData; // char binary data from d2s - private $filePath; // .d2s file path - private $fp; // file pointer + public $cData = null; // char data output + public $items = null; // char item data + private $sData = null; // char file structure data + private $bData = null; // char binary data from d2s + private $filePath = null; // .d2s file path + private $fp = null; // file pointer public function __construct($file) { $this->sData = new D2CharStructureData(); $this->filePath = $_SESSION['savepath'] . $file; - $this->fp = fopen($this->filePath, "rb+"); + $this->fp = fopen($this->filePath, "r+b"); // read offsets here from sData and put into $this->bData // which will be used for cData output @@ -25,62 +26,120 @@ class D2Char { fseek($this->fp, $k); $this->bData[$k] = fread($this->fp, $v); } - $this->strings = new D2Strings(); - return $this->parseChar(); - + + return $this->parseChar(); // end of parseChar() calls parseItems() } - public function parseItems(){ - + // parse items from d2s and add to $this->items[] + /* + * @return Array of items + */ + public function parseItems() { + $_data = file_get_contents($this->filePath); + // get offset of first JM and skip it + $_offset = strpos($_data, "JM") + 2; + // seek to items_total offset + fseek($this->fp, $_offset); + // item total is a SHORT 16 bits, 2 bytes + $_total = unpack('S*', (fread($this->fp, 2)))[1]; + + // Items start from 2nd JM + for ($i = 2; $i <= $_total; $i++) { + // seek to Each JM (every item begins with JM + fseek($this->fp, strposX($_data, 'JM', $i)); + // read and unpack 21 bytes as array of byte/char (8 bit each) + $_items[] = unpack('C*', fread($this->fp, 21)); + } + // Convert item bytes to 21x8 bitfield + // each byte is reversed with strrev() for correct bit position + // to convert back to ascii, read bits from any position, reverse, bindec(chr()) + foreach ($_items as $_item) { + $item = null; + foreach ($_item as $i_bytes) { + $item .= strrev(str_pad(decbin($i_bytes), 8, 0, STR_PAD_LEFT)); + // $item .= (str_pad(dechex($i_bytes), 2, 0, STR_PAD_LEFT)); + } + $this->items[] = new D2Item($item); // return an array of item details + } } - + public function parseChar() { + + // dump(unpack('l', $this->bData[4])[1]); + $cData = null; $cData['Identifier'] = bin2hex($this->bData[0]); - // 96 is v1.10+ - checks out - $cData['VersionID'] = $sData->version[unpack('l', $this->bData[4])[1]]; + // 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']); - $cData['Activeweapon'] = unpack('l', $this->bData['16']); + $cData['Checksum'] = bin2hex($this->bData[12]); + $cData['Activeweapon'] = unpack('l', $this->bData[16]); $cData['CharacterName'] = str_replace("\0", "", $this->bData[20]); $cData['CharacterStatus'] = array_filter(str_split(strtobits($this->bData[36]))); foreach ($cData['CharacterStatus'] as $k => $v) { $str .= ($characterStatus[$k]) . " "; } $cData['CharacterStatus'] = $str; - $cData['Characterprogression'] = bindec($this->bData['37']); + $cData['Characterprogression'] = bindec($this->bData[37]); $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]); - $cData['Assignedskills'] = (unpack('i16', $this->bData['56'])); + + $skills = (unpack('l16', $this->bData[56])); + 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]]; - $cData['Charactermenuappearance'] = unpack('i', $this->bData[136]); + + // 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); - $onDifficulty['Norm'] = $x[0][0]; + //$x[0][0] ? $diff = 'Normal' : ($x[1][0] ? $diff = 'Nitemare' : $diff = 'Hell'); + $onDifficulty['NM'] = $x[1][0]; $onDifficulty['NM'] = $x[1][0]; $onDifficulty['Hell'] = $x[2][0]; - $cData['Difficulty'] = array_filter($onDifficulty); - $cData['MapID'] = $this->bData['171']; - $cData['Mercenarydead'] = unpack('i', $this->bData['177']); - $cData['MercenaryID'] = $this->bData['179']; - $cData['MercenaryNameID'] = $this->bData['183']; - $cData['Mercenarytype'] = $this->bData['185']; - $cData['Mercenaryexperience'] = $this->bData['187']; + $cData['Difficulty'] = array_filter($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. + // $cData['MercenaryID'] = unpack('H*', $this->bData[179]); + $cData['MercenaryNameID'] = unpack('S', $this->bData[183])[1]; + $cData['MercenaryType'] = unpack('S', $this->bData[185])[1]; + $cData['MercenaryExperience'] = unpack('l', $this->bData[187])[1]; $cData['Quests'][] = $this->getQuestData($file); $cData['Waypoints'] = $this->getWaypointsData($file); $cData['NPCIntroductions'] = $this->bData[714]; $cData['filePath'] = $this->filePath; + + // 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; - $this->parseItems(); + + return $this->cData; } + + public function getQuestData($file) { $questsNorm = null; $questsNM = null; @@ -173,6 +232,5 @@ class D2Char { // } } return $waypoints; - } - + } } diff --git a/src/D2CharStructureData.php b/src/D2CharStructureData.php index b1d6242..a5f9393 100644 --- a/src/D2CharStructureData.php +++ b/src/D2CharStructureData.php @@ -274,7 +274,8 @@ class D2CharStructureData { foreach ($res as $r) { $this->skills[$r['Id']] = $r['String']; } - + + $this->_qNorm = array_flip($this->qNorm); $this->_qNM = array_flip($this->qNM); $this->_qHell = array_flip($this->qHell); diff --git a/src/D2Functions.php b/src/D2Functions.php index c6799c7..b1ce11c 100755 --- a/src/D2Functions.php +++ b/src/D2Functions.php @@ -41,21 +41,21 @@ */ function ddump($var) { - echo "
";
+	//echo "
";
 	var_dump($var);
-	echo "
"; + //echo "
"; - // header('Content-Type: application/json'); + // 'Content-Type: application/json'); // echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT); die(); } function dump($var) { - echo "
";
+	//echo "
";
 	var_dump($var);
-	echo "
"; + //echo "
"; - //header('Content-Type: application/json'); + //'Content-Type: application/json'); //echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT); } diff --git a/src/D2Item.php b/src/D2Item.php new file mode 100644 index 0000000..a5f83fb --- /dev/null +++ b/src/D2Item.php @@ -0,0 +1,147 @@ +bits = $bits; + return $this->parseItem(); + } + + /* set $this->iData to array of item details + * + * @return array of item details + */ + + private function parseItem() { + $b = new D2BitReader($this->bits); + + $b->skip(16); // Skip JM + $b->skip(4); // skip unknown 4 bytes + $this->iData['identified'] = $b->read(1); // bit 20, identified + $b->skip(6); // skip unknown 6 bytes + $this->iData['socketed'] = $b->read(1); // bit 27, socketed + $b->skip(1); + // This bit is set on items which you have picked up since the last time the game was saved. + $this->iData['pickedUpSinceLastSave'] = $b->read(1); // bit 29 + $b->skip(2); + $this->iData['ear'] = $b->read(1); // bit 32 bool + $this->iData['startingItem'] = $b->read(1); // bit 33 bool + $b->skip(3); + $this->iData['compact'] = $b->read(1); // bit 37 compact + $this->iData['ethereal'] = $b->read(1); // bit 38 ethereal + $b->skip(1); // unknown, seems always 1 + $this->iData['personalized'] = $b->read(1); // bit 40 Item has been personalized (by Anya in Act V) + $b->skip(1); + $this->iData['runeword'] = $b->read(1); // bit 42 the item has been given a Rune Word. + $b->skip(15); // unknown; some of these bits may be set + + + // item location + $location = bindec($b->readr(3)); // bit 58 parent Item location. + $body = bindec($b->readr(4)); // bit 61 If the item is equipped + $col = bindec($b->readr(4)); // bit 65 Column number of the left corner of the item + $row = bindec($b->readr(4)); // bit 69 Row number of the top of the item, counting from 0. + $_stored = bindec($b->readr(3)); // bit 73 + + + // weird behavior/bug + // if item is in a container, bodypart will be NULL + // if item is on bodypart, container will be NULL + switch ($location) { + case D2ItemLocation::STORED: + switch ($_stored) { + case D2ItemLocationStored::NONE: + $this->iData['container'] = ''; // item is not stored, check bit 58 + break; + case D2ItemLocationStored::INVENTORY: + $this->iData['container'] = 'Inventory'; + break; + case D2ItemLocationStored::CUBE: + $this->iData['container'] = 'Horadric Cube'; + break; + case D2ItemLocationStored::STASH: + $this->iData['container'] = 'Stash'; + break; + default: $this->iData['container'] = 'Unknown'; + break; + } + break; + case D2ItemLocation::EQUIPPED: + switch ($body) { + case D2ItemLocationBody::HELMET: $this->iData['bodypart'] = 'Helmet'; + break; + case D2ItemLocationBody::AMULET: $this->iData['bodypart'] = 'Amulet'; + break; + case D2ItemLocationBody::ARMOR: $this->iData['bodypart'] = 'Armor'; + break; + case D2ItemLocationBody::WEAPONR: $this->iData['bodypart'] = 'Weapon R'; + break; + case D2ItemLocationBody::WEAPONL: $this->iData['bodypart'] = 'Weapon L'; + break; + case D2ItemLocationBody::RINGR: $this->iData['bodypart'] = 'Ring R'; + break; + case D2ItemLocationBody::RINGL: $this->iData['bodypart'] = 'Ring L'; + break; + case D2ItemLocationBody::BELT: $this->iData['bodypart'] = 'Belt'; + break; + case D2ItemLocationBody::BOOTS: $this->iData['bodypart'] = 'Boots'; + break; + case D2ItemLocationBody::GLOVES: $this->iData['bodypart'] = 'Gloves'; + break; + case D2ItemLocationBody::WEAPONR2: $this->iData['bodypart'] = 'Weapon Alt R'; + break; + case D2ItemLocationBody::WEAPONL2: $this->iData['bodypart'] = 'Weapon Alt L'; + break; + default: $this->iData['bodypart'] = 'Unknown'; + break; + } + break; + } + + // if item is ear + if ($this->iData['ear']){ + // set item code/basename + $this->iData['itemCode'] = 'ear'; + $this->iData['basename'] = 'Ear'; + // get ear class/level + $eclass = bindec($b->readr(3)); // bit 76 + $elevel = bindec($b->readr(7)); // bit 79 + // get ear char's name + } + + // get item code + $b->seek(76); + $itemCode = ''; + foreach (str_split($b->read(32), 8) as $byte) { + $itemCode .= chr(bindec(strrev($byte))); + } + $this->iData['itemCode'] = trim($itemCode); + + // get namestr + $sql = "SELECT code,namestr FROM armor WHERE code='{$this->iData['itemCode']}'"; + $res = PDO_FetchAssoc($sql); + if (empty($res)){ + $sql = "SELECT code,namestr FROM misc WHERE code='{$this->iData['itemCode']}'"; + $res = PDO_FetchAssoc($sql); + } + if (empty($res)){ + $sql = "SELECT code,namestr FROM misc WHERE code='{$this->iData['itemCode']}'"; + $res = PDO_FetchAssoc($sql); + } + + $sql = "SELECT `String` FROM strings WHERE `Key`='{$res[$this->iData['itemCode']]}'"; + $res = PDO_FetchOne($sql); + $this->iData['basename'] = $res; + + return $this->iData; + } + +} diff --git a/src/D2ItemStructureData.php b/src/D2ItemStructureData.php new file mode 100644 index 0000000..0a89ca0 --- /dev/null +++ b/src/D2ItemStructureData.php @@ -0,0 +1,40 @@ +