Editor/WeaponImporter/Core/Formats/Dmx/Kv2Writer.cs
#nullable enable annotations
using System.Globalization;
using System.Numerics;
using System.Text;
namespace WeaponImporter.Core.Formats.Dmx;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Low-level DMX <c>keyvalues2_noids</c> text emitter: CRLF line endings, tab indentation,
/// top-level elements separated by blank lines (the layout fbx2dmx produces).
/// </summary>
public sealed class Kv2Writer
{
public const string ModelHeader = "<!-- dmx encoding keyvalues2_noids 4 format model 22 -->";
private readonly StringBuilder _sb = new();
private readonly bool _blankAfterInline;
private int _indent;
/// <param name="blankAfterInline">Emit an indentation-only line after each inline element (fbx2dmx animation quirk).</param>
public Kv2Writer(bool blankAfterInline = false) => _blankAfterInline = blankAfterInline;
public void Raw(string text) => _sb.Append(text).Append("\r\n");
private void Line(string text) => _sb.Append('\t', _indent).Append(text).Append("\r\n");
public void Attr(string name, string type, string value)
=> Line($"\"{Escape(name)}\" \"{type}\" \"{Escape(value)}\"");
public void BeginTop(string className)
{
Line($"\"{className}\"");
Line("{");
_indent++;
}
public void EndTop()
{
_indent--;
Line("}");
_sb.Append("\r\n");
}
public void BeginInline(string name, string className)
{
Line($"\"{Escape(name)}\" \"{className}\"");
Line("{");
_indent++;
}
public void EndInline()
{
_indent--;
Line("}");
if (_blankAfterInline)
Line("");
}
public void BeginArrayElement(string className)
{
Line($"\"{className}\"");
Line("{");
_indent++;
}
public void EndArrayElement(bool last)
{
_indent--;
Line(last ? "}" : "},");
}
public void BeginArray(string name, string type = "element_array")
{
Line($"\"{Escape(name)}\" \"{type}\" ");
Line("[");
_indent++;
}
public void EndArray()
{
_indent--;
Line("]");
}
public void ElementRef(string id, bool last) => Line($"\"element\" \"{id}\"" + (last ? "" : ","));
public void Value(string value, bool last) => Line($"\"{Escape(value)}\"" + (last ? "" : ","));
/// <summary>A whole value array on consecutive lines.</summary>
public void ValueArray<T>(string name, string type, IReadOnlyList<T> values, Func<T, string> format)
{
BeginArray(name, type);
for (var i = 0; i < values.Count; i++)
Value(format(values[i]), i == values.Count - 1);
EndArray();
}
public void Refs(string name, IReadOnlyList<string> ids)
{
BeginArray(name);
for (var i = 0; i < ids.Count; i++)
ElementRef(ids[i], i == ids.Count - 1);
EndArray();
}
public void EmptyBinary(string name)
{
Line($"\"{Escape(name)}\" \"binary\" ");
Line("\"");
Line("\"");
}
public override string ToString() => _sb.ToString();
private static string Escape(string value)
=> value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ").Replace("\n", " ");
// ------------------------------------------------------------------ formatting
/// <summary>Invariant float, up to 10 decimals, trailing zeros stripped, no negative zero.</summary>
public static string F(float value)
{
if (value == 0f || !float.IsFinite(value))
return "0";
return ((double)value).ToString("0.##########", CultureInfo.InvariantCulture);
}
public static string I(int value) => value.ToString(CultureInfo.InvariantCulture);
public static string Vec(Vector3 v) => $"{F(v.X)} {F(v.Y)} {F(v.Z)}";
public static string Vec(Vector2 v) => $"{F(v.X)} {F(v.Y)}";
public static string Quat(Quaternion q) => $"{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}";
public static string Time(double seconds) => seconds.ToString("0.0000", CultureInfo.InvariantCulture);
}
/// <summary>Deterministic element ids: a 128-bit FNV-1a hash of scope + path, formatted as a GUID.</summary>
public static class DmxIds
{
public static string Guid(string scope, string path)
{
var bytes = Encoding.UTF8.GetBytes(scope + "\n" + path);
var a = Fnv(bytes, 0xcbf29ce484222325UL);
var b = Fnv(bytes, 0x84222325cbf29ce4UL ^ (ulong)bytes.Length);
var hex = a.ToString("x16", CultureInfo.InvariantCulture) + b.ToString("x16", CultureInfo.InvariantCulture);
return $"{hex[..8]}-{hex[8..12]}-{hex[12..16]}-{hex[16..20]}-{hex[20..]}";
}
private static ulong Fnv(byte[] data, ulong basis)
{
var h = basis;
foreach (var d in data)
{
h ^= d;
h *= 0x100000001b3UL;
}
// Final avalanche (splitmix64) so similar paths differ in every digit.
h ^= h >> 30;
h *= 0xbf58476d1ce4e5b9UL;
h ^= h >> 27;
h *= 0x94d049bb133111ebUL;
h ^= h >> 31;
return h;
}
}