ByteReader->setData($this->data); // update bytereader data $this->ByteReader->writeBytes(12, "00000000"); // clear old checksum $this->data = $this->ByteReader->getData(); // update this data to what we get from bytereader after clearing checksum $checksum = checksum(unpack('C*', $this->data)); // get new checksum $this->ByteReader->setData($this->data); // update bytereader data $this->ByteReader->writeBytes(12, $checksum); // write new checksum $this->data = $this->ByteReader->getData(); // update this data file_put_contents($this->filePath, $this->data); // write file } /** * @param $file */ public function __construct($file) { $this->sData = new D2CharStructureData(); $this->filePath = $_SESSION['savepath'] . $file; $this->fp = fopen($this->filePath, "r+b"); $data = file_get_contents($this->filePath); $this->ByteReader = new D2ByteReader($data); $this->data = $this->ByteReader->getData(); $sql = "SELECT ID,Stat,CSvBits,ValShift FROM itemstatcost WHERE Saved=1"; $ISCData = PDO_FetchAll($sql); foreach ($ISCData as $k => $v) { $this->ISC[$v['ID']] = $v; $this->_ISC[$v['Stat']] = $v; } // read offsets here from sData and put into $this->bData // which will be used for cData output foreach ($this->sData->offsets as $k => $v) { fseek($this->fp, $k); $this->bData[$k] = fread($this->fp, $v); } $classes = array_flip(['ama' => 0, 'sor' => 1, 'nec' => 2, 'pal' => 3, 'bar' => 4, 'dru' => 5, 'ass' => 6]); $class = $classes[hexdec($this->ByteReader->readh(40, 1))]; $sql = "SELECT sk.id,sk.Skill,sk.skilldesc,`str name`,strings.`String`,skilldesc.SkillPage,skilldesc.SkillRow,skilldesc.SkillColumn,skilldesc.ListRow,skilldesc.ListPool,skilldesc.IconCel FROM skills as sk LEFT JOIN skilldesc ON sk.skilldesc = skilldesc.skilldesc LEFT JOIN strings on skilldesc.`str name` = strings.Key WHERE sk.charclass = '$class'"; $sd = PDO_FetchAll($sql); foreach ($sd as $k => $v) { $this->skillData[$k + 1] = $v; } return $this->parseChar(); // end of parseChar() calls parseItems(), parseStats, etc. } /** * @return void */ public function parseItems() { $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($this->data, "JM", $i + 2); } foreach ($i_Offsets as $k => $v) { $itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k]; } array_pop($itemOffsets); $_items = []; foreach ($itemOffsets as $offset => $bytes) { $this->items[] = new D2Item($this->ByteReader->toBits($this->ByteReader->readh($offset, $bytes))); } } /** * @return array|null */ public function parseChar() { $cData = null; $cData['Identifier'] = bin2hex($this->bData[0]); // 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['Filesize'] = unpack('L', $this->bData[8])[1]; $cData['Checksum'] = bin2hex($this->bData[12]); $cData['Activeweapon'] = unpack('L', $this->bData[16]); $cData['CharacterName'] = str_replace("\0", "", $this->bData[20]); $characterStatus = array_filter(str_split(strrev(strtobits($this->bData[36])))); foreach ($characterStatus as $k => $v) { $str .= $this->sData->characterStatus[$k] . " "; } $cData['CharacterStatus'] = trim($str); $progression = hexdec(bin2hex($this->bData[37])); $cData['CharacterProgression'] = $this->sData->characterProgressionClassic[$progression]; if ($cData['CharacterStatus'] == 'Hardcore Expansion') { $cData['CharacterProgression'] = $this->sData->characterProgressionExpHC[$progression]; } if ($cData['CharacterStatus'] == "Expansion") { $cData['CharacterProgression'] = $this->sData->characterProgressionExp[$progression]; } if ($cData['CharacterStatus'] == "Hardcore") { $cData['CharacterProgression'] = $this->sData->characterProgressionClassicHC[$progression]; } $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('L', $this->bData[48])[0]); $skills = (unpack('l16', $this->bData[56])); // Hotkey assigned skills foreach ($skills as $skill) { $cData['Assignedskills'][] = ($this->sData->skills[$skill]); $cData['Assignedskills'] = array_filter($cData['Assignedskills']); } $cData['LeftmousebuttonskillID'] = $this->sData->skills[unpack('L', $this->bData[120])[1]]; $cData['RightmousebuttonskillID'] = $this->sData->skills[unpack('L', $this->bData[124])[1]]; $cData['LeftswapmousebuttonskillID'] = $this->sData->skills[unpack('L', $this->bData[128])[1]]; $cData['RightswapmousebuttonskillID'] = $this->sData->skills[unpack('L', $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['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 // 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; // parse stats $this->parseStats(); $this->cData['skills'] = $this->parseSkills(); unset($this->items); //unset($this->bData); unset($this->sData); //unset($this->fp); return $this->cData; } /** * @return array */ public function parseSkills() { $if = strposX($this->data, 'if', 1) + 2; // find if and skip it $jm = strposX($this->data, 'JM', 1); $skills = ($this->ByteReader->readc($if, ($jm - $if))); foreach ($skills as $k => $v) { if ($this->skillData[$k]['String']) { $cData['skills'][$k] = [ 'skill' => $this->skillData[$k]['String'], 'points' => $v, 'page' => $this->skillData[$k]['SkillPage'], 'row' => $this->skillData[$k]['ListRow'], 'col' => $this->skillData[$k]['SkillColumn'], 'icon' => $this->skillData[$k]['IconCel'], ]; } } $cData['skills'] = array_values($cData['skills']); return $cData; } /** * @param int $points * @return void */ public function setAllSkills(int $points) { $if = strposX($this->data, 'if', 1) + 2; // find if and skip it $jm = strposX($this->data, 'JM', 1); $len = $jm - $if; for ($i = 0; $i < $len; $i++) { $this->ByteReader->writeByte($if + $i, $points); } $this->data = $this->ByteReader->getData(); $this->save(); } /** * @param int $skill * @param int $points * @return void */ public function setSkill(int $skill, int $points) { $skill -= 1; $if = strposX($this->data, 'if', 1) + 2; // find if and skip it $jm = strposX($this->data, 'JM', 1); // set $kill to $points $this->ByteReader->writeByte($if + $skill, $points); $this->data = $this->ByteReader->getData(); $this->save(); } /** * @return void */ 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(); $bytes = $this->ByteReader->toBytes($bits); $stats->rewind(); for ($i = 0; $i <= count($this->ISC); $i++) { $id = hexdec($this->ByteReader->toBytesR($stats->readb(9))); if ($this->ISC[$id]['CSvBits'] !== NULL && $this->ISC[$id]['CSvBits'] !== '') { $stats->skip($this->ISC[$id]['CSvBits']); } $ids[$id] = $id; } $stats->rewind(); foreach ($ids as $id) { $stats->skip(9); if ($this->ISC[$id]['CSvBits'] !== NULL && $this->ISC[$id]['CSvBits'] !== '') { $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['soulcounter'] = (int) round($values['soulcounter'] / 2); // $values['killcounter'] = (int) round($values['killcounter'] / 2); $this->cData['stats'] = $values; } /** * @param string $stat * @param mixed $val * @param mixed|null $val2 * @return false|void */ public function setChar(string $stat, mixed $val, mixed $val2 = null) { switch ($stat) { case 'CharacterName': if (strlen($val) < 1 || strlen($val) > 15) { return false; } $pack = $this->ByteReader->bitsToHexString($this->ByteReader->toBits(pack('Z16', $val))); $this->ByteReader->writeBytes(20, $pack); rename($this->filePath, $_SESSION['savepath'] . "$val.d2s"); break; case "CharacterClass": $classes = [ 'Amazon' => 0, 'Sorceress' => 1, 'Necromancer' => 2, 'Paladin' => 3, 'Barbarian' => 4, 'Druid' => 5, 'Assassin' => 6 ]; $this->ByteReader->writeByte(40, $classes[$val]); break; case "CharacterLevel": if ($val > 99) { $val = 99; } // level is edited in two places. // byte 43 $this->ByteReader->writeByte(43, $val); // and in charstats $this->setStat('level', $val); // now we have to set the experience stat for this level // it's 1 level below as it starts from 0, for next level // $val -= 1; $sql = "SELECT {$this->cData['CharacterClass']} FROM experience WHERE level = '$val'"; $res = PDO_FetchOne($sql); $this->setStat('experience', $res); break; case 'CharacterStatus': switch ($val) { case 'Died': $status = (strtobits($this->data[36])); $status[3] = $val2; $byte = $this->ByteReader->bitsToHexString($status); $this->ByteReader->writeByte(36, hexdec($byte)); break; case 'Hardcore': $status = (strtobits($this->data[36])); $status[2] = $val2; $byte = $this->ByteReader->bitsToHexString($status); $this->ByteReader->writeByte(36, hexdec($byte)); break; case 'Expansion': $status = strrev(strtobits($this->data[36])); $status[5] = $val2; $byte = $this->ByteReader->bitsToHexString($status); $this->ByteReader->writeByte(36, hexdec($byte)); break; } break; case 'CharacterProgression': // 0 in normal, 1 finished normal, 2 finished nm, 3 finished hell switch ($val) { case 0: // in normal $this->data[37] = pack('C', 3); break; case 1: // finished normal $this->data[37] = pack('C', 8); break; case 2: // finished nm $this->data[37] = pack('C', 13); break; case 3: // finished hell $this->data[37] = pack('C', 15); break; $this->save(); } break; case 'LeftmousebuttonskillID': $this->ByteReader->writeBytes(120, dechex($val)); break; case 'RightmousebuttonskillID': $this->ByteReader->writeBytes(124, dechex($val)); break; case 'LeftswapmousebuttonskillID': $this->ByteReader->writeBytes(128, dechex($val)); break; case 'RightswapmousebuttonskillID': $this->ByteReader->writeBytes(132, dechex($val)); break; } // finally save char data to d2s file $this->data = $this->ByteReader->getData(); $this->save(); } /** * @return void */ public function resetFileSize() { $filesize = strlen($this->data); $this->fp = fopen($this->filePath, "r+b"); fseek($this->fp, 8); fwrite($this->fp, pack('L', $filesize)); fclose($this->fp); $this->fp = fopen($this->filePath, "r+b"); checksumFix($this->filePath); fseek($this->fp, 8); //ddump(unpack('L', fread($this->fp, 4))[1]); } /** * @return false|string */ public function generateAllStats() { // 003C08E081000F067860C001071C0008020800F040020064A000000A2C00C0030C000D000000006E05000000FE3F $stats = ''; for ($i = 0; $i < 16; $i++) { $id = strrev(str_pad((decbin((int) $this->ISC[$i]['ID'])), 9, 0, STR_PAD_LEFT)); $val = strrev(str_pad((decbin(20)), (int) $this->ISC[$i]['CSvBits'], 0, STR_PAD_LEFT)); // dump($id); // dump($val); $stat = $id . $val; $stats .= $stat; } $stats .= "000011111111100"; //dump($stats); $gf = strposX($this->data, 'gf', 1) + 2; // find gf and skip it $if = strposX($this->data, 'if', 1); $len = $if - $gf; $bytes = $this->ByteReader->toBytes($stats); // refresh this data $data = $this->ByteReader->getData(); // delete everything between GF---and---IF $data = substr_replace($data, "", $gf, $len); // pack hex bites into binary string $packedbytes = (pack('H*', $bytes)); // now insert new packed byte stat data between gf and if $data = substr_replace($data, $packedbytes, 767, 0); $this->data = $data; $this->save(); $this->resetFileSize(); $filedata = file_get_contents($this->filePath); $this->ByteReader = new D2ByteReader($filedata); $this->data = $this->ByteReader->getData(); return $this->data; } /** * @param string $stat * @param mixed $val * @return void */ public function setStat(string $stat, mixed $val) { $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(); $_offsets = []; for ($i = 0; $i <= strlen($bits); $i++) { $id = hexdec($this->ByteReader->toBytesR($stats->readb(9))); $_offsets[$id] = $stats->getOffset(); if ($this->ISC[$id]['CSvBits'] !== null && $this->ISC[$id]['CSvBits'] !== '') { $stats->skip($this->ISC[$id]['CSvBits']); } } $_offsets[0] = 9; $offsets = null; foreach ($_offsets as $k => $v) { $_stat = $this->ISC[$k]['Stat']; $_stats[$_stat] = $this->ISC[$k]['Stat']; $csvbits[$_stat] = $this->ISC[$k]['CSvBits']; if ($this->ISC[$k]['CSvBits'] !== null && $this->ISC[$k]['CSvBits'] !== '') { $maxValues[$_stat] = pow(2, $this->ISC[$k]['CSvBits']) - 1; } $offsets[$_stat] = $v; } if ($stat == 'hitpoints' || $stat == 'maxhp' || $stat == 'mana' || $stat == 'maxmana' || $stat == 'stamina' || $stat == 'maxstamina') { $val = (int) ($val << 8); } // << 8 multiplication, if the value is larger than what the stat can hold, set it to the max. if ($val > $maxValues[$stat]) { $val = $maxValues[$stat]; } $bitsToWrite = strrev(str_pad(decbin(intval($val)), $csvbits[$_stats[$stat]], 0, STR_PAD_LEFT)); $statOffset = $offsets[$_stats[$stat]]; if (!array_key_exists($stat, $_stats)) { $this->ByteReader->toBits($this->generateAllStats()); } else { $newBits = $stats->writeBits($bits, $bitsToWrite, $statOffset) . "11111111100"; // 0x1FF padding $stats->setBits($newBits); $bytes = $this->ByteReader->toBytes($newBits); $this->ByteReader->writeBytes($gf, $bytes); $this->data = $this->ByteReader->getData(); $this->save(); } } /** * @param $file * @return array */ public function getQuestData($file) { $questsNorm = null; $questsNM = null; $questsHell = null; $quests = null; foreach ($this->sData->qNorm as $k => $v) { fseek($this->fp, $k); $questsNorm[$k] = fread($this->fp, 2); } foreach ($this->sData->qNM as $k => $v) { fseek($this->fp, $k); $questsNM[$k] = fread($this->fp, 2); } foreach ($this->sData->qHell as $k => $v) { fseek($this->fp, $k); $questsHell[$k] = fread($this->fp, 2); } foreach ($questsNorm as $k => $v) { $x = (str_split(strtobits($v), 8)); // if ($x[0][0]) { $quests['Norm'][$this->sData->qNorm[$k]] = $x[0][0]; // } } foreach ($questsNM as $k => $v) { $x = array_filter(str_split(strtobits($v), 8)); // if ($x[0][0]) { $quests['NM'][$this->sData->qNM[$k]] = $x[0][0]; // } } foreach ($questsHell as $k => $v) { $x = array_filter(str_split(strtobits($v), 8)); // if ($x[0][0]) { $quests['Hell'][$this->sData->qHell[$k]] = $x[0][0]; // } } return $quests; } /** * @param $file * @return array */ public function getWaypointsData($file) { $wp = null; fseek($this->fp, $this->sData->wpOffsetsNorm); $a1 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNorm + 1); $a2 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNorm + 2); $a3 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNorm + 3); $a4 = strrev(strtobits(fread($this->fp, 1))); 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); $a1 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNM + 1); $a2 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNM + 2); $a3 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNM + 3); $a4 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsNM + 4); $a5 = strrev(strtobits(fread($this->fp, 1))); $wp['NM'] = str_split($a1 . $a2 . $a3 . $a4 . $a5); fseek($this->fp, $this->sData->wpOffsetsHell); $a1 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsHell + 1); $a2 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsHell + 2); $a3 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsHell + 3); $a4 = strrev(strtobits(fread($this->fp, 1))); fseek($this->fp, $this->sData->wpOffsetsHell + 4); $a5 = strrev(strtobits(fread($this->fp, 1))); $wp['Hell'] = str_split($a1 . $a2 . $a3 . $a4 . $a5); foreach ($wp['Norm'] as $k => $v) { // if ($v == 1) { $waypoints['Norm'][$this->sData->wpNames[$k]] = $v; // } } foreach ($wp['NM'] as $k => $v) { // if ($v == 1) { $waypoints['NM'][$this->sData->wpNames[$k]] = $v; // } } foreach ($wp['Hell'] as $k => $v) { // if ($v == 1) { $waypoints['Hell'][$this->sData->wpNames[$k]] = $v; // } } return $waypoints; } }