Exemple #1
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,
     )
Exemple #2
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
Exemple #3
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)