ValveKeyValue

repository·master·Indexed 41 days ago

https://github.com/valveresourceformat/valvekeyvalue

.NET library for parsing and manipulating Valve's KeyValues format, supporting KV1 (text/binary) and KV3 (text) formats used in Steam and the Source engine. Features include serialization/deserialization via KVSerializer, mapping to typed C# objects, support for KV1 conditionals and includes, and KV3 flags.

Tokens
5.2K
Snippets
15
Records
18
Agent score
2%

What's inside ValveKeyValue

  1. Handle KV1 root names

    master

    KV1 files require a single named root object.

    • When reading: The name is stored in KVDocument.Name.
    • When writing: Pass a KVDocument (which contains the name) or pass a bare KVObject along with a string representing the root name.
    var root = KVObject.ListCollection();
    root.Add("Developer", "Valve Software");
    root.Add("Name", "Dota 2");
    
    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
    using var stream = File.OpenWrite("file.vdf");
    
    // Option 1: Serialize bare object with a root name
    kv.Serialize(stream, root, "root object name");
    
    // Option 2: Wrap in a KVDocument
    var doc = new KVDocument(null, "root object name", root);
    kv.Serialize(stream, doc);
  2. Understand KV3 structure and headers

    master

    KV3 (Source 2) text files start with a header comment containing encoding and format identifiers.

    Key differences from KV1:

    • KVDocument.Name is always null (no root key name).
    • KVDocument.Root can be any value (scalar, array, collection, etc.).
    • Collections are dictionary-backed; duplicate keys are not preserved.
    • KVDocument.Header contains the KVHeader (Encoding and Format).
    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text);
    using var stream = File.OpenRead("file.kv3");
    KVDocument data = kv.Deserialize(stream);
    
    if (data.Header is { } header)
    {
        Console.WriteLine(header.Encoding);
        Console.WriteLine(header.Format.Name);
    
        bool isGeneric = header.Format.Id == ValveKeyValue.KeyValues3.Format.Generic;
    }
  3. How KVObject and KVDocument work together

    master

    The library is built around two primary types:

    • KVObject: A single value node. It can be a scalar (string, int, float, bool, etc.), a binary blob, an array, or a named collection of children. It implements IReadOnlyDictionary<string, KVObject> and IConvertible. Keys are stored in the parent container.
    • KVDocument: The result of deserialization. It contains the Root (KVObject), the root key Name (null for KV3), and a Header (only for KV3). The KVDocument indexer is read-only and delegates to Root.

    Collection Types:

    • Use KVObject.Collection() for general use and KV3 output (dictionary-backed, O(1) lookup, no duplicate keys).
    • Use KVObject.ListCollection() for KV1 files or when you need duplicate keys (list-backed, preserves insertion order).
  4. Quick start with Valve KeyValue

    master

    To use ValveKeyValue, create a KVSerializer with the desired KVSerializationFormat, then use Deserialize to read from a stream and Serialize to write to a stream. The library supports KeyValues1Text, KeyValues1Binary, and KeyValues3Text.

    using ValveKeyValue;
    
    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
    
    // Read
    using var input = File.OpenRead("gameinfo.txt");
    KVDocument data = kv.Deserialize(input);
    
    string game = (string)data["game"];
    int appId = (int)data["FileSystem"]["SteamAppId"];
    
    // Modify
    data.Root["game"] = "My Mod";
    
    // Write
    using var output = File.OpenWrite("gameinfo_modified.txt");
    kv.Serialize(output, data);
  5. Handle KeyValue errors

    master

    Common exceptions thrown by the library:

    • KeyValueException: Thrown when input is malformed.
    • NotSupportedException: Thrown when converting a value to an incompatible type (e.g., casting a collection to an int).
    • InvalidOperationException: Thrown when calling collection or array methods on a scalar value, or when calling source map methods on binary formats.
  6. Generate Source Maps for text formats

    master

    For text formats (KV1/KV3), you can use DeserializeWithSourceMap or SerializeWithSourceMap to obtain a collection of KvSourceSpan objects. Each span provides the Start offset, End offset, and TokenType (e.g., key, string, brace, comment, flag) for syntax highlighting or editor integration.

    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text);
    
    // Parsing with source map
    string text = File.ReadAllText("file.kv3");
    var (document, spans) = kv.DeserializeWithSourceMap(text);
    
    foreach (var span in spans)
    {
        var token = text.AsSpan(span.Start, span.End - span.Start);
        Console.WriteLine($"{span.TokenType}: {token}");
    }
    
    // Serializing with source map
    var (output, outputSpans) = kv.SerializeWithSourceMap(document);
  7. Modifying KVObject data

    master

    Mutations must be performed on the Root KVObject of a KVDocument. The KVDocument indexer itself is read-only.

    Operations:

    • Assignment: data.Root["key"] = value; or data["path"]["to"]["key"] = value; (chained writes work via reference semantics).
    • Adding Children: data.Root.Add("key", value) (throws if key exists in dict-backed collections) or data.Root.TryAdd("key", value) (returns false instead of throwing).
    • Array Mutation: Use .Add(value) or .RemoveAt(index) on KVObject instances that are arrays.
    • Removal: data.Root.Remove("key") (removes all entries with that key in list-backed collections) or arr.RemoveAt(0).
    • Flags: Set directly via data["key"].Flag = KVFlag.Resource;.
    // Mutations via Root
    data.Root["name"] = "new name";
    
    // Chained writes
    data["config"]["resolution"] = "1920x1080";
    
    // Adding/Removing
    data.Root.Add("newprop", 42);
    data.Root.TryAdd("text", "other");
    data.Root.Remove("deprecated");
    
    // Array mutation
    var arr = KVObject.Array();
    arr.Add(3.14f);
    arr.RemoveAt(0);
  8. Map KeyValues to typed C# objects

    master

    You can map KeyValues directly to custom classes using generic Deserialize<T> and Serialize methods. Properties are matched by name. Use attributes to customize mapping:

    • [KVProperty("name")]: Maps a property to a specific key name.
    • [KVIgnore]: Excludes a property from serialization.

    Arrays and List<T> are automatically populated from KV1 numeric-key objects or KV3 arrays.

    class DataObject
    {
        public string Name { get; set; }
        public string Developer { get; set; }
    
        [KVProperty("description")]
        public string Summary { get; set; }
    
        [KVIgnore]
        public string ExtraData { get; set; }
    }
    
    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
    
    // Deserialize
    using var input = File.OpenRead("file.vdf");
    DataObject loaded = kv.Deserialize<DataObject>(input);
    
    // Serialize
    var data = new DataObject
    {
        Developer = "Valve Software",
        Name = "Dota 2",
        Summary = "Dota 2 is a complex game.",
        ExtraData = "This will not be serialized."
    };
    
    using var output = File.OpenWrite("file.vdf");
    kv.Serialize(output, data, "root object name");
  9. Resolve KV1 Includes and Bases

    master

    KV1 text files can use #include (appends keys) and #base (recursively merges keys) directives. The library does not access the file system directly; you must provide an IIncludedFileLoader via KVSerializerOptions.FileLoader to resolve these paths into Stream objects.

    class DirectoryFileLoader(string directory) : IIncludedFileLoader
    {
        public Stream OpenFile(string filePath)
        {
            return File.OpenRead(Path.Combine(directory, filePath));
        }
    }
    
    var options = new KVSerializerOptions
    {
        FileLoader = new DirectoryFileLoader("C:/game/cfg"),
    };
    
    var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
    using var stream = File.OpenRead("C:/game/cfg/file.vdf");
    var data = kv.Deserialize(stream, options);
  10. Enumerating KVObject children

    master

    Since KVObject implements IReadOnlyDictionary<string, KVObject>, you can iterate over its children using foreach.

    • Collections: Iterate over data.Root or data.Root.Children to get (key, child) pairs.
    • Keys/Values: Use data.Root.Keys or data.Root.Values to get IEnumerable collections.
    • Arrays: When iterating over an array, the key in the (key, value) pair is null. You can also use array.Values to get the elements directly without the KVP wrapper.
    // Iterate over collection
    foreach (var (key, child) in data.Root)
    {
        Console.WriteLine($"{key} = {child}");
    }
    
    // Iterate over array elements
    KVObject array = data["position"];
    foreach (var (key, element) in array)
    {
        // key is null for array elements
        Console.WriteLine((float)element);
    }
    
    // Direct values on arrays
    foreach (var element in array.Values)
    {
        Console.WriteLine((float)element);
    }
  11. Reading values from a KVDocument

    master

    Access data using string indexers or named conversion methods. The KVDocument indexer is read-only; for full API access (like ContainsKey), use data.Root.

    Access Patterns:

    • Chained Indexing: (string)data["config"]["name"].
    • Named Conversions: data["version"].ToInt32(), data["scale"].ToDouble(), data["config"]["name"].ToString().
    • Array Access: (float)data["position"][0].
    • Safe Access: Use data.Root.TryGetValue("key", out var child) to avoid KeyNotFoundException.
    • Inspection: Use root.ValueType, root.IsNull, root.IsArray, root.IsCollection, or root.Count.
    • KV3 Specifics: Access flags via .Flag and blobs via .AsBlob() or casting to byte[].
    // String indexer (supports chaining)
    string name = (string)data["config"]["name"];
    int version = (int)data["version"];
    
    // Named conversion methods
    int version2 = data["version"].ToInt32();
    string text = data["config"]["name"].ToString();
    
    // Array and Blob access
    float x = (float)data["position"][0];
    byte[] bytes = data["blob"].AsBlob();
    Span<KVObject> elements = data["position"].AsArraySpan();
    
    // Existence checks
    if (data.Root.ContainsKey("optional")) { ... }