https://github.com/ValveResourceFormat/ValvePak
📦 Fully fledged library to work with Valve's Pak archives in .NET
https://github.com/ValveResourceFormat/ValvePak
csharp dotnet source2 steam valve vpk
Last synced: about 11 hours ago
JSON representation
📦 Fully fledged library to work with Valve's Pak archives in .NET
- Host: GitHub
- URL: https://github.com/ValveResourceFormat/ValvePak
- Owner: ValveResourceFormat
- License: mit
- Created: 2016-09-18T06:30:55.000Z (almost 10 years ago)
- Default Branch: master
- Last Pushed: 2026-06-22T08:54:10.000Z (18 days ago)
- Last Synced: 2026-06-22T10:26:58.521Z (18 days ago)
- Topics: csharp, dotnet, source2, steam, valve, vpk
- Language: C#
- Homepage: https://www.nuget.org/packages/ValvePak/
- Size: 354 KB
- Stars: 150
- Watchers: 5
- Forks: 20
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-game-file-format-reversing - ValvePak - C# .NET library for reading and writing Source 2 VPK (Valve PacK) archives. Part of the ValveResourceFormat project. (⚙️ Engines / Source (Valve))
README
Valve Pak for .NET
A .NET library for reading and extracting VPK (Valve Pak) files, the uncompressed archive format used to package game content in Source and Source 2 engine games.
## Usage
```csharp
using var package = new Package();
// Open a vpk file
package.Read("pak01_dir.vpk");
// Can also pass in a stream
package.Read(File.OpenRead("pak01_dir.vpk"));
// Optionally verify hashes and signatures of the file if there are any
package.VerifyHashes();
// Find a file, this returns a PackageEntry
var file = package.FindEntry("path/to/file.txt");
if (file != null) {
// Read a file to a byte array
package.ReadEntry(file, out byte[] fileContents);
// Inspect entry metadata
Console.WriteLine(file.GetFullPath()); // "path/to/file.txt"
Console.WriteLine(file.GetFileName()); // "file.txt"
Console.WriteLine(file.TotalLength); // file size in bytes
Console.WriteLine(file.CRC32); // CRC32 checksum
}
```
Do note that files such as `pak01_001.vpk` are just data files, you have to open `pak01_dir.vpk`.
## Extract all files
```csharp
using var package = new Package();
package.Read("pak01_dir.vpk");
foreach (var group in package.Entries)
{
foreach (var entry in group.Value)
{
var filePath = entry.GetFullPath();
package.ReadEntry(entry, out byte[] data);
// Create the directory if needed, then write the file
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllBytes(filePath, data);
}
}
```
## Create a VPK
```csharp
using var package = new Package();
// Add files to the package
package.AddFile("path/to/file.txt", File.ReadAllBytes("file.txt"));
package.AddFile("models/example.vmdl", File.ReadAllBytes("example.vmdl"));
// Remove a file from the package
package.RemoveFile(package.FindEntry("path/to/file.txt"));
// Write the package to disk
package.Write("pak01_dir.vpk");
```
## Optimize for many lookups
By default, `FindEntry` performs a linear scan. If you need to look up many files, call `OptimizeEntriesForBinarySearch()` before `Read()` to sort entries and use binary search instead. You can also pass `StringComparison.OrdinalIgnoreCase` for case-insensitive lookups.
```csharp
using var package = new Package();
// Call before Read() to enable binary search for FindEntry
package.OptimizeEntriesForBinarySearch();
package.Read("pak01_dir.vpk");
// FindEntry calls are now significantly faster
var file = package.FindEntry("path/to/file.txt");
```
## Read into a user-provided buffer
```csharp
var entry = package.FindEntry("path/to/file.txt");
// Allocate your own buffer (must be at least entry.TotalLength bytes)
var buffer = new byte[entry.TotalLength];
package.ReadEntry(entry, buffer, validateCrc: true);
```
Using `ArrayPool` to avoid allocations when reading many files:
```csharp
var entry = package.FindEntry("path/to/file.txt");
var buffer = ArrayPool.Shared.Rent((int)entry.TotalLength);
try
{
package.ReadEntry(entry, buffer, validateCrc: true);
// Use buffer[..entry.TotalLength] here
}
finally
{
ArrayPool.Shared.Return(buffer);
}
```
## Stream-based access
`GetMemoryMappedStreamIfPossible` returns a memory-mapped stream for large files (over 4 KiB) and a `MemoryStream` for smaller ones. This avoids reading the entire file into a byte array.
```csharp
var entry = package.FindEntry("path/to/file.txt");
using var stream = package.GetMemoryMappedStreamIfPossible(entry);
```
## Verification
```csharp
using var package = new Package();
package.Read("pak01_dir.vpk");
// Verify MD5 hashes of the directory tree and whole file
package.VerifyHashes();
// Verify MD5/Blake3 hashes of individual chunk files (pak01_000.vpk, pak01_001.vpk, ...)
package.VerifyChunkHashes();
// Verify CRC32 checksums of every file in the package
package.VerifyFileChecksums();
// Verify the RSA signature if the package is signed
bool valid = package.IsSignatureValid();
```