Skip to content

Commit 994fef5

Browse files
committed
Merge KVValue into KVObject, remove Name from objects
1 parent baa897b commit 994fef5

File tree

61 files changed

+1669
-1990
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1669
-1990
lines changed

README.md

Lines changed: 70 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
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).
1010

11-
# Core Types
11+
# Core Type
1212

13-
The library is built around two types:
13+
The library is built around a single type:
1414

15-
- **`KVObject`** (class) -- a named tree node. Holds a `Name` and a `KVValue`. Supports navigation, mutation, and enumeration.
16-
- **`KVValue`** (readonly record struct) -- the data. Stores scalars inline (no boxing), strings, binary blobs, arrays, and collections. Supports implicit/explicit conversions, flags, and `with` expressions.
15+
- **`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.
16+
- **`KVDocument`** (class, extends `KVObject`) -- a deserialized document with a root key `Name` and optional `Header`.
1717

1818
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:
1919

@@ -32,46 +32,46 @@ When constructing objects programmatically, use `KVObject.Collection()` (dict-ba
3232
### Constructing
3333

3434
```csharp
35-
// Scalar value (typed constructors for common types, no cast needed)
36-
var obj = new KVObject("key", "hello");
37-
var obj = new KVObject("key", 42);
38-
var obj = new KVObject("key", 3.14f);
39-
var obj = new KVObject("key", true);
35+
// Scalar values (typed constructors)
36+
var obj = new KVObject("hello"); // string
37+
var obj = new KVObject(42); // int
38+
var obj = new KVObject(3.14f); // float
39+
var obj = new KVObject(true); // bool
40+
41+
// Implicit conversion from primitives
42+
KVObject obj = "hello";
43+
KVObject obj = 42;
4044

4145
// Dictionary-backed collection (O(1) lookup, no duplicate keys)
42-
var obj = KVObject.Collection("root"); // empty, can Add children
43-
var obj = KVObject.Collection("root", [ // with children
44-
new KVObject("name", "Dota 2"),
45-
new KVObject("appid", 570),
46-
]);
46+
var obj = KVObject.Collection(); // empty
47+
var obj = new KVObject(); // same as above
4748
4849
// List-backed collection (preserves insertion order, allows duplicate keys, for KV1)
49-
var obj = KVObject.ListCollection("root"); // empty
50-
var obj = KVObject.ListCollection("root", [ // with children
51-
new KVObject("key", "first"),
52-
new KVObject("key", "second"), // duplicate keys allowed
53-
]);
54-
55-
// Array from values (implicit conversions from primitives)
56-
var arr = KVObject.Array("items"); // empty, can Add elements
57-
var arr = KVObject.Array("tags", new KVValue[] { "action", "moba" }); // from KVValue[]
58-
59-
// Array from KVObjects (when elements need flags, nested structure, etc.)
60-
var arr = KVObject.Array("data", [
61-
new KVObject(null, (KVValue)"element"),
62-
new KVObject(null, flaggedValue),
63-
]);
50+
var obj = KVObject.ListCollection(); // empty
51+
52+
// Build up children
53+
var obj = new KVObject();
54+
obj["name"] = "Dota 2"; // implicit string -> KVObject
55+
obj["appid"] = 570; // implicit int -> KVObject
56+
57+
// Array
58+
var arr = KVObject.Array(); // empty
59+
var arr = KVObject.Array([ new KVObject("a"), new KVObject("b") ]); // from elements
6460
6561
// Binary blob
66-
var blob = KVObject.Blob("data", new byte[] { 0x01, 0x02, 0x03 });
67-
```
62+
var blob = KVObject.Blob(new byte[] { 0x01, 0x02, 0x03 });
6863

69-
> `new KVObject("name")` is equivalent to `KVObject.Collection("name")` (empty dict-backed collection).
64+
// Null value
65+
var nul = KVObject.Null();
66+
```
7067

7168
### Reading values
7269

