Deadlock Research
verifiednote 0002build 66792026-08-15vdatakv3heroesitemsabilities

Gameplay data in vdata files

Question

Where do heroes, abilities, and purchasable items live, and what is the shape of that data once decompiled?

Summary

Findings

The files that matter

pathpackedwhat
scripts/abilities.vdata_c484.6 KBabilities and items
scripts/heroes.vdata_c83.7 KBhero definitions
scripts/npc_units.vdata_c28.7 KBtroopers, guardians, bosses
scripts/misc.vdata_c20.9 KBassorted gameplay constants
scripts/generic_data.vdata_c11.7 KBglobal constants, price table
scripts/modifiers.vdata_c10.5 KBmodifier definitions
scripts/scale_functions.vdata_c1.1 KBstat scaling function defs
scripts/loot_tables.vdata_c4.5 KBloot
scripts/ranked_seasons.vdata_c1.8 KBranked season config
stats/tracked_stats_*.vdata_c~1.5 KB ea.tracked match/player/team stats

Also present: scripts/tarot/*.vdata_c, scripts/bots/bot_difficulty.vdata_c, 48 scripts/tagged_sounds/<hero>_anim_sounds.vdata_c, and ~110 .vrr_c response-rule files under scripts/talker/ (voice line logic, one per hero).

The full list is 77 .vdata_c under scripts/ plus 6 under stats/, 1 soundstacks/, and 1 at the archive root.

Decompiled shape (KV3)

heroes.vdata opens with:

<!-- kv3 encoding:text:version{e21c7f3c-...} format:generic:version{7412167c-...} -->
{
	generic_data_type = "CitadelHeroData_t"
	hero_base =
	{
		_class = "CitadelHeroData_t"
		m_HeroID = 0
		m_strModelName = resource_name:"models/heroes_staging/gen_man/gen_man.vmdl"
		m_mapStartingStats =
		{
			EMaxHealth = 780.0
			EMaxMoveSpeed = 7.2
			ESprintSpeed = 1.6
			EStamina = 3
			EBaseHealthRegen = 2.0
			...
		}

Top-level keys are entry codenames (hero_base, hero_inferno, weapon_upgrade_t1, …).

Ability and item taxonomy

Counts of m_eAbilityType in abilities.vdata:

valuecount
EAbilityType_Item277
EAbilityType_Signature241
EAbilityType_Weapon88
EAbilityType_Ultimate78
EAbilityType_Melee60
EAbilityType_Innate40
EAbilityType_Cosmetic3

Items are further split by m_eItemSlotTypeWeaponMod 97, Armor 93, Tech 85, Invalid 3 — and by m_iItemTierEModTier_1 42, _2 72, _3 72, _4 64, _5 27.

Of the 277 EAbilityType_Item entries, 84 are disabled, leaving 193 live items:

slotlive entries
Armor67
WeaponMod62
Tech62
no slot type (base entries)2

Corrected 2026-08-15. This section previously reported 73 disabled and 204 live (Armor 75 / Tech 65 / WeaponMod 62). Those numbers came from the text search body LIKE '%m_bDisabled = true%', which silently misses 11 entries — see Gotcha 7. The figures above come from tools/kv3.py, which parses the document.

m_bDisabled is spelled three ways within this one file: the boolean true (73 entries), the string "true" (6), and the integer 1 (5). The negative cases vary too — false (20 entries) and "false" (1). Entries missed by a = true text search include upgrade_rocket_boots, upgrade_rebirth, upgrade_camouflage and upgrade_disarm.

None of the three EItemSlotType_Invalid entries is an item, so the disabled flag is the only filter that matters here.

Inferred: ~193 purchasable items in the live game, ~191 excluding the two base entries. Still an inference: "not disabled" is not proven to mean "purchasable in a match", and this was not checked against the in-game shop.

Annotated 2026-08-15. The 193 figure counts inheritance scaffolding as items. Of the 277 typed entries, 2 are slotless bases (upgrade_base, ability_item_pickup_effects) and 80 have no localization name — 18 live tier-scaffolding entries ({armor,tech,weapon}_upgrade_{base,t1..t5}) plus 62 disabled test items. The named-shop-item census is therefore 195 named, 173 live (193 live = 173 named + 18 unnamed scaffolding + 2 slotless). Likewise, 5 of the 43 m_bPlayerSelectable heroes are also m_bDisabled (hero_boho, hero_fortuna, hero_graf, hero_skyrunner, hero_swan), so the shipping roster is 38, not 43. Both derivations are pinned in tools/test_gallery.py and reverified against the current build's site/gallery/data/gallery.json and the vdata.

Tier maps to cost via generic_data.vdata:

m_nItemPricePerTier =
[
	0, 800, 1600, 3200,
	6400, 9999,
]

Index 0 is a placeholder; EModTier_1 → 800 souls, and so on. The trailing 9999 is suspicious and unconfirmed — see Open Questions.

An item entry carries a stat map keyed by property name:

weapon_upgrade_t1 =
{
	_class = "citadel_item"
	m_mapAbilityProperties =
	{
		AbilityCooldown =
		{
			m_strValue = "0"
			m_strCSSClass = "cooldown"
			m_subclassScaleFunction = subclass: { _class = "scale_function_single_stat" ... }
		}
	}
	_multibase = [ "upgrade_base", ]
	m_eAbilityType = "EAbilityType_Item"
	m_eItemSlotType = "EItemSlotType_WeaponMod"
	m_nUpgradeSlotCost = 1
}

Reproduce

Decompile per note 0003, then:

cd out/scripts
grep -o 'm_eAbilityType = "[^"]*"'  abilities.vdata | sort | uniq -c
grep -o 'm_eItemSlotType = "[^"]*"' abilities.vdata | sort | uniq -c
grep -o 'm_iItemTier = "[^"]*"'     abilities.vdata | sort | uniq -c
grep -c  'm_HeroID = '              heroes.vdata
grep -A6 'm_nItemPricePerTier'      generic_data.vdata

Listing the source files without decompiling:

python tools/vpk_list.py --prefix scripts/ --ext vdata_c --list

The item counts, parsed rather than grepped — this is the authoritative form, and the grep/LIKE variants above are only safe for fields that are consistently typed:

python tools/decompile.py --fetch      # once, to populate out/
python -c "
import sys; sys.path.insert(0, 'tools')
from kv3 import parse, as_bool
import collections
doc = parse(open('out/scripts/abilities.vdata', encoding='utf-8').read())
items = {k: v for k, v in doc.items()
         if isinstance(v, dict) and v.get('m_eAbilityType') == 'EAbilityType_Item'}
live = {k: v for k, v in items.items() if not as_bool(v.get('m_bDisabled'))}
print('items', len(items), 'disabled', len(items) - len(live), 'live', len(live))
print(collections.Counter(v.get('m_eItemSlotType', '(none)') for v in live.values()))"

Expected at build 6679: items 277 disabled 84 live 193.

Gotchas

  1. Inheritance is not resolved by the decompiler. Entries use _class and _multibase = ["upgrade_base"] to inherit from other top-level entries in the same file. A naive per-entry parse yields incomplete stats — fields present on the base are simply absent from the child. This is the single largest source of wrong numbers in third-party Deadlock datasets. kv3.flatten() resolves it; see note 0007 for the grammar.
  2. Stats are functions, not scalars. m_subclassScaleFunction encodes scaling by spirit/level. Reading m_strValue alone gives the base, not the in-game number.
  3. m_strValue is a string even when numeric ("0", not 0). Coerce carefully.
  4. _include is already flattened. abilities.vdata lists ~30 scripts/abilities/<hero>.vdata_inc includes, but those files are not in the VPK — the compiler merged them. Do not go looking for them.
  5. Unreleased content is present. Entries exist for heroes not in the live game. Filter on m_bPlayerSelectable / m_bDisabled / m_bInDevelopment before publishing anything as "the hero roster".
  6. A grep -c 'm_HeroID' of 60 is not the roster size. It counts hero_base (m_HeroID = 0), leaving 59; only 43 of those are m_bPlayerSelectable. The document also has a scalar top-level key (generic_data_type) that is not an entry at all, so "top-level keys" (61) is a third different number.
  7. Booleans are not consistently typed, and text searches get them wrong. Within abilities.vdata, m_bDisabled appears as a bool, as "true"/"false" strings, and as 0/1 integers. Grepping for m_bDisabled = true misses 11 disabled items; plain Python truthiness then misclassifies the string "false" as disabled. Use kv3.as_bool, and treat any boolean field in this data as multi-spelled.
  8. m_bDisabled prunes about a third of the item table. Publishing the raw 277 as "the item list" overstates it by 84.

Open questions

Annotated 2026-08-15. Partially resolved. Flattening is implemented as kv3.flatten (note 0007), and scale-function evaluation turned out to be fully machine-readable per property — class + coefficient (m_flStatScale) + stat (m_eSpecificStatScaleType), e.g. barrier = 325 + 1.8 × Spirit — documented in note 0009. The gallery still publishes base values only; nothing has been validated against the in-game UI.

Annotated 2026-08-15. Both read in note 0009: modifiers.vdata holds 80 mostly map/game-state entries, and scale_functions.vdata is effectively empty (181 chars, no entries).

Sources

Derived entirely from the local install at build 6679. No external sources used.