"""
.. 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)))