Editor/WeaponImporter/Core/Output/PrefabBuilder.cs
#nullable enable annotations
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace WeaponImporter.Core.Output;
/// <summary>A component on a generated prefab object: type name plus property values.</summary>
public sealed record PrefabComponent(string Type, IReadOnlyDictionary<string, JsonNode?> Properties);
/// <summary>A GameObject in a generated prefab.</summary>
public sealed class PrefabObject
{
public required string Name { get; init; }
public string Tags { get; init; } = "";
public System.Numerics.Vector3 Position { get; init; }
public System.Numerics.Quaternion Rotation { get; init; } = System.Numerics.Quaternion.Identity;
public float Scale { get; init; } = 1f;
public List<PrefabComponent> Components { get; } = new();
public List<PrefabObject> Children { get; } = new();
}
/// <summary>
/// Writes s&box prefab JSON. GUIDs are derived from the prefab path and each object's role,
/// so re-baking keeps references stable.
/// </summary>
public static class PrefabBuilder
{
public static string Build(string prefabPath, PrefabObject root)
{
var doc = new JsonObject
{
["RootObject"] = Object(prefabPath, root, "root"),
["ShowInMenu"] = false,
["MenuPath"] = null,
["MenuIcon"] = null,
["DontBreakAsTemplate"] = false,
["ResourceVersion"] = 2,
["__references"] = new JsonArray(),
["__version"] = 2,
};
return doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true }).Replace("\r\n", "\n");
}
private static JsonObject Object(string prefab, PrefabObject o, string role)
{
var components = new JsonArray();
var index = 0;
foreach (var c in o.Components)
{
var node = new JsonObject
{
["__type"] = c.Type,
["__guid"] = Guid(prefab, $"{role}/component:{index++}:{c.Type}"),
};
foreach (var (k, v) in c.Properties)
node[k] = v?.DeepClone();
components.Add(node);
}
var children = new JsonArray();
foreach (var child in o.Children)
children.Add(Object(prefab, child, $"{role}/{child.Name}"));
return new JsonObject
{
["__guid"] = Guid(prefab, role),
["__version"] = 1,
["Flags"] = 0,
["Name"] = o.Name,
["Position"] = $"{F(o.Position.X)},{F(o.Position.Y)},{F(o.Position.Z)}",
["Rotation"] = $"{F(o.Rotation.X)},{F(o.Rotation.Y)},{F(o.Rotation.Z)},{F(o.Rotation.W)}",
["Scale"] = $"{F(o.Scale)},{F(o.Scale)},{F(o.Scale)}",
["Tags"] = o.Tags,
["Enabled"] = true,
["NetworkMode"] = 2,
["NetworkInterpolation"] = true,
["NetworkOrphaned"] = 0,
["OwnerTransfer"] = 1,
["Components"] = components,
["Children"] = children,
};
}
private static string F(float v) => v.ToString("0.######", System.Globalization.CultureInfo.InvariantCulture);
/// <summary>Stable name-based GUID (SHA-256 of prefab path + role, formatted as a v5-style GUID).</summary>
public static string Guid(string prefab, string role)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(prefab.ToLowerInvariant() + "|" + role));
var bytes = hash.Take(16).ToArray();
bytes[7] = (byte)((bytes[7] & 0x0F) | 0x50);
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
return new System.Guid(bytes).ToString("D");
}
/// <summary>The SkinnedModelRenderer entry for a weapon model.</summary>
public static PrefabComponent ModelRenderer(string model, bool useAnimGraph = false) => new("Sandbox.SkinnedModelRenderer", new Dictionary<string, JsonNode?>
{
["__enabled"] = true,
["BodyGroups"] = 18446744073709551615UL,
["CreateAttachments"] = true,
["CreateBoneObjects"] = false,
["Model"] = model,
["RenderType"] = "On",
["Tint"] = "1,1,1,1",
["UseAnimGraph"] = useAnimGraph,
["PlaybackRate"] = 1,
});
}