Quickstart

Installation

pip install valvemdl

Reading a model

from valvemdl import Mdl

mdl = Mdl('models/props_farm/ambulance.mdl')

mdl.version     # 48
mdl.name        # 'ambulance.mdl'
mdl.checksum    # ties the mdl to its vvd and vtx

The whole studiohdr_t is available as mdl.header, and studiohdr2_t as mdl.header2 when the model has one:

mdl.header.numbones
mdl.header.mass
mdl.header.flags.STUDIOHDR_FLAGS_STATIC_PROP

A model is a set of files

An mdl on its own contains no vertices and no triangles. Those live in the .vvd and .vtx files sitting next to it, tied together by a checksum.

Opening an mdl notices which of those files are present but does not read them. They are parsed the first time you ask for them, and cached afterwards:

mdl.sidecar_paths     # {'.vvd': '...', '.dx90.vtx': '...', ...}

mdl.vvd               # parsed now
mdl.vvd               # same object, not reparsed

studiomdl compiles one vtx per graphics backend. mdl.vtx gives you the dx90 one, which is what Team Fortress 2 loads; the others are reachable explicitly:

mdl.get_vtx('.sw.vtx')

Not every model has sidecars. Animation-only models carry no geometry at all, and asking for their vertices raises:

from valvemdl import MdlSidecarMissingError

try:
    mdl.vvd
except MdlSidecarMissingError:
    pass

Checking a model over

validate checks the invariants the format promises – that the header’s length really is the size of the file, that the version is one we have seen, and that the sidecars’ checksums agree with the mdl’s. It reports rather than raises, because a model that fails one of these is usually the interesting one:

for problem in mdl.validate():
    print(problem)

Writing it back

Each of the three files knows how to write itself:

mdl.save('out.mdl')       # or mdl.save() to overwrite the original
mdl.vvd.save('out.vvd')
mdl.vtx.save('out.vtx')

The bytes are rebuilt from the object graph, not copied from the file, so what lands on disk reflects the model as it now is. Across every model in a stock Team Fortress 2 install this rebuild is byte-for-byte identical to what studiomdl wrote – the one exception being a single file with a jigglebone that nothing points at, which the object graph cannot reach and so cannot reproduce.

coverage reports how much of a file ValveMDL can account for, split into bytes regenerated from structures, bytes copied at a measured extent (the compressed animation streams), and bytes nothing wrote:

mdl.coverage()       # {'struct': ..., 'opaque': ..., 'differ': 0, ...}
mdl.vvd.coverage()   # a vvd is all struct: no compressed regions