Checksum code fixed. Added DocBlocks to D2Classes. TODO: Fill in docblocks, refactor, code cleanup

This commit is contained in:
Hash Borgir
2022-07-06 17:41:24 -06:00
parent f3b24de2a8
commit eb28039f88
28 changed files with 1285 additions and 340 deletions

View File

@@ -8,24 +8,70 @@ require_once 'D2Item.php';
require_once 'D2ByteReader.php';
require_once 'D2Functions.php';
/**
*
*/
class D2Char {
/**
* @var null
*/
public $cData = null; // char data output
/**
* @var null
*/
public $items = null; // char item data
/**
* @var D2ByteReader|null
*/
public $ByteReader = null; // put $data into bytereader
/**
* @var string|null
*/
public $filePath = null; // .d2s file path
/**
* @var D2CharStructureData
*/
private $sData = null; // char file structure data
/**
* @var null
*/
private $bData = null; // char binary data from d2s
/**
* @var false|resource
*/
private $fp = null; // file pointer
/**
* @var false|string
*/
private $data = null; // full d2s file loaded in $data
/**
* @var null
*/
private $ISC = null;
/**
* @var null
*/
private $skillData = null;
/**
* @return void
*/
public function save() {
file_put_contents($this->filePath, $this->data);
checksumFix($this->filePath);
$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
}
/**
* @param $file
*/
public function __construct($file) {
$this->sData = new D2CharStructureData();
$this->filePath = $_SESSION['savepath'] . $file;
@@ -52,7 +98,7 @@ class D2Char {
$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
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'";
@@ -64,6 +110,9 @@ WHERE sk.charclass = '$class'";
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);
@@ -83,6 +132,9 @@ WHERE sk.charclass = '$class'";
}
}
/**
* @return array|null
*/
public function parseChar() {
$cData = null;
$cData['Identifier'] = bin2hex($this->bData[0]);
@@ -90,30 +142,46 @@ WHERE sk.charclass = '$class'";
$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'] = 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['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]) . " ";
$characterStatus = array_filter(str_split(strrev(strtobits($this->bData[36]))));
foreach ($characterStatus as $k => $v) {
$str .= $this->sData->characterStatus[$k] . " ";
}
$cData['CharacterStatus'] = $str;
$cData['Characterprogression'] = bindec($this->bData[37]);
$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('I', $this->bData[48])[0]);
$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('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['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]);
@@ -128,7 +196,7 @@ WHERE sk.charclass = '$class'";
// 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];
$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];
@@ -149,13 +217,16 @@ WHERE sk.charclass = '$class'";
$this->cData['skills'] = $this->parseSkills();
unset($this->items);
unset($this->bData);
//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);
@@ -177,6 +248,10 @@ WHERE sk.charclass = '$class'";
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);
@@ -188,6 +263,11 @@ WHERE sk.charclass = '$class'";
$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
@@ -198,6 +278,9 @@ WHERE sk.charclass = '$class'";
$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);
@@ -205,13 +288,13 @@ WHERE sk.charclass = '$class'";
$stats = new D2BitReader($this->ByteReader->toBits($this->ByteReader->readh($gf, $len)));
$bits = $stats->getBits();
$bytes = $this->ByteReader->toBytes($bits);
$bytes = $this->ByteReader->toBytes($bits);
$stats->rewind();
for ($i = 0; $i <= strlen($bits); $i++) {
$id = hexdec($this->ByteReader->toBytesR($stats->readb(9)));
if ($this->ISC[$id]['CSvBits'] !== NULL) {
if ($this->ISC[$id]['CSvBits'] !== NULL && $this->ISC[$id]['CSvBits'] !== '') {
$stats->skip($this->ISC[$id]['CSvBits']);
}
$ids[$id] = $id;
@@ -219,11 +302,11 @@ WHERE sk.charclass = '$class'";
$stats->rewind();
foreach ($ids as $id) {
$stats->skip(9);
if ($this->ISC[$id]['CSvBits'] !== NULL) {
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));
}
$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);
@@ -231,12 +314,19 @@ WHERE sk.charclass = '$class'";
$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);
$values['soulcounter'] = (int) round($values['soulcounter'] / 2);
$values['killcounter'] = (int) round($values['killcounter'] / 2);
$this->cData['stats'] = $values;
}
public function setChar(string $stat, mixed $val) {
/**
* @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) {
@@ -244,15 +334,20 @@ WHERE sk.charclass = '$class'";
}
$pack = $this->ByteReader->bitsToHexString($this->ByteReader->toBits(pack('Z16', $val)));
$this->ByteReader->writeBytes(20, $pack);
$this->data = $this->ByteReader->getData();
$this->save();
rename($this->filePath, $_SESSION['savepath'] . "$val.d2s");
break;
case "CharacterClass":
$this->ByteReader->writeByte(40, $val);
$this->data = $this->ByteReader->getData();
$this->save();
$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) {
@@ -271,26 +366,111 @@ WHERE sk.charclass = '$class'";
$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 = '';
foreach ($this->ISC as $i) {
$id = strrev(str_pad((decbin($i['ID'])), 9, 0, STR_PAD_LEFT));
$val = strrev(str_pad((decbin(20)), (int) $i['CSvBits'], 0, STR_PAD_LEFT));
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;
$statall = $stats . "11111111100";
$bytes = $this->ByteReader->toBytes($statall);
$bytes = $this->ByteReader->toBytes($stats);
// refresh this data
$data = $this->ByteReader->getData();
// delete everything between GF---and---IF
@@ -303,19 +483,27 @@ WHERE sk.charclass = '$class'";
$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();
$bits = $stats->getBits();
$cleanbits = substr($bits, 0, -11);
$stats->setBits($cleanbits);
$bits = $stats->getBits();
@@ -324,7 +512,9 @@ WHERE sk.charclass = '$class'";
for ($i = 0; $i <= strlen($bits); $i++) {
$id = hexdec($this->ByteReader->toBytesR($stats->readb(9)));
$_offsets[$id] = $stats->getOffset();
$stats->skip($this->ISC[$id]['CSvBits']);
if ($this->ISC[$id]['CSvBits'] !== null && $this->ISC[$id]['CSvBits'] !== '') {
$stats->skip($this->ISC[$id]['CSvBits']);
}
}
$_offsets[0] = 9;
$offsets = null;
@@ -332,7 +522,9 @@ WHERE sk.charclass = '$class'";
$_stat = $this->ISC[$k]['Stat'];
$_stats[$_stat] = $this->ISC[$k]['Stat'];
$csvbits[$_stat] = $this->ISC[$k]['CSvBits'];
$maxValues[$_stat] = pow(2, $this->ISC[$k]['CSvBits']) - 1;
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') {
@@ -344,14 +536,22 @@ WHERE sk.charclass = '$class'";
}
$bitsToWrite = strrev(str_pad(decbin(intval($val)), $csvbits[$_stats[$stat]], 0, STR_PAD_LEFT));
$statOffset = $offsets[$_stats[$stat]];
$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();
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;
@@ -390,6 +590,10 @@ WHERE sk.charclass = '$class'";
return $quests;
}
/**
* @param $file
* @return array
*/
public function getWaypointsData($file) {
$wp = null;
fseek($this->fp, $this->sData->wpOffsetsNorm);