Editor/Embedded/ValveResourceFormat/Serialization/KeyValues/KV3TextSerializer.cs

Serializer for KV3 text format, writes KVValue instances to an IndentedTextWriter. It handles flags, all KV value types (collections, arrays, strings, numbers, booleans, binary blobs, null) and escapes strings and binary data formatting.

File Access
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.Utils;
using System.Globalization;
using System.Runtime.CompilerServices;
using KVValueType = HumanoidRetargeter.EditorTools.Embedded.ValveKeyValue.KVValueType;

namespace HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.Serialization.KeyValues;

internal static class KV3TextSerializer
{
    public static void WriteValue(KVValue value, IndentedTextWriter writer)
    {
        var Type = value.Type;
        var Flag = value.Flag;
        var Value = value.Value;

        if (Flag != KVFlag.None)
        {
            switch (Flag)
            {
                case KVFlag.Resource:
                    writer.Write("resource:");
                    break;
                case KVFlag.ResourceName:
                    writer.Write("resource_name:");
                    break;
                case KVFlag.Panorama:
                    writer.Write("panorama:");
                    break;
                case KVFlag.SoundEvent:
                    writer.Write("soundevent:");
                    break;
                case KVFlag.SubClass:
                    writer.Write("subclass:");
                    break;
                case KVFlag.EntityName:
                    writer.Write("entity_name:");
                    break;
                default:
                    throw new InvalidOperationException($"Trying to print unknown keyvalues flag ({Flag})");
            }
        }

        switch (Type)
        {
            case KVValueType.Collection:
            case KVValueType.Array:
                ((KVObject)Value!).Serialize(writer);
                break;
            case KVValueType.String:
                {
                    var text = (string)Value!;
                    var isMultiline = text.Contains('\n', StringComparison.Ordinal);

                    if (isMultiline)
                    {
                        writer.Write("\"\"\"\n");
                        writer.Write(text);
                        writer.Write("\n\"\"\"");
                    }
                    else
                    {
                        WriteEscapedString(text, writer);
                    }
                    break;
                }
            case KVValueType.Boolean:
                writer.Write((bool)Value! ? "true" : "false");
                break;
            case KVValueType.FloatingPoint:
                writer.Write(Convert.ToSingle(Value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture));
                break;
            case KVValueType.FloatingPoint64:
                writer.Write(Convert.ToDouble(Value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int64:
                writer.Write(Convert.ToInt64(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt64:
                writer.Write(Convert.ToUInt64(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int32:
                writer.Write(Convert.ToInt32(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt32:
                writer.Write(Convert.ToUInt32(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int16:
                writer.Write(Convert.ToInt16(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt16:
                writer.Write(Convert.ToUInt16(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Null:
                writer.Write("null");
                break;
            case KVValueType.BinaryBlob:
                var byteArray = (byte[])Value!;
                var count = 0;

                {
                    // This might be longer than required
                    var lines = byteArray.Length / 32;
                    var size = 12 + byteArray.Length * 3;
                    size += (writer.Indent + 1) * lines;
                    size += Environment.NewLine.Length * lines;
                    writer.Grow(size);
                }

                writer.WriteLine();
                writer.WriteLine("#[");
                writer.Indent++;

                foreach (var oneByte in byteArray)
                {
                    writer.Write(HexToCharUpper(oneByte >> 4));
                    writer.Write(HexToCharUpper(oneByte));

                    if (++count % 32 == 0)
                    {
                        writer.WriteLine();
                    }
                    else
                    {
                        writer.Write(' ');
                    }
                }

                writer.Indent--;

                if (count % 32 != 0)
                {
                    writer.WriteLine();
                }

                writer.Write("]");
                break;
            default:
                throw new InvalidOperationException($"Trying to print unknown type '{Type}'");
        }
    }

    private static void WriteEscapedString(string input, IndentedTextWriter writer)
    {
        writer.Grow(input.Length + 2);
        writer.Write("\"");

        foreach (var c in input)
        {
            switch (c)
            {
                case '\n':
                    writer.Write("\\n");
                    break;
                case '\t':
                    writer.Write("\\t");
                    break;
                case '\\':
                    writer.Write("\\\\");
                    break;
                case '"':
                    writer.Write("\\\"");
                    break;
                default:
                    writer.Write(c);
                    break;
            }
        }

        writer.Write("\"");
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static char HexToCharUpper(int value)
    {
        value &= 0xF;
        value += '0';

        if (value > '9')
        {
            value += ('A' - ('9' + 1));
        }

        return (char)value;
    }
}