Diablo II將您的遊戲字符存儲在磁盤上,作為.D2S文件。這是一種二進製文件格式,它編碼所有統計數據,項目,名稱和其他數據。
整數存儲在Little Endian字節順序中,這是X86 Architecture Diablo II上的本機字節順序。
每個.D2S文件以765個字節標頭開始,然後數據的長度可變。
| 十六進位 | 位元組 | 長度 | desc |
|---|---|---|---|
| 0x00 | 0 | 4 | 簽名(0xAA55AA55) |
| 0x04 | 4 | 4 | 版本ID |
| 0x08 | 8 | 4 | 文件大小 |
| 0x0c | 12 | 4 | 校驗和 |
| 0x10 | 16 | 4 | 活躍武器 |
| 0x14 | 20 | 16 | 角色名稱 |
| 0x24 | 36 | 1 | 角色狀態 |
| 0x25 | 37 | 1 | 角色進展 |
| 0x26 | 38 | 2 | ? |
| 0x28 | 40 | 1 | 角色類 |
| 0x29 | 41 | 2 | ? |
| 0x2b | 43 | 1 | 等級 |
| 0x2c | 44 | 4 | ? |
| 0x30 | 48 | 4 | 時間 |
| 0x34 | 52 | 4 | ? |
| 0x38 | 56 | 64 | 熱鍵 |
| 0x78 | 120 | 4 | 左鼠標 |
| 0x7c | 124 | 4 | 正確的鼠標 |
| 0x80 | 128 | 4 | 左鼠標(武器開關) |
| 0x84 | 132 | 4 | 右鼠標(武器開關) |
| 0x88 | 136 | 32 | 字符菜單外觀 |
| 0xa8 | 168 | 3 | 困難 |
| 0xab | 171 | 4 | 地圖 |
| 0xaf | 175 | 2 | ? |
| 0xb1 | 177 | 2 | merc死了? |
| 0xb3 | 179 | 4 | merc種子? |
| 0xB7 | 183 | 2 | MERC名稱ID |
| 0xb9 | 185 | 2 | MERC類型 |
| 0xbb | 187 | 4 | MERC經驗 |
| 0xbf | 191 | 144 | ? |
| 0x14f | 335 | 298 | 尋求 |
| 0x279 | 633 | 81 | 航點 |
| 0x2CA | 714 | 51 | NPC |
| 0x2fd | 765 | 統計 | |
| 專案 |
文件版本。已知以下值:
71是1.00至V1.0687是1.07或擴展集v1.0889是標準遊戲v1.0892是v1.09(標準遊戲和擴展集。)96為v1.10+要計算校驗和在.D2S數據中設置其值為零,並通過數據計算32位校驗和的數據中的所有字節迭代:
sum = ( sum << 1 ) + data [ i ];來源:#5
const fs = require ( "fs" ) ;
const path = require ( "path" ) ;
const file = path . join ( process . cwd ( ) , "path_to_save.d2s" ) ;
function calculateSum ( data ) {
let sum = 0 ;
for ( let i = 0 ; i < data . length ; i ++ ) {
let ch = data [ i ] ;
if ( i >= 12 && i < 16 ) {
ch = 0 ;
}
ch += sum < 0 ;
sum = ( sum << 1 ) + ch ;
}
return sum ;
}
function littleToBigEndian ( number ) {
return new DataView (
Int32Array . of (
new DataView ( Int32Array . of ( number ) . buffer ) . getUint32 ( 0 , true )
) . buffer
) ;
}
function ashex ( buffer ) {
return buffer . getUint32 ( 0 , false ) . toString ( 16 ) ;
}
async function readSafeFile ( ) {
return await new Promise ( ( resolve , reject ) => {
fs . readFile ( file , ( err , data ) => {
if ( err ) return reject ( err ) ;
return resolve ( data ) ;
} ) ;
} ) ;
}
async function writeCheckSumToSafeFile ( data ) {
return await new Promise ( ( resolve , reject ) => {
fs . writeFile ( file , data , err => {
if ( err ) reject ( err ) ;
resolve ( ) ;
} ) ;
} ) ;
}
readSafeFile ( ) . then ( data => {
const sum = calculateSum ( data ) ;
const bufferSum = littleToBigEndian ( sum ) ;
const hex = ashex ( bufferSum ) ;
const newData = data ;
for ( let i = 0 ; i < 4 ; i ++ ) {
newData [ 12 + i ] = bufferSum . getInt8 ( i ) ;
}
writeCheckSumToSafeFile ( newData ) . then ( ( ) => console . log ( hex ) ) ;
} ) ;資料來源:https://github.com/gucio321/d22s/blob/66f91e2af7b3949ca7f279aae397bd8904519e2d/pkg/pkg/pkg/d2s/d2s/d2s.go#l397
// CalculateChecksum calculates a checksum and saves in a byte slice
func CalculateChecksum ( data * [] byte ) {
var sum uint32
for i := range * data {
sum = (( sum << 1 ) % math . MaxUint32 ) | ( sum >> ( int32Size * byteLen - 1 ))
sum += uint32 (( * data )[ i ])
}
sumBytes := make ([] byte , int32Size )
binary . LittleEndian . PutUint32 ( sumBytes , sum )
const (
int32Size = 4
checksumPosition = 12
)
for i := 0 ; i < int32Size ; i ++ {
( * data )[ checksumPosition + i ] = sumBytes [ i ]
}
}如果校驗和無效,則暗黑破壞神II不會打開保存文件。
托多
字符名稱存儲為16個字符的數組,其中包含一個帶有0x00的空字符串,用於其餘字節。字符被存儲為8位ASCII,但請記住,有效必須遵循以下規則:
- )或下劃線( _ )這是一個8位的領域:
| 位元 | desc |
|---|---|
| 0 | ? |
| 1 | ? |
| 2 | 鐵桿 |
| 3 | 死了 |
| 4 | ? |
| 5 | 擴張 |
| 6 | ? |
| 7 | ? |
托多
| ID | 班級 |
|---|---|
| 0 | 亞馬遜 |
| 1 | 巫婆 |
| 2 | 死靈法師 |
| 3 | 聖騎士 |
| 4 | 野蠻人 |
| 5 | 德魯伊 |
| 6 | 刺客 |
此級別值僅在字符選擇屏幕中可見,並且必須與統計部分中的該級別相同。
托多
32字節結構定義菜單中字符的外觀不會在遊戲中更改
3個字節,指示該字符已解鎖的三個困難中的哪一個。每個字節都是困難之一的代表性。按照此順序:正常,噩夢和地獄。每個字節都是這樣構成的Bitfield:
| 7 | 6 | 5 | 4 | 3 | 2、1、0 |
|---|---|---|---|---|---|
| 積極的? | 未知 | 未知 | 未知 | 未知 | 哪個行為(0-4)? |
托多
托多
WayPoint數據以2個字符“ WS”和6個未知字節開始,始終= {0x01,0x00,0x00,0x00,0x00,0x50,0x00}
每個難度都有三個結構,在偏移641、665和689處。
該結構的內容如下
| 位元組 | 數字 | 內容 |
|---|---|---|
| 0 | 2個字節 | {0x02,0x01}未知目的 |
| 2 | 5個字節 | Waypoint Bitfield按最少重大的順序 |
| 7 | 17個字節 | 未知 |
在Waypoint Bitfield中,位值為1表示啟用了航路點,它是從最低到最高的順序,因此0是Rogue營地(ACT I)等。每個難度中的第一個航點總是被激活。
托多
TODO(9位編碼)
項目存儲在此標頭描述的列表中:
| 位元組 | 尺寸 | desc |
|---|---|---|
| 0 | 2 | “ JM” |
| 2 | 2 | 項目計數 |
此後,n個項目。每個項目以基本的14字節結構開始。該結構中的許多字段都不是“與字節對準”,並且用它們的位置和大小來描述。
| 位元 | 尺寸 | desc |
|---|---|---|
| 0 | 16 | “ JM”(與列表標題分開) |
| 16 | 4 | ? |
| 20 | 1 | 確定 |
| 21 | 6 | ? |
| 27 | 1 | 插座 |
| 28 | 1 | ? |
| 29 | 1 | 自上次保存以來就接了 |
| 30 | 2 | ? |
| 32 | 1 | 耳朵 |
| 33 | 1 | 入門裝備 |
| 34 | 3 | ? |
| 37 | 1 | 袖珍的 |
| 38 | 1 | 空靈 |
| 39 | 1 | ? |
| 40 | 1 | 個性化 |
| 41 | 1 | ? |
| 42 | 1 | Runeword |
| 43 | 15 | ? |
| 58 | 3 | 父母 |
| 61 | 4 | 配備了 |
| 65 | 4 | 柱子 |
| 69 | 3 | 排 |
| 72 | 1 | ? |
| 73 | 3 | 藏 |
| 76 | 4 | ? |
| 80 | 24 | 類型代碼(3個字母) |
| 108 | 擴展項目數據 |
如果將項目標記為Compact (設置位37),則將不存在擴展項目信息,並且該項目已完成。
帶有擴展信息存儲位的項目基於項目標題中的信息。例如,標記為Socketed項目將存儲一個額外的3位整數編碼該項目的插座。
| 位元 | 尺寸 | desc |
|---|---|---|
| 108 | 插座 | |
| 自定義圖形 | ||
| 特定課程 | ||
| 品質 | ||
| mods |
自定義圖形用一個位表示,如果設置表示圖形索引的3位號碼。如果鑽頭未設置,則不存在3位。
| 位元 | 尺寸 | desc |
|---|---|---|
| 0 | 1 | 項目具有自定義圖形 |
| 1 | 3 | 替代圖形索引 |
諸如Barbarian Helms或Amazon Bows之類的班級物品具有針對此類物品的特殊特性。如果第一個位為空,則其餘11位將不存在。
| 位元 | 尺寸 | desc |
|---|---|---|
| 0 | 1 | 項目具有特定於類的數據 |
| 1 | 11 | 特定班級的位 |
項目質量被編碼為4位整數。
在每個項目為mod列表之後。該列表是一系列鑰匙值對,其中鍵是9位編號,該值取決於密鑰。當找到鍵511 ( 0x1ff )時,列表結束了,這是設置的所有9位。
使用文件ItemStatCost.txt作為選項卡 - 刪除的CSV文件,您可以提取映射到9位鍵的ID列。列Save Bits和Param Bits描述了mod的大小。
唯一的例外是Min-Max樣式修飾符,它使用CSV中的下一行來存儲MOD的“最大”部分。這兩個的位尺寸可能不同,您應該總結它們以獲取總尺寸。
托多
所有物品都位於某個地方,並有一個“父母”,它可以是另一個項目,例如插入珠寶時。
| 價值 | desc |
|---|---|
| 0 | 存儲 |
| 1 | 配備了 |
| 2 | 腰帶 |
| 4 | 游標 |
| 6 | 物品 |
對於“存儲” 3位整數編碼的項目,從位73開始,描述了可以存儲該項目的位置:
| 價值 | desc |
|---|---|
| 1 | 存貨 |
| 4 | Horadric Cube |
| 5 | 藏 |
裝備的項目描述了他們的插槽:
| 價值 | 投幣口 |
|---|---|
| 1 | 頭盔 |
| 2 | 護身符 |
| 3 | 盔甲 |
| 4 | 武器(右) |
| 5 | 武器(左) |
| 6 | 戒指(右) |
| 7 | 戒指(左) |
| 8 | 腰帶 |
| 9 | 靴子 |
| 10 | 手套 |
| 11 | 替代武器(右) |
| 12 | 替代武器(左) |