D2S Parsing almost done. Todo: full item parsing, Editor GUI, NPC Intro Data, refactor code, writeQuest/writeStat function etc.

This commit is contained in:
Hash Borgir 2022-06-29 01:53:27 -06:00
parent 29063867c5
commit e51e40175e
5 changed files with 146 additions and 59 deletions

View File

@ -66,25 +66,48 @@ $stats->setBits($cleanbits);
//} //}
$stats->rewind(); $stats->rewind();
for($i=0; $i <= strlen($bits); $i++) { for($i=0; $i <= strlen($bits); $i++) {
$id = hexdec($ByteReader->toBytesR($stats->readb(9))); $id = hexdec($ByteReader->toBytesR($stats->readb(9)));
if (!empty($ISC[$id])){ $stats->skip($ISC[$id]['CSvBits']);
$val = $stats->readb($ISC[$id]['CSvBits']); $ids[$id] = $id;
$stat = $ISC[$id]['Stat'];
$values[$stat] = hexdec($ByteReader->toBytesR($val));
}
} }
$values['hitpoints'] = (int) round($values['hitpoints'] / 2048); $stats->rewind();
$values['maxhp'] = (int) round($values['maxhp'] / 2048); foreach($ids as $id){
$values['mana'] = (int) round($values['mana'] / 2048); $stats->skip(9);
$values['maxmana'] = (int) round($values['maxmana'] / 2048); $val = $stats->readb($ISC[$id]['CSvBits']);
$values['stamina'] = (int) round($values['stamina'] / 2048); $stat = $ISC[$id]['Stat'];
$values['maxstamina'] = (int) round($values['maxstamina'] / 2048); $values[$stat] = hexdec($ByteReader->toBytesR($val));
$values['killcounter'] = (int) round($values['killcounter'] / 2); }
$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['killcounter'] = (int) round($values['killcounter'] >> 1);
var_dump($values); var_dump($values);
//$stats->rewind();
//for($i=0; $i <= strlen($bits); $i++) {
// $id = hexdec($ByteReader->toBytesR($stats->readb(9)));
// if (!empty($ISC[$id])){
// $val = $stats->readb($ISC[$id]['CSvBits']);
// $stat = $ISC[$id]['Stat'];
// $values[$stat] = hexdec($ByteReader->toBytesR($val));
// }
//}
//$values['hitpoints'] = (int) round($values['hitpoints'] / 2048);
//$values['maxhp'] = (int) round($values['maxhp'] / 2048);
//$values['mana'] = (int) round($values['mana'] / 2048);
//$values['maxmana'] = (int) round($values['maxmana'] / 2048);
//$values['stamina'] = (int) round($values['stamina'] / 2048);
//$values['maxstamina'] = (int) round($values['maxstamina'] / 2048);
//$values['killcounter'] = (int) round($values['killcounter'] / 2);
//
//var_dump($values);
//array_pop($ids); //array_pop($ids);
// //

View File

