Source code for valvemdl.vvd

"""
.. Vvd API
"""

from construct import Array

from valvemdl.structs.common import Vector4D
from valvemdl.structs.vvd import (mstudiovertex_t, vertexFileFixup_t,
                                  vertexFileHeader_t)


[docs] class Vvd(object): """Contains the data from a vvd file. An mdl carries no vertex positions; they all live here, in one flat pool shared by every mesh in the model. The header is parsed when the file is opened; the pools are large, so they are parsed on first access and cached. **The pools are stored exactly as they sit on disk**, which for a model with fixups means LOD-sorted rather than mesh-sorted. :py:meth:`mesh_vertices` replays the fixups to hand back the mesh order that ``mstudiomodel_t.vertexindex`` expects. Keeping the raw order is what lets a vvd be written back byte for byte. """
[docs] def __init__(self, path=None): """Creates an empty instance of Vvd. :param path: A path to an existing vvd 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 vertexFileHeader_t. # the file verbatim, kept for the pools and for writing back self._data = None self._vertices = None self._tangents = None self._fixups = None if self.source_path: with open(self.source_path, 'rb') as f: self._data = f.read() self.header = vertexFileHeader_t.parse(self._data)
@property def checksum(self): """:type: (int) - Ties this vvd to its mdl and vtx.""" return self.header.checksum @property def vertices(self): """The vertex pool, in the order it is stored. That order is LOD-sorted when :py:attr:`header` ``.numFixups`` is non-zero. Use :py:meth:`mesh_vertices` for the order a mesh indexes. :type: (list[construct.Container]) - ``numLODVertexes[0]`` vertices. """ if self._vertices is None: self._vertices = self._read_pool( mstudiovertex_t, self.header.vertexDataStart) return self._vertices @property def tangents(self): """The tangent pool, parallel to :py:attr:`vertices`. Tangents are kept in their own pool rather than inside the vertex, which is what holds :any:`mstudiovertex_t` at 48 bytes. Empty when ``tangentDataStart`` is 0. :type: (list[construct.Container]) - One ``Vector4D`` per vertex. """ if self._tangents is None: if not self.header.tangentDataStart: self._tangents = [] else: self._tangents = self._read_pool( Vector4D, self.header.tangentDataStart) return self._tangents @property def fixups(self): """The fixup table. Empty unless the pool is LOD-sorted. Each entry names a run of the pool and the LOD it belongs to; replaying them rebuilds mesh order. :type: (list[construct.Container]) - ``numFixups`` fixups. """ if self._fixups is None: if not self.header.numFixups: self._fixups = [] else: start = self.header.fixupTableStart size = vertexFileFixup_t.sizeof() raw = self._data[start:start + self.header.numFixups * size] self._fixups = list( Array(self.header.numFixups, vertexFileFixup_t).parse(raw)) return self._fixups def _read_pool(self, struct, start): # the pool always holds numLODVertexes[0] entries -- the top lod count, # which is the largest, per Studio_LoadVertexes count = self.header.numLODVertexes[0] size = struct.sizeof() # clamp to what the file actually holds, so a truncated pool reports # short rather than raising from inside construct available = max(0, (len(self._data) - start) // size) count = min(count, available) raw = self._data[start:start + count * size] return list(Array(count, struct).parse(raw))
[docs] def mesh_vertices(self, root_lod=0): """The vertices in mesh order, for a given root LOD. When the pool is LOD-sorted (``numFixups`` non-zero), a mesh's ``vertexoffset`` indexes an order that only exists once the fixup table has been replayed. This does that, following ``Studio_LoadVertexes``: each fixup at or below the requested LOD copies a run of the stored pool into the next free slot, and the runs land consecutively. With no fixups the pool is already in mesh order and this returns it unchanged. :param root_lod: The most detailed LOD to include; 0 is full detail. :type root_lod: int, optional :returns: The vertices a mesh can index into. :rtype: list[construct.Container] """ return self._replay(self.vertices, root_lod)
[docs] def mesh_tangents(self, root_lod=0): """The tangents in mesh order, for a given root LOD. Tangents run parallel to vertices, so they take the identical fixup replay -- reordering one without the other would pair each vertex with the wrong tangent. :param root_lod: The most detailed LOD to include; 0 is full detail. :type root_lod: int, optional :returns: The tangents, in the same order as :py:meth:`mesh_vertices`. :rtype: list[construct.Container] """ return self._replay(self.tangents, root_lod)
def _replay(self, pool, root_lod): if not self.fixups: return list(pool) out = [] for fixup in self.fixups: if fixup.lod < root_lod: # working from full detail, skip the higher-detail lods continue out.extend(pool[fixup.sourceVertexID: fixup.sourceVertexID + fixup.numVertexes]) return out def _replay_count(self, root_lod): # how many vertices mesh_vertices(root_lod) would return, without # touching the pool -- a fixup at or below the lod contributes its run return sum(f.numVertexes for f in self.fixups if f.lod >= root_lod)
[docs] def validate(self): """Checks the vvd's internal invariants. Like :py:meth:`Mdl.validate<valvemdl.Mdl.validate>` this reports rather than raises: a vvd that fails one of these is the interesting one. It is deliberately cheap -- every check reads the header and the small fixup table, never the vertex pool -- so the corpus sweep can run it on every file without parsing a quarter megabyte of vertices each time. :returns: One string per broken invariant, empty when sound. :rtype: list[str] """ problems = [] header = self.header pool = header.numLODVertexes[0] if header.id != b'IDSV': problems.append('bad magic ({0!r})'.format(header.id)) span = header.tangentDataStart - header.vertexDataStart if header.tangentDataStart and span != pool * mstudiovertex_t.sizeof(): problems.append( 'the vertex pool is {0} bytes, not the {1} its vertex count ' 'implies'.format(span, pool * mstudiovertex_t.sizeof())) # each fixup names a run of the pool; none may point past it for i, fixup in enumerate(self.fixups): if fixup.sourceVertexID < 0 or \ fixup.sourceVertexID + fixup.numVertexes > pool: problems.append( 'fixup {0} runs off the end of the vertex pool'.format(i)) # replaying the fixups at a lod must reproduce that lod's vertex count if self.fixups: for lod in range(header.numLODs): got = self._replay_count(lod) if got != header.numLODVertexes[lod]: problems.append( 'fixups give {0} vertices at lod {1}, but the header ' 'says {2}'.format(got, lod, header.numLODVertexes[lod])) return problems
[docs] def save(self, destination=None): """Writes the vvd back out, rebuilt from its pools. :param destination: Where to write. Overwrites the original when not given. :type destination: str, optional """ from valvemdl.unloader import rebuild_vvd with open(destination or self.source_path, 'wb') as f: f.write(bytes(rebuild_vvd(self).data))
[docs] def coverage(self): """How much of the vvd |proj_name| can account for. A vvd has no compressed regions, so a sound one comes back essentially all ``struct`` -- every byte regenerated from the object graph, none merely copied. See :py:meth:`Mdl.coverage<valvemdl.Mdl.coverage>`. :returns: Byte counts, plus ``differ``: bytes the rebuild got wrong. :rtype: dict """ from valvemdl.unloader import rebuild_vvd out = rebuild_vvd(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)))