Skip to content

Repository files navigation

Valve Key Value for .NET

Build StatusNuGetCode Coverage

KeyValues is a simple key-value pair format used by Valve in Steam and the Source engine for configuration files, game data, and more (.vdf, .res, .acf, etc.). This library aims to be fully compatible with Valve's various implementations of KeyValues format parsing (believe us, it's not consistent).

Core Type

The library is built around a single type:

  • KVObject (class) -- a value node. Can be a scalar (string, int, float, bool, etc.), a binary blob, an array, or a named collection of children. Keys (names) are stored in the parent container, not on the child -- similar to how JSON works. Implements IReadOnlyDictionary<string, KVObject> and IConvertible.
  • KVDocument (class) -- a deserialized document containing a Root KVObject, a root key Name, and an optional Header. Has a read-only string indexer that delegates to Root, and an implicit conversion to KVObject.

All types are shared across KV1 and KV3 -- you can deserialize from one format and serialize to another. However, not all value types are supported by all formats:

FeatureKV1 TextKV1 BinaryKV3 Text
CollectionsYes (list-backed, allows duplicate keys)Yes (list-backed)Yes (dict-backed, O(1) lookup)
ArraysEmulated as objects with numeric keysNo (throws)Yes (native)
Binary blobsNoNo (throws)Yes (native)
ScalarsYesYesYes
FlagsNoNoYes

When constructing objects programmatically, use KVObject.Collection() (dict-backed) for general use and KV3 output, or KVObject.ListCollection() (list-backed) when you need duplicate keys or KV1 compatibility. Deserialization picks the appropriate backing store automatically.

KVObject

Constructing

// Scalar values (typed constructors)varobj=newKVObject("hello");// stringvarobj=newKVObject(42);// intvarobj=newKVObject(3.14f);// floatvarobj=newKVObject(true);// bool// Implicit conversion from primitivesKVObjectobj="hello";KVObjectobj=42;// Dictionary-backed collection (O(1) lookup, no duplicate keys)varobj=KVObject.Collection();// emptyvarobj=newKVObject();// same as above// List-backed collection (preserves insertion order, allows duplicate keys, for KV1)varobj=KVObject.ListCollection();// empty// Build up childrenvarobj=newKVObject();obj["name"]="Dota 2";// implicit string -> KVObjectobj["appid"]=570;// implicit int -> KVObject// Arrayvararr=KVObject.Array();// emptyvararr=KVObject.Array([newKVObject("a"),newKVObject("b")]);// from elements// Binary blobvarblob=KVObject.Blob(newbyte[]{0x01,0x02,0x03});// Null valuevarnul=KVObject.Null();

Reading values

KVDocumentdata=kv.Deserialize(stream);// Root key name (only on KVDocument)string?rootName=data.Name;// String indexer returns KVObject (supports chaining)stringname=(string)data["config"]["name"];intversion=(int)data["version"];floatscale=(float)data["scale"];boolenabled=(bool)data["settings"]["enabled"];// Array elements by indexfloatx=(float)data["position"][0];// Access the root KVObject for full API (mutations, ContainsKey, etc.)KVObjectroot=data.Root;// Check existence (on the root KVObject)if(data.Root.ContainsKey("optional")){ ...}if(data.Root.TryGetValue("optional",outvarchild)){ ...}// Indexer throws KeyNotFoundException for missing keys// Use TryGetValue for safe access// Direct access to value properties (on KVObject)KVValueTypetype=data.Root.ValueType;KVFlagflag=data["texture"].Flag;byte[]bytes=data["blob"].AsBlob();

Modifying

// Mutations require the Root KVObject (KVDocument indexer is read-only)data.Root["name"]="new name";data.Root["count"]=42;// Chained writes work (reference semantics, first lookup goes through KVDocument indexer)data["config"]["resolution"]="1920x1080";// Add children to collectionsdata.Root.Add("newprop",42);// implicit int -> KVObjectdata.Root.Add("text","value");// implicit string -> KVObject// Add elements to arraysarr.Add(3.14f);// implicit float -> KVObject// Removedata.Root.Remove("deprecated");arr.RemoveAt(2);data.Root.Clear();// Set flags directlydata["texture"].Flag=KVFlag.Resource;

Enumerating

