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
- They are a mix. Of the 82 loose text entries, 35 are KeyValues v1, 26 are KV3 text, and 21 are neither (Lua, CSS, console
cfg, positional tables). Extension is no guide:.txtcovers all three. - KV1 is the older
"key" "value"/"key" { … }format with//comments.tools/kv1.pyparses it; all 35 parse without error. - Platform conditionals are the trap. 58
[$X360]and 55[$WIN32]suffixes appear. Ignoring them merges Xbox 360 values over Windows ones. Deadlock is a Windows game, so the parser resolvesWIN32by default. - Conditionals are expressions, not simple tags:
[!$OSX],[0 && $X360],[($X360WIDE && $X360HIDEF)]and its negation all occur. - Duplicate keys are legal and used. Repeats collapse into a list, so nothing is lost.
- Format detection must be structural, not statistical — see Gotchas.
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
| expression | count |
|---|---|
[$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
| format | files | examples |
|---|---|---|
| KeyValues v1 | 35 | items_game.txt, clientscheme.res, game.gameevents |
| KV3 text | 26 | collision_properties.txt, cfg/valve_builds.kv3 |
| not structured | 21 | *.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
- 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. - 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. - Braces alone do not mean KV3.
core/scripts/dsp_presets.txtis a positional table wearing braces:{ 0 "core.null" LINEAR 0.2 … }. A KV3 root map containskey = value, so require an assignment before believing the braces. - Never ignore conditionals. Without them,
[$X360]values silently overwrite[$WIN32]ones, because the X360 line usually comes second. - A simple
\[\$NAME\]regex is not enough. It leaves[0 && $X360]to be parsed as stray tokens, which derails the rest of the document. - Duplicate keys are normal. A dict that overwrites on repeat loses data.
- Extension tells you nothing.
.txtfiles are KV1, KV3 and Lua alike.
Open questions
#baseand#includedirectives are part of KeyValues but appear in no file in this build, so they are unimplemented.- Escaped-string mode is not implemented; no shipped file needs it, but a future file containing
\"would parse wrongly rather than fail loudly. - The
WIN32default was chosen deliberately, but nothing verifies that the game itself resolves these conditionals the same way. .vdffiles were not encountered; the format is the same but that is untested here.- Whether
||appears in other Source 2 titles' KeyValues is unknown.
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.