Exemplo n.º 1
0
 def _read_static_props_models(static_lump: BytesIO) -> Iterator[str]:
     """Read the static prop dictionary from the lump."""
     [dict_num] = struct_read('<i', static_lump)
     for _ in range(dict_num):
         [padded_name] = struct_read('<128s', static_lump)
         # Strip null chars off the end, and convert to a str.
         yield padded_name.rstrip(b'\x00').decode('ascii')
Exemplo n.º 2
0
 def _parse_phy(self, f: BinaryIO, filename: str) -> None:
     """Parse the physics data file, if present.
     """
     [
         size,
         header_id,
         solid_count,
         checksum,
     ] = ST_PHY_HEADER.unpack(f.read(ST_PHY_HEADER.size))
     f.read(size - ST_PHY_HEADER.size)  # If the header is larger ever.
     for solid in range(solid_count):
         [solid_size] = struct_read('i', f)
         f.read(solid_size)  # Skip the header.
     self.phys_keyvalues = Property.parse(
         read_nullstr(f),
         filename + ":keyvalues",
         allow_escapes=False,
         single_line=True,
     )
Exemplo n.º 3
0
    def static_props(self) -> Iterator['StaticProp']:
        """Read in the Static Props lump."""
        # The version of the static prop format - different features.
        try:
            version = self.game_lumps[b'sprp'].version
        except KeyError:
            raise ValueError('No static prop lump!') from None

        if version > 11:
            raise ValueError('Unknown version ({})!'.format(version))
        if version < 4:
            # Predates HL2...
            raise ValueError('Static prop version {} is too old!')

        static_lump = BytesIO(self.game_lumps[b'sprp'].data)

        # Array of model filenames.
        model_dict = list(self._read_static_props_models(static_lump))

        [visleaf_count] = struct_read('<i', static_lump)
        visleaf_list = list(struct_read('H' * visleaf_count, static_lump))

        [prop_count] = struct_read('<i', static_lump)

        for i in range(prop_count):
            origin = Vec(struct_read('fff', static_lump))
            angles = Vec(struct_read('fff', static_lump))

            [model_ind] = struct_read('<H', static_lump)

            (
                first_leaf,
                leaf_count,
                solidity,
                flags,
                skin,
                min_fade,
                max_fade,
            ) = struct_read('<HHBBiff', static_lump)

            model_name = model_dict[model_ind]

            visleafs = visleaf_list[first_leaf:first_leaf + leaf_count]
            lighting_origin = Vec(struct_read('<fff', static_lump))

            if version >= 5:
                fade_scale = struct_read('<f', static_lump)[0]
            else:
                fade_scale = 1  # default

            if version in (6, 7):
                min_dx_level, max_dx_level = struct_read('<HH', static_lump)
            else:
                # Replaced by GPU & CPU in later versions.
                min_dx_level = max_dx_level = 0  # None

            if version >= 8:
                (
                    min_cpu_level,
                    max_cpu_level,
                    min_gpu_level,
                    max_gpu_level,
                ) = struct_read('BBBB', static_lump)
            else:
                # None
                min_cpu_level = max_cpu_level = 0
                min_gpu_level = max_gpu_level = 0

            if version >= 7:
                r, g, b, renderfx = struct_read('BBBB', static_lump)
                # Alpha isn't used.
                tint = Vec(r, g, b)
            else:
                # No tint.
                tint = Vec(255, 255, 255)
                renderfx = 255

            if version >= 11:
                # Unknown data, though it's float-like.
                unknown_1 = struct_read('<i', static_lump)

            if version >= 10:
                # Extra flags, post-CSGO.
                flags |= struct_read('<I', static_lump)[0] << 8

            flags = StaticPropFlags(flags)

            scaling = 1.0
            disable_on_xbox = False

            if version >= 11:
                # XBox support was removed. Instead this is the scaling factor.
                [scaling] = struct_read("<f", static_lump)
            elif version >= 9:
                # The single boolean byte also produces 3 pad bytes.
                [disable_on_xbox] = struct_read('<?xxx', static_lump)

            yield StaticProp(
                model_name,
                origin,
                angles,
                scaling,
                visleafs,
                solidity,
                flags,
                skin,
                min_fade,
                max_fade,
                lighting_origin,
                fade_scale,
                min_dx_level,
                max_dx_level,
                min_cpu_level,
                max_cpu_level,
                min_gpu_level,
                max_gpu_level,
                tint,
                renderfx,
                disable_on_xbox,
            )
