Editor/ProjectConverter/ProjectConverterDialog.cs
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
namespace SpriteTools.ProjectConverter;
public class ProjectConverterDialog : Dialog
{
List<string> OutdatedSprites { get; set; } = new();
public ProjectConverterDialog ( List<string> outdatedSprites )
{
WindowTitle = "Sprite Tools Project Converter";
Window.Title = WindowTitle;
OutdatedSprites = outdatedSprites;
Window.FixedSize = new Vector2( 640, 360 + 56 );
Layout = Layout.Column();
Layout.Margin = 8f;
Layout.Spacing = 8f;
if ( OutdatedSprites.Count == 0 )
{
var legacySprites = ResourceLibrary.GetAll<SpriteResource>().Count();
var newSprites = ResourceLibrary.GetAll<Sprite>().Count();
Layout.Add( new Label( $"Your project has {legacySprites} .spr Resources and {newSprites} .sprite Resources." ) );
}
else
{
Layout.Add( new Label( $"Your project has {OutdatedSprites.Count} outdated Sprite Resource(s). Backup your project before continuing.\n\nPlease select one of the upgrade paths below:" ) );
}
{
var rowWidget = Layout.Add( new Widget( this ) );
rowWidget.Layout = Layout.Row();
rowWidget.Layout.Margin = 8f;
rowWidget.Layout.Spacing = 8f;
rowWidget.HorizontalSizeMode = SizeMode.Flexible;
rowWidget.FixedHeight = 256 + 56;
{
var panel = rowWidget.Layout.Add( new Widget( this ) );
panel.SetStyles( "background-color: #222;" );
panel.SetSizeMode( SizeMode.Flexible, SizeMode.Flexible );
panel.FixedWidth = 300;
panel.Layout = Layout.Column();
panel.Layout.Margin = 8;
panel.Layout.Add( new Label( $"Convert to new in-engine Sprite Resource" ) );
var lblSub = panel.Layout.Add( new Label( $"Recommended. More performant and continued support." ) );
lblSub.SetStyles( "font-size: 10px; color: rgb(0, 220, 0); margin-top: 4px; margin-bottom: 8px;" );
var lblDesc = panel.Layout.Add( new Label( $"Your existing .sprite resources will be converted to\nin-engine .sprite resources. This is will give you more\nperformance and stability, but there are some minor\nAPI differences and gaps that will be implemented\nover-time.\n\n" +
$"This will not affect any Tilesets you have. And if you\nDO have any you may have to upgrade again\nin the future." ) );
lblDesc.Color = Color.Gray;
var lblWarn = panel.Layout.Add( new Label( $"\nWhen converting, you may have a few code errors\ndue to API differences. You can also come back and\ndo this later if you choose the option on the right." ) );
lblWarn.Color = Theme.Red;
panel.Layout.AddStretchCell( 1 );
var btn1 = panel.Layout.Add( new Button.Primary( "Update Sprite Tools .sprite -> Sandbox .sprite" ) );
btn1.Clicked += () =>
{
ConvertResourceToEngineFormat();
btn1.Enabled = false;
};
panel.Layout.AddSpacingCell( 4 );
var btn2 = panel.Layout.Add( new Button.Primary( "Update SpriteComponent -> SpriteRenderer" ) );
btn2.Clicked += () =>
{
ConvertCodeToEngineFormat();
btn2.Enabled = false;
};
}
{
var panel = rowWidget.Layout.Add( new Widget( this ) );
panel.SetStyles( "background-color: #222;" );
panel.SetSizeMode( SizeMode.Flexible, SizeMode.Flexible );
panel.Layout = Layout.Column();
panel.Layout.Margin = 8;
panel.Layout.Add( new Label( $"Continue using Sprite Tools format" ) );
var lblSub = panel.Layout.Add( new Label( $"Tools are outdated and will no longer receive updates." ) );
lblSub.SetStyles( "font-size: 10px; color: orange; margin-top: 4px; margin-bottom: 8px;" );
var lblDesc = panel.Layout.Add( new Label( $"The sprite resource extension has changed from\n.sprite -> .spr to prevent conflicts with the new\nin-engine sprite resource." ) );
lblDesc.Color = Color.Gray;
var lblWarn = panel.Layout.Add( new Label( $"\nIf your code loads any .sprite resources via a string\nyou may have to update those yourself as the code\nupgrader is going to miss any manually constructed\nstrings.\n\n" +
$"Also keep in mind that if you choose this path, you\nare not guaranteed future support. If changes are\nmade to in-engine sprites in the future, this\nconverter will no longer work. Proceed with caution." ) );
lblWarn.Color = Theme.Red;
panel.Layout.AddStretchCell( 1 );
var btn1 = panel.Layout.Add( new Button.Primary( "Update Sprite Resources .sprite -> .spr" ) );
btn1.Clicked += () =>
{
ConvertResourceToNewFormat();
btn1.Enabled = false;
};
panel.Layout.AddSpacingCell( 4 );
var btn2 = panel.Layout.Add( new Button.Primary( "Replace .sprite -> .spr file paths in Code" ) );
btn2.Clicked += () =>
{
ConvertCodeToNewFormat();
btn2.Enabled = false;
};
}
}
Layout.AddStretchCell( 1 );
}
bool convertingToNewFormat = false;
async void ConvertResourceToNewFormat ()
{
if ( convertingToNewFormat ) return;
using var progress = Progress.Start( "Updating to new Sprite Tools format" );
convertingToNewFormat = true;
int index = 0;
foreach ( var sprite in OutdatedSprites )
{
var relativePath = sprite;
Progress.Update( relativePath, index, OutdatedSprites.Count );
index++;
var usedBy = AssetSystem.FindByPath( relativePath ).GetDependants( false );
var assetsFolder = Project.Current.GetAssetsPath();
var filePath = System.IO.Path.Combine( assetsFolder, relativePath );
if ( !System.IO.File.Exists( filePath ) )
continue;
var jsonStr = await System.IO.File.ReadAllTextAsync( filePath );
if ( string.IsNullOrWhiteSpace( jsonStr ) )
continue;
System.IO.File.Delete( filePath );
if ( System.IO.File.Exists( filePath + "_c" ) )
{
System.IO.File.Delete( filePath + "_c" );
}
var newRelativePath = System.IO.Path.ChangeExtension( relativePath, ".spr" );
var newFilePath = System.IO.Path.ChangeExtension( filePath, ".spr" );
System.IO.File.WriteAllText( newFilePath, jsonStr );
foreach ( var usingAsset in usedBy )
{
Progress.Update( $"Updating any references to {relativePath}", index, OutdatedSprites.Count );
var file = usingAsset.GetSourceFile( true );
if ( !System.IO.File.Exists( file ) )
continue;
var assetStr = await System.IO.File.ReadAllTextAsync( file );
assetStr = assetStr.Replace( relativePath, newRelativePath );
assetStr = assetStr.Replace( relativePath + "_c", newRelativePath + "_c" );
await System.IO.File.WriteAllTextAsync( file, assetStr );
}
convertingToNewFormat = false;
}
}
async void ConvertCodeToNewFormat ()
{
if ( convertingToNewFormat ) return;
using var progress = Progress.Start( "Updating code to new Sprite Tools format" );
convertingToNewFormat = true;
var codePath = Project.Current.GetCodePath();
var codeFiles = System.IO.Directory.GetFiles( codePath, "*.cs", System.IO.SearchOption.AllDirectories );
int index = 0;
foreach ( var file in codeFiles )
{
Progress.Update( $"Updating file {file}", index, codeFiles.Length );
index++;
if ( !System.IO.File.Exists( file ) )
continue;
var codeStr = await System.IO.File.ReadAllTextAsync( file );
if ( string.IsNullOrWhiteSpace( codeStr ) )
continue;
foreach ( var sprite in OutdatedSprites )
{
var relativePath = sprite;
var newRelativePath = System.IO.Path.ChangeExtension( relativePath, ".spr" );
codeStr = codeStr.Replace( relativePath, newRelativePath );
codeStr = codeStr.Replace( relativePath + "_c", newRelativePath + "_c" );
}
await System.IO.File.WriteAllTextAsync( file, codeStr );
}
convertingToNewFormat = false;
}
async void ConvertResourceToEngineFormat ()
{
using var progress = Progress.Start( "Updating to new in-engine Sprite resource format" );
int index = 0;
foreach ( var sprite in OutdatedSprites )
{
var relativePath = sprite;
Progress.Update( relativePath, index, OutdatedSprites.Count );
index++;
var usedBy = AssetSystem.FindByPath( relativePath ).GetDependants( false );
var assetsFolder = Project.Current.GetAssetsPath();
var filePath = System.IO.Path.Combine( assetsFolder, relativePath );
if ( !System.IO.File.Exists( filePath ) )
continue;
var jsonStr = await System.IO.File.ReadAllTextAsync( filePath );
if ( string.IsNullOrWhiteSpace( jsonStr ) )
continue;
// Update the JSON to the new format
var json = Json.ParseToJsonObject( jsonStr );
if ( json.TryGetPropertyValue( "Animations", out var animationsNode ) )
{
var animationsArray = animationsNode.AsArray();
if ( animationsArray is null ) continue;
// Loop through all animations and convert them to the new format
foreach ( var animEntry in animationsArray )
{
var animObject = animEntry.AsObject();
if ( animEntry is null ) continue;
// Check for Looping bool and set loopMode accordingly
var loopMode = Sprite.LoopMode.None;
if ( animObject.TryGetPropertyValue( "Looping", out var loopingNode ) )
{
loopMode = loopingNode.GetValue<bool>() ? Sprite.LoopMode.Loop : Sprite.LoopMode.None;
}
// Check for LoopMode string and set loopMode accordingly
if ( animObject.TryGetPropertyValue( "LoopMode", out var loopModeNode ) )
{
loopMode = loopModeNode.GetValue<string>() switch
{
"None" => Sprite.LoopMode.None,
"Forward" => Sprite.LoopMode.Loop,
"PingPong" => Sprite.LoopMode.PingPong,
_ => loopMode
};
}
// Re-create the frame list so SpriteAnimationFrame -> Texture
if ( animObject.TryGetPropertyValue( "Frames", out var framesNode ) )
{
var framesArray = framesNode.AsArray();
if ( framesArray is null ) continue;
var newFrames = new JsonArray();
foreach ( var frameEntry in framesArray )
{
var frameObject = frameEntry.AsObject();
if ( frameObject is null ) continue;
// Get the FilePath from the SpriteAnimationFrame
if ( frameObject.TryGetPropertyValue( "FilePath", out var filePathNode ) )
{
// Create a new texture generator for the frame
var frameFilePath = filePathNode.GetValue<string>();
if ( string.IsNullOrWhiteSpace( frameFilePath ) )
{
newFrames.Add( null );
continue;
}
var texture = Texture.Load( frameFilePath );
var resourceData = new JsonObject()
{
["FilePath"] = frameFilePath
};
// Check if SpriteSheetRect is set (not 0,0,0,0)
if ( frameObject.TryGetPropertyValue( "SpriteSheetRect", out var rectNode ) && rectNode is JsonObject rectObject )
{
if ( rectObject.TryGetPropertyValue( "Size", out var sizeNode ) && sizeNode is JsonValue sizeValue )
{
var sizeStr = sizeValue.GetValue<string>();
var sizeParts = sizeStr.Split( ',' );
if ( sizeParts.Length == 2 &&
int.TryParse( sizeParts[0], out var width ) &&
int.TryParse( sizeParts[1], out var height ) &&
( width > 0 || height > 0 ) )
{
// Add cropping to the texture generator
// Set up cropping based on SpriteSheetRect
if ( rectObject.TryGetPropertyValue( "Position", out var posNode ) && posNode is JsonValue posValue )
{
var posStr = posValue.GetValue<string>();
var posParts = posStr.Split( ',' );
if ( posParts.Length == 2 &&
int.TryParse( posParts[0], out var x ) &&
int.TryParse( posParts[1], out var y ) )
{
var cropping = new JsonObject();
cropping["Left"] = x;
cropping["Top"] = y;
cropping["Right"] = texture.Width - x - width;
cropping["Bottom"] = texture.Height - y - height;
resourceData["Cropping"] = cropping;
}
}
}
}
}
newFrames.Add( new JsonObject()
{
["Texture"] = new JsonObject()
{
["$compiler"] = "texture",
["$source"] = "imagefile",
["data"] = resourceData,
["compiled"] = null
}
} );
}
}
// Update the animation with it's new values
animObject["LoopMode"] = loopMode.ToString();
animObject["Frames"] = newFrames;
}
}
}
// Convert back to JSON string
jsonStr = json.ToJsonString( new()
{
WriteIndented = true
} );
System.IO.File.Delete( filePath );
if ( System.IO.File.Exists( filePath + "_c" ) )
{
System.IO.File.Delete( filePath + "_c" );
}
var newRelativePath = System.IO.Path.ChangeExtension( relativePath, ".sprite" );
var newFilePath = System.IO.Path.ChangeExtension( filePath, ".sprite" );
System.IO.File.WriteAllText( newFilePath, jsonStr );
foreach ( var usingAsset in usedBy )
{
Progress.Update( $"Updating any references to {relativePath}", index, OutdatedSprites.Count );
var file = usingAsset.GetSourceFile( true );
if ( !System.IO.File.Exists( file ) )
continue;
var assetStr = await System.IO.File.ReadAllTextAsync( file );
assetStr = assetStr.Replace( relativePath, newRelativePath );
assetStr = assetStr.Replace( relativePath + "_c", newRelativePath + "_c" );
assetStr = assetStr.Replace( "\"__type\": \"SpriteTools.SpriteComponent\"", "\"__type\": \"SpriteTools.SpriteRendererLayer\"" );
assetStr = assetStr.Replace( "\"component_type\": \"SpriteComponent\"", "\"__type\": \"SpriteRendererLayer\"" );
await System.IO.File.WriteAllTextAsync( file, assetStr );
}
}
convertingToNewFormat = false;
}
async void ConvertCodeToEngineFormat ()
{
using var progress = Progress.Start( "Updating code to use SpriteRenderer instead of SpriteComponent" );
var codePath = Project.Current.GetCodePath();
var codeFiles = System.IO.Directory.GetFiles( codePath, "*.cs", System.IO.SearchOption.AllDirectories );
int index = 0;
foreach ( var file in codeFiles )
{
Progress.Update( $"Updating file {file}", index, codeFiles.Length );
index++;
if ( !System.IO.File.Exists( file ) )
continue;
var codeStr = await System.IO.File.ReadAllTextAsync( file );
if ( string.IsNullOrWhiteSpace( codeStr ) )
continue;
// Replace SpriteComponent references with SpriteRenderer, let the user manually fix any errors that come from this
// since a lot of it is done on a case-by-case basis since not all properties are 1:1, but all features are.
codeStr = codeStr.Replace( "SpriteTools.SpriteComponent", "SpriteTools.SpriteRendererLayer" );
codeStr = codeStr.Replace( "SpriteTools.SpriteResource", "Sandbox.Sprite" );
codeStr = codeStr.Replace( "SpriteComponent", "SpriteRendererLayer" );
codeStr = codeStr.Replace( "SpriteResource", "Sprite" );
await System.IO.File.WriteAllTextAsync( file, codeStr );
}
}
}