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 | 替代武器(左) |