7370
```csharp
74-
KVObject data = kv.Deserialize(stream);
71+
KVDocument data = kv.Deserialize(stream);
72+
73+
// Root key name (only on KVDocument)
74+
string rootName = data.Name;
7575

7676
// String indexer returns KVObject (supports chaining)
7777
string name = (string)data["config"]["name"];
@@ -84,15 +84,15 @@ float x = (float)data["position"][0];
8484

8585
// Check existence
8686
if (data.ContainsKey("optional")) { ... }
87-
if (data.TryGetChild("optional", out var child)) { ... }
87+
if (data.TryGetValue("optional", out var child)) { ... }
8888

8989
// Null-safe (indexer returns null for missing keys)
9090
KVObject val = data["missing"]; // null
9191
92-
// Access the underlying KVValue directly
93-
KVValueType type = data.ValueType; // forwarded from Value
94-
KVFlag flag = data["texture"].Value.Flag; // flags live on KVValue
95-
ReadOnlySpan<byte> bytes = data["blob"].Value.AsSpan();
92+
// Direct access to value properties
93+
KVValueType type = data.ValueType;
94+
KVFlag flag = data["texture"].Flag;
95+
ReadOnlySpan<byte> bytes = data["blob"].AsSpan();
9696
```
9797

9898
### Modifying
@@ -105,70 +105,51 @@ data["count"] = 42;
105105
// Chained writes work (reference semantics)
106106
data["config"]["resolution"] = "1920x1080";
107107

108-
// Add/remove children
109-
data.Add(new KVObject("newprop", 42));
110-
data.Add("shorthand", (KVValue)"value");
111-
data.Remove("deprecated");
108+
// Add children to collections
109+
data.Add("newprop", 42); // implicit int -> KVObject
110+
data.Add("text", "value"); // implicit string -> KVObject
112111
113-
// Array mutation
114-
arr.Add((KVValue)"new element");
115-
arr.RemoveAt(2);
112+
// Add elements to arrays
113+
arr.Add(3.14f); // implicit float -> KVObject
116114
117-
// Clear
115+
// Remove
116+
data.Remove("deprecated");
117+
arr.RemoveAt(2);
118118
data.Clear();
119119

120-
// Modify flags via with expression
121-
var child = data.GetChild("texture");
122-
child.Value = child.Value with { Flag = KVFlag.Resource };
120+
// Set flags directly
121+
data["texture"].Flag = KVFlag.Resource;
123122
```
124123

125124
### Enumerating
126125

127126
```csharp
128-
// KVObject implements IEnumerable<KVObject>
129-
foreach (var child in data)
127+
// KVObject implements IEnumerable<KeyValuePair<string, KVObject>>
128+
// Keys are the child names, values are the child KVObjects
129+
foreach (var (key, child) in data)
130130
{
131-
Console.WriteLine($"{child.Name} = {(string)child}");
131+
Console.WriteLine($"{key} = {(string)child}");
132132
}
133133

134-
// LINQ works naturally
135-
var names = data.Children.Select(c => c.Name);
134+
// Keys and Values properties
135+
var keys = data.Keys; // IEnumerable<string>
136+
var values = data.Values; // IEnumerable<KVObject>
136137
137-
// Scalars yield nothing
138-
foreach (var child in scalarObj) { } // empty
139-
```
140-
141-
## KVValue
138+
// Array elements have null keys
139+
foreach (var (key, element) in arrayObj)
140+
{
141+
// key is null for array elements
142+
Console.WriteLine((string)element);
143+
}
142144

143-
A `readonly record struct` that stores scalar data inline (no boxing):
145+
// Values on arrays returns elements directly (no KVP wrapper)
146+
foreach (var element in arrayObj.Values)
147+
{
148+
Console.WriteLine((string)element);
149+
}
144150

145-
```csharp
146-
// Implicit from primitives
147-
KVValue v = "hello";
148-
KVValue v = 42;
149-
KVValue v = 3.14f;
150-
KVValue v = true;
151-
152-
// Explicit to primitives
153-
string s = (string)value;
154-
int n = (int)value;
155-
156-
// Typed accessors
157-
string s = value.AsString();
158-
int n = value.ToInt32(CultureInfo.InvariantCulture);
159-
ReadOnlySpan<byte> data = value.AsSpan();
160-
byte[] blob = value.AsBlob();
161-
162-
// Properties
163-
value.ValueType // KVValueType enum
164-
value.Flag // KVFlag enum
165-
value.IsNull // true if ValueType == Null
166-
167-
// with expressions (readonly record struct)
168-
var flagged = value with { Flag = KVFlag.Resource };
169-
170-
// default is null
171-
default(KVValue).IsNull == true
151+
// Scalars yield nothing
152+
foreach (var child in scalarObj) { } // empty
172153
```
173154

174155
# KeyValues1
@@ -182,7 +163,7 @@ Used by Steam and the Source engine.
182163
var stream = File.OpenRead("file.vdf"); // or any other Stream
183164
184165
var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
185-
KVObject data = kv.Deserialize(stream);
166+
KVDocument data = kv.Deserialize(stream);
186167

187168
Console.WriteLine(data["some key"]);
188169
```
@@ -279,7 +260,7 @@ Used by the Source 2 engine.
279260
var stream = File.OpenRead("file.kv3"); // or any other Stream
280261
281262
var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text);
282-
KVObject data = kv.Deserialize(stream);
263+
KVDocument data = kv.Deserialize(stream);
283264

