Editor/AssetCreator.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GeneralGame.Editor;

/// <summary>
/// Helper class for creating assets in the tree browser.
/// Based on the built-in CreateAsset functionality.
/// </summary>
public static class AssetCreator
{
    public struct Entry
    {
        public string Name { get; init; }
        public string Icon { get; init; }
        public Pixmap IconImage { get; init; }
        public string Category { get; init; }
        public bool Pinned { get; init; }
        public Action<DirectoryInfo> Action { get; init; }
        public string Default { get; init; }

        public void Execute(DirectoryInfo folder)
        {
            if (Action is not null)
            {
                Action(folder);
                return;
            }

            if (!string.IsNullOrEmpty(Default))
            {
                CreateFromTemplate(Name, Default, folder);
            }
        }
    }

    public static void AddOptions(Menu parent, string folderPath)
    {
        var folder = new DirectoryInfo(folderPath);
        var locationType = GetLocationType(folderPath);

        parent.AddOption("Folder", "folder", () =>
        {
            var newFolderPath = Path.Combine(folderPath, "New Folder");
            var counter = 1;
            while (Directory.Exists(newFolderPath))
            {
                newFolderPath = Path.Combine(folderPath, $"New Folder ({counter++})");
            }
            Directory.CreateDirectory(newFolderPath);
        });

        parent.AddSeparator();

        var gameResources = EditorTypeLibrary.GetAttributes<AssetTypeAttribute>().Select(
            x => new Entry()
            {
                Name = x.Name,
                Category = x.Category,
                IconImage = AssetType.FromType(x.TargetType)?.Icon64,
                Action = (DirectoryInfo d) => CreateGameResource(x, d),
                Pinned = x.Extension == "sound" || x.Extension == "prefab" || x.Extension == "scene"
            }
        );

        var entries = new List<Entry>();

        if (locationType == LocationType.Code)
        {
            entries.Add(new Entry { Name = "Empty C# File", Icon = "description", Default = "default.cs", Category = "Code", Pinned = true });
            entries.Add(new Entry { Name = "Component", Icon = "sports_esports", Default = "component.cs", Category = "Code", Pinned = true });
            entries.Add(new Entry { Name = "Panel Component", Icon = "desktop_windows", Default = "default.razor", Category = "Razor" });
            entries.Add(new Entry { Name = "Style Sheet", Icon = "brush", Default = "default.scss", Category = "Razor" });
        }
        else if (locationType == LocationType.Assets)
        {
            entries.Add(new Entry { Name = "Material", IconImage = AssetType.FromType(typeof(Material))?.Icon64, Default = "default.vmat", Category = "Rendering", Pinned = true });
            entries.Add(new Entry { Name = "Model", IconImage = AssetType.FromType(typeof(Model))?.Icon64, Default = "default.vmdl", Category = "Rendering", Pinned = true });
            entries.Add(new Entry { Name = "Map", Icon = "hardware", Default = "default.vmap", Category = "World" });

            entries.Add(new Entry { Name = "Standard Material Shader", Icon = "brush", Default = "material.shader", Category = "Shader" });
            entries.Add(new Entry { Name = "Unlit Shader", Icon = "brush", Default = "unlit.shader", Category = "Shader" });
            entries.Add(new Entry { Name = "Compute Shader", Icon = "brush", Default = "compute.shader", Category = "Shader" });
            entries.Add(new Entry { Name = "Shader Graph", Icon = "account_tree", Default = "default.shdrgrph", Category = "Shader" });
            entries.Add(new Entry { Name = "Shader Graph Function", Icon = "account_tree", Default = "subgraph.shdrfunc", Category = "Shader" });

            entries.AddRange(gameResources);
        }

        // Add pinned entries first
        foreach (var entry in entries.Where(x => x.Pinned).OrderBy(x => x.Name))
        {
            if (entry.IconImage != null)
                parent.AddOptionWithImage(entry.Name, entry.IconImage, () => entry.Execute(folder));
            else
                parent.AddOption(entry.Name, entry.Icon, () => entry.Execute(folder));
        }

        parent.AddSeparator();

        // Group remaining by category
        var grouped = entries.OrderBy(x => x.Name).GroupBy(x => x.Category).OrderBy(x => x.Key);
        foreach (var group in grouped.Where(x => x.Key is not null))
        {
            var menu = parent.FindOrCreateMenu(group.Key);

            foreach (var entry in group)
            {
                if (entry.IconImage != null)
                    menu.AddOptionWithImage(entry.Name, entry.IconImage, () => entry.Execute(folder));
                else
                    menu.AddOption(entry.Name, entry.Icon, () => entry.Execute(folder));
            }
        }
    }

    private enum LocationType
    {
        Unknown,
        Assets,
        Code,
        Localization
    }

    private static LocationType GetLocationType(string path)
    {
        var assetsPath = Project.Current?.GetAssetsPath();
        var codePath = Project.Current?.GetCodePath();

        if (!string.IsNullOrEmpty(assetsPath) && path.StartsWith(assetsPath, StringComparison.OrdinalIgnoreCase))
            return LocationType.Assets;

        if (!string.IsNullOrEmpty(codePath) && path.StartsWith(codePath, StringComparison.OrdinalIgnoreCase))
            return LocationType.Code;

        return LocationType.Unknown;
    }

    private static string GetNewFilename(DirectoryInfo folder, string typeName, string extension)
    {
        typeName = typeName.ToLower();
        string destName = $"new {typeName}{extension}";

        int i = 1;
        while (File.Exists(Path.Combine(folder.FullName, destName)))
        {
            destName = $"new {typeName} {i++}{extension}";
        }

        return destName;
    }

    private static void CreateFromTemplate(string name, string defaultFile, DirectoryInfo folder)
    {
        var extension = Path.GetExtension(defaultFile);

        var sourceFile = global::Editor.FileSystem.Root.GetFullPath($"/templates/{defaultFile}");
        if (!File.Exists(sourceFile))
        {
            Log.Error($"Can't create asset! Missing template: {defaultFile}");
            return;
        }

        string destName = GetNewFilename(folder, name, extension);
        string destPath = Path.Combine(folder.FullName, destName);
        File.Copy(sourceFile, destPath);

        AssetSystem.RegisterFile(destPath);
    }

    public static void CreateGameResource(AssetTypeAttribute gameResource, DirectoryInfo folder)
    {
        int slash = gameResource.Name.LastIndexOf('/');
        string name = slash == -1 ? gameResource.Name : gameResource.Name.Substring(slash + 1, gameResource.Name.Length - slash - 1);

        string destName = GetNewFilename(folder, name, $".{gameResource.Extension}");
        string destPath = Path.Combine(folder.FullName, destName);

        AssetSystem.CreateResource(gameResource.Extension, destPath);
    }
}