Deadlock Research

Deadlock

Reverse-engineering the data formats of Deadlock (Valve, Source 2 — internal codename Citadel), with the goal of extracting game data — heroes, purchasable items, abilities — into usable structured form.

Nothing here is documented by Valve. Everything we know is derived from a specific installed build and re-verified when the game patches.

Start here

git clone https://github.com/coolhome/Deadlock && cd Deadlock
python tools/install_hooks.py   # one-time: enables the docs site build hook
python tools/find_game.py       # locate your install, print its build stamp
python tools/vpk_list.py        # see what's in the archives

Then read docs/research/ — five notes covering archive layout, where gameplay data lives, the decompilation toolchain, localization, and prior art.

toolwhat it does
find_game.pyLocate the install across Windows/Linux/macOS Steam libraries; print the build stamp
vpkdb.pyIndex every VPK into SQLite; search, inspect, extract, diff across patches
vpk_list.pyOne-shot VPK parser and raw entry reader — the layer vpkdb.py is built on
kv3.pyParse KV3 text; resolve _multibase inheritance; as_bool for its three boolean spellings
kv1.pyParse KeyValues v1 (.txt/.res/.vdf); resolve platform conditionals; sniff KV1 vs KV3
serialize.pyDependency-free YAML emitter for the parsers' output
decompile.pyFetch and drive the Source 2 Viewer CLI into out/
build_site.pyRender the Markdown and the explorer into site/
build_explorer.pyGenerate the browsable file explorer from the index
build_content.pyGenerate the file contents the explorer renders
fetch_openapi.pySnapshot the community APIs' OpenAPI specs into tools/data/openapi/
build_api.pyRender those snapshots into the static /api/ reference
install_hooks.pyPoint core.hooksPath at tools/hooks/

Everything is stdlib-only Python 3.10+. No dependencies, no virtualenv, no setup step.

Exploring the game files

tools/vpkdb.py build indexes all 21 archives — 134,166 entries, 28.2 GiB — into a single SQLite file in about three seconds. After that, exploring is queries rather than re-parsing:

python tools/vpkdb.py build
python tools/vpkdb.py sniff                               # identify formats from headers
python tools/vpkdb.py find 'scripts/*.vdata_c' --sort size
python tools/vpkdb.py show scripts/heroes.vdata_c
python tools/vpkdb.py cat scripts/items/items_game.txt    # raw bytes, no decompiler
python tools/vpkdb.py stats --by dir
python tools/vpkdb.py diff db/build-6600.db               # what a patch changed

sniff reads the leading bytes of all 134,166 entries in ~12s and records what each file actually is — 133,657 are Source 2 resources — plus each resource's version and its block tags, which say what it contains. PHYS appears only on models, SrMa only on stylesheets. See note 0006.

Everything takes --json, and sql is a read-only escape hatch for anything the subcommands miss:

python tools/vpkdb.py sql "SELECT ext, count(*) n FROM file GROUP BY ext ORDER BY n DESC LIMIT 5"

Once you have decompiled output (see note 0003), index-text adds full-text search over KV3 entries:

python tools/vpkdb.py index-text out/
python tools/vpkdb.py search "EItemSlotType_Tech EModTier_4"

The database lives in db/ and is gitignored — it is derived data, and it rebuilds in seconds. Because every entry carries its CRC32, diff between two builds gives an exact list of what a patch added, removed, or changed.

Browsing it in a browser

deadlock.xerodefect.ai/explorer/ is a file explorer over the same index — directory tree, virtualised listing of all 134,166 entries, search, and type/mount/block filters. Selecting a file shows its detected format, resource version, block tags, and the vpkdb.py command to extract it locally. Clicking a block chip filters the listing to every resource carrying that block.

It is generated by tools/build_explorer.py as part of build_site.py, so vpkdb.py build then build_site.py refreshes it. If the index has not been built, the page renders a placeholder instead of breaking the nav.

Search notes: a leading . matches extensions only (.vdata_c), a / makes the query match against the full path, and search is always global rather than scoped to the open directory.

