Skip to content

Validating a file before you write it

wowlib will happily write a file the game cannot load. That is deliberate: write() serializes exactly the entity you hand it, so a half-finished edit round-trips instead of being rejected, and nothing silently "repairs" your data behind your back.

Checking is therefore a separate step you ask for:

report = wmo.validate()
if not report.ok:
    for issue in report.issues:
        print(issue.severity, issue.path, issue.message)

or, when you would rather it just raise:

wmo.ensure_valid()   # raises InvalidEntityState listing what is wrong
wmo.write(fs, key)

Every format entity has both — WMO, M2, ADT, WDT, WDL, BLP and the client-database tables — on the concrete per-version classes and on the abstract family bases, so def check(w: WMO) type-checks.

Reading a report

validate() never stops at the first problem: it returns a [ValidationReport][wowlib.formats.ValidationReport] holding every violated contract, each with the member path it was found at — groups[3].body.indices, chunks[17].layers[2] — so a finding points at the thing to fix rather than at the file as a whole.

Findings come in two severities, and the difference is about the client, not about how tidy the data is:

Severity Meaning
error The client would misread, or crash on, a file written in this state.
warning Suspicious, but real Blizzard files ship it — the file loads.

report.ok is true when there are no errors; warnings never fail it. That distinction is load-bearing, and it was drawn empirically: every contract below was run against whole client corpora, and any check that fired on shipped, working files was either corrected or demoted. Vanilla WMOs carry float garbage in unused material texture slots; a Battle for Azeroth group sets the "two colour layers" flag while shipping one; textures carry junk in the alpha depth where nothing reads it. A freshly read, unmodified client file reports zero errors — that is asserted for every file in wowlib's test corpora, so a new error means your edit, not a quirk of the format.

A report is capped (1000 findings) so validating a badly corrupted file cannot become a memory problem; report.truncated tells you when findings were dropped.

What is checked

Most contracts are declared on the entities themselves as annotations, which is what makes the table below generated rather than written — it is scanned out of the C++ sources, so it cannot drift from what the code actually enforces.

The remaining contracts cannot be expressed that way: record-interior ranges (a render batch's slice of the index buffer), flag/presence coherence (a header bit promising data that must exist), and cross-file references (a skin's vertices into the model body, a group's references into its root). Those are hand-written per entity, and the entities carrying them are listed under each format.

WMO

Entity Member Must hold
GroupBody90 largeIndices a multiple of 3 entries; every value indexes vertices
GroupBody90 ambientVolumeRefs every value indexes the root's ambientVolumes
GroupBody90 boxVolumeRefs every value indexes the root's ambientBoxVolumes
GroupBody90 fogVolumeRefs every value indexes the root's fogs
GroupBody90 vertexColors2 one per vertices entry
GroupBodyCata batchOverrides one per batches entry
Root73 uvAnimations one per materials entry
Root83 doodadColorMults one per doodadDefs entry
Root90 fogExtras one per fogs entry
Root90 groupInfos2 one per groupInfos entry
WMOGroup mver always WmoVersionV17
WMOGroupBody indices a multiple of 3 entries; every value indexes vertices
WMOGroupBody normals one per vertices entry
WMOGroupBody texcoords one per vertices entry
WMOGroupBody lightRefs every value indexes the root's lights
WMOGroupBody doodadRefs every value indexes the root's doodadDefs
WMOGroupBody vertexColors one per vertices entry
WMORoot mver always WmoVersionV17

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for WMO.

M2

Entity Member Must hold
FBlock keys one per timestamps entry
M2PartTrack values one per times entry
M2Root sequenceLookups every value indexes sequences (or is the 'none' sentinel)
M2Root replacableTextureLookup every value indexes textures (or is the 'none' sentinel)
M2Root textureLookupTable every value indexes textures (or is the 'none' sentinel)
M2Root transparencyLookupTable every value indexes textureWeights (or is the 'none' sentinel)
M2Root textureTransformsLookupTable every value indexes textureTransforms (or is the 'none' sentinel)
M2Root collisionTriangles a multiple of 3 entries; every value indexes collisionVertices
M2Root collisionNormals one per 3 collisionTriangles entries
M2Root attachmentLookupTable every value indexes attachments (or is the 'none' sentinel)
M2Root cameraLookupTable every value indexes cameras (or is the 'none' sentinel)
M2SkinProfile indices a multiple of 3 entries; every value indexes vertices
M2SkinProfile bones one per vertices entry
M2Track values one per timestamps entry
M2Track values one per timestamps entry

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for M2.

ADT

Entity Member Must hold
MapChunk heights exactly detail::McvtCount entries
MapChunk normals exactly detail::McvtCount entries
MapChunk alphaMaps one per layers entry
MapChunk shadowMap exactly detail::AlphaTexels entries
MapChunk doodadRefs every value indexes the root's doodadPlacements
MapChunk objectRefs every value indexes the root's wmoPlacements
MapChunkCata vertexLighting exactly McvtCount entries
MapChunkColor vertexColors exactly McvtCount entries

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for ADT.

WDT

Entity Member Must hold
WDTRoot mver always WdtVersion18

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for WDT.

BLP

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for BLP.

Client databases

Additional checks that no annotation can express — record-interior ranges, flag/presence coherence, cross-file references — are hand-written for Table, TableBase, TableCore.

What is deliberately not checked

Derived fields are not validated, because they are not authored: chunk offset tables, the MOHD counts, the MCNK sub-chunk offsets and the WDL tile offsets are all recomputed on every write, so a stale value in memory is not a defect. Counts that real client files disagree with their own containers on — MOHD's texture and light counts, for instance — are stored as read and left alone, since "fixing" them would break the byte-perfect round trip.