Editor/CitizenRetarget/CitizenRetargetPaths.cs
namespace Editor.CitizenRetarget;
internal static class CitizenRetargetPaths
{
public const string LibraryName = "CitizenRetarget";
private const string ExternalPathPrefix = "__external__:";
public const string EditorRunLogFileName = "editor_run_log.txt";
public static string ProjectRoot => GetProjectRoot();
public static string AssetsRoot => ProjectAssetsRoot;
public static string ProjectAssetsRoot => GetAssetsRoot();
public static string DocsRoot => Path.Combine( ProjectRoot, "docs" );
public static string TempRoot => Path.Combine( ProjectRoot, ".tmp", "citizen_retarget" );
public static string PluginRoot => GetPluginRoot();
public static string PluginAssetsRoot => Path.Combine( PluginRoot, "Assets" );
public static string PluginDocsRoot => Path.Combine( PluginRoot, "docs" );
public static string PluginEditorRoot => Path.Combine( PluginRoot, "Editor", "CitizenRetarget" );
public static string NativeRoot => Path.Combine( PluginEditorRoot, "Native" );
public static string DataRoot => Path.Combine( PluginEditorRoot, "Data" );
public static string LocalSettingsRoot => Path.Combine( ProjectRoot, ".sbox", "citizen_retarget" );
public static string GetAssetAbsolutePath( string relativeAssetPath )
{
var relative = relativeAssetPath
.Replace( '\\', '/' )
.Trim()
.TrimStart( '/' );
if ( relative.StartsWith( "Assets/", StringComparison.OrdinalIgnoreCase ) )
relative = relative["Assets/".Length..];
return Path.Combine( ProjectAssetsRoot, relative.Replace( '/', Path.DirectorySeparatorChar ) );
}
public static string GetPluginAssetAbsolutePath( string relativeAssetPath )
{
var relative = relativeAssetPath
.Replace( '\\', '/' )
.Trim()
.TrimStart( '/' );
if ( relative.StartsWith( "Assets/", StringComparison.OrdinalIgnoreCase ) )
relative = relative["Assets/".Length..];
return Path.Combine( PluginAssetsRoot, relative.Replace( '/', Path.DirectorySeparatorChar ) );
}
public static string GetProjectAbsolutePath( string relativeProjectPath )
{
var relative = relativeProjectPath
.Replace( '\\', Path.DirectorySeparatorChar )
.Trim()
.TrimStart( Path.DirectorySeparatorChar );
return Path.Combine( ProjectRoot, relative );
}
public static string GetPluginAbsolutePath( string relativePluginPath )
{
var relative = relativePluginPath
.Replace( '\\', Path.DirectorySeparatorChar )
.Trim()
.TrimStart( Path.DirectorySeparatorChar );
return Path.Combine( PluginRoot, relative );
}
public static string GetTempPath( params string[] parts )
{
var segments = new List<string> { TempRoot };
foreach ( var part in parts )
{
if ( string.IsNullOrWhiteSpace( part ) )
continue;
var normalized = part
.Replace( '\\', Path.DirectorySeparatorChar )
.Replace( '/', Path.DirectorySeparatorChar )
.Trim()
.Trim( Path.DirectorySeparatorChar );
if ( string.IsNullOrWhiteSpace( normalized ) )
continue;
segments.Add( normalized );
}
return Path.Combine( segments.ToArray() );
}
public static string GetAssetResourcePath( string absoluteAssetPath )
{
var normalizedAbsolute = absoluteAssetPath.Replace( '\\', '/' );
var normalizedAssetsRoot = ProjectAssetsRoot.Replace( '\\', '/' ).TrimEnd( '/' );
if ( !normalizedAbsolute.StartsWith( normalizedAssetsRoot, StringComparison.OrdinalIgnoreCase ) )
throw new InvalidOperationException( $"Asset path '{absoluteAssetPath}' is outside the project Assets folder." );
return normalizedAbsolute[normalizedAssetsRoot.Length..].TrimStart( '/' );
}
public static string EncodeExternalPath( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
return string.Empty;
if ( path.StartsWith( ExternalPathPrefix, StringComparison.Ordinal ) )
return path;
var normalized = path.Trim();
if ( !Path.IsPathRooted( normalized ) )
return normalized;
return ExternalPathPrefix + Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes( normalized ) );
}
public static string DecodeExternalPath( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
return string.Empty;
if ( !path.StartsWith( ExternalPathPrefix, StringComparison.Ordinal ) )
return path;
var encoded = path[ExternalPathPrefix.Length..];
try
{
return System.Text.Encoding.UTF8.GetString( Convert.FromBase64String( encoded ) );
}
catch
{
return string.Empty;
}
}
private static string GetProjectRoot()
{
var root = Project.Current?.GetRootPath();
if ( string.IsNullOrWhiteSpace( root ) )
throw new InvalidOperationException( "Unable to resolve the current s&box project root" );
return root;
}
private static string GetAssetsRoot()
{
var assets = Project.Current?.GetAssetsPath();
if ( string.IsNullOrWhiteSpace( assets ) )
throw new InvalidOperationException( "Unable to resolve the current s&box Assets path" );
return assets;
}
private static string GetPluginRoot()
{
var libraryRoot = Path.Combine( ProjectRoot, "Libraries", LibraryName );
if ( Directory.Exists( Path.Combine( libraryRoot, "Editor", "CitizenRetarget" ) ) )
return libraryRoot;
var librariesRoot = Path.Combine( ProjectRoot, "Libraries" );
if ( Directory.Exists( librariesRoot ) )
{
foreach ( var candidate in Directory.GetDirectories( librariesRoot ) )
{
if ( Directory.Exists( Path.Combine( candidate, "Editor", "CitizenRetarget" ) ) )
return candidate;
}
}
var legacyRoot = ProjectRoot;
if ( Directory.Exists( Path.Combine( legacyRoot, "Editor", "CitizenRetarget" ) ) )
return legacyRoot;
return libraryRoot;
}
}