Source code for valvemdl.vtx

"""
.. Vtx API
"""

from construct import Array, Int16ul

from valvemdl.loader import read_array, read_string
from valvemdl.structs.vtx import (BodyPartHeader_t, BoneStateChangeHeader_t,
                                  FileHeader_t, MaterialReplacementHeader_t,
                                  MaterialReplacementListHeader_t,
                                  MeshHeader_t, ModelHeader_t,
                                  ModelLODHeader_t, StripGroupHeader_t,
                                  StripHeader_t, Vertex_t)


[docs] class Vtx(object): """Contains the data from a vtx file. An mdl carries no triangle indices; they all live here, arranged into the strips and stripgroups the graphics hardware draws. The header is parsed when the file is opened; the tree below it is large, so it is walked on first access to :py:attr:`bodyparts` and cached. The tree mirrors the mdl's bodypart / model / mesh levels one to one, which is the check :py:meth:`Mdl.validate<valvemdl.Mdl.validate>` uses to tie the two files together. Below a mesh the vtx adds what the mdl does not have: stripgroups, strips, and the index buffer that assembles the vvd's vertices into triangles. """
[docs] def __init__(self, path=None): """Creates an empty instance of Vtx. :param path: A path to an existing vtx file. :type path: str, optional """ self.source_path = path #: :type: (str) - The location of the parsed file. self.header = None #: :type: (construct.Container) - The parsed FileHeader_t. # the file verbatim, kept for the tree and for writing back self._data = None self._bodyparts = None self._material_replacements = None if self.source_path: with open(self.source_path, 'rb') as f: self._data = f.read() self.header = FileHeader_t.parse(self._data)
@property def checksum(self): """Note the field is spelled ``checkSum`` in the vtx header and ``checksum`` everywhere else. This property smooths that over; the struct keeps the format's spelling. :type: (int) - Ties this vtx to its mdl and vvd. """ return self.header.checkSum @property def bodyparts(self): """The optimized mesh tree, walked on first access. Each bodypart carries ``.models``, each model ``.lods``, each lod ``.meshes``, each mesh ``.stripgroups``. A stripgroup carries ``.vertices`` (:any:`Vertex_t`), ``.indices`` (the ``unsigned short`` index buffer) and ``.strips``, and each strip its ``.bonestatechanges``. :type: (list[construct.Container]) - The bodyparts. """ if self._bodyparts is None: self._bodyparts = self._read_tree() return self._bodyparts @property def material_replacements(self): """The per-lod material replacement tables. One :any:`MaterialReplacementListHeader_t` per lod, each holding replacements that swap a material out at that detail level -- how the engine drops to cheaper materials at distance. :type: (list[construct.Container]) - One entry per lod. """ if self._material_replacements is None: self._material_replacements = self._read_material_replacements() return self._material_replacements def _read_tree(self): data = self._data bodyparts = read_array(data, BodyPartHeader_t, self.header.bodyPartOffset, self.header.numBodyParts) for bodypart in bodyparts: bodypart.models = read_array( data, ModelHeader_t, bodypart._offset + bodypart.modelOffset, bodypart.numModels) for model in bodypart.models: model.lods = read_array( data, ModelLODHeader_t, model._offset + model.lodOffset, model.numLODs) for lod in model.lods: lod.meshes = read_array( data, MeshHeader_t, lod._offset + lod.meshOffset, lod.numMeshes) for mesh in lod.meshes: self._read_mesh(mesh) return bodyparts def _read_mesh(self, mesh): data = self._data mesh.stripgroups = read_array( data, StripGroupHeader_t, mesh._offset + mesh.stripGroupHeaderOffset, mesh.numStripGroups) for group in mesh.stripgroups: group.vertices = read_array( data, Vertex_t, group._offset + group.vertOffset, group.numVerts) group.indices = self._read_indices( group._offset + group.indexOffset, group.numIndices) group.strips = read_array( data, StripHeader_t, group._offset + group.stripOffset, group.numStrips) for strip in group.strips: strip.bonestatechanges = read_array( data, BoneStateChangeHeader_t, strip._offset + strip.boneStateChangeOffset, strip.numBoneStateChanges) def _read_indices(self, offset, count): # the index buffer: unsigned shorts into the stripgroup's vertex array end = offset + count * 2 if offset < 0 or end > len(self._data): count = max(0, (len(self._data) - offset) // 2) end = offset + count * 2 return list(Array(count, Int16ul).parse(self._data[offset:end])) def _read_material_replacements(self): data = self._data lists = read_array(data, MaterialReplacementListHeader_t, self.header.materialReplacementListOffset, self.header.numLODs) for entry in lists: entry.replacements = read_array( data, MaterialReplacementHeader_t, entry._offset + entry.replacementOffset, entry.numReplacements) for replacement in entry.replacements: replacement.name = read_string( data, replacement._offset + replacement.replacementMaterialNameOffset) return lists
[docs] def validate(self): """Checks the vtx's internal invariants. Reports rather than raises, like the mdl and vvd do. Every check is structural -- counts add up, strips stay inside their stripgroup, the index buffer points at vertices that exist -- so a vtx that fails one is worth looking at. :returns: One string per broken invariant, empty when sound. :rtype: list[str] """ problems = [] for bp, bodypart in enumerate(self.bodyparts): if len(bodypart.models) != bodypart.numModels: problems.append( 'bodypart {0} declares {1} models but only {2} fit'.format( bp, bodypart.numModels, len(bodypart.models))) for model in bodypart.models: for lod in model.lods: for mesh in lod.meshes: problems.extend(self._validate_mesh(mesh)) return problems
def _validate_mesh(self, mesh): problems = [] for group in mesh.stripgroups: for strip in group.strips: if strip.vertOffset + strip.numVerts > group.numVerts: problems.append( 'a strip runs past the end of its stripgroup ' 'vertices') if strip.indexOffset + strip.numIndices > group.numIndices: problems.append( 'a strip runs past the end of its stripgroup indices') # every index must name a vertex the stripgroup actually holds for index in group.indices: if index >= group.numVerts: problems.append( 'an index ({0}) names a vertex outside the stripgroup ' '({1})'.format(index, group.numVerts)) break return problems
[docs] def save(self, destination=None): """Writes the vtx back out, rebuilt from its tree. :param destination: Where to write. Overwrites the original when not given. :type destination: str, optional """ from valvemdl.unloader import rebuild_vtx with open(destination or self.source_path, 'wb') as f: f.write(bytes(rebuild_vtx(self).data))
[docs] def coverage(self): """How much of the vtx |proj_name| can account for. Like the vvd and unlike the mdl, a sound vtx comes back essentially all ``struct``: it has no compressed regions, so every byte is regenerated rather than copied. :returns: Byte counts, plus ``differ``: bytes the rebuild got wrong. :rtype: dict """ from valvemdl.unloader import rebuild_vtx out = rebuild_vtx(self) report = out.report() report['differ'] = sum(1 for a, b in zip(out.data, self._data) if a != b) return report
[docs] def __repr__(self): return "<{0}.{1} '{2}' at {3}>".format(type(self).__module__, type(self).__name__, self.source_path, hex(id(self)))