deadlock.xerodefect.ai/gallery/ is the extracted data made visible: a hero selector (each hero's four abilities with icons, cooldowns and descriptions) and a shop-style item viewer (195 named items by slot and tier, with costs, stats, descriptions and build paths). Everything is resolved at build time by tools/build_gallery.py — inheritance flattened, localization joined, description templates substituted, small icons inlined — so the page's JavaScript only renders. Toggles reveal the 21 unreleased heroes and 22 disabled items hidden by default. Detail views render through the vendored ability-card design kit (tools/assets/); card art always comes from the payload's resolved icon field, never a raw game file path.

The API reference

deadlock.xerodefect.ai/api/ is a static reference for the programmatic surface around the game — the community-run deadlock-api.com family surveyed in note 0011 (match history, analytics, leaderboards, parsed game data; 117 endpoints in 31 groups). Not a Valve API — the pages carry that warning visibly, and it matters: Valve throttled the whole community layer offline for weeks in 2024.

It renders entirely from a committed snapshot of each service's OpenAPI spec (tools/data/openapi/, MIT-licensed upstream), so the site build stays offline and deterministic — no Swagger UI, no CDN, no external assets. Each endpoint shows its parameters, response shape, documented rate limits and a copyable curl line.

Refreshing after the upstream API changes:

python tools/fetch_openapi.py    # one polite GET per service
python tools/build_site.py       # re-render site/api/
git diff tools/data/openapi/     # review what actually changed
python tools/test_api.py         # rendered pages still match the snapshot

If no snapshot is present the page degrades to a placeholder, like the explorer without an index. Invariants live in tools/test_api.py: endpoint counts are asserted against the snapshot itself, so a refresh cannot silently disagree with the rendered pages.

Reading file contents

Selecting a file with renderable content gets a View button:

Content is generated by build_content.py and fetched only when a file is opened, so the initial page load stays at ~1.1 MB. Producing it needs a decompiled tree:

python tools/decompile.py --fetch    # once: downloads the Source 2 Viewer CLI
python tools/decompile.py            # all 99 vdata_c, both mounts, into out/<mount>/

# Images, so the table can render them. vtex_c -> PNG, vsvg_c -> SVG.
python tools/decompile.py --mounts citadel --ext vtex_c --filter panorama/images/
python tools/decompile.py --mounts citadel --ext vsvg_c --filter panorama/images/

# Styles, to (re)derive the gallery's inline stat icons: the class -> sprite
# mapping in tools/build_gallery.py STAT_ICONS comes from the first stylesheet,
# the wash-color hex values from the second. Only needed when re-verifying the
# table after a patch -- the gallery build reads STAT_ICONS, not the CSS.
python tools/decompile.py --mounts citadel --ext vcss_c --filter panorama/styles/ability_property_icons
python tools/decompile.py --mounts citadel --ext vcss_c --filter panorama/styles/citadel_base_styles

# Layout evidence for the same table: the shop tooltip includes that stylesheet
# and reads each important stat's icon from a `prop_*` class -- the join item
# stat rows reuse (property m_strCSSClass -> prop_<css>).
python tools/decompile.py --mounts citadel --ext vxml_c --filter panorama/layout/tooltips/citadel_tooltip_mod
python tools/decompile.py --mounts citadel --ext vcss_c --filter panorama/styles/tooltips/citadel_tooltip_mod

python tools/build_site.py           # picks up out/ automatically

The gallery also depends on the two panorama/images/ commands above: description templates embed AbilityPropertyIcon panels, item stat rows key icons off their property's m_strCSSClass, and build_gallery.py inlines the mapped hud sprites (29 table entries, every file ≤ 8 KB at this build) as data URIs — per occurrence in descriptions, and once per class in the payload's statIcons map for stat rows. If they are missing from out/, the build says so and the affected icons drop — test_gallery.py pins the rendered counts, so the gap fails loudly.

Any value that is an image reference renders as the image — in the table and inline in the entry view — with hover to enlarge. References take two forms and compile differently:

referencecompileddecompiled
file://{images}/x.psdpanorama/images/x_psd.vtex_c.png
file://{images}/x.svgpanorama/images/x.vsvg_c.svg

Icons at or below 8 KB are inlined into the document as data URIs — 345 of them in abilities.vdata — so scrolling the table fires no image requests at all. Larger art is fetched through a throttled queue (4 at a time) that retries with backoff and shows a placeholder meanwhile.

Only images the data actually references are published — 982 files, 36.6 MB, covering 985 references (the 3 misses are art for an unreleased hero that isn't in the archives). That filter is load-bearing: panorama/images/heroes/backgrounds/ alone is 126 MB of art nothing points at. To publish less, decompile fewer directories — everything downstream adapts, and unresolved references simply render as text.

Without out/, the site still builds — the explorer just offers no KV3 view.

decompile.py reads every mount (citadel and core) with no path filter, because gameplay vdata also lives outside scripts/ — under stats/, soundstacks/ and the archive root. Output is per mount since the same path exists in more than one with different contents.

No JavaScript dependencies — the tree, virtual scrolling, and search are hand-written, so the site stays self-contained and works offline. Regression tests for that logic live in tools/test_explorer.js and need Node (the build does not):

node tools/test_explorer.js

What we know so far

Verified against build 6679 (Aug 14 2026):

Repository layout

docs/research/*.md    numbered research notes + the pattern that produces them
tools/                stdlib-only Python helpers
tools/hooks/          version-controlled git hooks
AGENTS.md             conventions for automated contributors

site/   (gitignored)  rendered site, uploaded to Azure Static Web Apps
db/     (gitignored)  SQLite index of the game files, rebuilt by tools/vpkdb.py

Documentation site

The site is served by Azure Static Web Apps (Free plan) at https://deadlock.xerodefect.ai — nothing billable, no CI: the build needs the extracted game data that never leaves this machine, so publishing happens locally. See infra/README.md for the Azure setup (Bicep, deployed resources, DNS). The old GitHub Pages copy at coolhome.github.io/Deadlock is frozen at its last gh-pages publish and no longer updated.

main tracks Markdown only. tools/build_site.py renders it into site/ (gitignored), and tools/deploy_site.py uploads that to the Static Web App with the SWA CLI, fetching the deployment token at run time from the logged-in az session (nothing stored). Generated HTML never lands on main.

python tools/install_hooks.py           # one-time, per clone
python tools/build_site.py              # render to site/
python tools/deploy_site.py --dry-run   # build only, report what would ship
python tools/deploy_site.py             # build and upload to Azure

Two hooks keep it automatic:

hookdoes
pre-commitFails the commit if the Markdown no longer renders. Stages nothing.
pre-pushOn a main push, builds and uploads to Azure Static Web Apps.

Edit only the Markdown. Bypass either with --no-verify.

Every page footer shows when the site was built, rendered in your local timezone (the markup ships UTC and a few lines of JS localise it on load), and the explorer shows the same in its header with UTC in the tooltip. --check ignores the timestamp, so it reports staleness only when content actually differs.

Contributing

Research follows a fixed loop — ask, verify, write, stamp, re-verify — documented in docs/research/README.md. The short version:

Automated contributors: see AGENTS.md.

Licence

See LICENSE. This project contains no Valve assets — only scripts and findings. Deadlock and Source 2 are trademarks of Valve Corporation.