Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Diablo Immortal Model Viewer

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.Core reads 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. Read docs/FEASIBILITY-REVIEW.md for the whole plan and docs/recon/FINDINGS.md for what the shipped build actually says.

Why this is a new project and not a fork

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.

The blocker

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.

What's here

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.

The reader stack

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

SkinDetector is the interesting one

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.

Where this stands

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, → Material and → SkinSkeleton edges make asset binding a lookup, not a heuristic;
  • I4B and W4B are blend indices and weights, settled by their appearing 49,971 times each and never one without the other;
  • Q4H is 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 SkinSkeleton palettes 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.

Building

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.

The recon tool

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/.

Legal

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.

License

MIT — see LICENSE. (Covers this repository's own code and documents, not any game content.)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages