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, )
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
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)