Deadlock Research
verifiednote 0008build 66792026-08-16kv1keyvaluesparsertext

KeyValues v1, and telling it apart from KV3

Question

The archives carry loose text files alongside compiled resources. What format are they, and can they be converted into the same structured form as decompiled KV3?

Summary

Findings

Grammar

document := entry*
entry    := key condition? ( value | '{' entry* '}' ) condition?
key      := '"' text '"' | bare
value    := '"' text '"' | bare
comment  := '//' … end-of-line | '/*' … '*/'
condition:= '[' expression ']'

Bare (unquoted) tokens are legal for both keys and values. No shipped file contains a backslash inside a quoted value, so the parser treats \ literally — matching Valve's default non-escaped KeyValues mode.

Conditional expressions, by frequency

expressioncount
[$X360]58
[$WIN32]55
[!$OSX]7
[$OSX]6
[0 && $X360]3
[($X360WIDE && $X360HIDEF)]1
[!($X360WIDE && $X360HIDEF)]1

So a parser needs !, &&, parentheses and integer literals — 0 being a way to force an entry off. || is supported defensively but does not appear in this build.

Decision: WIN32 is the default platform. Deadlock ships on Windows, and the X360 entries are inherited from the shared Source scheme files rather than being meaningful here. parse_pairs() preserves every entry with its raw condition when that matters.

Duplicate keys

resource/game.gameevents wraps 101 events in one "gameevents" root block, and KV1 permits a key to repeat within a block. tools/kv1.py collapses repeats into a list:

parse('"e" "a" "e" "b"')   # -> {"e": ["a", "b"]}

KV1 has no list type of its own, so a list in the output always means the key appeared more than once. That is the one ambiguity the representation carries.

Format census of the loose text files

formatfilesexamples
KeyValues v135items_game.txt, clientscheme.res, game.gameevents
KV3 text26collision_properties.txt, cfg/valve_builds.kv3
not structured21*.lua, webkit.css, cfg, dsp_presets.txt

Note cfg/valve_builds.kv3 — a loose KV3 file with no _c suffix, parsed by the existing KV3 parser rather than this one.

Reproduce

python tools/vpkdb.py build && python tools/vpkdb.py sniff
python tools/build_site.py                 # writes the loose text files out
python tools/test_kv1.py                   # 36 tests, incl. the whole corpus

python tools/kv1.py site/explorer/data/content/text/citadel/scripts/items/items_game.txt.txt
python tools/kv1.py <file> --yaml
python tools/kv1.py <file> --sniff
python tools/kv1.py <file> --platform X360  # to see what WIN32 resolution discards

Census of the shipped loose files:

python -c "
import sys, collections, pathlib; sys.path.insert(0, 'tools')
from kv1 import sniff
base = pathlib.Path('site/explorer/data/content/text')
print(collections.Counter(
    sniff(p.read_text(encoding='utf-8', errors='replace'))
    for p in base.rglob('*.txt')))"

Expected at build 6679: Counter({'kv1': 35, 'kv3': 26, 'unknown': 21}).

Gotchas

  1. Detect format structurally, not statistically. Counting = signs classifies Lua as KV3, because table fields look like assignments. A KV3 document is a single root map so it opens with {; KV1 opens with a key.
  2. CSS looks like KV1. body{} is a valid bare root key followed by a block. prop: value; declaration syntax is the discriminator — no KeyValues file has it.
  3. Braces alone do not mean KV3. core/scripts/dsp_presets.txt is a positional table wearing braces: { 0 "core.null" LINEAR 0.2 … }. A KV3 root map contains key = value, so require an assignment before believing the braces.
  4. Never ignore conditionals. Without them, [$X360] values silently overwrite [$WIN32] ones, because the X360 line usually comes second.
  5. A simple \[\$NAME\] regex is not enough. It leaves [0 && $X360] to be parsed as stray tokens, which derails the rest of the document.
  6. Duplicate keys are normal. A dict that overwrites on repeat loses data.
  7. Extension tells you nothing. .txt files are KV1, KV3 and Lua alike.

Open questions

Sources

Derived from the loose text files of the local install at build 6679. No external format documentation was consulted; the grammar above is what the files actually contain.