Editor/FileRenamer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Editor;
using Sandbox;
using Sandbox.Diagnostics;
using FileSystem = Editor.FileSystem;
namespace Braxnet;
[EditorApp( "FileRenamer", "drive_file_rename_outline", "File Renamer" )]
public class FileRenamer : Window
{
private static Logger Log = new("FileRenamer");
public FileRenamer()
{
WindowTitle = "Hello";
MinimumSize = new Vector2( 300, 500 );
var canvas = new Widget( null );
canvas.Layout = Layout.Column();
canvas.Layout.Spacing = 5;
canvas.Layout.Margin = 5;
var introText = new Label();
introText.Text =
"First rename your component in your IDE, then use this tool to fix all dependants.\nMake sure to back up or use version control before using this tool.";
canvas.Layout.Add( introText );
var inputComponentName = new LineEdit();
inputComponentName.PlaceholderText = "MyOldNamespace.SourceComponent";
canvas.Layout.Add( inputComponentName );
/*var inputComponentNameValidator = new Label();
inputComponentNameValidator.Text = "Type the name of the component you want to rename";
canvas.Layout.Add( inputComponentNameValidator );
inputComponentName.TextChanged += ( string text ) =>
{
var component = TypeLibrary.GetType( text );
if ( component == null )
{
inputComponentNameValidator.Text = "Component not found";
}
else
{
inputComponentNameValidator.Text = $"Component found: {component.Namespace}.{component.Name}";
}
};*/
var outputComponentName = new LineEdit();
outputComponentName.PlaceholderText = "MyNewNamespace.DestinationComponent";
canvas.Layout.Add( outputComponentName );
var button = new Button( "Rename" );
button.Pressed += () => RenameComponent( inputComponentName.Text, outputComponentName.Text );
canvas.Layout.Add( button );
Canvas = canvas;
}
private void RenameComponent( string inputComponentName, string outputComponentName )
{
/*var inputComponent = TypeLibrary.GetType( inputComponentName );
if ( inputComponent == null )
{
Log.Error( $"Component not found: {inputComponentName}" );
return;
}
var outputComponent = TypeLibrary.GetType( outputComponentName );
if ( outputComponent != null )
{
Log.Error( $"Component already exists: {outputComponentName}" );
return;
}*/
/*var outputNamespace = outputComponentName.Substring( 0, outputComponentName.LastIndexOf( '.' ) );
var outputClassName = outputComponentName.Substring( outputComponentName.LastIndexOf( '.' ) + 1 );
var inputNamespace = inputComponent.Namespace;
var inputClassName = inputComponent.Name;
var codeRoot = Project.Current.GetCodePath();*/
var rootAssetPath = FileSystem.Mounted.GetFullPath( "." );
var fileTypesToSearch = new string[] { ".scene", ".prefab" };
var files = System.IO.Directory.GetFiles( rootAssetPath, "*.*", System.IO.SearchOption.AllDirectories )
.Where( x => fileTypesToSearch.Contains( System.IO.Path.GetExtension( x ) ) );
foreach ( var file in files )
{
var text = System.IO.File.ReadAllText( file );
if ( text.Contains( inputComponentName ) )
{
// text = text.Replace( inputComponentName, outputComponentName );
text = text.Replace( $"\"__type\": \"{inputComponentName}\"",
$"\"__type\": \"{outputComponentName}\"" );
System.IO.File.WriteAllText( file, text );
Log.Info( $"Replaced {inputComponentName} with {outputComponentName} in {file}" );
}
}
Log.Info( $"Renamed {inputComponentName} to {outputComponentName}" );
}
[Event( "asset.contextmenu", Priority = 50 )]
public static void RenameFileAssetContext( AssetContextMenu e )
{
// Are all the files we have selected sound assets?
// if ( !e.SelectedList.All( x => x.AssetType == AssetType.SoundFile ) )
// return;
e.Menu.AddOption( $"Rename/move and fix dependants", "audio_file",
action: () => ContextMenuClick( e.SelectedList ) );
}
private static string lastDirectory = "";
private static void ContextMenuClick( List<AssetEntry> assetEntries )
{
/*if ( assetEntries.Count > 1 )
{
throw new System.Exception( "Only one asset can be renamed at a time" );
}
var asset = assetEntries.First();*/
lastDirectory = "";
if ( assetEntries.Count > 1 )
{
_ = RenameMultipleAssetsPopup( assetEntries );
}
else
{
RenameSingleAssetPopup( assetEntries.First() );
}
}
private static void RenameSingleAssetPopup( AssetEntry asset )
{
/*if ( asset.AssetType == AssetType.Model )
{
throw new System.Exception( "Cannot rename models due to not all dependants being added" );
}*/
if ( string.IsNullOrEmpty( lastDirectory ) )
{
lastDirectory = System.IO.Path.GetDirectoryName( asset.AbsolutePath );
}
var originalPath = asset.AbsolutePath;
var fd = new FileDialog( null );
fd.Title = "Rename asset..";
// fd.Directory = System.IO.Path.GetDirectoryName( originalPath );
fd.Directory = lastDirectory;
fd.DefaultSuffix = System.IO.Path.GetExtension( originalPath );
fd.SelectFile( System.IO.Path.GetFileName( originalPath ) );
fd.SetModeSave();
fd.SetNameFilter( "All Files (*.*)" );
if ( !fd.Execute() )
return;
var newPath = fd.SelectedFile;
_ = RenameSingleAsset( asset, newPath );
}
private static async Task RenameMultipleAssetsPopup( List<AssetEntry> assetEntries )
{
var fd = new FileDialog( null );
fd.Title = "Move assets..";
fd.Directory = assetEntries.First().AbsolutePath;
// fd.SetModeSave();
fd.SetFindDirectory();
if ( !fd.Execute() )
return;
var newPath = fd.SelectedFile;
using var progress = Progress.Start( "Renaming assets" );
var token = Progress.GetCancel();
foreach ( var asset in assetEntries )
{
var originalPath = asset.AbsolutePath;
var relativePath =
System.IO.Path.GetRelativePath( System.IO.Path.GetDirectoryName( originalPath ), newPath );
var newAssetPath = System.IO.Path.Combine( newPath, System.IO.Path.GetFileName( originalPath ) );
Progress.Update( $"Renaming {originalPath} to {newAssetPath}", assetEntries.IndexOf( asset ) + 1,
assetEntries.Count );
await RenameSingleAsset( asset, newAssetPath );
if ( token.IsCancellationRequested )
return;
}
}
private static async Task RenameSingleAsset( AssetEntry asset, string newPath )
{
var originalPath = asset.AbsolutePath;
if ( newPath == originalPath )
{
Log.Error( "New path is the same as the original path" );
return;
}
if ( System.IO.File.Exists( newPath ) )
{
// throw new System.Exception( $"File already exists: {newPath}" );
Log.Error( $"File already exists: {newPath}" );
return;
}
Log.Info( $"!!! Renaming {originalPath} to {newPath} !!!" );
// System.IO.File.Move( originalPath, newPath );
var dependants = await GetDependants( asset );
foreach ( var dependant in dependants.Item1 )
{
Log.Info( $"Dependant: {dependant}" );
await FixDependant( asset, dependant, originalPath, newPath );
}
foreach ( var dependantPath in dependants.Item2 )
{
Log.Info( $"Dependant path found: {dependantPath}" );
}
Log.Info( $"!!! Renamed {originalPath} to {newPath} !!!" );
CleanupCompiledAsset( asset, originalPath );
// System.IO.File.Move( originalPath, newPath );
// asset.Rename( );
asset.FileInfo.MoveTo( newPath );
AssetSystem.RegisterFile( newPath );
}
private static string GetAssetRelativePathFromFullPath( string fullPath )
{
var basePath = FileSystem.Mounted.GetFullPath( "." );
return System.IO.Path.GetRelativePath( basePath, fullPath ).Replace( '\\', '/' );
}
private static readonly string[] ModelSourceExtensions = new string[] { ".fbx", ".obj", ".dmx" };
private static readonly string[] TextureSourceExtensions = new string[] { ".png", ".jpg", ".jpeg", ".tga" };
private static readonly string[] SoundSourceExtensions = new string[] { ".wav", ".mp3", ".ogg", ".flac" };
private static async Task<(List<Asset>, List<string>)> GetDependants( AssetEntry asset )
{
var dependants = new List<Asset>();
var dependantPaths = new List<string>();
// get built in
dependants.AddRange( asset.Asset.GetDependants( false ) );
// fbx files are not included in the built in dependants for models
if ( ModelSourceExtensions.Contains( System.IO.Path.GetExtension( asset.AbsolutePath ) ) )
{
// look through all vmdl files in the project
var basePath = FileSystem.Mounted.GetFullPath( "." );
var vmdlFiles = System.IO.Directory.GetFiles( basePath, "*.vmdl", System.IO.SearchOption.AllDirectories );
foreach ( var vmdlFile in vmdlFiles )
{
var text = await System.IO.File.ReadAllTextAsync( vmdlFile );
if ( text.Contains( GetAssetRelativePathFromFullPath( asset.AbsolutePath ) ) )
{
var vmdlAsset = AssetSystem.FindByPath( vmdlFile );
if ( vmdlAsset != null )
{
dependants.Add( vmdlAsset );
}
else
{
Log.Error( $"Dependant vmdl not found: {vmdlFile}" );
}
dependantPaths.Add( vmdlFile );
Log.Info( $"Found dependant {vmdlFile} for {asset.AbsolutePath} fbx" );
}
}
}
// image files are not included in the built in dependants for materials
if ( TextureSourceExtensions.Contains( System.IO.Path.GetExtension( asset.AbsolutePath ) ) )
{
// look through all vmat files in the project
var basePath = FileSystem.Mounted.GetFullPath( "." );
var vmatFiles = System.IO.Directory.GetFiles( basePath, "*.vmat", System.IO.SearchOption.AllDirectories );
foreach ( var vmatFile in vmatFiles )
{
var text = System.IO.File.ReadAllText( vmatFile );
if ( text.Contains( GetAssetRelativePathFromFullPath( asset.AbsolutePath ) ) )
{
var vmatAsset = AssetSystem.FindByPath( vmatFile );
if ( vmatAsset != null )
{
dependants.Add( vmatAsset );
}
else
{
Log.Error( $"Dependant vmat not found: {vmatFile}" );
}
dependantPaths.Add( vmatFile );
Log.Info( $"Found dependant {vmatFile} for {asset.AbsolutePath} image" );
}
}
}
// sound files have their extensions changed to .vsnd when referenced by sound events
if ( asset.AssetType == AssetType.SoundFile )
{
}
return (dependants, dependantPaths);
}
private static void CleanupCompiledAsset( AssetEntry asset, string originalPath )
{
var compiledPath = asset.Asset.GetCompiledFile( true );
if ( string.IsNullOrEmpty( compiledPath ) )
{
Log.Error( $"Compiled asset not found for {originalPath}" );
return;
}
if ( !System.IO.File.Exists( compiledPath ) )
{
Log.Error( $"Compiled asset not found: {compiledPath}" );
return;
}
Log.Info( $"Cleaning up compiled asset {compiledPath}" );
System.IO.File.Delete( compiledPath );
}
private static async Task FixDependant( AssetEntry asset, Asset dependant, string originalPath, string newPath )
{
if ( asset == null )
{
Log.Error( "Asset is null" );
return;
}
if ( dependant == null )
{
Log.Error( "Dependant is null" );
return;
}
var dependantPath = dependant.AbsolutePath;
if ( dependant.AssetType == AssetType.MapFile )
{
await FixMapFile( asset, dependant, originalPath, newPath );
return;
}
if ( !System.IO.File.Exists( dependantPath ) )
{
Log.Error( $"Dependant not found: {dependantPath}" );
return;
}
var text = await System.IO.File.ReadAllTextAsync( dependantPath );
// var relativeOriginalPath =
// System.IO.Path.GetRelativePath( FileSystem.Mounted.GetFullPath( "." ), originalPath ).Replace( '\\', '/' );
// var relativeNewPath = System.IO.Path.GetRelativePath( FileSystem.Mounted.GetFullPath( "." ), newPath )
// .Replace( '\\', '/' );
var relativeOriginalPath = GetAssetRelativePathFromFullPath( originalPath );
var relativeNewPath = GetAssetRelativePathFromFullPath( newPath );
Log.Info( $"Replacing {relativeOriginalPath} with {relativeNewPath} in {dependantPath}" );
// replace sound file extensions with .vsnd when referenced by sound events
if ( asset.AssetType == AssetType.SoundFile )
{
relativeOriginalPath = System.IO.Path.ChangeExtension( relativeOriginalPath, ".vsnd" );
relativeNewPath = System.IO.Path.ChangeExtension( relativeNewPath, ".vsnd" );
}
text = text.Replace( relativeOriginalPath, relativeNewPath );
await System.IO.File.WriteAllTextAsync( dependantPath, text );
dependant.Compile( true );
asset.Asset.Compile( true );
Log.Info( $"Fixed dependant {dependantPath}" );
}
// vmap files are stored in binary, so they need to be converted to text before replacing paths
private static async Task FixMapFile( AssetEntry asset, Asset dependant, string originalPath, string newPath )
{
var engineRoot = FileSystem.Root.GetFullPath( "." );
var dmxConvertPath = System.IO.Path.Combine( engineRoot, "bin", "win64", "dmxconvert.exe" );
if ( !System.IO.File.Exists( dmxConvertPath ) )
{
Log.Error( $"dmxconvert.exe not found at {dmxConvertPath}" );
return;
}
var mapFilePath = dependant.AbsolutePath;
var tempPath = System.IO.Path.GetTempFileName();
var args = $"-i \"{mapFilePath}\" -ie vmap -o \"{tempPath}\" -oe keyvalues2";
var process = new System.Diagnostics.Process();
process.StartInfo.FileName = dmxConvertPath;
process.StartInfo.Arguments = args;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
await process.WaitForExitAsync();
// Log.Info( $"{dmxConvertPath} {args}" );
// Log.Info( process.StandardOutput.ReadToEnd() );
// Log.Info( process.StandardError.ReadToEnd() );
if ( process.ExitCode != 0 )
{
Log.Error( $"dmxconvert.exe vmap->text failed with exit code {process.ExitCode}" );
return;
}
var text = await System.IO.File.ReadAllTextAsync( tempPath );
var relativeOriginalPath = GetAssetRelativePathFromFullPath( originalPath );
var relativeNewPath = GetAssetRelativePathFromFullPath( newPath );
Log.Info( $"Replacing {relativeOriginalPath} with {relativeNewPath} in {mapFilePath}" );
text = text.Replace( relativeOriginalPath, relativeNewPath );
Log.Info( $"Writing fixed text to {tempPath}" );
await System.IO.File.WriteAllTextAsync( tempPath, text );
Log.Info( $"Converting fixed text back to vmap: {mapFilePath}" );
args = $"-i \"{tempPath}\" -ie keyvalues2 -o \"{mapFilePath}\" -of vmap";
process = new System.Diagnostics.Process();
process.StartInfo.FileName = dmxConvertPath;
process.StartInfo.Arguments = args;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
await process.WaitForExitAsync();
// Log.Info( $"{dmxConvertPath} {args}" );
// Log.Info( process.StandardOutput.ReadToEnd() );
// Log.Info( process.StandardError.ReadToEnd() );
if ( process.ExitCode != 0 )
{
Log.Error( $"dmxconvert.exe text->vmap failed with exit code {process.ExitCode}" );
return;
}
// System.IO.File.Delete( tempPath );
Log.Info( $"Fixed dependant map {mapFilePath}" );
}
}