mirror of
https://gitlab.com/hashborgir/d2tools.git
synced 2024-11-30 12:36:03 +00:00
763 lines
28 KiB
PHP
763 lines
28 KiB
PHP
<?php
|
|
|
|
require_once 'D2CharStructureData.php';
|
|
require_once 'D2Files.php';
|
|
require_once 'D2BitReader.php';
|
|
require_once 'D2Strings.php';
|
|
require_once 'D2Item.php';
|
|
require_once 'D2ByteReader.php';
|
|
require_once 'D2Functions.php';
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class D2Char {
|
|
|
|
/**
|
|
* @var null|string char data output
|
|
*/
|
|
public $cData = null;
|
|
|
|
/**
|
|
* @var null|string char item data
|
|
*/
|
|
public $items = null;
|
|
|
|
/**
|
|
* @var D2ByteReader|null put $data into bytereader
|
|
*/
|
|
public $ByteReader = null;
|
|
|
|
/**
|
|
* @var null|string .d2s file path
|
|
*/
|
|
public $filePath = null;
|
|
|
|
/**
|
|
* @var D2CharStructureData char file structure data
|
|
*/
|
|
private $sData = null;
|
|
|
|
/**
|
|
* @var null|string char binary data from d2s
|
|
*/
|
|
private $bData = null;
|
|
|
|
/**
|
|
* @var false|resource file pointer
|
|
*/
|
|
private $fp = null;
|
|
|
|
/**
|
|
* @var false|string full d2s file loaded in $data
|
|
*/
|
|
private $data = null;
|
|
|
|
/**
|
|
* @var null
|
|
*/
|
|
private $ISC = null;
|
|
|
|
/**
|
|
* @var null
|
|
*/
|
|
private $skillData = null;
|
|
|
|
/**
|
|
* Saves the data to a file.
|
|
*
|
|
* This method updates the data with a new checksum and saves it to the specified file path.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function save() {
|
|
$this->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;
|
|
}
|
|
|
|
}
|