Exemplo n.º 4
0
    def read(self) -> None:
        """Load all data."""
        self.lumps.clear()
        self.game_lumps.clear()

        with open(self.filename, mode='br') as file:
            # BSP files start with 'VBSP', then a version number.
            magic_name, bsp_version = struct_read(HEADER_1, file)
            assert magic_name == BSP_MAGIC, 'Not a BSP file!'

            if self.version is None:
                try:
                    self.version = VERSIONS(bsp_version)
                except ValueError:
                    self.version = bsp_version
            else:
                assert bsp_version == self.version, 'Different BSP version!'

            lump_offsets = {}

            # Read the index describing each BSP lump.
            for index in range(LUMP_COUNT):
                offset, length, version, ident = struct_read(HEADER_LUMP, file)
                lump_id = BSP_LUMPS(index)
                self.lumps[lump_id] = Lump(
                    lump_id,
                    version,
                    ident,
                )
                lump_offsets[lump_id] = offset, length

            [self.map_revision] = struct_read(HEADER_2, file)

            for lump in self.lumps.values():
                # Now read in each lump.
                offset, length = lump_offsets[lump.type]
                file.seek(offset)
                lump.data = file.read(length)

            game_lump = self.lumps[BSP_LUMPS.GAME_LUMP]

            self.game_lumps.clear()

            [lump_count] = struct.unpack_from('<i', game_lump.data)
            lump_offset = 4

            for _ in range(lump_count):
                (
                    game_lump_id,
                    flags,
                    glump_version,
                    file_off,
                    file_len,
                ) = GameLump.ST.unpack_from(
                    game_lump.data,
                    lump_offset)  # type: bytes, int, int, int, int
                lump_offset += GameLump.ST.size

                file.seek(file_off)

                # The lump ID is backward..
                game_lump_id = game_lump_id[::-1]

                self.game_lumps[game_lump_id] = GameLump(
                    game_lump_id,
                    flags,
                    glump_version,
                    file.read(file_len),
                )
            # This is not valid any longer.
            game_lump.data = b''
Exemplo n.º 5
0
    def load_dirfile(self) -> None:
        """Read in the directory file to get all filenames.
        
        This erases all changes in the file.
        """
        if self.mode is OpenModes.WRITE:
            # Erase the directory file, we ignore current contents.
            open(self.path, 'wb').close()
            self.version = 1
            return

        try:
            dirfile = open(self.path, 'rb')
        except FileNotFoundError:
            if self.mode is OpenModes.APPEND:
                # No directory file - generate a blank file.
                open(self.path, 'wb').close()
                self.version = 1
                return
            else:
                raise  # In read mode, don't overwrite and error when reading.

        with dirfile:
            vpk_sig, version, tree_length = struct_read('<III', dirfile)

            if vpk_sig != VPK_SIG:
                raise ValueError('Bad VPK directory signature!')

            if version not in (1, 2):
                raise ValueError("Bad VPK version {}!".format(self.version))

            self.version = version

            if version >= 2:
                (
                    data_size,
                    ext_md5_size,
                    dir_md5_size,
                    sig_size,
                ) = struct_read('<4I', dirfile)

            self.header_len = dirfile.tell() + tree_length

            self._fileinfo.clear()

            # Read directory contents
            # These are in a tree of extension, directory, file. '' terminates a part.
            for ext in iter_nullstr(dirfile):
                ext_dict = self._fileinfo.setdefault(ext, {})
                for directory in iter_nullstr(dirfile):
                    dir_dict = ext_dict.setdefault(directory, {})
                    for file in iter_nullstr(dirfile):
                        crc, index_len, arch_ind, offset, arch_len, end = struct_read(
                            '<IHHIIH', dirfile)
                        if arch_ind == DIR_ARCH_INDEX:
                            arch_ind = None

                        if arch_len == 0:
                            offset = 0

                        if end != 0xffff:
                            raise Exception(
                                '"{}" has bad terminator! {}'.format(
                                    _join_file_parts(directory, file, ext),
                                    (crc, index_len, arch_ind, offset,
                                     arch_len, end),
                                ))
                        dir_dict[file] = FileInfo(
                            self,
                            directory,
                            file,
                            ext,
                            crc=crc,
                            offset=offset,
                            start_data=dirfile.read(index_len),
                            arch_len=arch_len,
                            arch_index=arch_ind,
                        )

                # 1 for the ending b'' section
                if dirfile.tell() + 1 == self.header_len:
                    dirfile.read(1)  # Skip null byte.
                    break

            self.footer_data = dirfile.read()
