Utility for packing, unpacking and analyzing The Witcher 3 content bundles.
Implements a subset of features from wcc_lite from the official The Witcher 3 mod tools.
The library and CLI support creating, inspecting, unpacking, converting, and merging Witcher 3 mod containers in Legacy and Remastered formats. Bundle packing accepts already-cooked files; cache creation accepts already-cooked GPU texture chunks. Asset cooking, image conversion, and DDS extraction are not implemented.
Generated containers are tested by reparsing and comparing data with this library; in-game acceptance has not been verified. This is not a replacement for the full official mod toolchain.
- Reads Legacy (version 3) and Remastered (version 5)
.bundlefiles, including version-5 payload offsets above 4 GiB - Unpacks
.bundlecontents- Supports unpacking
lz4,lz4hc,snappyandzlibcompressed as well as uncompressed bundles
- Supports unpacking
- Creates
.bundlefiles in either format and preserves versions when rewriting- Preserves per-item
lz4,lz4hc,snappy,zlib, and uncompressed payloads without re-encoding - Mixed-version bundle merges emit version 5; legacy-only merges retain version 3
- Preserves per-item
- Creates, reads, and writes legacy version-6 and remastered version-7
metadata.storefiles, including 64-bit offsets and bundle sizes in version 7 - Merges multiple mods into a single
(bundle, metadata)pair with first-wins priority and self-consistent cross-references - Creates, reads, writes, and merges version-6 and version-7
texture.cachefiles - Upgrades or downgrades bundles, metadata indexes, and texture caches between Legacy and Remastered container layouts without recompressing payloads
- Provides a
w3editCLI for inspecting containers, unpacking bundles, and merging a bundle, metadata index, and optional texture cache from each input
Not yet implemented:
- Extracting image data from
texture.cache - Doboz compression or decompression
| Target | .bundle |
metadata.store |
texture.cache |
|---|---|---|---|
legacy |
Version 3 | Version 6 | Version 6 |
remastered |
Version 5 | Version 7 | Version 7 |
Creation defaults to legacy; readers detect formats from the input files.
Bundle rewrites preserve compressed payloads, but rebuild their layout and are
not generally byte-identical to the original container.
Prebuilt binaries for Linux, Windows and macOS are available on the snapshot release. Download the archive for your platform and extract it.
Running on macOS
The macOS builds are not signed or notarized, so macOS may refuse to run w3edit
after downloading it through a browser. If you trust the release source, remove
the quarantine attribute from the executable:
xattr -d com.apple.quarantine ./w3editAlternatively, try to run it once and then approve it under System Settings > Privacy & Security > Open Anyway.
Building requires a Rust toolchain and a C compiler for the native LZ4 dependency.
git clone https://github.com/Systemcluster/w3edit.git
cd w3edit
cargo install --path .This installs w3edit into Cargo's binary directory, usually ~/.cargo/bin.
To build without installing, run cargo build --release and use
./target/release/w3edit instead.
Set W3_DIR to the game installation directory. For example, a default Steam
installation on macOS may use:
export W3_DIR="$HOME/Library/Application Support/Steam/steamapps/common/The Witcher 3"
w3edit metadata list "$W3_DIR/mods/modExample"
w3edit bundle unpack "$W3_DIR/mods/modExample" build/modExample-unpacked/Replace modExample with the installed mod's directory name. Container-based
mods normally keep metadata.store, one or more .bundle files, and an optional
texture.cache directly in mods/<mod-name>/content/.
Run w3edit --help or w3edit <command> --help for all commands and options.
Commands that create containers take --format legacy or --format remastered.
Mod-directory inputs accept either a mod root or its content/ directory.
merge and directory-to-directory convert take an output mod root and create
content/ automatically; an explicit output ending in content/ is used directly.
Container commands also accept mod roots: bundle pack, metadata create, and
texture create write content/blob0.bundle, content/metadata.store, and
content/texture.cache, respectively. For container outputs, an existing directory
or a new path without an extension means a mod root. An existing file or a new
path with an extension means an explicit file; its parent directory must exist.
Create a directory first when using a new mod name containing a dot.
The list commands and bundle unpack accept an existing mod root or content
directory in place of a container file. Bundle commands use blob0.bundle for
directory inputs; pass an explicit file to select another bundle. bundle pack
and metadata create resolve input mod roots to content/ before computing
relative paths. bundle unpack still writes raw extracted files directly into
its destination, not into an added content/ directory.
Warning
w3edit only replaces existing files with --force, but doesn't clear existing
directories, and a failed command can leave partial output behind. Use a fresh
output directory separate from your inputs, and keep backups of your mods.
w3edit bundle list "$W3_DIR/mods/modExample"
w3edit bundle unpack "$W3_DIR/mods/modExample" build/modExample-unpacked/
w3edit bundle pack cooked/ build/modMyMod --format remastered --compression zlibUnpacking extracts all files into the given directory, or into blob0.unpacked
beside the bundle when no output is given.
Packing recursively includes all files in the resolved input directory and compresses
them with zlib by default, or with none, snappy, lz4 or lz4hc.
Files are packed as they are, so they need to be already cooked for the
target game version.
Notes
- Stored paths are relative to the input directory with backslash separators. Names have to be ISO-8859-1 encodable and at most 255 bytes long, and symlinks are rejected.
- Keep the output bundle outside of the input directory, otherwise a previous output will be packed into the new bundle.
- Unpacking rejects absolute and parent-traversal paths, but follows existing symlinks in the output directory.
w3edit metadata list "$W3_DIR/mods/modExample"
w3edit metadata create build/modMyMod build/modMyMod --format remasteredMetadata creation recursively indexes all .bundle files in the resolved content
directory using their on-disk offsets. Bundle names are relative to that directory,
without an extra content/ prefix. All bundles have to match the selected format.
The index is built from the bundle records, not from the cooked resources inside
them, so resource-specific hashes and buffer information are not reconstructed.
To keep them for an existing mod, use merge with a single input instead.
w3edit texture list "$W3_DIR/mods/modExample"
w3edit texture create modMyMod-textures.json build/modMyMod --format remasteredTexture caches are created from a JSON manifest referencing already cooked GPU texture data. w3edit takes care of compression, streaming headers, page indexes, mip offsets and checksums.
Each chunk file contains uncompressed cooked GPU data without a DDS or PNG header,
ordered from the largest mip to the smallest. The last chunk can contain multiple
small mips. The descriptor and hash have to match the cooked texture resource;
copy name, hash, dimensions, mip and slice counts, alignment, and texture type
fields from the tooling that produced that resource. Do not guess or reuse a
descriptor from another texture.
Notes
- Chunk paths are resolved relative to the manifest. Absolute paths are accepted as well.
- Each texture needs between 1 and 256 chunks, and no more chunks than mips.
- Texture names have to be unique, relative and ISO-8859-1 encodable. Dimensions and slice counts have to be nonzero.
is_cube,unk1andtime_stampare optional and default to zero. Unknown fields are rejected.
When two mods supply the same file, choose which version should win. Inputs are ordered from highest to lowest priority; the first mod wins unless a per-file choice overrides it. For a full merged copy of their content:
w3edit merge \
"$W3_DIR/mods/modHighPriority" \
"$W3_DIR/mods/modLowPriority" \
--output build/modMerged \
--format remasteredInputs may be mod roots with a content/ directory, or content directories
directly. The output above is a mod root with files under build/modMerged/content/.
Bundled files are rebuilt into blob0.bundle and metadata.store with
fresh offsets and the winning files' metadata. All indexed bundles and additional
.bundle files under the content directory are included. Texture caches are
rebuilt by texture name, and loose files (including scripts) remain loose.
Script-only mods and bundles without an index are supported.
To create only an override layer for conflicting paths, leaving unique files in their original mods:
w3edit merge \
"$W3_DIR/mods/modHighPriority" \
"$W3_DIR/mods/modLowPriority" \
--output build/modConflictOverrides \
--conflicts-only \
--prefer 'scripts/game/player.ws=2' \
--format remastered--prefer PATH=INPUT picks a specific file from a numbered input (starting at 1),
overriding the default order for that path only. Repeat it for other files or
textures. Paths are relative to content/ for loose files, or depot paths for
bundled files and textures. Unknown paths, invalid input numbers, and duplicate
choices are rejected before writing.
A conflict means the same path occurs in at least two input mods, even when its
bytes are identical. Repeated entries within one mod do not count. Path matching
ignores ASCII case and treats / and \ as equivalent. Bundled files, cached
textures, and loose files are selected independently within their own groups.
Install modConflictOverrides under the game's mods/ directory and give it
higher priority than all source mods in your mod manager or mods.settings.
Keep the originals enabled: conflict-only output does not contain their unique
files. For a full merge, the output replaces the source mods' content; preserve
any required DLC, settings, or installation steps outside that content yourself.
The tool does not change game load order or modify its inputs.
Notes
- Merging picks whole files. It does not combine edits within scripts, XML, or cooked assets. Use a script/text merger when both mods' edits are needed.
- Only
content/is read when passing a mod root; files outside it are not included. Other cache formats are treated as opaque loose files and are not combined. --conflicts-onlyrequires a new or empty outputcontent/directory, even with--force, to avoid retaining stale files. No conflicts produce a mod root with an emptycontent/. Empty bundle/metadata pairs and empty conflict-only caches are omitted.- Output must not overlap any input directory. Existing outputs are protected
unless
--forceis used; this does not bypass the conflict-only empty-directory rule. - Files without source metadata use bundle-derived fields. Without
--format, bundle and metadata output use the newest input bundle format. - Without
--format, the texture cache format is selected independently of the bundle and metadata format. An explicit--formatapplies to all three, but doesn't convert cooked assets. --bundle-namechanges the name of the output bundle. It has to be a single ISO-8859-1 filename not ending in a dot or space, and can't bemetadata.storeortexture.cache.
To target a mod's content at another game format, pass its root on both sides. Directory conversion uses the single-mod merge path: it combines its bundles, rebuilds metadata, preserves source metadata fields, converts its texture cache, and copies loose files:
w3edit convert \
"$W3_DIR/mods/modLegacy" \
build/modLegacy-remastered \
--format remasteredConverting upgrades or downgrades bundles, metadata indexes and texture caches between Legacy and Remastered layouts. The file type is detected from its contents.
convert also accepts an individual container. Use an explicit output filename,
or a mod root to write the default filename for that container type. For an
explicit file, its output parent directory must already exist:
mkdir -p build/converted/
w3edit convert \
"$W3_DIR/mods/modExample/content/blob0.bundle" \
build/converted/blob0.bundle \
--format remasteredOnly the container layout is converted. Compressed payloads are carried over as they are, and cooked resources, scripts and textures are left untouched, so a converted container is not automatically compatible with the other game version.
An individually converted metadata.store keeps its recorded bundle offsets.
After converting individual bundles, recreate their metadata with metadata create.
Directory conversion does this rebuilding automatically.
Notes
- Fields that don't exist in the target layout are dropped, such as legacy bundle timestamps and metadata burst sizes when upgrading.
- Downgrading fails when values exceed the legacy limits instead of truncating them.
- Bundles are always rebuilt, even when converting to the same format.
w3edit loads containers into memory instead of streaming them, and merging keeps both the inputs and the merged output in memory. Individual bundle items are limited to less than 4 GiB, also in version 5 bundles.
[dependencies]
w3edit = { git = "https://github.com/Systemcluster/w3edit.git", default-features = false }w3edit can be used as a library with the same functionality as the CLI. Disabling the default features removes the CLI dependencies.
use w3edit::{Bundle, BundleCompression, BundleFormat, BundleItem, Metadata, TextureCache};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let format = BundleFormat::Remastered;
let item = BundleItem::new(c"example.bin".into(), b"cooked data", BundleCompression::Zlib)?;
let bundle = Bundle::from_items(format, vec![item]);
let store = Metadata::from_bundles(format, &[(c"blob0.bundle", &bundle)])?;
let bundle_bytes = bundle.try_write()?;
let store_bytes = store.try_write()?;
let cache_bytes = TextureCache::for_format(format).write();
println!(
"Serialized {} bundle bytes, {} metadata bytes, {} cache bytes",
bundle_bytes.len(), store_bytes.len(), cache_bytes.len()
);
Ok(())
}This creates a bundle with a placeholder payload, its metadata and an empty texture cache in memory. Other entry points include:
merge_modsandmerge_mods_for_formatmerge complete mods with automatic or explicit format selection.Bundle::mergeandMetadata::mergeon their own don't update the cross-references between each other.merge_mods_with_optionsacceptsMergeOptionswithconflicts_only, optionalformat, andpreferred_sourcesmapping depot paths to zero-based mod indices. The library operates on containers; loose-file handling is in the CLI.Metadata::from_bundle_filesindexes parsed bundles that won't be rewritten.TextureCache::insertadds texture descriptors and cooked texture chunks.convertconverts a container between formats.
Notes
- Bundle item offsets are 64-bit.
compute_offsets()panics when an offset exceedsu32, usecompute_offsets_u64()for large version 5 bundles. write()panics on serialization errors, such as values exceeding the legacy limits. Usetry_write()to handle them instead.
cargo test
cargo test --no-default-featuresThe tests cover parsing, creating, converting and merging all supported containers
as well as the CLI, using the fixtures in test/assets and generated
Legacy and Remastered data.
Additional tests against an installed copy of the game are ignored by default.
To run them, set W3_GAME_DIR to a Remastered installation:
W3_GAME_DIR='/path/to/The Witcher 3' cargo test --test bundle installed_remastered_bundles -- --ignored --nocapture
W3_GAME_DIR='/path/to/The Witcher 3' cargo test --test metadata installed_remastered_metadata_roundtrip -- --ignored
W3_GAME_DIR='/path/to/The Witcher 3' cargo test --test texture_cache installed_remastered_texture_creation -- --ignoredThese tests only read the game files, but they shouldn't be modified or updated while the tests are running. They check all installed bundles, round-trip the metadata index and rebuild a sample of cached textures.
BSD-2-Clause. See LICENSE.
Thanks to the team behind WolvenKit for specifications of some of the file format layouts.