284265
Console.WriteLine(data["some key"]);
285266
```

ValveKeyValue/ValveKeyValue.Console/Program.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,28 @@ static int Execute(
3636
var serializer = KVSerializer.Create(format);
3737
var root = serializer.Deserialize(stream, options);
3838

39-
RecursivePrint(root);
39+
RecursivePrint(root.Name, root);
4040

4141
return 0;
4242
}
4343

44-
static void RecursivePrint(KVObject obj, int indent = 0)
44+
static void RecursivePrint(string name, KVObject obj, int indent = 0)
4545
{
4646
Console.Write(new string('\t', indent));
4747

4848
indent++;
4949

50-
if (obj.Value.ValueType is KVValueType.Collection or KVValueType.Array)
50+
if (obj.ValueType is KVValueType.Collection or KVValueType.Array)
5151
{
52-
Console.WriteLine($"Name: {obj.Name}");
52+
Console.WriteLine($"Name: {name}");
5353

54-
foreach (var child in obj)
54+
foreach (var (key, child) in obj)
5555
{
56-
RecursivePrint(child, indent);
56+
RecursivePrint(key, child, indent);
5757
}
5858
}
5959
else
6060
{
61-
Console.WriteLine($"{obj.Name}: {obj.Value}");
61+
Console.WriteLine($"{name}: {obj}");
6262
}
6363
}

ValveKeyValue/ValveKeyValue.Test/Binary/BinaryObjectConsecutiveSerializationTestCase.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ class BinaryObjectConsecutiveSerializationTestCase
55
[Test]
66
public void SerializesToBinaryStructure()
77
{
8-
var first = new KVObject("FirstObject",
9-
[
10-
new KVObject("firstkey", "firstvalue")
11-
]);
8+
var firstObj = KVObject.ListCollection();
9+
firstObj.Add("firstkey", "firstvalue");
10+
var first = new KVDocument(null, "FirstObject", firstObj);
1211

13-
var second = new KVObject("SecondObject",
14-
[
15-
new KVObject("secondkey", "secondvalue")
16-
]);
12+
var secondObj = KVObject.ListCollection();
13+
secondObj.Add("secondkey", "secondvalue");
14+
var second = new KVDocument(null, "SecondObject", secondObj);
1715

1816
var expectedData = new byte[]
1917
{

ValveKeyValue/ValveKeyValue.Test/Binary/BinaryObjectSerializationTestCase.cs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ class BinaryObjectSerializationTestCase
55
[Test]
66
public void SerializesToBinaryStructure()
77
{
8-
var kvo = new KVObject("TestObject",
9-
[
10-
new KVObject("key", "value"),
11-
new KVObject("key_utf8", "邪恶的战"),
12-
new KVObject("int", 0x10203040),
13-
new KVObject("flt", 1234.5678f),
14-
new KVObject("ptr", new IntPtr(0x12345678)),
15-
new KVObject("lng", 0x8877665544332211u),
16-
new KVObject("i64", 0x0102030405060708)
17-
]);
8+
var kvo = KVObject.ListCollection();
9+
kvo.Add("key", "value");
10+
kvo.Add("key_utf8", "邪恶的战");
11+
kvo.Add("int", 0x10203040);
12+
kvo.Add("flt", 1234.5678f);
13+
kvo.Add("ptr", new IntPtr(0x12345678));
14+
kvo.Add("lng", 0x8877665544332211u);
15+
kvo.Add("i64", 0x0102030405060708);
16+
var doc = new KVDocument(null, "TestObject", kvo);
1817

1918
var expectedData = new byte[]
2019
{
@@ -46,12 +45,19 @@ public void SerializesToBinaryStructure()
4645
};
4746

4847
using var ms = new MemoryStream();
49-
KVSerializer.Create(KVSerializationFormat.KeyValues1Binary).Serialize(ms, kvo);
48+
KVSerializer.Create(KVSerializationFormat.KeyValues1Binary).Serialize(ms, doc);
5049
Assert.That(ms.ToArray(), Is.EqualTo(expectedData));
5150

5251
ms.Seek(0, SeekOrigin.Begin);
5352
var deserialized = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary).Deserialize(ms);
54-
Assert.That(deserialized, Is.EqualTo(kvo));
53+
Assert.That(deserialized.Name, Is.EqualTo("TestObject"));
54+
Assert.That((string)deserialized["key"], Is.EqualTo("value"));
55+
Assert.That((string)deserialized["key_utf8"], Is.EqualTo("邪恶的战"));
56+
Assert.That((int)deserialized["int"], Is.EqualTo(0x10203040));
57+
Assert.That((float)deserialized["flt"], Is.EqualTo(1234.5678f));
58+
Assert.That((IntPtr)deserialized["ptr"], Is.EqualTo(new IntPtr(0x12345678)));
59+
Assert.That((ulong)deserialized["lng"], Is.EqualTo(0x8877665544332211u));
60+
Assert.That((long)deserialized["i64"], Is.EqualTo(0x0102030405060708));
5561
}
5662
}
5763
}

ValveKeyValue/ValveKeyValue.Test/Binary/SimpleBinaryTestCase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ public int HasChildren()
3030
[TestCase("i64", 0x0102030405060708, typeof(long))]
3131
public void HasNamedChildWithValue(string name, object value, Type valueType)
3232
{
33-
Assert.That(Convert.ChangeType(obj[name].Value, valueType, CultureInfo.InvariantCulture), Is.EqualTo(value));
33+
Assert.That(Convert.ChangeType(obj[name], valueType, CultureInfo.InvariantCulture), Is.EqualTo(value));
3434
}
3535

36-
KVObject obj;
36+
KVDocument obj;
3737

3838
[OneTimeSetUp]
3939
public void SetUp()

ValveKeyValue/ValveKeyValue.Test/Binary/StringTableFromScratchTestCase.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@ class StringTableFromScratchTestCase
77
[Test]
88
public void PopulatesStringTableDuringSerialization()
99
{
10-
var kv = new KVObject("root",
11-
[
12-
new KVObject("key", "value"),
13-
new KVObject("child", [
14-
new KVObject("key", 123),
15-
]),
16-
]);
10+
var child = KVObject.ListCollection();
11+
child.Add("key", 123);
12+
var kv = KVObject.ListCollection();
13+
kv.Add("key", "value");
14+
kv.Add("child", child);
15+
var doc = new KVDocument(null, "root", kv);
1716

1817
var stringTable = new StringTable();
1918

2019
var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);
2120

2221
using var ms = new MemoryStream();
23-
serializer.Serialize(ms, kv, new KVSerializerOptions { StringTable = stringTable });
22+
serializer.Serialize(ms, doc, new KVSerializerOptions { StringTable = stringTable });
2423

2524
var strings = stringTable.ToArray();
2625
Assert.That(strings, Is.EqualTo(ExpectedStrings));

ValveKeyValue/ValveKeyValue.Test/Binary/StringTableTestCase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public int HasChildren()
2828
[TestCase("i64", 0x0102030405060708, typeof(long))]
2929
public void HasNamedChildWithValue(string name, object value, Type valueType)
3030
{
31-
Assert.That(Convert.ChangeType(obj[name].Value, valueType, CultureInfo.InvariantCulture), Is.EqualTo(value));
31+
Assert.That(Convert.ChangeType(obj[name], valueType, CultureInfo.InvariantCulture), Is.EqualTo(value));
3232
}
3333

3434
[Test]
@@ -42,7 +42,7 @@ public void SymmetricStringTableSerialization()
4242
Assert.That(ms.ToArray(), Is.EqualTo(TestData.ToArray()));
4343
}
4444

45-
KVObject obj;
45+
KVDocument obj;
4646

4747
[OneTimeSetUp]
4848
public void SetUp()

0 commit comments

Comments
 (0)