Exemplo n.º 6
0
    def _cull_skins_table(self, f: BinaryIO, body_count: int) -> None:
        """Fix the table of used skins to correspond to those actually used.

        StudioMDL is rather messy, and adds many extra columns that are not used
        on the actual model.
        We're following  mstudiobodyparts_t -> mstudiomodel_t -> mstudiomesh_t -> material.
        """
        used_inds = set()

        # Iterate through bodygroups.
        for body_ind in range(body_count):
            body_start = f.tell()
            (
                body_name_off,  # Offset to find the bodygroup name
                model_count,  # Number of models in this group
                base,  # Unknown
                model_off,
            ) = struct_read('iiii', f)
            body_end = f.tell()

            f.seek(body_start + model_off)
            for model_ind in range(model_count):
                model_start = f.tell()
                (
                    mdl_name,
                    mdl_type,
                    bound_radius,
                    mesh_count,
                    mesh_off,
                    num_verts,
                    vert_off,
                    tangent_off,
                    attach_count,
                    attach_ind,
                    eyeball_count,
                    eyeball_ind,
                    # Two void* pointers,
                    # 32 empty bytes
                ) = struct_read('64s i f 9i 8x 32x', f)
                model_end = f.tell()

                f.seek(model_start + mesh_off)
                for mesh_ind in range(mesh_count):
                    (
                        material,
                        mesh_model_ind,
                        mesh_vert_count,
                        mesh_vert_off,
                        mesh_flex_count,
                        mesh_flex_ind,
                        mesh_mat_type,
                        mesh_mat_param,
                        mesh_id,
                        mesh_cent_x,
                        mesh_cent_y,
                        mesh_cent_z,
                        # Void pointer
                        # Array of LOD vertex counts ints, 8 of them
                        # 8 unused int spaces.
                    ) = struct_read('9i 3f 4x 32x 32x', f)
                    used_inds.add(material)

                f.seek(model_end)
            f.seek(body_end)

        for skin_ind, tex in enumerate(self.skins):
            self.skins[skin_ind] = [tex[i] for i in used_inds]
Exemplo n.º 7
0
    def _read_sequences(f: BinaryIO, count: int) -> List[MDLSequence]:
        """Split this off to decrease stack in main parse method."""
        sequences = [None] * count  # type: List[MDLSequence]
        for i in range(count):
            start_pos = f.tell()
            (
                base_ptr,
                label_pos,
                act_name_pos,
                flags,
                _,  # Seems to be a pointer.
                act_weight,
                event_count,
                event_pos,
            ) = struct_read('8i', f)
            bbox_min = str_readvec(f)
            bbox_max = str_readvec(f)

            # Skip 20 ints, 9 floats to get to keyvalues = 29*4 bytes
            # Then 8 unused ints.
            (
                keyvalue_pos,
                keyvalue_size,
            ) = struct_read('116xii32x', f)
            end_pos = f.tell()

            f.seek(start_pos + event_pos)
            events = [None] * event_count  # type: List[SeqEvent]
            for j in range(event_count):
                event_start = f.tell()
                (
                    event_cycle,
                    event_index,
                    event_flags,
                    event_options,
                    event_nameloc,
                ) = struct_read('fii64si', f)
                event_end = f.tell()

                # There are two event systems.
                if event_flags == 1 << 10:
                    # New system, name in the file.
                    event_name = read_nullstr(f, event_start + event_nameloc)
                    if event_name.isdigit():
                        try:
                            event_type = ANIM_EVENT_BY_INDEX[int(event_name)]
                        except KeyError:
                            raise ValueError('Unknown event index!')
                    else:
                        try:
                            event_type = ANIM_EVENT_BY_NAME[event_name]
                        except KeyError:
                            # NPC-specific events, declared dynamically.
                            event_type = event_name
                else:
                    # Old system, index.
                    try:
                        event_type = ANIM_EVENT_BY_INDEX[event_index]
                    except KeyError:
                        # raise ValueError('Unknown event index!')
                        print('Unknown: ', event_index,
                              event_options.rstrip(b'\0'))
                        continue

                f.seek(event_end)
                events[j] = SeqEvent(
                    type=event_type,
                    cycle=event_cycle,
                    options=event_options.rstrip(b'\0').decode('ascii'))

            if keyvalue_size:
                keyvalues = read_nullstr(f, start_pos + keyvalue_pos)
            else:
                keyvalues = ''

            sequences[i] = MDLSequence(
                label=read_nullstr(f, start_pos + label_pos),
                act_name=read_nullstr(f, start_pos + act_name_pos),
                flags=flags,
                act_weight=act_weight,
                events=events,
                bbox_min=bbox_min,
                bbox_max=bbox_max,
                keyvalues=keyvalues,
            )

            f.seek(end_pos)

        return sequences
