A viewer and exporter for Diablo Immortal models — the same idea as D3ModelViewer, for a very different engine.
Status: the reader stack works against the real game; no UI yet.
DIModelViewer.Corereads the archives, the catalogue and its dependency graph, textures, mesh geometry with skinning, and the per-mesh bone palette. 96 tests: synthetic fixtures, plus a lane that runs over thousands of assets in a live install. What is missing is animation keyframes and the viewer itself — see Where this stands. Readdocs/FEASIBILITY-REVIEW.mdfor the whole plan anddocs/recon/FINDINGS.mdfor what the shipped build actually says.
Diablo Immortal is not built on Blizzard technology. It runs on NetEase's Messiah engine, so
nothing in the Diablo III input stack has a counterpart here — no CASC, no CoreTOC.dat, no SNO
ids, no .app / .ani / .acr / .ans / .tex. Assets live in plain .mpk archives on disk.
The Diablo III output stack, on the other hand, is game-neutral in everything but its types. The
.m3 writer in particular — StarCraft II MD34 sections, sequence mapping, ragdoll physics, and two
alpha-test fixes that took live in-game testing to find — is the single most valuable thing to carry
across, and re-deriving it would cost more than the rest of this project put together.
So the review's recommendation is not to fork D3ModelViewer, but to refactor it into a game-neutral core with per-game providers and add Diablo Immortal as the second one.
Diablo Immortal meshes carry bone indices and skin weights, and the rig they address is in two
pieces. Both have now been found. A mesh's bone palette is a SkinSkeleton resource that its
Model names outright — a flat list of up to 64 names and transforms, and this reader parses 20,000
of 20,000 shipped ones exactly. The hierarchy is in the character's .anim file, whose header is
the whole bone list with a u16 parent index each.
What is left is the .anim keyframe payload. Until that lands there is no animation playback,
and .m3 export needs at least the bind rig, because StarCraft II will not take a boneless model.
That is the critical path and the main risk; §5.1 and §5.2 of the review lay out how to attack it.
| Path | |
|---|---|
src/DIModelViewer.Core |
The reader library. Platform-neutral by design, so it builds and tests anywhere. |
src/DIModelViewer.Core.Tests |
96 tests: synthetic fixtures built from the format specs, plus a real-install lane gated on DIMV_GAME_DIR. |
docs/FEASIBILITY-REVIEW.md |
The full review: reuse ledger, format mapping, the three unsolved problems, recommended architecture, phased plan, risk register, and confirmed binary layouts. |
tools/di-recon/di_recon.py |
The recon tool: catalogues, name-resolves, probes and extracts, straight out of the archives. |
docs/recon/FINDINGS.md |
What a live PC install actually contains, and the measurement behind every claim. |
docs/HANDOFF.md |
Start here to continue the work — ordered sessions, open questions, and the known gaps in the current code. |
Every non-obvious claim in the review is labelled [C] confirmed (read from working code or executed), [R] reported (community sources, second-hand), [I] inferred, or [V] verified against the shipped build. Nothing is stated as fact that hasn't been checked.
DIModelViewer.Core
├── Mpk/
│ ├── Lz4Block LZ4 block decompression, no third-party dependency
│ ├── MpkPayload the four per-file encodings (ZZZ4, CCCC, masked zlib, stored)
│ ├── MpkArchive .mpkinfo index + numbered .mpk data files
│ ├── Repository .repository names, types, folders and the dependency graph
│ └── Catalogue the two joined: a resource by name, without unpacking 78 GiB
├── Formats/
│ ├── PixelFormat EPixelFormat, block sizes, desktop vs mobile families
│ ├── Texture2DReader header + mip chain, reversed to largest-first
│ ├── DdsWriter BC1-BC7 out as DDS, FourCC or DX10 as the format requires
│ ├── VertexLayout the self-describing stream descriptors ("P3F_N4B_T2F")
│ ├── MeshReader .mesh geometry: submeshes, bounds, skinning, tangent frames
│ ├── SkinSkeletonReader the per-mesh bone palette
│ └── SkinDetector corroborates the bone-index and weight channels against the data
└── Scene/ SceneModel / SceneGeoset / SceneBone, shaped to match the D3 viewer's
types so its .m3 and glTF writers can be retargeted, not rewritten
Knowing which vertex fields carry bone indices and weights is the first step to a rig. The stream descriptors name every field, but which semantic letter means what is not documented — and guessing wrong yields a rig that looks plausible and deforms wrongly.
Both channels are strongly self-identifying, so SkinDetector reads the data instead of trusting the
letter: a weight field's components sum to 1 per vertex (or to 255 stored as bytes), and an index
field holds small, varied, non-negative integers. Run across the build it settles the question
empirically — W4B wins as weights and I4B as indices, everywhere, and a test asserts exactly
that. It also yields the highest bone index in a mesh, which is the cheapest oracle for a candidate
palette: the rig must have at least that many bones plus one.
The reader stack was written blind, from format specifications, in a container with no copy of the
game. It has since been run against a live PC install and corrected. The measurements, and the method
behind each, are in docs/recon/FINDINGS.md:
- the catalogue parses to 567,610 records with nothing left over and no dangling dependency edge;
Model → Mesh,→ Materialand→ SkinSkeletonedges make asset binding a lookup, not a heuristic;I4BandW4Bare blend indices and weights, settled by their appearing 49,971 times each and never one without the other;Q4His a tangent-frame quaternion, not the packed position it was assumed to be — positions are plain float3 in all 85,532 meshes;- a PC install is BC7 and RGBA8888 only, so no transcoder is needed;
- 20,000 of 20,000
SkinSkeletonpalettes parse and consume their bytes exactly.
Three original assumptions were wrong and are now fixed: the .repository folder-block length is
32-bit behind a sentinel, the Texture2D magic and mip count were both misread, and there is no
MODL container in this build at all.
What is missing is animation keyframes, the property-tree container behind .skeleton and .graph,
the material key mapping, and the viewer itself. See docs/HANDOFF.md.
Requires the .NET 10 SDK.
dotnet build src/DIModelViewer.sln
dotnet test src/DIModelViewer.sln
# and, on a machine with the game installed, the real-file lane too
set DIMV_GAME_DIR=C:\Games\Diablo Immortal
dotnet test src/DIModelViewer.sln
The Core library targets net10.0 and has no UI dependency, so it builds on any platform. The
Windows viewer front-end, when it lands, will target a Windows TFM and reference this project
unchanged.
Stdlib only, Python 3.8+, no build step and no dependencies — it carries its own LZ4 block decompressor so it runs anywhere, including next to the game on a machine you'd rather not install packages on.
di_recon.py list Resources.mpkinfo # catalogue an archive set
di_recon.py repo resource.repository # types, folders, dependency edges
di_recon.py names Resources.mpkinfo resource.repository # join them; name-resolution coverage
di_recon.py scan <both> --type Mesh --limit 2000 # probe assets in place, out of the .mpk
di_recon.py pull <both> out/ --folder Char/f_monk # extract a named selection
di_recon.py extract Resources.mpkinfo out/ # unpack everything (78 GiB — rarely what you want)
scan and pull are the ones to reach for. The shipped Resources set is 78 GiB, so unpacking it
whole to look at a few hundred meshes is the wrong move; both read and decompress straight out of the
archives, selected by real name, type or folder.
scan is where the format answers came from. Messiah's vertex format is self-describing — each
stream is declared by a string like P3F_N4B_T2F — so it histograms every attribute token across the
whole build, which is how I4B and W4B were identified rather than guessed. It also reports the
texture pixel-format mix and the largest meshes in the game, that last one because an .m3 region
caps out at 65,536 vertices.
repo prints the typed dependency graph that no public tool uses. Its edges bind models to their
meshes, materials and skinning palettes, which collapses three separate problems at once. See §5.3.
The tool has been run against a full retail install: 680,440 archive entries, 567,610 catalogued
resources, 85,532 meshes probed. Its output is in docs/recon/.
This is a fan-made tool for viewing and converting assets from your own legally installed copy of Diablo Immortal. It ships no game assets and cannot download any, and it reads files on disk only — it does not touch the live client, its network traffic, or anything account-adjacent.
Diablo is a trademark of Blizzard Entertainment, Inc.; the Messiah engine is NetEase technology and Diablo Immortal is a co-development of the two. Do not redistribute extracted game assets; keep them for personal and modding use as permitted by the relevant EULAs.
Nothing here is a decryption or DRM bypass. The archives are not encrypted — the one piece of obfuscation in the format is an XOR mask over a zlib header — and it should stay that way. If a future patch introduces real encryption, that is the point to stop rather than to work around.
MIT — see LICENSE. (Covers this repository's own code and documents, not any game content.)