A Python library for parsing Valve game files, extracted from my casual-preloader project. This library provides support for:
- VPK (Valve Package) files - Valve's archive format used in Source engine games
- PCF (Particle) files - Valve's particle system files - See constants.py for supported versions
- Support for single-file and multi-file VPK archives (creation and modification)
- Full VPK directory parsing and file extraction
- In-place VPK file patching with size checking
- PCF parsing and encoding
- Support for all PCF attribute types (see constants.py for these as well)
pip install valve-parsersfrom valve_parsers import VPKFile
# Create a single-file VPK
success = VPKFile.create("source_directory", "output/archive.vpk")
# Create a multi-file VPK with size limit (100MB per archive split)
success = VPKFile.create("source_directory", "output/archive", split_size=100*1024*1024)from valve_parsers import VPKFile
# Open a VPK file
vpk = VPKFile("path/to/archive.vpk")
# List all files
files = vpk.list_files()
print(f"Found {len(files)} files")
# Find files matching a pattern
texture_files = vpk.list_files(extension="vtf")
material_files = vpk.find_files("materials/*.vmt")
# Extract all files matching a pattern
count = vpk.extract_all("output_dir", pattern="materials/*.vmt")
print(f"Extracted {count} material files")
# Extract a file
vpk.extract_file("materials/models/player/scout.vmt", "output/scout.vmt")
# Or load it directly into memory
file_data = vpk.get_file_data("materials/models/player/scout.vmt")
if file_data:
content = file_data.decode('utf-8')
# Patch a file
# Read a new material file from disk
new_texture_path = "custom_scout_red.vmt"
with open(new_texture_path, 'rb') as f:
new_texture_data = f.read()
# Target file path inside the VPK
target_file = "materials/models/player/scout_red.vmt"
# Check if a file exists
if vpk.file_exists(target_file):
print("File found!")
# Get file info
info = vpk.get_file_info(target_file)
if info:
print(f"Size: {info['size']} bytes, CRC: 0x{info['crc']:08X}")
# IMPORTANT: Patched files must match original size exactly!
original_size = info['size']
if len(new_texture_data) != original_size:
if len(new_texture_data) < original_size:
# Pad with spaces to match original size
padding_needed = original_size - len(new_texture_data)
print(f"Adding {padding_needed} bytes of padding")
new_texture_data = new_texture_data + b' ' * padding_needed
else:
print(f"ERROR: New file is {len(new_texture_data) - original_size} bytes larger!")
print("File cannot be patched - size must match exactly")
# Now patch the file
vpk.patch_file(target_file, new_texture_data, create_backup=False)from valve_parsers import PCFFile
# Open and decode a PCF file
pcf = PCFFile("path/to/particles.pcf").decode()
print(f"PCF Version: {pcf.version}")
print(f"String dictionary: {len(pcf.string_dictionary)} entries")
print(f"Elements: {len(pcf.elements)} particle systems")
# Print particle system data
for element in pcf.elements:
print(f"Element: {element.element_name}")
for attr_name, (attr_type, attr_value) in element.attributes.items():
print(f" {attr_name.decode()}: {attr_value}")
# Rename all operators to ''
for i, element in enumerate(pcf.elements):
type_name = pcf.string_dictionary[element.type_name_index].decode('ascii')
if type_name == 'DmeParticleOperator':
element.element_name = str('').encode('ascii')
# Encode back to file
pcf.encode("output/modified_particles.pcf")
# Find a specific element by name
element = pcf.find_element_by_name("my_explosion_effect")
if element:
print(f"Found element with {len(element.attributes)} attributes")
# Get all elements of a specific type
operators = pcf.get_elements_by_type('DmeParticleOperator')
print(f"Found {len(operators)} operators")
# Get and set attribute values with helper methods
from valve_parsers import AttributeType
element = pcf.find_element_by_name("my_effect")
if element:
# Get attribute value
radius = pcf.get_attribute_value(element, "radius", default=5.0)
print(f"Current radius: {radius}")
# Set attribute value
pcf.set_attribute_value(element, "radius", 10.0, AttributeType.FLOAT)
pcf.set_attribute_value(element, "color", (255, 0, 0, 255), AttributeType.COLOR)
pcf.encode("output/modified_particles.pcf")The main class for working with VPK archives.
VPKFile(vpk_path: Union[str, Path], auto_parse: bool = True)- Initialize with path to VPK filevpk_path: Path to the VPK fileauto_parse: Automatically parse directory on init (default: True)
parse_directory() -> VPKFile- Parse the VPK directory structure (called automatically unlessauto_parse=False)list_files(extension: str = None, path: str = None) -> List[str]- List files with optional filteringfind_files(pattern: str) -> List[str]- Find files matching a glob patternfind_file_path(filename: str) -> Optional[str]- Find the full path of a filenameextract_file(filepath: str, output_path: str) -> bool- Extract a file from the archiveextract_all(output_dir: str, pattern: str = None) -> int- Extract multiple files to a directorypatch_file(filepath: str, new_data: bytes, create_backup: bool = False) -> bool- Modify a file in the archiveget_file_data(filepath: str) -> Optional[bytes]- Read a file's contents directly into memoryfile_exists(filepath: str) -> bool- Check if a file exists in the archiveget_file_info(filepath: str) -> Optional[Dict]- Get comprehensive information about a filecreate(source_dir: str, output_base_path: str, split_size: int = None) -> bool- Create new VPK archive (class method)
directory- Parsed directory structureis_dir_vpk- Whether this is a directory VPK filevpk_path- Path to the VPK file
Represents an entry in the VPK directory.
crc: int- CRC32 checksumpreload_bytes: int- Number of preload bytesarchive_index: int- Archive file indexentry_offset: int- Offset within archiveentry_length: int- Length of file datapreload_data: Optional[bytes]- Preloaded data
The main class for working with PCF particle files.
PCFFile(input_file: Union[Path, str], version: str = "DMX_BINARY2_PCF1")- Initialize with file path, default version is "DMX_BINARY2_PCF1"
decode() -> PCFFile- Parse the PCF fileencode(output_path: Union[Path, str]) -> PCFFile- Write PCF file to diskfind_element_by_name(name: str) -> Optional[PCFElement]- Find a particle element by its nameget_elements_by_type(type_name: str) -> List[PCFElement]- Get all elements of a specific typeget_attribute_value(element: PCFElement, attr_name: str, default=None)- Get an attribute value from an element (static method)set_attribute_value(element: PCFElement, attr_name: str, value, attr_type: AttributeType)- Set an attribute value on an element
version- PCF version stringstring_dictionary- List of strings used in the fileelements- List of particle system elements
Represents a particle system element.
type_name_index: int- Index into string dictionary for type nameelement_name: bytes- Name of the elementdata_signature: bytes- 16-byte signatureattributes: Dict[bytes, Tuple[AttributeType, Any]]- Element attributes
PCFVersion- Enum of supported PCF versionsAttributeType- Enum of PCF attribute types
This library works with VPK and PCF files from Orange Box titles. Mostly intended for TF2, YMMV with other games.
See:
https://developer.valvesoftware.com/wiki/PCF
https://developer.valvesoftware.com/wiki/VPK_(file_format)
This library was yoinked from my casual-pre-loader project. Contributions are welcome!
- Performance: Replaced rglob with os.walk for VPK creation
- Performance: Replaced Path.match with fnmatch in find_files()
- Bug fix: find_files() now uses correct glob matching behavior
- Updated README with corrected examples
- Added documentation for all VPK methods: extract_all(), get_file_data(), file_exists(), get_file_info()
- Added file patching size-checking examples
- Made get_file_entry() private
- Cleaned up internal method documentation
- Auto-parse support
- Reading and writing is now 2-3x faster
- Single file VPK no longer has _dir name
- Nothing
- Initial release
- VPK parsing and creation support
- PCF parsing and encoding support