Exemplo n.º 8
0
    def _load(self, f: BinaryIO) -> None:
        """Read data from the MDL file."""
        assert f.tell() == 0, "Doesn't begin at start?"
        if f.read(4) != b'IDST':
            raise ValueError('Not a model!')
        (
            self.version,
            self.checksum,
            name,
            file_len,
        ) = struct_read('i 4s 64s i', f)

        if not 44 <= self.version <= 49:
            raise ValueError('Unknown MDL version {}!'.format(self.version))

        self.name = name.rstrip(b'\0').decode('ascii')
        self.eye_pos = str_readvec(f)
        self.illum_pos = str_readvec(f)
        # Approx dimensions
        self.hull_min = str_readvec(f)
        self.hull_max = str_readvec(f)

        self.view_min = str_readvec(f)
        self.view_max = str_readvec(f)

        # Break up the reading a bit to limit the stack size.
        (
            flags,
            bone_count,
            bone_off,
            bone_controller_count,
            bone_controller_off,
            hitbox_count,
            hitbox_off,
            anim_count,
            anim_off,
            sequence_count,
            sequence_off,
        ) = struct_read('11I', f)

        self.flags = Flags(flags)

        (
            activitylistversion,
            eventsindexed,
            texture_count,
            texture_offset,
            cdmat_count,
            cdmat_offset,
            skinref_count,  # Number of skin "groups"
            skin_count,  # Number of model skins.
            skinref_ind,  # Location of skins reference table.

            # The number of $body in the model (mstudiobodyparts_t).
            bodypart_count,
            bodypart_offset,
            attachment_count,
            attachment_offset,
        ) = struct_read('13i', f)

        (
            localnode_count,
            localnode_index,
            localnode_name_index,

            # mstudioflexdesc_t
            flexdesc_count,
            flexdesc_index,

            # mstudioflexcontroller_t
            flexcontroller_count,
            flexcontroller_index,

            # mstudioflexrule_t
            flexrules_count,
            flexrules_index,

            # IK probably refers to inverse kinematics
            # mstudioikchain_t
            ikchain_count,
            ikchain_index,

            # Information about any "mouth" on the model for speech animation
            # More than one sounds pretty creepy.
            # mstudiomouth_t
            mouths_count,
            mouths_index,

            # mstudioposeparamdesc_t
            localposeparam_count,
            localposeparam_index,
        ) = struct_read('15I', f)

        # VDC:
        # For anyone trying to follow along, as of this writing,
        # the next "surfaceprop_index" value is at position 0x0134 (308)
        # from the start of the file.
        assert f.tell() == 308, 'Offset wrong? {} != 308 {}'.format(
            f.tell(), f)

        (
            # Surface property value (single null-terminated string)
            surfaceprop_index,

            # Unusual: In this one index comes first, then count.
            # Key-value data is a series of strings. If you can't find
            # what you're interested in, check the associated PHY file as well.
            keyvalue_index,
            keyvalue_count,

            # More inverse-kinematics
            # mstudioiklock_t
            iklock_count,
            iklock_index,
        ) = struct_read('5I', f)

        (
            self.mass,  # Mass of object (float)
            self.contents,  # ??

            # Other models can be referenced for re-used sequences and
            # animations
            # (See also: The $includemodel QC option.)
            # mstudiomodelgroup_t
            includemodel_count,
            includemodel_index,

            # In-engine, this is a pointer to the combined version of this +
            # included models. In the file it's useless.
            virtualModel,

            # mstudioanimblock_t
            animblocks_name_index,
            animblocks_count,
            animblocks_index,
            animblockModel,  # Placeholder for mutable-void*

            # Points to a series of bytes?
            bonetablename_index,
            vertex_base,  # Placeholder for void*
            offset_base,  # Placeholder for void*
        ) = struct_read('f 11I', f)

        (
            # Used with $constantdirectionallight from the QC
            # Model should have flag #13 set if enabled
            directionaldotproduct,  # byte

            # Preferred rather than clamped
            rootLod,  # byte

            # 0 means any allowed, N means Lod 0 -> (N-1)
            self.numAllowedRootLods,  # byte

            #unknown byte;
            #unknown int;

            # mstudioflexcontrollerui_t
            flexcontrollerui_count,
            flexcontrollerui_index,
        ) = struct_read('3b 5x 2I', f)

        # Build CDMaterials data
        f.seek(cdmat_offset)
        self.cdmaterials = read_offset_array(f, cdmat_count)

        for ind, cdmat in enumerate(self.cdmaterials):
            cdmat = cdmat.replace('\\', '/').lstrip('/')
            if cdmat and cdmat[-1:] != '/':
                cdmat += '/'
            self.cdmaterials[ind] = cdmat

        # Build texture data
        f.seek(texture_offset)
        textures = [None] * texture_count  # type: List[Tuple[str, int, int]]
        tex_temp = [
            None
        ] * texture_count  # type: List[Tuple[int, Tuple[int, int, int]]]
        for tex_ind in range(texture_count):
            tex_temp[tex_ind] = (
                f.tell(),
                # Texture data:
                # int: offset to the string, from start of struct.
                # int: flags - appears to solely indicate 'teeth' materials...
                # int: used, whatever that means.
                # 4 unused bytes.
                # 2 4-byte pointers in studiomdl to the material class, for
                #      server and client - shouldn't be in the file...
                # 40 bytes of unused space (for expansion...)
                struct_read('iii 4x 8x 40x', f))
        for tex_ind, (offset, data) in enumerate(tex_temp):
            name_offset, flags, used = data
            textures[tex_ind] = (
                read_nullstr(f, offset + name_offset),
                flags,
                used,
            )

        # Now parse through the family table, to match skins to textures.
        f.seek(skinref_ind)
        ref_data = f.read(2 * skinref_count * skin_count)
        self.skins = [None] * skin_count  # type: List[List[str]]
        skin_group = Struct('<{}H'.format(skinref_count))
        offset = 0
        for ind in range(skin_count):
            self.skins[ind] = [
                textures[i][0].replace('\\', '/').lstrip('/')
                for i in skin_group.unpack_from(ref_data, offset)
            ]
            offset += skin_group.size

        # If models have folders, add those folders onto cdmaterials.
        for tex, flags, used in textures:
            tex = tex.replace('\\', '/')
            if '/' in tex:
                folder = tex.rsplit('/', 1)[0]
                if folder not in self.cdmaterials:
                    self.cdmaterials.append(folder)

        # All models fallback to checking the texture at a root folder.
        if '' not in self.cdmaterials:
            self.cdmaterials.append('')

        f.seek(surfaceprop_index)
        self.surfaceprop = read_nullstr(f)

        if keyvalue_count:
            self.keyvalues = read_nullstr(f, keyvalue_index)
        else:
            self.keyvalues = ''

        f.seek(includemodel_index)
        self.included_models = [
            None
        ] * includemodel_count  # type: List[IncludedMDL]
        for i in range(includemodel_count):
            pos = f.tell()
            # This is two offsets from the start of the structures.
            lbl_pos, filename_pos = struct_read('II', f)
            self.included_models[i] = IncludedMDL(
                read_nullstr(f, pos + lbl_pos) if lbl_pos else '',
                read_nullstr(f, pos + filename_pos) if filename_pos else '',
            )
            # Then return to after that struct - 4 bytes * 2.
            f.seek(pos + 4 * 2)

        f.seek(sequence_off)
        self.sequences = self._read_sequences(f, sequence_count)

        f.seek(bodypart_offset)
        self._cull_skins_table(f, bodypart_count)