more d2s parsing, refactor

This commit is contained in:
Hash Borgir 2022-06-21 07:06:41 -06:00
parent 5907c8c8d3
commit 5c00d05494
9 changed files with 342 additions and 66 deletions

View File

@ -18,11 +18,9 @@ require_once './src/D2BitReader.php';
define('DB_FILE', $_SESSION['modname'] . ".db"); define('DB_FILE', $_SESSION['modname'] . ".db");
PDO_Connect("sqlite:" . DB_FILE); PDO_Connect("sqlite:" . DB_FILE);
$sql = "SELECT * FROM strings"; $sql = "SELECT * FROM strings";
$strings = PDO_FetchAssoc($sql); $strings = PDO_FetchAssoc($sql);
$sql = "SELECT code,namestr $sql = "SELECT code,namestr
FROM armor FROM armor
UNION SELECT code,namestr UNION SELECT code,namestr
@ -32,9 +30,7 @@ $sql = "SELECT code,namestr
"; ";
$namestr = PDO_FetchAssoc($sql); $namestr = PDO_FetchAssoc($sql);
//ddump($namestr); //ddump($namestr);
//$filePath = "D:\Diablo II\MODS\ironman-dev\save\Aldur.d2s"; //$filePath = "D:\Diablo II\MODS\ironman-dev\save\Aldur.d2s";
$filePath = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s"; $filePath = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s";
//$filePath = "D:\Diablo II\MODS\MedianXL2012\save\Lok.d2s"; //$filePath = "D:\Diablo II\MODS\MedianXL2012\save\Lok.d2s";
@ -43,6 +39,8 @@ $filePath = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s";
$fp = fopen($filePath, 'r+b'); $fp = fopen($filePath, 'r+b');
$data = file_get_contents($filePath); $data = file_get_contents($filePath);
$i_offset = strpos($data, "JM"); $i_offset = strpos($data, "JM");
fseek($fp, $i_offset + 2); fseek($fp, $i_offset + 2);
@ -82,28 +80,36 @@ foreach ($items as $_item) {
$b = new D2BitReader($_item); $b = new D2BitReader($_item);
//$b->bseek(27); $b->seek(58);
//dump($b->bread(1)); $parent = bindec(strrev($b->read(3)));
if ($parent == 0) {
$b->seek(73);
$_stored = bindec(strrev($b->read(3)));
switch ($_stored) {
case 0:
$stored = ''; // item is not stored, check bit 58
break;
case 1:
$stored = 'Inventory';
break;
case 4:
$stored = 'Horadric Cube';
break;
case 5:
$stored = 'Stash';
break;
}
dump($stored);
}
$b->seek(76); $b->seek(76);
// dump($b->bread(32));
$codeBits = str_split($b->read(32), 8); $codeBits = str_split($b->read(32), 8);
$itemCode = ''; $itemCode = '';
foreach ($codeBits as $byte) { foreach ($codeBits as $byte) {
// dump(strrev($byte));
//dump(sprintf('%c', bindec($byte)));
$itemCode .= chr(bindec(strrev($byte))); $itemCode .= chr(bindec(strrev($byte)));
} }
$itemCode = trim($itemCode); $itemCode = trim($itemCode);
dump($namestr[$itemCode]); dump($namestr[$itemCode]);
dump($strings[$namestr[$itemCode]]); dump($strings[$namestr[$itemCode]]);
// dump($strings[trim($itemCode)]);
} }

View File

@ -4,5 +4,5 @@ php.version=PHP_81
source.encoding=UTF-8 source.encoding=UTF-8
src.dir=. src.dir=.
tags.asp=false tags.asp=false
tags.short=false tags.short=true
web.root=. web.root=.

View File

@ -26,6 +26,15 @@ class D2BitReader {
return $bits; return $bits;
} }
public function readr(int $numBits = 0) {
$bits = null;
for ($i = $this->offset; $i < $this->offset + $numBits; $i++) {
$bits .= $this->bits[$i];
}
$this->offset += $numBits;
return strrev($bits);
}
/* seek to offset (like fseek) */ /* seek to offset (like fseek) */
public function seek(int $pos): bool { public function seek(int $pos): bool {
@ -63,4 +72,19 @@ class D2BitReader {
return true; return true;
} }
public function getBits(): string {
return $this->bits;
}
public function getOffset(): int {
return $this->offset;
}
public function setOffset(int $offset): bool {
if ($offset < 0 || $offset > strlen($this->bits))
return false;
$this->offset = $offset;
return true;
}
} }

