Reading client files (MPQ & CASC)¶
Every WoW client ships its data in an archive storage: MPQ archives with
their patch chains for pre-Warlords clients (< 6.0), CASC storage for
everything newer. wowlib wraps both (StormLib and CascLib) behind one gateway —
FileSystem — so the code that uses files never branches on the storage
technology. You say which client version you are opening; the version decides
the backend, the archive/patch chain order, and how files are addressed.
Files are identified by a FileKey — a path, a FileDataID (the numeric
identity CASC clients use), or both. On an MPQ client paths address everything;
on a CASC client the FileDataID does, and a listfile (the community
fileDataId;filepath CSV) lets you keep using paths there too.
Opening a client¶
#include <wowlib/fs/filesystem.hpp>
// Wrath of the Lich King 3.3.5a — an MPQ-era client.
auto fs = wowlib::fs::FileSystem::open({
.clientPath = "/games/World of Warcraft 3.3.5a",
.version = wowlib::versions::Wotlk,
});
if (!fs)
return report(fs.error()); // Result<T> everywhere — no exceptions
// Shadowlands 9.2.7 — a CASC client; the listfile enables path lookups.
auto retail = wowlib::fs::FileSystem::open({
.clientPath = "/games/World of Warcraft 9.2.7",
.version = wowlib::versions::Shadowlands,
.listfileCsv = "/data/listfile.csv",
});
import wowlib
# Wrath of the Lich King 3.3.5a — an MPQ-era client.
fs = wowlib.fs.FileSystem.open(wowlib.fs.FileSystemSettings(
client_path="/games/World of Warcraft 3.3.5a",
version=wowlib.versions.wotlk))
# Shadowlands 9.2.7 — a CASC client; the listfile enables path lookups.
retail = wowlib.fs.FileSystem.open(wowlib.fs.FileSystemSettings(
client_path="/games/World of Warcraft 9.2.7",
version=wowlib.versions.shadowlands,
listfile_csv="/data/listfile.csv"))
# FileSystem is a context manager if you prefer scoped lifetime:
with wowlib.fs.FileSystem.open(wowlib.fs.FileSystemSettings(
client_path="/games/World of Warcraft 3.3.5a",
version=wowlib.versions.wotlk)) as scoped:
...
using WoWLib;
using Fs = WoWLib.Filesystem;
using Versions = WoWLib.Versions;
// Wrath of the Lich King 3.3.5a — an MPQ-era client.
using var settings = new Fs.FileSystemSettings(
clientPath: "/games/World of Warcraft 3.3.5a",
version: Versions.Global.Wotlk,
locale: Locale.enUS,
projectDirectory: null,
listfileCsv: null,
customFdidStart: new FileDataId(),
cascProduct: "wow");
using var fs = Fs.FileSystem.Open(settings);
Detecting what is installed¶
Spelling the version out is fine when you know it. When you do not — and for
any Classic client, where the build number decides which engine's file
formats you get — read it off the installation instead. A CASC install
records its product code in .flavor.info beside Data/, and its exact
version in .build.info there or one directory up.
#include <wowlib/fs/client_install.hpp>
auto install = wowlib::fs::ClientInstall::detect(
"/games/World of Warcraft/_classic_era_");
if (!install)
return report(install.error());
// install->version 1.15.9.69109 (ClassicEra)
// install->cascProduct "wow_classic_era"
auto fs = wowlib::fs::FileSystem::open({.clientPath = install->path,
.version = install->version,
.cascProduct = install->cascProduct});
settings = wowlib.fs.FileSystemSettings.detect(
"/games/World of Warcraft/_classic_era_",
listfile_csv="/data/listfile.csv")
with wowlib.fs.FileSystem.open(settings) as fs:
print(fs.version) # 1.15.9.69109 (ClassicEra)
print(fs.version.format_lineage) # the retail engine its files follow
# Or just the facts, without building settings:
install = wowlib.fs.ClientInstall.detect("/games/World of Warcraft/_retail_")
install.version, install.casc_product
Detection is CASC-only: MPQ-era clients (< 6.0) record no such file, and
neither do bare repacks that ship only Data/. Both raise NotSupported —
construct their ClientVersion directly, which is unambiguous anyway, since
no Classic product shares those version numbers and build range.
If casc_product is left unset, FileSystem.open derives it from the
version's flavor (wow, wow_classic, wow_classic_era,
wow_anniversary). Set it explicitly for PTR, beta and one-off products —
or let detection supply the exact code the installation recorded.
Reading files¶
// By path (MPQ, or CASC through the listfile).
auto map = fs->readFile("DBFilesClient/Map.dbc");
// By FileDataID on a CASC client.
auto blp = retail->readFile(wowlib::FileDataID{189077});
// A FileKey can carry either half; resolve() fills in the other
// from the listfile.
auto key = retail->resolve(wowlib::FileKey{"Interface/FrameXML/UIParent.lua"});
# By path (MPQ, or CASC through the listfile).
map_bytes = fs.read_file("DBFilesClient\\Map.dbc")
# By FileDataID on a CASC client.
blp = retail.read_file(wowlib.FileDataID(189077))
# A FileKey can carry either half; resolve() fills in the other
# from the listfile.
key = retail.resolve(wowlib.FileKey("Interface/FrameXML/UIParent.lua"))
// By path (MPQ, or CASC through the listfile).
byte[] map = fs.ReadFile(@"DBFilesClient\Map.dbc");
// By FileDataID on a CASC client.
byte[] blp = retail.ReadFile(new FileDataId(189077));
// A FileKey can carry either half; Resolve() fills in the other
// from the listfile.
var key = retail.Resolve(new FileKey("Interface/FrameXML/UIParent.lua"));
exists() takes the same three shapes as read_file(). Paths are
case-insensitive and both slash directions work — they are canonicalized
before lookup.
The filesystem remembers what it opened: version (the full client version —
the anchor for version-agnostic code) and kind
(MPQ or CASC) are properties in every language.
Beyond reading¶
- Writing back: pass a
project_directoryin the settings and writes land there as loose files, overlaying the client — the standard modding workflow. Reads prefer the project directory over the archives. - Encrypted retail content: some CASC-era files are TACT-encrypted; import
community key lists with
import_keys()and the storage hands back decrypted bytes wherever a key matches. - Custom FileDataIDs: on CASC-era projects,
custom_fdid_startreserves a range for files your mod adds; the listfile records them.
The format entities all take the FileSystem + FileKey pair directly —
table.read(fs, key), wmo.read(fs, key) — so after this page you rarely
touch raw bytes yourself. See Filesystem for the full
surface.