D2S Editing WIP

This commit is contained in:
Hash Borgir
2023-06-01 23:42:12 -06:00
parent 3357f757ea
commit b9f4054028
4 changed files with 450 additions and 436 deletions

View File

@@ -14,47 +14,60 @@ require_once 'D2Functions.php';
class D2Char {
/**
* @var null
* @var null|string char data output
*/
public $cData = null; // char data output
public $cData = null;
/**
* @var null
* @var null|string char item data
*/
public $items = null; // char item data
public $items = null;
/**
* @var D2ByteReader|null
* @var D2ByteReader|null put $data into bytereader
*/
public $ByteReader = null; // put $data into bytereader
public $ByteReader = null;
/**
* @var string|null
* @var null|string .d2s file path
*/
public $filePath = null; // .d2s file path
public $filePath = null;
/**
* @var D2CharStructureData
* @var D2CharStructureData char file structure data
*/
private $sData = null; // char file structure data
private $sData = null;
/**
* @var null
* @var null|string char binary data from d2s
*/
private $bData = null; // char binary data from d2s
private $bData = null;
/**
* @var false|resource
* @var false|resource file pointer
*/
private $fp = null; // file pointer
private $fp = null;
/**
* @var false|string
* @var false|string full d2s file loaded in $data
*/
private $data = null; // 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() {
@@ -62,7 +75,7 @@ class D2Char {
$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
@@ -70,65 +83,70 @@ class D2Char {
}
/**
* @param $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();
$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();
$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 here from sData and put into $this->bData
// which will be used for cData output
// Read offsets from sData and store them in $this->bData
foreach ($this->sData->offsets as $k => $v) {
fseek($this->fp, $k);
$this->bData[$k] = fread($this->fp, $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))];
$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);
$sd = PDO_FetchAll($sql); // Fetch skill data from the database
foreach ($sd as $k => $v) {
$this->skillData[$k + 1] = $v;
}
return $this->parseChar(); // end of parseChar() calls parseItems(), parseStats, etc.
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");
fseek($this->fp, $i_TotalOffset + 2);
$i_Total = unpack('S*', (fread($this->fp, 2)))[1];
$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
$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
$i_Offsets = [];
for ($i = 0; $i <= $i_Total; $i++) {
$i_Offsets[] = strposX($this->data, "JM", $i + 2);
$i_Offsets[] = strposX($this->data, "JM", $i + 2); // Find the offsets of the "JM" markers for each item
}
foreach ($i_Offsets as $k => $v) {
$itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k];
$itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k]; // Calculate the length of each item's data by subtracting consecutive offsets
}
array_pop($itemOffsets);
array_pop($itemOffsets); // Remove the last element from the itemOffsets array
$_items = [];
foreach ($itemOffsets as $offset => $bytes) {
$this->items[] = new D2Item($this->ByteReader->toBits($this->ByteReader->readh($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
}
}
@@ -166,7 +184,6 @@ WHERE sk.charclass = '$class'";
$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]);
@@ -228,10 +245,12 @@ WHERE sk.charclass = '$class'";
* @return array
*/
public function parseSkills() {
$if = strposX($this->data, 'if', 1) + 2; // find if and skip it
$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] = [
@@ -244,83 +263,108 @@ WHERE sk.charclass = '$class'";
];
}
}
$cData['skills'] = array_values($cData['skills']);
$cData['skills'] = array_values($cData['skills']); // Reset the array keys
return $cData;
}
/**
* @param int $points
* 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
$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->ByteReader->writeByte($if + $i, $points); // Set the skill points to the given value
}
$this->data = $this->ByteReader->getData();
$this->save();
$this->data = $this->ByteReader->getData(); // Update the character data
$this->save(); // Save the changes to the character file
}
/**
* @param int $skill
* @param int $points
* 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;
$if = strposX($this->data, 'if', 1) + 2; // find if and skip it
$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 $kill to $points
// Set the points for the specified skill
$this->ByteReader->writeByte($if + $skill, $points);
$this->data = $this->ByteReader->getData();
$this->save();
$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
$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();
$ids = []; // Array to store the encountered stat IDs
// Iterate over the stats and collect their IDs
for ($i = 0; $i <= count($this->ISC); $i++) {
$id = hexdec($this->ByteReader->toBytesR($stats->readb(9)));
// Skip the bits corresponding to the stat if needed
if ($this->ISC[$id]['CSvBits'] !== NULL && $this->ISC[$id]['CSvBits'] !== '') {
$stats->skip($this->ISC[$id]['CSvBits']);
}
$ids[$id] = $id;
$ids[$id] = $id; // Store the ID in the array
}
$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));
}
}
// 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);
// $values['soulcounter'] = (int) round($values['soulcounter'] / 2);
// $values['killcounter'] = (int) round($values['killcounter'] / 2);
$this->cData['stats'] = $values;
$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
@@ -349,61 +393,54 @@ WHERE sk.charclass = '$class'";
];
$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':
$status = strrev(strtobits($this->data[36]));
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;
}
$byte = $this->ByteReader->bitsToHexString($status);
$this->ByteReader->writeByte(36, hexdec($byte));
break;
case 'CharacterProgression': // 0 in normal, 1 finished normal, 2 finished nm, 3 finished hell
case 'CharacterProgression':
switch ($val) {
case 0: // in normal
case 0:
$this->data[37] = pack('C', 3);
break;
case 1: // finished normal
case 1:
$this->data[37] = pack('C', 8);
break;
case 2: // finished nm
case 2:
$this->data[37] = pack('C', 13);
break;
case 3: // finished hell
case 3:
$this->data[37] = pack('C', 15);
break;
$this->save();
}
$this->save();
break;
case 'LeftmousebuttonskillID':
@@ -423,12 +460,13 @@ WHERE sk.charclass = '$class'";
break;
}
// finally save char data to d2s file
$this->data = $this->ByteReader->getData();
$this->save();
}
/**
* Reset the file size of the D2S file.
*
* @return void
*/
public function resetFileSize() {
@@ -436,8 +474,8 @@ WHERE sk.charclass = '$class'";
$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);
@@ -446,54 +484,51 @@ WHERE sk.charclass = '$class'";
}
/**
* @return false|string
* Generate all stats and update the D2S file.
*
* @return false|string The updated data of the D2S file.
*/
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);
$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";
//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
// Refresh the 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);
// 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);
$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
* 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) {
@@ -549,44 +584,59 @@ WHERE sk.charclass = '$class'";
}
/**
* @param $file
* @return array
* 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;
}
@@ -595,44 +645,26 @@ WHERE sk.charclass = '$class'";
* @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);
$wp = [];
$offsets = [
'Norm' => $this->sData->wpOffsetsNorm,
'NM' => $this->sData->wpOffsetsNM,
'Hell' => $this->sData->wpOffsetsHell,
];
// ddump($wp['Norm']);
foreach ($offsets as $difficulty => $offset) {
fseek($this->fp, $offset);
$waypointData = '';
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);
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);
}
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) {