City/ModelSources.cs

Static helper that loads a Model from a .vmdl path, a mount:// path, or a Workshop package ident. Caches the load Task and mounts Workshop packages before loading their primary asset, falling back to Model.Error on failure.

External Download
namespace HorizonRP;

/// <summary>
/// Charge un modèle à partir de ce que le staff a saisi : un chemin « .vmdl »,
/// un chemin « mount://… » (jeu monté) ou un identifiant de paquet du Workshop
/// « org.paquet ». Chaque machine charge elle-même : le réseau ne transporte que
/// le texte (CityObject.ModelSource), jamais la ressource.
/// </summary>
public static class ModelSources
{
	static readonly Dictionary<string, Task<Model>> cache = new();

	/// <summary>Ce que l'API accepte aussi (modelOf dans api.php).</summary>
	public static bool LooksValid( string source )
	{
		if ( string.IsNullOrWhiteSpace( source ) || source.Length > 255 )
			return false;

		return source.EndsWith( ".vmdl", StringComparison.OrdinalIgnoreCase )
			|| source.StartsWith( "mount://", StringComparison.OrdinalIgnoreCase )
			|| Package.TryParseIdent( source, out _ );
	}

	public static Task<Model> Load( string source )
	{
		if ( string.IsNullOrWhiteSpace( source ) )
			return Task.FromResult( Model.Error );

		// Un modèle en erreur n'est pas gardé : son paquet peut être monté un peu plus tard.
		if ( !cache.TryGetValue( source, out var task ) || task.IsCompleted && (task.Result is null || task.Result.IsError) )
		{
			task = LoadUncached( source );
			cache[source] = task;
		}

		return task;
	}

	static async Task<Model> LoadUncached( string source )
	{
		try
		{
			if ( source.EndsWith( ".vmdl", StringComparison.OrdinalIgnoreCase ) || source.StartsWith( "mount://", StringComparison.OrdinalIgnoreCase ) )
				return await Model.LoadAsync( source ) ?? Model.Error;

			// Un paquet du Workshop : on le télécharge et on prend son modèle principal.
			var package = await Package.Fetch( source, false );
			if ( package is null || !string.Equals( package.TypeName, "model", StringComparison.OrdinalIgnoreCase ) )
			{
				Log.Warning( $"[Horizon] {source} n'est pas un modèle du Workshop" );
				return Model.Error;
			}

			await package.MountAsync();
			return await Model.LoadAsync( package.PrimaryAsset ) ?? Model.Error;
		}
		catch ( Exception e )
		{
			Log.Warning( $"[Horizon] Modèle {source} illisible : {e.Message}" );
			return Model.Error;
		}
	}
}