View File

@ -4,20 +4,21 @@ require_once 'D2CharStructureData.php';
require_once 'D2Files.php'; require_once 'D2Files.php';
require_once 'D2BitReader.php'; require_once 'D2BitReader.php';
require_once 'D2Strings.php'; require_once 'D2Strings.php';
require_once 'D2Item.php';
class D2Char { class D2Char {
public $cData; // char data output public $cData = null; // char data output
public $items; // char item data public $items = null; // char item data
private $sData; // char file structure data private $sData = null; // char file structure data
private $bData; // char binary data from d2s private $bData = null; // char binary data from d2s
private $filePath; // .d2s file path private $filePath = null; // .d2s file path
private $fp; // file pointer private $fp = null; // file pointer
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, "rb+"); $this->fp = fopen($this->filePath, "r+b");
// 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
@ -25,62 +26,120 @@ class D2Char {
fseek($this->fp, $k); fseek($this->fp, $k);
$this->bData[$k] = fread($this->fp, $v); $this->bData[$k] = fread($this->fp, $v);
} }
$this->strings = new D2Strings();
return $this->parseChar();
return $this->parseChar(); // end of parseChar() calls parseItems()
} }
// parse items from d2s and add to $this->items[]
/*
* @return Array of items
*/
public function parseItems() { public function parseItems() {
$_data = file_get_contents($this->filePath);
// get offset of first JM and skip it
$_offset = strpos($_data, "JM") + 2;
// seek to items_total offset
fseek($this->fp, $_offset);
// item total is a SHORT 16 bits, 2 bytes
$_total = unpack('S*', (fread($this->fp, 2)))[1];
// Items start from 2nd JM
for ($i = 2; $i <= $_total; $i++) {
// seek to Each JM (every item begins with JM
fseek($this->fp, strposX($_data, 'JM', $i));
// read and unpack 21 bytes as array of byte/char (8 bit each)
$_items[] = unpack('C*', fread($this->fp, 21));
}
// Convert item bytes to 21x8 bitfield
// each byte is reversed with strrev() for correct bit position
// to convert back to ascii, read bits from any position, reverse, bindec(chr())
foreach ($_items as $_item) {
$item = null;
foreach ($_item as $i_bytes) {
$item .= strrev(str_pad(decbin($i_bytes), 8, 0, STR_PAD_LEFT));
// $item .= (str_pad(dechex($i_bytes), 2, 0, STR_PAD_LEFT));
}
$this->items[] = new D2Item($item); // return an array of item details
}
} }
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'] = $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]);
$cData['Activeweapon'] = unpack('l', $this->bData['16']); $cData['Activeweapon'] = unpack('l', $this->bData[16]);
$cData['CharacterName'] = str_replace("\0", "", $this->bData[20]); $cData['CharacterName'] = str_replace("\0", "", $this->bData[20]);
$cData['CharacterStatus'] = array_filter(str_split(strtobits($this->bData[36]))); $cData['CharacterStatus'] = array_filter(str_split(strtobits($this->bData[36])));
foreach ($cData['CharacterStatus'] as $k => $v) { foreach ($cData['CharacterStatus'] as $k => $v) {
$str .= ($characterStatus[$k]) . " "; $str .= ($characterStatus[$k]) . " ";
} }
$cData['CharacterStatus'] = $str; $cData['CharacterStatus'] = $str;
$cData['Characterprogression'] = bindec($this->bData['37']); $cData['Characterprogression'] = bindec($this->bData[37]);
$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]);
$cData['Assignedskills'] = (unpack('i16', $this->bData['56']));
$skills = (unpack('l16', $this->bData[56]));
foreach($skills as $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]];
$cData['Charactermenuappearance'] = unpack('i', $this->bData[136]);
// 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 = str_split(strtobits($this->bData[168]), 8);
$onDifficulty['Norm'] = $x[0][0]; //$x[0][0] ? $diff = 'Normal' : ($x[1][0] ? $diff = 'Nitemare' : $diff = 'Hell');
$onDifficulty['NM'] = $x[1][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); $cData['Difficulty'] = array_filter($onDifficulty); // $diff;
$cData['MapID'] = $this->bData['171'];
$cData['Mercenarydead'] = unpack('i', $this->bData['177']); // Map ID. This value looks like a random number, but it corresponds with one of the longwords
$cData['MercenaryID'] = $this->bData['179']; // found in the character.map file, according to the difficulty being played. Not needed
$cData['MercenaryNameID'] = $this->bData['183']; //$cData['MapID'] = $this->bData[171];
$cData['Mercenarytype'] = $this->bData['185'];
$cData['Mercenaryexperience'] = $this->bData['187']; $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['Quests'][] = $this->getQuestData($file);
$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,
// 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; $this->cData = $cData;
$this->parseItems();
return $this->cData; return $this->cData;
} }
public function getQuestData($file) { public function getQuestData($file) {
$questsNorm = null; $questsNorm = null;
$questsNM = null; $questsNM = null;
@ -174,5 +233,4 @@ class D2Char {
} }
return $waypoints; return $waypoints;
} }
} }

