The DS3 Codex
Chapter I

Archives & containers

21 GB in six sealed archives. How DS3 hides its files, how the names are recovered, and the binders inside binders inside them.

A great vault, its keys cast into the sea. Yet the lock was made by men, and what men seal, men may open.Codex · The Archives

In short
  • Five archives (Data1-5) have RSA-encrypted indexes and some AES-encrypted byte ranges. Files are found by a hash of their path, and a community dictionary names all 13,619. data
  • Data0.bdt is the regulation: every game rule as 103 param tables, AES-256 encrypted. data
  • Inside are binders (BND4, or BXF4 split into header and data), usually DCX-compressed (zlib). Most asset types are a binder of FromSoft formats and Havok 2014 packfiles. data
  • We read all of it with Python tools we wrote ourselves, so we don't need UXM (the community's unpacker) or Windows. The DLC's data is in the base archives too.

1The install

Steam app 374320, build 10167187. Everything sits in the install's Game/ folder:

FileSizeWhat
DarkSoulsIII.exe89 MBThe game: x64, image base 0x140000000, with MSVC RTTI and Havok's reflection text. YEBIS's 3,534 post-processing shaders are embedded in it.
Data0.bdt0.8 MBThe regulation: all params
Data1-4.bdt1-2.6 GB eachCharacters, objects, effects, menus, scripts, shaders, sound projects
Data5.bdt13.9 GBThe maps, by far the largest
sound/FMOD Ex banks (.fsb) loose on disk: music, ambience, voices (Sound)
movie/0.9 GBBink videos: fdp_opening.bik, the credits
fmod_event64.dll, fmodex64.dllFMOD Ex 4.44.61 with FromSoft's changes. The Vorbis setup headers the banks leave out are compiled in here.

fdp is DS3's internal project name. It prefixes the sound banks, the videos and many file names.

2The sealed archives (BHD5)

How they're sealed

  • Each .bhd header is RSA-encrypted with a public key per archive. Decrypting is a raw modular exponentiation of 256-byte blocks into 255 bytes. data
  • The header lists buckets of entries: a 32-bit hash of the path, offset and size in the .bdt, and for some files an AES key and the byte ranges it encrypts, decrypted with AES-ECB. data
  • The hash is h = h × 37 + c over the lower-cased path, so /map/m30_00_00_00/h30_00_00_00.hkxbhd becomes a number. Names can only be recovered by guessing paths. UXM's dictionary names all 13,619 entries of this install.

How we open them

Our unpacker, a Python script, decrypts the headers, files and ranges itself, using the same public RSA keys the community tools use. It writes each file out under its real name, but only the files the importer needs. It doesn't depend on UXM or Windows.

Faithful

Every entry decrypts and every name resolves. This is the same scheme the community tools use, re-implemented in Python.

3Binders inside binders

Almost nothing in the archives is a single asset. Each is a container of containers:

DataN.bdt
 └─ c0000.anibnd.dcx         DCX: a compressed wrapper ("DFLT" = zlib)
     └─ BND4 binder           names and ids for each entry, one file
         ├─ Skeleton.HKX      a Havok 2014 packfile
         ├─ a000_000000.hkx   one spline-compressed animation each …
         └─ c0000.tae         the animation events
map/m40_00_00_00/h40_00_00_00.hkxbhd + .hkxbdt
     └─ BXF4                  the same, split: header (.bhd) and data (.bdt) files

The extension says what a binder holds. We wrote a Python reader for each of these formats, apart from the Lua scripts, which the community's DSLuaDecompiler turns back into source. The ones this codex meets:

ContainerHoldsChapter
.msbA map's layout: models, parts, regions, eventsLevels
.mapbndOne map-piece FLVERLevels
.hkxbhd/.hkxbdtCollision: one Havok packfile per collision modelLevels
.tpf, .tpfbhdTextures: DDS files, plus a float block on the lightmapsCharacters
.chrbndA character: FLVER, ragdoll HKX, cloth HKXCharacters
.anibndA skeleton, clips, TAE eventsCharacters
.behbndThe Havok Behavior graph: events to states to clipsMovement
.partsbndOne piece of equipment: FLVER and TPFCharacters
.objbndAn object: model, collision, skeleton and clipsLevels
.ffxbndEffects (FXR3), their textures and modelsEffects
.hks / .luabndHavok Script (Lua 5.1) state machines / Lua 5.0 AIEnemies
.emevdEvent scripts: bosses, doors, bonfiresEnemies
.talkesdbndTalk scripts (ESD state machines): bonfires, NPCsLevels
.gparamDraw params: lights, fog, tone map, shadows per areaRenderer
.mtdbnd, .shaderbdlebndMaterials (MTD) and compiled DXBC shadersRenderer
.btl .btab .btpbMap lights, lightmap atlas UVs, SH light probesRenderer
.fmgText: names, menus, prompts
.fev / .fsbFMOD event projects / sample banksSound

4The regulation: every rule as a table

Most of DS3's design numbers aren't in code. They're in params: typed tables of rows, each a fixed struct. The whole set ships as the regulation, Data0.bdt. It is AES-256-CBC encrypted with a fixed key, then DCX-compressed, then a BND4 of 103 .param files data. The save file carries a copy too.

A param file has rows (an id and an offset each) pointing at fixed-size structs. The layouts aren't in the file. Field names and types come from the community's definitions (Smithbox's DS3 defs, Paramdex's row names), which our param reader uses. DS3's files have format byte 0x2D = 0x85: the type name at an offset, and 64-bit row headers.

The params this codex relies on, and what they decide:

ParamDecides
NpcParamAn enemy: HP, capsule, turn speed, weight, which clips and graph it borrows, display masks, poise, stamina regen
NpcThinkParamIts AI: which Lua battle goal, sight range and angles, smell, how far it chases
EquipParamWeapon / Protector / GoodsWeapons (damage, scaling, motion set, where it's held), armour (absorption, poise rate), items (the Estus Flask)
BehaviorParam_PC / BehaviorParamAn attack: stamina cost, and which AtkParam or Bullet it uses
AtkParam_Pc / AtkParam_NpcHit shapes on dummy polys, radii, damage corrections, hit-stop, damage level, knockback
SpEffectParamStatus effects. One of them switches Gundyr into his second phase.
CalcCorrectGraphCurves: VIG → HP, attunement → FP, fall height → damage, stat scaling
HitMtrlParam, FootSfxParam, SeMaterialConvertParamFloor materials: footstep sounds, splashes, wading
LockCamParam, LodParam, KnockBackParam, ToughnessParam, Bullet, ActionButtonParam, BonfireWarpParam, ModelSfxParam, ObjActParam, HitEffectSeParamCamera, levels of detail, knockback timing, poise windows, projectiles, interaction prompts, bonfires, object effects, doors, hit sounds

5Our side: one importer, neutral formats

The engine never sees a DS3 file. The unpacker extracts what's needed, the readers parse it, and the importer writes flat little-endian files that the engine uses in place after one read. Each has a header (a four-character magic and a version), arrays aligned to 16 bytes, one string table, and right-handed Y-up space in metres. When a format changes, its version goes up and the importer runs again: there are no compatibility layers.

FileFrom DS3
.mesh v9FLVER: one interleaved 52-byte vertex, LOD face sets, per-submesh boxes, materials with 15 texture slots
.scene v16MSB + gparam + btl + btab + btpb: placements, groups, light sets, fog, probes, lightmaps, enemies, effects, bonfires, sound regions, objects
.col v2, .objcolHavok compressed meshes: triangles with material and slide class, per-body room records
.skel .anim .chrHavok skeletons, decoded clips at 30 Hz with root motion and TAE events, character assembly
.ragdoll .clothHavok ragdoll bodies and joints, Havok Cloth operators
.fxFXR3 effects reduced to emitters, models and lights
.sbankFMOD events and Vorbis samples
.paramsThe params the engine uses, as typed tables

Handedness. DS3 is left-handed, Y up, in metres. The importer negates Z on every position, normal and tangent, and that alone makes the winding counter-clockwise (it agrees with the vertex normals on 99.4 % of a map piece's triangles). Quaternions become (−x, −y, z, w) and root yaw is negated. A play-camera screenshot compared with the real game settled which side is which data.

Our design

The formats are ours on purpose: a later importer for another game writes the same files. Everything DS3-specific stays on the importer's side of the line.