Editor/WeaponImporter/Core/Output/Kv3.cs
#nullable enable annotations
using System.Globalization;
using System.Text;
namespace WeaponImporter.Core.Output;
/// <summary>A KV3 value: object, array, string, number, bool or null.</summary>
public abstract record KvNode;
public sealed record KvObject : KvNode
{
public List<(string Key, KvNode Value)> Entries { get; } = new();
public KvObject Add(string key, KvNode value)
{
Entries.Add((key, value));
return this;
}
public KvObject Add(string key, string value) => Add(key, new KvString(value));
public KvObject Add(string key, double value) => Add(key, new KvNumber(value));
public KvObject Add(string key, int value) => Add(key, new KvNumber(value, true));
public KvObject Add(string key, bool value) => Add(key, new KvBool(value));
public KvObject Add(string key, params double[] values) => Add(key, new KvArray(values.Select(v => (KvNode)new KvNumber(v)).ToList()));
}
public sealed record KvArray(List<KvNode> Items) : KvNode
{
public KvArray() : this(new List<KvNode>()) { }
public KvArray Add(KvNode node)
{
Items.Add(node);
return this;
}
}
public sealed record KvString(string Value) : KvNode;
public sealed record KvNumber(double Value, bool Integer = false) : KvNode;
public sealed record KvBool(bool Value) : KvNode;
public sealed record KvNull : KvNode;
/// <summary>
/// Text KV3 in the layout ModelDoc writes: CRLF, tabs, `key = ` before blocks, trailing commas
/// after array entries, scalar arrays on one line. Numbers never use exponent notation
/// (ModelDoc reads "1E-05" as 1).
/// </summary>
public static class Kv3
{
public const string ModelDocHeader = "<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->";
public const string GenericHeader = "<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:generic:version{7412167c-06e9-4698-aff2-e63eb59037e7} -->";
public static string Serialize(KvObject root, string header = ModelDocHeader)
{
var sb = new StringBuilder();
sb.Append(header).Append("\r\n");
WriteObject(sb, root, 0);
sb.Append("\r\n");
return sb.ToString();
}
private static void Indent(StringBuilder sb, int depth) => sb.Append('\t', depth);
private static void WriteObject(StringBuilder sb, KvObject obj, int depth)
{
sb.Append("{\r\n");
foreach (var (key, value) in obj.Entries)
{
Indent(sb, depth + 1);
sb.Append(Key(key)).Append(" = ");
WriteValue(sb, value, depth + 1);
sb.Append("\r\n");
}
Indent(sb, depth);
sb.Append('}');
}
private static void WriteValue(StringBuilder sb, KvNode value, int depth)
{
switch (value)
{
case KvObject o:
sb.Append("\r\n");
Indent(sb, depth);
WriteObject(sb, o, depth);
break;
case KvArray a when a.Items.All(i => i is KvNumber or KvBool or KvString) && a.Items.Count <= 16 && a.Items.All(i => i is not KvString s || s.Value.Length < 40):
sb.Append("[ ");
sb.Append(string.Join(", ", a.Items.Select(Scalar)));
sb.Append(" ]");
break;
case KvArray a:
sb.Append("\r\n");
Indent(sb, depth);
sb.Append("[\r\n");
foreach (var item in a.Items)
{
Indent(sb, depth + 1);
if (item is KvObject io)
WriteObject(sb, io, depth + 1);
else if (item is KvArray)
WriteValue(sb, item, depth + 1);
else
sb.Append(Scalar(item));
sb.Append(",\r\n");
}
Indent(sb, depth);
sb.Append(']');
break;
default:
sb.Append(Scalar(value));
break;
}
}
private static string Scalar(KvNode node) => node switch
{
KvString s => "\"" + s.Value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
KvNumber { Integer: true } n => ((long)n.Value).ToString(CultureInfo.InvariantCulture),
KvNumber n => Number(n.Value),
KvBool b => b.Value ? "true" : "false",
KvNull => "null",
_ => "null",
};
/// <summary>Fixed-point, no exponent, at least one decimal.</summary>
public static string Number(double v)
{
if (!double.IsFinite(v))
v = 0;
if (Math.Abs(v) < 1e-9)
return "0.0";
var s = v.ToString("0.0#########", CultureInfo.InvariantCulture);
return s;
}
private static string Key(string key)
=> key.All(c => char.IsLetterOrDigit(c) || c == '_') ? key : "\"" + key + "\"";
}