Files
sbox-public/engine/Sandbox.Tools/Assets/AssetSystem.Cloud.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

340 lines
9.5 KiB
C#

using Sandbox.Engine;
using System;
using System.Text.Json.Nodes;
using System.Threading;
namespace Editor;
public static partial class AssetSystem
{
internal static CloudAssetDirectory CloudDirectory { get; set; }
/// <inheritdoc cref="InstallAsync(Package, bool, Action{float}, CancellationToken)"/>
public static async Task<Asset> InstallAsync( string packageIdent, bool skipIfInstalled = true, Action<float> loading = null, CancellationToken token = default )
{
loading?.Invoke( 0.0f );
var package = await Package.FetchAsync( packageIdent, false, false );
return await InstallAsync( package, skipIfInstalled, loading, token );
}
/// <summary>
/// Install a cloud package. Will return the primary asset on completion (if it has one)
/// </summary>
/// <param name="package"></param>
/// <param name="skipIfInstalled">Skip downloading the remote package if any version of this package is already installed.</param>
/// <param name="loading"></param>
/// <param name="token"></param>
public static async Task<Asset> InstallAsync( Package package, bool skipIfInstalled = true, Action<float> loading = null, CancellationToken token = default )
{
if ( package == null )
return null;
if ( package.Revision == null )
return null;
if ( !CanCloudInstall( package ) )
return null;
if ( !skipIfInstalled || !IsCloudInstalled( package ) )
{
// download the manifest
await package.Revision.DownloadManifestAsync( token );
await DownloadCloudFiles( package, loading, token );
foreach ( var file in package.Revision.Manifest.Files )
{
var fullPath = FileSystem.Cloud.GetFullPath( file.Path );
var asset = RegisterFile( fullPath );
if ( asset is null )
continue;
asset.Package = package;
// update ref version on any assets that depend on this
ReplaceReferences( asset.GetDependants( false ), package, package );
}
CloudDirectory.AddPackage( package );
EditorEvent.Run( "package.changed.installed", package );
}
var primaryAssetName = package.PrimaryAsset;
return FindByPath( primaryAssetName );
}
internal static void UninstallPackage( Package package )
{
if ( !IsCloudInstalled( package ) )
{
return;
}
var contents = GetPackageFiles( package );
CloudDirectory.RemovePackage( package );
foreach ( var file in contents )
{
var fullPath = FileSystem.Cloud.GetFullPath( file );
var asset = FindByPath( fullPath );
if ( asset is null || asset.Package != package )
continue;
// some files are shared between packages, so check if any other packages need this file
var dependants = asset.GetDependants( false );
Package requiredBy = null;
foreach ( var dependant in dependants )
{
if ( dependant.Package is null ) continue;
if ( !IsCloudInstalled( dependant.Package ) )
continue;
requiredBy = dependant.Package;
break;
}
if ( requiredBy == null )
{
// no other packages need this, we can delete it
asset.Delete();
}
else
{
// update any assets that depend on this to point to the package that remains
ReplaceReferences( asset.GetDependants( false ), package, requiredBy );
asset.Package = requiredBy;
}
}
}
/// <summary>
/// Replaces any references in <paramref name="assets"/> to <paramref name="fromPackage"/> with a reference to <paramref name="toPackage"/>
/// (does a replace by ident to avoid having to open the map etc)
/// </summary>
private static void ReplaceReferences( IEnumerable<Asset> assets, Package fromPackage, Package toPackage )
{
if ( assets.Count() < 1 ) return;
string newIdent = toPackage?.GetIdent( false, true );
string projectPath = Project.Current.GetAssetsPath().Replace( '\\', '/' );
foreach ( var asset in assets )
{
// more bs
if ( !asset.AbsolutePath.StartsWith( projectPath, StringComparison.OrdinalIgnoreCase ) )
continue;
if ( !asset.AssetType.IsGameResource )
{
var config = asset?.Publishing?.ProjectConfig;
if ( config is null || config.EditorReferences is null )
continue;
int count = config.EditorReferences.RemoveAll( fromPackage.IsNamed );
if ( count > 0 )
{
if ( newIdent != null && !config.EditorReferences.Contains( newIdent ) )
config.EditorReferences.Add( newIdent );
config.EditorReferences = config.EditorReferences.OrderBy( x => x ).ToList();
asset.MetaData.Set( "publish", asset.Publishing );
}
}
else if ( typeof( GameResource ).IsAssignableFrom( asset.AssetType.ResourceType ) && asset.LoadResource() is GameResource gameResource )
{
string filename = asset.GetSourceFile( true );
if ( string.IsNullOrWhiteSpace( filename ) )
continue;
var json = System.IO.File.ReadAllText( filename );
if ( JsonNode.Parse( json ) is not JsonObject jso )
continue;
if ( jso["__references"] is not JsonArray references )
continue;
var newReferences = references.Select( x => x.ToString() ).ToList();
int count = newReferences.RemoveAll( fromPackage.IsNamed );
if ( count > 0 )
{
if ( newIdent != null && !newReferences.Contains( newIdent ) )
newReferences.Add( newIdent );
jso["__references"] = JsonValue.Create( newReferences.OrderBy( x => x ) );
gameResource.SaveToDisk( filename, jso.ToJsonString( Json.options ) );
}
}
}
}
/// <summary>
/// Gets the locally installed package revision by ident
/// </summary>
public static Package.IRevision GetInstalledRevision( string packageIdent )
{
return CloudDirectory.FindPackage( packageIdent )?.Revision;
}
/// <summary>
/// Is this package installed in our cloud directory?
/// </summary>
public static bool IsCloudInstalled( string packageIdent )
{
return CloudDirectory.FindPackage( packageIdent ) != null;
}
/// <summary>
/// Is a version this package installed in our cloud directory?
/// </summary>
public static bool IsCloudInstalled( Package package, bool exactVersion = false )
{
if ( package is null ) return false;
if ( package.Org is null )
{
Log.Trace( $"{package.FullIdent} no org" );
return false;
}
if ( !package.IsRemote )
{
Log.Trace( $"{package.FullIdent} not remote" );
return false;
}
var local = CloudDirectory.FindPackage( package.FullIdent );
if ( local is null )
{
Log.Trace( $"{package.FullIdent} wasn't found in CloudDirectory" );
return false;
}
if ( exactVersion && package.Revision is not null && package.Revision.VersionId != local.Revision.VersionId )
{
Log.Trace( $"{package.Revision.VersionId} != {local.Revision.VersionId} version is different" );
return false; // version is different
}
return true;
}
/// <summary>
/// Get all packages in the download cache
/// </summary>
public static IReadOnlyCollection<Package> GetInstalledPackages()
{
return CloudDirectory.GetPackages();
}
/// <summary>
/// Get all packages, referenced by assets in the current project, in the download cache
/// </summary>
public static IReadOnlyCollection<Package> GetReferencedPackages()
{
return CloudAsset.GetAssetReferences( true )
.Select( CloudDirectory.FindPackage )
.Where( p => p != null ).ToList();
}
public static IReadOnlyCollection<string> GetPackageFiles( Package package )
{
return CloudDirectory.GetPackageFiles( package ).ToList();
}
/// <summary>
/// Is this package type something we can install?
/// </summary>
public static bool CanCloudInstall( Package package )
{
if ( package.TypeName == "map" ) return false;
if ( package.TypeName == "collection" ) return false;
return true;
}
/// <summary>
/// Initialize the files from the
/// </summary>
private static async Task DownloadCloudFiles( Package package, Action<float> progress, CancellationToken token )
{
long totalSize = package.Revision.Manifest.Files.Sum( x => x.Size );
long downloaded = 0;
IToolsDll.Current?.RunEvent( "package.download.start", package, token );
//
// Process 16 files at a time
//
await package.Revision.Manifest.Files.ForEachTaskAsync( async ( e ) =>
{
await DownloadFile( package, e, token );
downloaded += e.Size;
float frac = (float)((double)downloaded / (double)totalSize);
progress?.Invoke( frac );
IToolsDll.Current?.RunEvent( "package.download.update", package, frac );
}, 16 );
IToolsDll.Current?.RunEvent( "package.download.complete", package );
}
static async Task DownloadFile( Package package, ManifestSchema.File entry, CancellationToken token )
{
ThreadSafe.AssertIsMainThread();
//
// Ignore thumbnails
//
string path = entry.Path.StartsWith( "/" ) ? entry.Path : $"/{entry.Path}";
if ( path == $"/{package.FullIdent}/thumb.png" )
return;
//
// Ignore assemblies
//
if ( entry.Path.EndsWith( ".dll" ) )
return;
//
// This file exists in core, no need to download it (at this moment)
//
if ( EngineFileSystem.CoreContent.FileExists( entry.Path ) )
return;
//
// File exists - we should probably check crcs and shit
//
if ( FileSystem.Cloud.FileExists( entry.Path ) && FileSystem.Cloud.FileSize( entry.Path ) == entry.Size )
return;
var targetPath = System.IO.Path.GetDirectoryName( entry.Path );
FileSystem.Cloud.CreateDirectory( targetPath );
var targetFile = FileSystem.Cloud.GetFullPath( entry.Path );
if ( System.IO.File.Exists( targetFile ) )
{
System.IO.File.Delete( targetFile );
}
CloudDirectory.AddFile( entry.Path, entry.Crc, entry.Size, package.FullIdent, package.Revision.VersionId );
if ( entry.Size == 0 )
{
await System.IO.File.WriteAllTextAsync( targetFile, "" );
}
else
{
var url = $"{entry.Url}";
await Sandbox.Utility.Web.DownloadFile( url, targetFile, token );
}
}
}