@ -2,7 +2,7 @@
class D2BitReader { class D2BitReader {
private string $bits; private string $bits = '';
private int $offset = 0; private int $offset = 0;
public function __construct(string $bits = '') { public function __construct(string $bits = '') {
@ -17,7 +17,7 @@ class D2BitReader {
/* read X number of bits, like fread */ /* read X number of bits, like fread */
public function read(int $numBits = 0, bool $str = true) { public function read(int $numBits = 0, bool $str = true) : string {
$bits = null; $bits = null;
for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { for ($i = $this->offset; $i < $this->offset + $numBits; $i++) {
$str ? $bits .= $this->bits[$i] : $bits[] = $this->bits[$i]; $str ? $bits .= $this->bits[$i] : $bits[] = $this->bits[$i];
@ -26,7 +26,16 @@ class D2BitReader {
return $bits; return $bits;
} }
public function readr(int $numBits = 0) { public function readb(int $numBits = 0, bool $str = true) : string {
$bits = null;
for ($i = $this->offset; $i < $this->offset + $numBits; $i++) {
$str ? $bits .= $this->bits[$i] : $bits[] = $this->bits[$i];
}
$this->offset += $numBits;
return strrev(str_pad($bits, 16, 0, STR_PAD_RIGHT));
}
public function readr(int $numBits = 0) : string {
$bits = null; $bits = null;
for ($i = $this->offset; $i < $this->offset + $numBits; $i++) { for ($i = $this->offset; $i < $this->offset + $numBits; $i++) {
$bits .= $this->bits[$i]; $bits .= $this->bits[$i];
@ -74,6 +83,9 @@ class D2BitReader {
public function getBits(): string { public function getBits(): string {
return $this->bits; return $this->bits;
} }
public function setBits(string $bits) {
$this->bits = $bits;
}
public function getOffset(): int { public function getOffset(): int {
return $this->offset; return $this->offset;

View File

@ -5,7 +5,7 @@ require_once './src/D2BitReader.php';
class D2ByteReader { class D2ByteReader {
private string $data; private string $data = '';
private int $offset = 0; private int $offset = 0;
public function __construct(string $data) { public function __construct(string $data) {
@ -102,13 +102,18 @@ class D2ByteReader {
} }
} }
public function toBytes(string $bits) { public function toBytesR(string $bits) : string {
foreach (str_split($bits, 8) as $byteString) { foreach (str_split($bits, 8) as $byteString) {
$bytes[] = (bindec(strrev($byteString))); $bytes .= strtoupper(str_pad(dechex(bindec(($byteString))), 2, 0, STR_PAD_LEFT));
} }
foreach ($bytes as $byte) { return $bytes;
dump($byte); }
public function toBytes(string $bits) : string {
foreach (str_split($bits, 8) as $byteString) {
$bytes .= strtoupper(str_pad(dechex(bindec(strrev($byteString))), 2, 0, STR_PAD_LEFT));
} }
return $bytes;
} }
public function bitsToHexString(string $bits): string { public function bitsToHexString(string $bits): string {

View File

@ -15,11 +15,23 @@ class D2Char {
private $bData = null; // char binary data from d2s private $bData = null; // char binary data from d2s
private $filePath = null; // .d2s file path private $filePath = null; // .d2s file path
private $fp = null; // file pointer private $fp = null; // file pointer
private $data = null; // full d2s file loaded in $data
private $ByteReader = null; // put $data into bytereader
private $ISC = null;
public function __construct($file) { public function __construct($file) {
$this->sData = new D2CharStructureData(); $this->sData = new D2CharStructureData();
$this->filePath = $_SESSION['savepath'] . $file; $this->filePath = $_SESSION['savepath'] . $file;
$this->fp = fopen($this->filePath, "r+b"); $this->fp = fopen($this->filePath, "r+b");
$this->data = file_get_contents($this->filePath);
$this->ByteReader = new D2ByteReader($this->data);
$sql = "SELECT ID,Stat,CSvBits FROM itemstatcost WHERE Saved=1";
$ISCData = PDO_FetchAll($sql);
foreach ($ISCData as $k => $v) {
$this->ISC[$v['ID']] = $v;
}
// read offsets here from sData and put into $this->bData // read offsets here from sData and put into $this->bData
// which will be used for cData output // which will be used for cData output
@ -28,40 +40,34 @@ class D2Char {
$this->bData[$k] = fread($this->fp, $v); $this->bData[$k] = fread($this->fp, $v);
} }
return $this->parseChar(); // end of parseChar() calls parseItems() return $this->parseChar(); // end of parseChar() calls parseItems(), parseStats, etc.
} }
public function parseItems() { public function parseItems() {
$data = file_get_contents($this->filePath); $i_TotalOffset = strpos($this->data, "JM");
$ByteReader = new D2ByteReader($data);
$i_TotalOffset = strpos($data, "JM");
fseek($this->fp, $i_TotalOffset + 2); fseek($this->fp, $i_TotalOffset + 2);
$i_Total = unpack('S*', (fread($this->fp, 2)))[1]; $i_Total = unpack('S*', (fread($this->fp, 2)))[1];
$i_Offsets = []; $i_Offsets = [];
for ($i = 0; $i <= $i_Total; $i++) { for ($i = 0; $i <= $i_Total; $i++) {
$i_Offsets[] = strposX($data, "JM", $i + 2); $i_Offsets[] = strposX($this->data, "JM", $i + 2);
} }
foreach ($i_Offsets as $k => $v) { foreach ($i_Offsets as $k => $v) {
$itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k]; $itemOffsets[$v] = $i_Offsets[$k + 1] - $i_Offsets[$k];
} }
array_pop($itemOffsets); array_pop($itemOffsets);
$_items=[]; $_items = [];
foreach ($itemOffsets as $offset => $bytes) { foreach ($itemOffsets as $offset => $bytes) {
$this->items[] = new D2Item($ByteReader->toBits($ByteReader->readh($offset, $bytes))); $this->items[] = new D2Item($this->ByteReader->toBits($this->ByteReader->readh($offset, $bytes)));
} }
} }
public function parseChar() { public function parseChar() {
// dump(unpack('l', $this->bData[4])[1]);
$cData = null; $cData = null;
$cData['Identifier'] = bin2hex($this->bData[0]); $cData['Identifier'] = bin2hex($this->bData[0]);
// 96 is v1.10+ - checks out // 96 is v1.10+ - checks out
$cData['VersionID'] = ($this->sData->version[unpack('l', $this->bData[4])[1]]); $cData['VersionID'] = ($this->sData->version[unpack('l', $this->bData[4])[1]]);
// 1.41 KB (1,447 bytes) - checks out // 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['Checksum'] = bin2hex($this->bData[12]); $cData['Checksum'] = bin2hex($this->bData[12]);
@ -76,38 +82,32 @@ class D2Char {
$cData['CharacterClass'] = $this->sData->class[unpack('C', $this->bData[40])[1]]; $cData['CharacterClass'] = $this->sData->class[unpack('C', $this->bData[40])[1]];
$cData['CharacterLevel'] = unpack('C', $this->bData[43])[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('I', $this->bData[48])[0]);
$skills = (unpack('l16', $this->bData[56])); $skills = (unpack('l16', $this->bData[56]));
foreach($skills as $skill){ foreach ($skills as $skill) {
$cData['Assignedskills'][] = $this->sData->skills[$skill]; $cData['Assignedskills'][] = $this->sData->skills[$skill];
} }
//ddump($this->bData);
//ddump($cData);
$cData['LeftmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[120])[1]]; $cData['LeftmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[120])[1]];
$cData['RightmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[124])[1]]; $cData['RightmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[124])[1]];
$cData['LeftswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[128])[1]]; $cData['LeftswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[128])[1]];
$cData['RightswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[132])[1]]; $cData['RightswapmousebuttonskillID'] = $this->sData->skills[unpack('i', $this->bData[132])[1]];
// Char menu appearance not needed // Char menu appearance not needed
// $cData['Charactermenuappearance'] = unpack('i', $this->bData[136]); // $cData['Charactermenuappearance'] = unpack('i', $this->bData[136]);
// todo: refactor to use D2BitstreamReader here // todo: refactor to use D2BitstreamReader here
$x = str_split(strtobits($this->bData[168]), 8); $x = str_split(strtobits($this->bData[168]), 8);
//$x[0][0] ? $diff = 'Normal' : ($x[1][0] ? $diff = 'Nitemare' : $diff = 'Hell'); //$x[0][0] ? $diff = 'Normal' : ($x[1][0] ? $diff = 'Nitemare' : $diff = 'Hell');
$onDifficulty['NM'] = $x[1][0]; $onDifficulty['Normal'] = $x[0][0];
$onDifficulty['NM'] = $x[1][0]; $onDifficulty['NM'] = $x[1][0];
$onDifficulty['Hell'] = $x[2][0]; $onDifficulty['Hell'] = $x[2][0];
$cData['Difficulty'] = array_filter($onDifficulty); // $diff; $cData['Difficulty'] = ($onDifficulty); // $diff;
// Map ID. This value looks like a random number, but it corresponds with one of the longwords
// 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 // found in the character.map file, according to the difficulty being played. Not needed
//$cData['MapID'] = $this->bData[171]; //$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. // This looks like a random ID for your mercenary.
// $cData['MercenaryID'] = unpack('H*', $this->bData[179]); // $cData['MercenaryID'] = unpack('H*', $this->bData[179]);
$cData['MercenaryNameID'] = unpack('S', $this->bData[183])[1]; $cData['MercenaryNameID'] = unpack('S', $this->bData[183])[1];
$cData['MercenaryType'] = unpack('S', $this->bData[185])[1]; $cData['MercenaryType'] = unpack('S', $this->bData[185])[1];
@ -116,20 +116,66 @@ class D2Char {
$cData['Waypoints'] = $this->getWaypointsData($file); $cData['Waypoints'] = $this->getWaypointsData($file);
$cData['NPCIntroductions'] = $this->bData[714]; $cData['NPCIntroductions'] = $this->bData[714];
$cData['filePath'] = $this->filePath; $cData['filePath'] = $this->filePath;
// returns an array of items, // returns an array of items,
// each item is an array of item details // each item is an array of item details
$this->parseItems(); // parse items will populate $this->items $this->parseItems(); // parse items will populate $this->items
$cData['items'] = $this->items; // cData[items] will be $this->items $cData['items'] = $this->items; // cData[items] will be $this->items
$this->cData = $cData; $this->cData = $cData;
// parse stats
$this->parseStats();
unset($this->items);
unset($this->bData);
unset($this->sData);
unset($this->ByteReader);
unset($this->data);
unset($this->fp);
unset($this->ISC);
return $this->cData; return $this->cData;
} }
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();
$cleanbits = substr($bits, 0, -11);
$stats->setBits($cleanbits);
$bits = $stats->getBits();
$stats->rewind();
for($i=0; $i <= strlen($bits); $i++) {
$id = hexdec($this->ByteReader->toBytesR($stats->readb(9)));
if($this->ISC[$id]['CSvBits'] !== NULL){
$stats->skip($this->ISC[$id]['CSvBits']);
}
$ids[$id] = $id;
}
$stats->rewind();
foreach($ids as $id){
$stats->skip(9);
if($this->ISC[$id]['CSvBits'] !== NULL){
$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['killcounter'] = (int) round($values['killcounter'] >> 1);
$this->cData['stats'] = $values;
}
public function getQuestData($file) { public function getQuestData($file) {
$questsNorm = null; $questsNorm = null;
$questsNM = null; $questsNM = null;
@ -181,7 +227,7 @@ class D2Char {
fseek($this->fp, $this->sData->wpOffsetsNorm + 4); fseek($this->fp, $this->sData->wpOffsetsNorm + 4);
$a5 = strrev(strtobits(fread($this->fp, 1))); $a5 = strrev(strtobits(fread($this->fp, 1)));
$wp['Norm'] = str_split($a1 . $a2 . $a3 . $a4 . $a5); $wp['Norm'] = str_split($a1 . $a2 . $a3 . $a4 . $a5);
// ddump($wp['Norm']); // ddump($wp['Norm']);
fseek($this->fp, $this->sData->wpOffsetsNM); fseek($this->fp, $this->sData->wpOffsetsNM);
@ -224,5 +270,6 @@ class D2Char {
// } // }
} }
return $waypoints; return $waypoints;
} }
} }

View File

@ -42,7 +42,7 @@ $form = new Formr\Formr();
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
ddump($charData); //dump($charData);
?> ?>
@ -215,7 +215,7 @@ EOT;
<select id='CharacterClass'> <select id='CharacterClass'>
$option $option
</select> </select>
<input style="border: 1px solid black;width: 34px;" type="number" id="CharacterLevel" value="{$c->cData['CharacterLevel']}"><br> Level: <input style="border: 1px solid black;width: 54px;" type="number" id="CharacterLevel" value="{$c->cData['CharacterLevel']}"><br>
$radio $radio
</div> </div>
<div class="col"><h2>Quests</h2> <div class="col"><h2>Quests</h2>