Editor/FenceLibrary/FenceDefinitionAssetContextMenu.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Editor;
internal static class FenceDefinitionAssetContextMenu
{
private const string FenceDefinitionExtension = ".fencedef";
private static readonly string[] SupportedSourceExtensions =
[
".vmdl",
".prefab"
];
[Event( "asset.contextmenu", Priority = 50 )]
public static void OnAssetContextMenu( AssetContextMenu e )
{
if ( e.SelectedList is null || e.SelectedList.Count <= 0 )
return;
if ( !e.SelectedList.All( IsFenceSourceAsset ) )
return;
var sources = e.SelectedList
.Select( FenceSource.FromAssetEntry )
.Where( x => x.IsValid )
.ToList();
if ( sources.Count <= 0 )
return;
e.Menu.AddOption( "Create Fence Definition From Selected", "fence", action: () => CreateFenceDefinitionFromSources( sources ) );
}
private static bool IsFenceSourceAsset( AssetEntry entry )
{
if ( entry is null )
return false;
var extension = Path.GetExtension( entry.AbsolutePath );
return SupportedSourceExtensions.Any( x => string.Equals( x, extension, StringComparison.OrdinalIgnoreCase ) );
}
private static void CreateFenceDefinitionFromSources( IReadOnlyList<FenceSource> sources )
{
if ( sources is null || sources.Count <= 0 )
return;
var firstSource = sources[0];
var defaultDirectory = GetDefaultOutputDirectory( firstSource.AbsolutePath );
var defaultFileName = $"{BuildDefinitionAssetName( sources )}{FenceDefinitionExtension}";
var dialog = new FileDialog( null )
{
Title = "Create Fence Definition From Selected..",
Directory = defaultDirectory,
DefaultSuffix = FenceDefinitionExtension
};
dialog.SelectFile( defaultFileName );
dialog.SetFindFile();
dialog.SetModeSave();
dialog.SetNameFilter( "Fence Definition (*.fencedef)" );
if ( !dialog.Execute() )
return;
var selectedFile = EnsureExtension( dialog.SelectedFile, FenceDefinitionExtension );
var outputDirectory = Path.GetDirectoryName( selectedFile );
if ( string.IsNullOrWhiteSpace( outputDirectory ) )
return;
if ( !IsInsideAssetsDirectory( outputDirectory ) )
{
EditorUtility.DisplayDialog( "Invalid Fence Definition Folder", "Fence definitions must be saved inside this project's Assets folder." );
return;
}
Directory.CreateDirectory( outputDirectory );
if ( File.Exists( selectedFile ) )
{
EditorUtility.DisplayDialog( "Fence Definition Exists", "A fence definition already exists at that path. Choose a new file name." );
return;
}
File.WriteAllText( selectedFile, BuildFenceDefinitionJson( sources ) );
RegisterAndCompileFile( selectedFile );
MainAssetBrowser.Instance?.Local.UpdateAssetList();
EditorUtility.DisplayDialog( "Fence Definition Created", $"Created a fence definition with {sources.Count} selected source asset(s)." );
}
private static string BuildFenceDefinitionJson( IReadOnlyList<FenceSource> sources )
{
var entries = new JsonArray();
foreach ( var source in sources )
{
entries.Add( new JsonObject
{
["DisplayName"] = string.Empty,
["Model"] = source.IsModel ? source.ResourcePath : null,
["Prefab"] = source.IsPrefab ? source.ResourcePath : null,
["RotationOffset"] = "0,0,0",
["Weight"] = 1
} );
}
var root = new JsonObject
{
["RootName"] = "Fence",
["Entries"] = entries,
["UseWeightedRandom"] = true,
["MaximumSegments"] = 256,
["SegmentGap"] = 0,
["RotationRandomizationMin"] = "0,0,0",
["RotationRandomizationMax"] = "0,0,0",
["DistanceRandomizationMin"] = 0,
["DistanceRandomizationMax"] = 0,
["CreateBlockerVolumes"] = true,
["__references"] = new JsonArray(),
["__version"] = 0
};
return root.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );
}
private static void RegisterAndCompileFile( string absolutePath )
{
AssetSystem.RegisterFile( absolutePath );
var resourcePath = GetProjectResourcePath( absolutePath );
var asset = AssetSystem.FindByPath( resourcePath ) ?? AssetSystem.FindByPath( absolutePath );
asset?.Compile( true );
}
private static string GetDefaultOutputDirectory( string sourceAbsolutePath )
{
var sourceDirectory = Path.GetDirectoryName( sourceAbsolutePath );
if ( !string.IsNullOrWhiteSpace( sourceDirectory ) && IsInsideAssetsDirectory( sourceDirectory ) )
return sourceDirectory;
return Project.Current.GetAssetsPath();
}
private static string BuildDefinitionAssetName( IReadOnlyList<FenceSource> sources )
{
if ( sources.Count == 1 )
return SanitizeFileName( sources[0].Name );
var firstDirectory = Path.GetDirectoryName( sources[0].AbsolutePath );
if ( !string.IsNullOrWhiteSpace( firstDirectory ) )
{
var directoryName = Path.GetFileName( firstDirectory );
if ( !string.IsNullOrWhiteSpace( directoryName ) )
return SanitizeFileName( $"{directoryName} Fence" );
}
return "Fence Definition";
}
private static bool IsInsideAssetsDirectory( string path )
{
var assetsPath = NormalizeDirectory( Project.Current.GetAssetsPath() );
var targetPath = NormalizeDirectory( path );
return targetPath.Equals( assetsPath, StringComparison.OrdinalIgnoreCase )
|| targetPath.StartsWith( assetsPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase );
}
private static string GetProjectResourcePath( string absoluteSourcePath )
{
var absolutePath = Path.GetFullPath( absoluteSourcePath );
var assetsPath = Path.GetFullPath( Project.Current.GetAssetsPath() );
if ( absolutePath.StartsWith( NormalizeDirectory( assetsPath ) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase ) )
{
return Path.GetRelativePath( assetsPath, absolutePath ).Replace( '\\', '/' );
}
return Path.GetFileName( absolutePath ).Replace( '\\', '/' );
}
private static string SanitizeFileName( string name )
{
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new string( name.Select( c => invalid.Contains( c ) ? '_' : c ).ToArray() ).Trim();
return string.IsNullOrWhiteSpace( sanitized ) ? "Fence Definition" : sanitized;
}
private static string EnsureExtension( string path, string extension )
{
return string.Equals( Path.GetExtension( path ), extension, StringComparison.OrdinalIgnoreCase )
? path
: Path.ChangeExtension( path, extension );
}
private static string NormalizeDirectory( string path )
{
return Path.GetFullPath( path ).TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar );
}
private readonly record struct FenceSource( string AbsolutePath, string ResourcePath, string Name, string Extension )
{
public bool IsValid => !string.IsNullOrWhiteSpace( AbsolutePath ) && !string.IsNullOrWhiteSpace( ResourcePath );
public bool IsModel => string.Equals( Extension, ".vmdl", StringComparison.OrdinalIgnoreCase );
public bool IsPrefab => string.Equals( Extension, ".prefab", StringComparison.OrdinalIgnoreCase );
public static FenceSource FromAssetEntry( AssetEntry entry )
{
if ( entry is null )
return default;
var absolutePath = Path.GetFullPath( entry.AbsolutePath );
var resourcePath = entry.Asset?.RelativePath;
if ( string.IsNullOrWhiteSpace( resourcePath ) )
{
resourcePath = GetProjectResourcePath( absolutePath );
}
return new FenceSource(
absolutePath,
resourcePath.Replace( '\\', '/' ),
Path.GetFileNameWithoutExtension( absolutePath ),
Path.GetExtension( absolutePath ) );
}
}
}