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 } /** * Initializes the class instance with the given file. * * @param string $file The file to be processed. */ public function __construct($file) { $this->sData = new D2CharStructureData(); // Create a new instance of D2CharStructureData $this->filePath = $_SESSION['savepath'] . $file; // Set the file path based on the session save path and the provided file $this->fp = fopen($this->filePath, "r+b"); // Open the file in read/write binary mode $data = file_get_contents($this->filePath); // Read the contents of the file $this->ByteReader = new D2ByteReader($data); // Create a new instance of D2ByteReader with the file data $this->data = $this->ByteReader->getData(); // Get the data from the ByteReader instance // Fetch itemstatcost data from the database and store it in $this->ISC $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 from sData and store them in $this->bData foreach ($this->sData->offsets as $k => $v) { fseek($this->fp, $k); // Move the file pointer to the specified offset $this->bData[$k] = fread($this->fp, $v); // Read data from the file at the current offset and store it in $this->bData } $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))]; // Determine the character class based on the byte read from the file $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); // Fetch skill data from the database foreach ($sd as $k => $v) { $this->skillData[$k + 1] = $v; } return $this->parseChar(); // Call the parseChar() method and return its result } /** * Parses the items in the data. * * This method extracts and processes the item data from the provided data. * * @return void */ public function parseItems() { $i_TotalOffset = strpos($this->data, "JM"); // Find the offset of the "JM" marker in the data fseek($this->fp, $i_TotalOffset + 2); // Move the file pointer to the next position after the "JM" marker // get total # of items $i_Total = unpack('S*', (fread($this->fp, 2)))[1]; // Read 2 bytes from the file and unpack them as an unsigned short (16-bit) value // get offsets for each item from total, so if 50, run loop 50 times // grab the offset of each item, each item starts with JM, skim JM (+2) $i_Offsets = []; for ($i = 0; $i <= $i_Total; $i++) { $i_Offsets[] = strposX($this->data, "JM", $i + 2); // Find the offsets of the "JM" markers for each item } // foreach item offset, get item data byte length between the JMs // k = item #, like 0,1,2,3, etc. // v = actual offset of item foreach ($i_Offsets as $k => $v) { $itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k]; // Calculate the length of each item's data by subtracting consecutive offsets in bytes } array_pop($itemOffsets); // Remove the last element from the itemOffsets array, not sure why //now get items. For each itemOffsets, create new D2Item object that parses the bitstream // readh(offset, numbytes), convert toBits, feed into D2Item $_items = []; foreach ($itemOffsets as $offset => $bytes) { $this->items[] = new D2Item($this->ByteReader->toBits($this->ByteReader->readh($offset, $bytes))); // Create a new D2Item object and add it to the items array } } /** * @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 (str_contains($cData['CharacterStatus'], "Expansion")) { $cData['CharacterProgression'] = $this->sData->characterProgressionExp[$progression]; $cData['CharacterStatusExpansion'] = 1; } if (str_contains($cData['CharacterStatus'], "Hardcore")) { $cData['CharacterProgression'] = $this->sData->characterProgressionClassicHC[$progression]; $cData['CharacterStatusHardcore'] = 1; } if (str_contains($cData['CharacterStatus'], "Died")) { $cData['CharacterProgression'] = $this->sData->characterProgressionClassicHC[$progression]; $cData['CharacterStatusDied'] = 1; } $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))); $cData = []; // Initialize the result array foreach ($skills as $k => $v) { if ($this->skillData[$k]['String']) { $cData['skills'][$k] = [ 'id' => $this->skillData[$k]['Id'], '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']); // Reset the array keys return $cData; } /** * Set all skills to a given number of points. * * @param int $points The number of points to set for all skills. * @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); // Set the skill points to the given value } $this->data = $this->ByteReader->getData(); // Update the character data $this->save(); // Save the changes to the character file } /** * Set the points for a specific skill. * * @param int $skill The skill ID. * @param int $points The number of points to set for the skill. * @return void */ public function setSkill(int $skill, int $points) { $skill -= 1; // Adjust the skill ID to match the array index $if = strposX($this->data, 'if', 1) + 2; // Find 'if' and skip it $jm = strposX($this->data, 'JM', 1); // Set the points for the specified skill $this->ByteReader->writeByte($if + $skill, $points); $this->data = $this->ByteReader->getData(); // Update the character data $this->save(); // Save the changes to the character file } /** * Parse the character stats. * * @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); // var_dump($bits); // // Split the bits into 8-bit pieces // var_dump(str_split($bits, 8)); /* * CSvBits for Example, how many bits they store Stat ID CSvBits strength 0 10 energy 1 10 dexterity 2 10 vitality 3 10 statpts 4 10 newskills 5 8 hitpoints 6 21 maxhp 7 21 mana 8 21 maxmana 9 21 stamina 10 21 maxstamina 11 21 level 12 7 experience 13 32 gold 14 25 goldbank 15 25 */ $stats->rewind(); $ids = []; // Array to store the encountered stat IDs // Iterate over the stats and collect their IDs for ($i = 0; $i <= count($this->ISC); $i++) { //var_dump("Offset Before Starting: " . $stats->getOffset()); $id = hexdec($this->ByteReader->toBytesR($stats->readb(9))); // read 9 bits for stat id //var_dump("$id Stat Done: ". $stats->getOffset()); // if ISC saves the stat, meaning CSVBits is set, it means it saves that my bits for this stat // then we can skip that many number of bits, so we can get the next stat id if ($this->ISC[$id]['CSvBits'] !== NULL && $this->ISC[$id]['CSvBits'] !== '') { $stats->skip($this->ISC[$id]['CSvBits']); //var_dump("Offset After Skipping Bits for $id: ". $stats->getOffset()); } $ids[$id] = $id; // Store the ID in the array } //ddump($ids); $stats->rewind(); $values = []; // Array to store the parsed stat values // Iterate over the collected stat IDs and retrieve their values foreach ($ids as $id) { $stats->skip(9); // Skip the bits corresponding to the stat if needed 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)); } } //ddump($values); // Perform additional calculations or conversions on specific stats $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); $this->cData['stats'] = $values; // Assign the parsed stats to the character data } /** * Set character attributes. * * @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': $pack = $this->ByteReader->bitsToHexString($this->ByteReader->toBits(pack('Z16', $val))); $this->ByteReader->writeBytes(20, $pack); $this->data = $this->ByteReader->getData(); $this->save(); // file is not being saved, and copied before being saved, hence the bug, fixed $fileName = pathinfo($this->filePath, PATHINFO_FILENAME); $originalFileNames = glob($_SESSION['savepath'] . $fileName . '*'); foreach ($originalFileNames as $originalFileName) { $newFileName = $_SESSION['savepath'] . $val . '.' . pathinfo($originalFileName, PATHINFO_EXTENSION); if ($originalFileName !== $newFileName) { copy($originalFileName, $newFileName); unlink($originalFileName); } } 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; } $this->ByteReader->writeByte(43, $val); $this->setStat('level', $val); $sql = "SELECT {$this->cData['CharacterClass']} FROM experience WHERE level = '$val'"; $res = PDO_FetchOne($sql); $this->setStat('experience', $res); break; case 'CharacterStatus': $status = strrev(strtobits($this->data[36])); switch ($val) { case 'Died': $status[3] = $val2; break; case 'Hardcore': $status[2] = $val2; break; case 'Expansion': $status[5] = $val2; break; } $byte = $this->ByteReader->bitsToHexString($status); $this->ByteReader->writeByte(36, hexdec($byte)); break; case 'CharacterProgression': switch ($val) { case 0: $this->data[37] = pack('C', 3); break; case 1: $this->data[37] = pack('C', 8); break; case 2: $this->data[37] = pack('C', 13); break; case 3: $this->data[37] = pack('C', 15); break; } $this->save(); break; case 'Difficulty': switch ($val) { case "Normal": $this->data[168] = pack('C', 255); // 1000 0000 MSB = Difficulty break; case "NM": $this->data[169] = pack('C', 255); break; case "Hell": $this->data[170] = pack('C', 255); break; } 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; } $this->data = $this->ByteReader->getData(); $this->save(); } /** * Reset the file size of the D2S file. * * @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]); } /** * Generate all stats and update the D2S file. * * @return false|string The updated data of the D2S file. */ public function generateAllStats() { $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)); $stat = $id . $val; $stats .= $stat; } $stats .= "000011111111100"; $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 the data $data = $this->ByteReader->getData(); // Delete everything between GF and IF $data = substr_replace($data, '', $gf, $len); // Pack hex bytes into a binary string $packedBytes = pack('H*', $bytes); // Insert the 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; } /** * Set a specific stat in the D2S file. * * @param string $stat The stat to set. * @param mixed $val The value to set for the stat. * @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(); } } /** * Get the quest data from the specified file. * * @param string $file The file to read the quest data from. * @return array The quest data. */ public function getQuestData($file) { $questsNorm = null; $questsNM = null; $questsHell = null; $quests = null; // Read quests from qNorm foreach ($this->sData->qNorm as $k => $v) { fseek($this->fp, $k); $questsNorm[$k] = fread($this->fp, 2); } // Read quests from qNM foreach ($this->sData->qNM as $k => $v) { fseek($this->fp, $k); $questsNM[$k] = fread($this->fp, 2); } // Read quests from qHell foreach ($this->sData->qHell as $k => $v) { fseek($this->fp, $k); $questsHell[$k] = fread($this->fp, 2); } // Process quests from qNorm foreach ($questsNorm as $k => $v) { $x = (str_split(strtobits($v), 8)); // if ($x[0][0]) { $quests['Norm'][$this->sData->qNorm[$k]] = $x[0][0]; // } } // Process quests from qNM 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]; // } } // Process quests from qHell 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 = []; $offsets = [ 'Norm' => $this->sData->wpOffsetsNorm, 'NM' => $this->sData->wpOffsetsNM, 'Hell' => $this->sData->wpOffsetsHell, ]; foreach ($offsets as $difficulty => $offset) { fseek($this->fp, $offset); $waypointData = ''; for ($i = 0; $i < 5; $i++) { $a = strrev(strtobits(fread($this->fp, 1))); $waypointData .= $a; fseek($this->fp, $offset + $i + 1); } $wp[$difficulty] = str_split($waypointData); } 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; } }