View File

@ -275,6 +275,7 @@ class D2CharStructureData {
$this->skills[$r['Id']] = $r['String']; $this->skills[$r['Id']] = $r['String'];
} }
$this->_qNorm = array_flip($this->qNorm); $this->_qNorm = array_flip($this->qNorm);
$this->_qNM = array_flip($this->qNM); $this->_qNM = array_flip($this->qNM);
$this->_qHell = array_flip($this->qHell); $this->_qHell = array_flip($this->qHell);

View File

@ -41,21 +41,21 @@
*/ */
function ddump($var) { function ddump($var) {
echo "<pre>"; //echo "<pre>";
var_dump($var); var_dump($var);
echo "</pre>"; //echo "</pre>";
// header('Content-Type: application/json'); // 'Content-Type: application/json');
// echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT); // echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT);
die(); die();
} }
function dump($var) { function dump($var) {
echo "<pre>"; //echo "<pre>";
var_dump($var); var_dump($var);
echo "</pre>"; //echo "</pre>";
//header('Content-Type: application/json'); //'Content-Type: application/json');
//echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT); //echo json_encode($var, JSON_INVALID_UTF8_IGNORE | JSON_PRETTY_PRINT);
} }

147
src/D2Item.php Normal file
View File

@ -0,0 +1,147 @@
<?php
require_once 'D2BitReader.php';
require_once 'D2ItemStructureData.php';
class D2Item {
private $bits = null;
public $iData = null;
public function __construct($bits) {
if ($bits == '')
return false;
$this->bits = $bits;
return $this->parseItem();
}
/* set $this->iData to array of item details
*
* @return array of item details
*/
private function parseItem() {
$b = new D2BitReader($this->bits);
$b->skip(16); // Skip JM
$b->skip(4); // skip unknown 4 bytes
$this->iData['identified'] = $b->read(1); // bit 20, identified
$b->skip(6); // skip unknown 6 bytes
$this->iData['socketed'] = $b->read(1); // bit 27, socketed
$b->skip(1);
// This bit is set on items which you have picked up since the last time the game was saved.
$this->iData['pickedUpSinceLastSave'] = $b->read(1); // bit 29
$b->skip(2);
$this->iData['ear'] = $b->read(1); // bit 32 bool
$this->iData['startingItem'] = $b->read(1); // bit 33 bool
$b->skip(3);
$this->iData['compact'] = $b->read(1); // bit 37 compact
$this->iData['ethereal'] = $b->read(1); // bit 38 ethereal
$b->skip(1); // unknown, seems always 1
$this->iData['personalized'] = $b->read(1); // bit 40 Item has been personalized (by Anya in Act V)
$b->skip(1);
$this->iData['runeword'] = $b->read(1); // bit 42 the item has been given a Rune Word.
$b->skip(15); // unknown; some of these bits may be set
// item location
$location = bindec($b->readr(3)); // bit 58 parent Item location.
$body = bindec($b->readr(4)); // bit 61 If the item is equipped
$col = bindec($b->readr(4)); // bit 65 Column number of the left corner of the item
$row = bindec($b->readr(4)); // bit 69 Row number of the top of the item, counting from 0.
$_stored = bindec($b->readr(3)); // bit 73
// weird behavior/bug
// if item is in a container, bodypart will be NULL
// if item is on bodypart, container will be NULL
switch ($location) {
case D2ItemLocation::STORED:
switch ($_stored) {
case D2ItemLocationStored::NONE:
$this->iData['container'] = ''; // item is not stored, check bit 58
break;
case D2ItemLocationStored::INVENTORY:
$this->iData['container'] = 'Inventory';
break;
case D2ItemLocationStored::CUBE:
$this->iData['container'] = 'Horadric Cube';
break;
case D2ItemLocationStored::STASH:
$this->iData['container'] = 'Stash';
break;
default: $this->iData['container'] = 'Unknown';
break;
}
break;
case D2ItemLocation::EQUIPPED:
switch ($body) {
case D2ItemLocationBody::HELMET: $this->iData['bodypart'] = 'Helmet';
break;
case D2ItemLocationBody::AMULET: $this->iData['bodypart'] = 'Amulet';
break;
case D2ItemLocationBody::ARMOR: $this->iData['bodypart'] = 'Armor';
break;
case D2ItemLocationBody::WEAPONR: $this->iData['bodypart'] = 'Weapon R';
break;
case D2ItemLocationBody::WEAPONL: $this->iData['bodypart'] = 'Weapon L';
break;
case D2ItemLocationBody::RINGR: $this->iData['bodypart'] = 'Ring R';
break;
case D2ItemLocationBody::RINGL: $this->iData['bodypart'] = 'Ring L';
break;
case D2ItemLocationBody::BELT: $this->iData['bodypart'] = 'Belt';
break;
case D2ItemLocationBody::BOOTS: $this->iData['bodypart'] = 'Boots';
break;
case D2ItemLocationBody::GLOVES: $this->iData['bodypart'] = 'Gloves';
break;
case D2ItemLocationBody::WEAPONR2: $this->iData['bodypart'] = 'Weapon Alt R';
break;
case D2ItemLocationBody::WEAPONL2: $this->iData['bodypart'] = 'Weapon Alt L';
break;
default: $this->iData['bodypart'] = 'Unknown';
break;
}
break;
}
// if item is ear
if ($this->iData['ear']){
// set item code/basename
$this->iData['itemCode'] = 'ear';
$this->iData['basename'] = 'Ear';
// get ear class/level
$eclass = bindec($b->readr(3)); // bit 76
$elevel = bindec($b->readr(7)); // bit 79
// get ear char's name
}
// get item code
$b->seek(76);
$itemCode = '';
foreach (str_split($b->read(32), 8) as $byte) {
$itemCode .= chr(bindec(strrev($byte)));
}
$this->iData['itemCode'] = trim($itemCode);
// get namestr
$sql = "SELECT code,namestr FROM armor WHERE code='{$this->iData['itemCode']}'";
$res = PDO_FetchAssoc($sql);
if (empty($res)){
$sql = "SELECT code,namestr FROM misc WHERE code='{$this->iData['itemCode']}'";
$res = PDO_FetchAssoc($sql);
}
if (empty($res)){
$sql = "SELECT code,namestr FROM misc WHERE code='{$this->iData['itemCode']}'";
$res = PDO_FetchAssoc($sql);
}
$sql = "SELECT `String` FROM strings WHERE `Key`='{$res[$this->iData['itemCode']]}'";
$res = PDO_FetchOne($sql);
$this->iData['basename'] = $res;
return $this->iData;
}
}

View File

@ -0,0 +1,40 @@
<?php
class D2ItemLocation {
//location
const STORED = 0;
const EQUIPPED = 1;
const BELTI = 2;
const CURSOR = 4;
const ITEM = 6;
}
class D2ItemLocationStored {
//storage
const NONE = 0;
const INVENTORY = 1;
const CUBE = 4;
const STASH = 5;
}
class D2ItemLocationBody {
//body parts
const HELMET = 1;
const AMULET = 2;
const ARMOR = 3;
const WEAPONR = 4;
const WEAPONL = 5;
const RINGR = 6;
const RINGL = 7;
const BELT = 8;
const BOOTS = 9;
const GLOVES = 10;
const WEAPONR2 = 11;
const WEAPONL2 = 12;
}