// KVObject implements IReadOnlyDictionary<string, KVObject>// Keys are the child names, values are the child KVObjectsforeach(var(key,child)indata.Root){Console.WriteLine($"{key} = {(string)child}");}// Keys and Values propertiesvarkeys=data.Root.Keys;// IEnumerable<string>varvalues=data.Root.Values;// IEnumerable<KVObject>// Array elements have null keysforeach(var(key,element)inarrayObj){// key is null for array elementsConsole.WriteLine((string)element);}// Values on arrays returns elements directly (no KVP wrapper)foreach(varelementinarrayObj.Values){Console.WriteLine((string)element);}// Scalars yield nothingforeach(varchildinscalarObj){}// empty

KeyValues1

Used by Steam and the Source engine.

Deserializing text

Basic deserialization

varstream=File.OpenRead("file.vdf");// or any other Streamvarkv=KVSerializer.Create(KVSerializationFormat.KeyValues1Text);KVDocumentdata=kv.Deserialize(stream);Console.WriteLine(data["some key"]);

Typed deserialization

publicclassSimpleObject{publicstringName{get;set;}publicstringValue{get;set;}}varstream=File.OpenRead("file.vdf");// or any other Streamvarkv=KVSerializer.Create(KVSerializationFormat.KeyValues1Text);SimpleObjectdata=kv.Deserialize<SimpleObject>(stream);

Options

The Deserialize method also accepts a KVSerializerOptions object.

By default, operating system specific conditionals are enabled based on the OS the code is running on (RuntimeInformation).

KVSerializerOptions has the following options:

  • Conditions - List of conditions to use to match conditional values.
  • HasEscapeSequences - Whether the parser should translate escape sequences (e.g. \n, \t).
  • EnableValveNullByteBugBehavior - Whether invalid escape sequences should truncate strings rather than throwing an InvalidDataException.
  • FileLoader - Provider for referenced files with #include or #base directives.
  • SkipHeader - Whether to skip writing the KV3 header comment during serialization.
varoptions=newKVSerializerOptions{HasEscapeSequences=true,};options.Conditions.Clear();// Remove default conditionals set by the libraryoptions.Conditions.Add("X360WIDE");varstream=File.OpenRead("file.vdf");varkv=KVSerializer.Create(KVSerializationFormat.KeyValues1Text);vardata=kv.Deserialize(stream,options);

Deserializing binary

Essentially the same as text, just change KeyValues1Text to KeyValues1Binary.

Serializing to text

Dynamic serialization

varroot=KVObject.ListCollection();root.Add("Developer","Valve Software");root.Add("Name","Dota 2");vardoc=newKVDocument(null,"root object name",root);usingvarstream=File.OpenWrite("file.vdf");varkv=KVSerializer.Create(KVSerializationFormat.KeyValues1Text);kv.Serialize(stream,doc);

Typed serialization

classDataObject{publicstringName{get;set;}publicstringDeveloper{get;set;}[KVProperty("description")]publicstringSummary{get;set;}[KVIgnore]publicstringExtraData{get;set;}}vardata=newDataObject{Developer="Valve Software",Name="Dota 2",Summary="Dota 2 is a complex game.",ExtraData="This will not be serialized."};usingvarstream=File.OpenWrite("file.vdf");varkv=KVSerializer.Create(KVSerializationFormat.KeyValues1Text);kv.Serialize(stream,data,"root object name");

Serializing to binary

Essentially the same as text, just change KeyValues1Text to KeyValues1Binary.

KeyValues2 (Datamodel)

This library does not currently support KeyValues2 (Datamodel). If you need KV2/Datamodel support, use our fork of Datamodel.NET instead.

KeyValues3

Used by the Source 2 engine.

Deserializing text

varstream=File.OpenRead("file.kv3");// or any other Streamvarkv=KVSerializer.Create(KVSerializationFormat.KeyValues3Text);KVDocumentdata=kv.Deserialize(stream);Console.WriteLine(data["some key"]);

Serializing to text

usingvarstream=File.OpenWrite("file.kv3");varkv=KVSerializer.Create(KVSerializationFormat.KeyValues3Text);kv.Serialize(stream,data);

About

📃 Next-generation Valve's key value framework for .NET

Topics

Resources

Stars

192 stars

Watchers

6 watching

Forks

Releases

Used by

Contributors

Languages