You can contact me if you're in need of help with parsing .D2S files (or any other technical information regarding Diablo II for that matter)
In regards to your specific issue, the bulk of the file is static except for the stats section and the items. The stats section is very straight-forward to parse; each entry begins with a 9-bit identifier followed by the value (of which's length can be gleamed from d2_patch.mpq/global/excel/ItemStatCost.txt)
Below is taken from a project of mine that you may find helpful.
/*
* D2SDecode.cpp
* =============
* Version: 1.0
* Author: Matt.J
*
*
* Description:
* Provides an interface for dealing with Diablo II save-files (.D2S)
*
*
*********************************************************************************************************************/
#include "D2SDecode.h"
using namespace D2S;
/////////////////////////////////////////////////////////////////////
// Internal Functions
/////////////////////////////////////////////////////////////////////
Dword STDCALL ReadBits(Void* pStream, Dword dwOffset, Dword dwSize);
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// General Routines
//
///////////////////////////////////////////////////////////////////////////////////////////////////
Void FASTCALL D2S::Decode(Game::Player* pPlayer, Byte* pD2S)
{
Dword dwOffset = 0;
Dword dwStatId;
Dword dwStatVal;
// Save class.
pPlayer->Character.wClass = ((D2SBase*) pD2S)->Class;
pPlayer->Character.wLevel = ((D2SBase*) pD2S)->Level;
// Traverse to the start of player stats.
pD2S += sizeof(D2SBase);
pD2S += ((D2SQuest*) pD2S)->wSize;
pD2S += sizeof(D2SWaypoints);
pD2S += ((D2SNPC*) pD2S)->wSize;
pD2S += 2; // Skip 'gf'
// Reset total stat count.
pPlayer->Character.wStatsUsed = 0;
// Decode stats and store in the given Player.
while((dwStatId = ReadBits(pD2S, dwOffset, 9)) != 511)
{
// Read the stat value.
dwStatVal = ReadBits(pD2S, (dwOffset += 9), SIT[dwStatId].Size);
dwOffset += SIT[dwStatId].Size;
// Adjust for Diablo II's bullshit.
dwStatVal = (SIT[dwStatId].Div ? (dwStatVal / SIT[dwStatId].Div) : dwStatVal);
// Save.
switch(dwStatId)
{
case 0: pPlayer->Character.wStr = (Word) dwStatVal; break;
case 1: pPlayer->Character.wNrg = (Word) dwStatVal; break;
case 2: pPlayer->Character.wDex = (Word) dwStatVal; break;
case 3: pPlayer->Character.wVit = (Word) dwStatVal; break;
case 4: pPlayer->Character.wStats = (Word) dwStatVal; break;
case 5: pPlayer->Character.wSkills = (Word) dwStatVal; break;
case 6: pPlayer->Character.dwCurLife = dwStatVal; break;
case 7: pPlayer->Character.dwMaxLife = dwStatVal; break;
case 8: pPlayer->Character.dwCurMana = dwStatVal; break;
case 9: pPlayer->Character.dwMaxMana = dwStatVal; break;
}
// Total stat counter.
if(dwStatId < 4) pPlayer->Character.wStatsUsed += ((Word) dwStatVal);
} dwOffset += 9;
// Traverse to skills, stats assumed to be padded to the next byte.
pD2S += (dwOffset / 8) + ((dwOffset % 8) ? 1 : 0);
// Verify header integrity.
if(((D2SSkills*) pD2S)->strTag[0] != 'i' || ((D2SSkills*) pD2S)->strTag[1] != 'f')
{
// ERROR:
Out("[ERROR] Failed to find 'if' header.");
return;
}
// Count total skill points used.
pPlayer->Character.wSkillsUsed = 0;
pPlayer->Character.wHighestSkill = 0;
for(Int i = 0; i < 30; i++)
{
pPlayer->Character.wSkillsUsed += ((D2SSkills*) pD2S)->Level[i];
pPlayer->Character.wHighestSkill = (((D2SSkills*) pD2S)->Level[i] > pPlayer->Character.wHighestSkill ? ((D2SSkills*) pD2S)->Level[i] : pPlayer->Character.wHighestSkill);
}
// Offset based on class.
dwOffset = (BST[pPlayer->Character.wClass].Str + BST[pPlayer->Character.wClass].Dex + BST[pPlayer->Character.wClass].Vit + BST[pPlayer->Character.wClass].Nrg);
pPlayer->Character.wStatsUsed = (Word) (pPlayer->Character.wStatsUsed > dwOffset ? (pPlayer->Character.wStatsUsed - dwOffset) : 0);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Helper Functions
//
///////////////////////////////////////////////////////////////////////////////////////////////////
NAKED Dword STDCALL ReadBits(Void* pStream, Dword dwOffset, Dword dwSize)
{
__asm
{
// PROLOGUE:
push ebx
// Calculate the bytewise offset, and remainder.
mov eax, [esp+8]
mov edx, [esp+12]
mov ecx, 7
and ecx, edx
shr edx, 3
// Read in the containing dword.
mov ebx, [eax + edx]
// Compensate for the offset.
shr ebx, cl
// Generate a mask for the significant bits of the desired sequence.
mov ecx, [esp+16]
mov eax, 1
shl eax, cl
sub eax, 1
// Extract the result.
and eax, ebx
// EPILOGUE:
pop ebx
ret 12
}
}
In regards to your specific issue, the bulk of the file is static except for the stats section and the items. The stats section is very straight-forward to parse; each entry begins with a 9-bit identifier followed by the value (of which's length can be gleamed from d2_patch.mpq/global/excel/ItemStatCost.txt)
Below is taken from a project of mine that you may find helpful.