// MonoGame - Copyright (C) The MonoGame Team // This file is subject to the terms and conditions defined in // file 'LICENSE.txt', which is part of this source code package. using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; using Microsoft.Xna.Framework.Graphics; using System.Globalization; using Microsoft.Xna.Framework.Content.Pipeline.Builder.Convertors; using System.Diagnostics; namespace MonoGame.Framework.Content.Pipeline.Builder { public class PipelineManager { [DebuggerDisplay("ImporterInfo: {type.Name}")] private struct ImporterInfo { public ContentImporterAttribute attribute; public Type type; public DateTime assemblyTimestamp; }; private List _importers; [DebuggerDisplay("ProcessorInfo: {type.Name}")] private struct ProcessorInfo { public ContentProcessorAttribute attribute; public Type type; public DateTime assemblyTimestamp; }; private List _processors; private List _writers; // Keep track of all built assets. (Required to resolve automatic names "AssetName_n".) // Key = absolute, normalized path of source file // Value = list of build events // (Note: When using external references, an asset may be built multiple times // with different parameters.) private readonly Dictionary> _pipelineBuildEvents; // Store default values for content processor parameters. (Necessary to compare processor // parameters. See PipelineBuildEvent.AreParametersEqual.) // Key = name of content processor // Value = processor parameters private readonly Dictionary _processorDefaultValues; public string ProjectDirectory { get; private set; } public string OutputDirectory { get; private set; } public string IntermediateDirectory { get; private set; } public ContentStatsCollection ContentStats { get; private set; } private ContentCompiler _compiler; public ContentBuildLogger Logger { get; set; } public List Assemblies { get; private set; } /// /// The current target graphics profile for which all content is built. /// public GraphicsProfile Profile { get; set; } /// /// The current target platform for which all content is built. /// public TargetPlatform Platform { get; set; } /// /// The build configuration passed thru to content processors. /// public string Config { get; set; } /// /// Gets or sets if the content is compressed. /// public bool CompressContent { get; set; } /// /// If true exceptions thrown from within an importer or processor are caught and then /// thrown from the context. Default value is true. /// public bool RethrowExceptions { get; set; } public PipelineManager(string projectDir, string outputDir, string intermediateDir) { _pipelineBuildEvents = new Dictionary>(); _processorDefaultValues = new Dictionary(); RethrowExceptions = true; Assemblies = new List(); Assemblies.Add(null); Logger = new PipelineBuildLogger(); ProjectDirectory = PathHelper.NormalizeDirectory(projectDir); OutputDirectory = PathHelper.NormalizeDirectory(outputDir); IntermediateDirectory = PathHelper.NormalizeDirectory(intermediateDir); RegisterCustomConverters(); // Load the previous content stats. ContentStats = new ContentStatsCollection(); ContentStats.PreviousStats = ContentStatsCollection.Read(intermediateDir); } public void AssignTypeConverter () { TypeDescriptor.AddAttributes (typeof (TType), new TypeConverterAttribute (typeof (TTypeConverter))); } private void RegisterCustomConverters () { AssignTypeConverter (); } public void AddAssembly(string assemblyFilePath) { if (assemblyFilePath == null) throw new ArgumentException("assemblyFilePath cannot be null!"); if (!Path.IsPathRooted(assemblyFilePath)) throw new ArgumentException("assemblyFilePath must be absolute!"); // Make sure we're not adding the same assembly twice. assemblyFilePath = PathHelper.Normalize(assemblyFilePath); if (!Assemblies.Contains(assemblyFilePath)) { Assemblies.Add(assemblyFilePath); //TODO need better way to update caches _processors = null; _importers = null; _writers = null; } } private void ResolveAssemblies() { _importers = new List(); _processors = new List(); _writers = new List(); // Finally load the pipeline assemblies. foreach (var assemblyPath in Assemblies) { Type[] exportedTypes; DateTime assemblyTimestamp; try { Assembly a; if (string.IsNullOrEmpty(assemblyPath)) a = Assembly.GetExecutingAssembly(); else a = Assembly.LoadFrom(assemblyPath); exportedTypes = a.GetTypes(); assemblyTimestamp = File.GetLastWriteTime(a.Location); } catch (BadImageFormatException e) { Logger.LogWarning(null, null, "Assembly is either corrupt or built using a different " + "target platform than this process. Reference another target architecture (x86, x64, " + "AnyCPU, etc.) of this assembly. '{0}': {1}", assemblyPath, e.Message); // The assembly failed to load... nothing // we can do but ignore it. continue; } catch (Exception e) { Logger.LogWarning(null, null, "Failed to load assembly '{0}': {1}", assemblyPath, e.Message); continue; } foreach (var t in exportedTypes) { if (t.IsAbstract) continue; if (t.GetInterface(@"IContentImporter") != null) { var attributes = t.GetCustomAttributes(typeof (ContentImporterAttribute), false); if (attributes.Length != 0) { var importerAttribute = attributes[0] as ContentImporterAttribute; _importers.Add(new ImporterInfo { attribute = importerAttribute, type = t, assemblyTimestamp = assemblyTimestamp }); } else { // If no attribute specify default one var importerAttribute = new ContentImporterAttribute(".*"); importerAttribute.DefaultProcessor = ""; importerAttribute.DisplayName = t.Name; _importers.Add(new ImporterInfo { attribute = importerAttribute, type = t, assemblyTimestamp = assemblyTimestamp }); } } else if (t.GetInterface(@"IContentProcessor") != null) { var attributes = t.GetCustomAttributes(typeof (ContentProcessorAttribute), false); if (attributes.Length != 0) { var processorAttribute = attributes[0] as ContentProcessorAttribute; _processors.Add(new ProcessorInfo { attribute = processorAttribute, type = t, assemblyTimestamp = assemblyTimestamp }); } } else if (t.GetInterface(@"ContentTypeWriter") != null) { // TODO: This doesn't work... how do i find these? _writers.Add(t); } } } } public Type[] GetImporterTypes() { if (_importers == null) ResolveAssemblies(); List types = new List(); foreach (var item in _importers) { types.Add(item.type); } return types.ToArray(); } public Type[] GetProcessorTypes() { if (_processors == null) ResolveAssemblies(); List types = new List(); foreach (var item in _processors) { types.Add(item.type); } return types.ToArray(); } public IContentImporter CreateImporter(string name) { if (_importers == null) ResolveAssemblies(); // Search for the importer. foreach (var info in _importers) { if (info.type.Name.Equals(name)) return Activator.CreateInstance(info.type) as IContentImporter; } return null; } public string FindImporterByExtension(string ext) { if (_importers == null) ResolveAssemblies(); // Search for the importer. foreach (var info in _importers) { if (info.attribute.FileExtensions.Any(e => e.Equals(ext, StringComparison.InvariantCultureIgnoreCase))) return info.type.Name; } return null; } public DateTime GetImporterAssemblyTimestamp(string name) { if (_importers == null) ResolveAssemblies(); // Search for the importer. foreach (var info in _importers) { if (info.type.Name.Equals(name)) return info.assemblyTimestamp; } return DateTime.MaxValue; } public string FindDefaultProcessor(string importer) { if (_importers == null) ResolveAssemblies(); // Search for the importer. foreach (var info in _importers) { if (info.type.Name == importer) return info.attribute.DefaultProcessor; } return null; } public Type GetProcessorType(string name) { if (_processors == null) ResolveAssemblies(); // Search for the processor type. foreach (var info in _processors) { if (info.type.Name.Equals(name)) return info.type; } return null; } public void ResolveImporterAndProcessor(string sourceFilepath, ref string importerName, ref string processorName) { // Resolve the importer name. if (string.IsNullOrEmpty(importerName)) importerName = FindImporterByExtension(Path.GetExtension(sourceFilepath)); if (string.IsNullOrEmpty(importerName)) throw new Exception(string.Format("Couldn't find a default importer for '{0}'!", sourceFilepath)); // Resolve the processor name. if (string.IsNullOrEmpty(processorName)) processorName = FindDefaultProcessor(importerName); if (string.IsNullOrEmpty(processorName)) throw new Exception(string.Format("Couldn't find a default processor for importer '{0}'!", importerName)); } public IContentProcessor CreateProcessor(string name, OpaqueDataDictionary processorParameters) { var processorType = GetProcessorType(name); if (processorType == null) return null; // Create the processor. var processor = (IContentProcessor)Activator.CreateInstance(processorType); // Convert and set the parameters on the processor. foreach (var param in processorParameters) { var propInfo = processorType.GetProperty(param.Key, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); if (propInfo == null || propInfo.GetSetMethod(false) == null) continue; // If the property value is already of the correct type then set it. if (propInfo.PropertyType.IsInstanceOfType(param.Value)) propInfo.SetValue(processor, param.Value, null); else { // Find a type converter for this property. var typeConverter = TypeDescriptor.GetConverter(propInfo.PropertyType); if (typeConverter.CanConvertFrom(param.Value.GetType())) { var propValue = typeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, param.Value); propInfo.SetValue(processor, propValue, null); } } } return processor; } /// /// Gets the default values for the content processor parameters. /// /// The name of the content processor. /// /// A dictionary containing the default value for each parameter. Returns /// if the content processor has not been created yet. /// public OpaqueDataDictionary GetProcessorDefaultValues(string processorName) { // null is not allowed as key in dictionary. if (processorName == null) processorName = string.Empty; OpaqueDataDictionary defaultValues; if (!_processorDefaultValues.TryGetValue(processorName, out defaultValues)) { // Create the content processor instance and read the default values. defaultValues = new OpaqueDataDictionary(); var processorType = GetProcessorType(processorName); if (processorType != null) { try { var processor = (IContentProcessor)Activator.CreateInstance(processorType); var properties = processorType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); foreach (var property in properties) defaultValues.Add(property.Name, property.GetValue(processor, null)); } catch { // Ignore exception. Will be handled in ProcessContent. } } _processorDefaultValues.Add(processorName, defaultValues); } return defaultValues; } public DateTime GetProcessorAssemblyTimestamp(string name) { if (_processors == null) ResolveAssemblies(); // Search for the processor. foreach (var info in _processors) { if (info.type.Name.Equals(name)) return info.assemblyTimestamp; } return DateTime.MaxValue; } public OpaqueDataDictionary ValidateProcessorParameters(string name, OpaqueDataDictionary processorParameters) { var result = new OpaqueDataDictionary(); var processorType = GetProcessorType(name); if (processorType == null || processorParameters == null) { return result; } foreach (var param in processorParameters) { var propInfo = processorType.GetProperty(param.Key, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); if (propInfo == null || propInfo.GetSetMethod(false) == null) continue; // Make sure we can assign the value. if (!propInfo.PropertyType.IsInstanceOfType(param.Value)) { // Make sure we can convert the value. var typeConverter = TypeDescriptor.GetConverter(propInfo.PropertyType); if (!typeConverter.CanConvertFrom(param.Value.GetType())) continue; } result.Add(param.Key, param.Value); } return result; } private void ResolveOutputFilepath(string sourceFilepath, ref string outputFilepath) { // If the output path is null... build it from the source file path. if (string.IsNullOrEmpty(outputFilepath)) { var filename = Path.GetFileNameWithoutExtension(sourceFilepath) + ".xnb"; var directory = PathHelper.GetRelativePath(ProjectDirectory, Path.GetDirectoryName(sourceFilepath) + Path.DirectorySeparatorChar); outputFilepath = Path.Combine(OutputDirectory, directory, filename); } else { // If the extension is not XNB or the source file extension then add XNB. var sourceExt = Path.GetExtension(sourceFilepath); if (outputFilepath.EndsWith(sourceExt, StringComparison.InvariantCultureIgnoreCase)) outputFilepath = outputFilepath.Substring(0, outputFilepath.Length - sourceExt.Length); if (!outputFilepath.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) outputFilepath += ".xnb"; // If the path isn't rooted then put it into the output directory. if (!Path.IsPathRooted(outputFilepath)) outputFilepath = Path.Combine(OutputDirectory, outputFilepath); } outputFilepath = PathHelper.Normalize(outputFilepath); } private PipelineBuildEvent LoadBuildEvent(string destFile, out string eventFilepath) { var contentPath = Path.ChangeExtension(PathHelper.GetRelativePath(OutputDirectory, destFile), PipelineBuildEvent.Extension); eventFilepath = Path.Combine(IntermediateDirectory, contentPath); return PipelineBuildEvent.Load(eventFilepath); } public void RegisterContent(string sourceFilepath, string outputFilepath = null, string importerName = null, string processorName = null, OpaqueDataDictionary processorParameters = null) { sourceFilepath = PathHelper.Normalize(sourceFilepath); ResolveOutputFilepath(sourceFilepath, ref outputFilepath); ResolveImporterAndProcessor(sourceFilepath, ref importerName, ref processorName); var contentEvent = new PipelineBuildEvent { SourceFile = sourceFilepath, DestFile = outputFilepath, Importer = importerName, Processor = processorName, Parameters = ValidateProcessorParameters(processorName, processorParameters), }; // Register pipeline build event. (Required to correctly resolve external dependencies.) TrackPipelineBuildEvent(contentEvent); } public PipelineBuildEvent BuildContent(string sourceFilepath, string outputFilepath = null, string importerName = null, string processorName = null, OpaqueDataDictionary processorParameters = null) { sourceFilepath = PathHelper.Normalize(sourceFilepath); ResolveOutputFilepath(sourceFilepath, ref outputFilepath); ResolveImporterAndProcessor(sourceFilepath, ref importerName, ref processorName); // Record what we're building and how. var contentEvent = new PipelineBuildEvent { SourceFile = sourceFilepath, DestFile = outputFilepath, Importer = importerName, Processor = processorName, Parameters = ValidateProcessorParameters(processorName, processorParameters), }; // Load the previous content event if it exists. string eventFilepath; var cachedEvent = LoadBuildEvent(contentEvent.DestFile, out eventFilepath); BuildContent(contentEvent, cachedEvent, eventFilepath); return contentEvent; } private void BuildContent(PipelineBuildEvent pipelineEvent, PipelineBuildEvent cachedEvent, string eventFilepath) { if (!File.Exists(pipelineEvent.SourceFile)) { Logger.LogMessage("{0}", pipelineEvent.SourceFile); throw new PipelineException("The source file '{0}' does not exist!", pipelineEvent.SourceFile); } Logger.PushFile(pipelineEvent.SourceFile); // Keep track of all build events. (Required to resolve automatic names "AssetName_n".) TrackPipelineBuildEvent(pipelineEvent); var rebuild = pipelineEvent.NeedsRebuild(this, cachedEvent); if (rebuild) Logger.LogMessage("{0}", pipelineEvent.SourceFile); else Logger.LogMessage("Skipping {0}", pipelineEvent.SourceFile); Logger.Indent(); try { if (!rebuild) { // While this asset doesn't need to be rebuilt the dependent assets might. foreach (var asset in cachedEvent.BuildAsset) { string assetEventFilepath; var assetCachedEvent = LoadBuildEvent(asset, out assetEventFilepath); // If we cannot find the cached event for the dependancy // then we have to trigger a rebuild of the parent content. if (assetCachedEvent == null) { rebuild = true; break; } var depEvent = new PipelineBuildEvent { SourceFile = assetCachedEvent.SourceFile, DestFile = assetCachedEvent.DestFile, Importer = assetCachedEvent.Importer, Processor = assetCachedEvent.Processor, Parameters = assetCachedEvent.Parameters, }; // Give the asset a chance to rebuild. BuildContent(depEvent, assetCachedEvent, assetEventFilepath); } } // Do we need to rebuild? if (rebuild) { var startTime = DateTime.UtcNow; // Import and process the content. var processedObject = ProcessContent(pipelineEvent); // Write the content to disk. WriteXnb(processedObject, pipelineEvent); // Store the timestamp of the DLLs containing the importer and processor. pipelineEvent.ImporterTime = GetImporterAssemblyTimestamp(pipelineEvent.Importer); pipelineEvent.ProcessorTime = GetProcessorAssemblyTimestamp(pipelineEvent.Processor); // Store the new event into the intermediate folder. pipelineEvent.Save(eventFilepath); var buildTime = DateTime.UtcNow - startTime; // Record stat for this file. ContentStats.RecordStats(pipelineEvent.SourceFile, pipelineEvent.DestFile, pipelineEvent.Processor, processedObject.GetType(), (float)buildTime.TotalSeconds); } else { // Copy the stats from the previous build. ContentStats.CopyPreviousStats(pipelineEvent.SourceFile); } } finally { Logger.Unindent(); Logger.PopFile(); } } public object ProcessContent(PipelineBuildEvent pipelineEvent) { if (!File.Exists(pipelineEvent.SourceFile)) throw new PipelineException("The source file '{0}' does not exist!", pipelineEvent.SourceFile); // Store the last write time of the source file // so we can detect if it has been changed. pipelineEvent.SourceTime = File.GetLastWriteTime(pipelineEvent.SourceFile); // Make sure we can find the importer and processor. var importer = CreateImporter(pipelineEvent.Importer); if (importer == null) throw new PipelineException("Failed to create importer '{0}'", pipelineEvent.Importer); // Try importing the content. object importedObject; if (RethrowExceptions) { try { var importContext = new PipelineImporterContext(this); importedObject = importer.Import(pipelineEvent.SourceFile, importContext); } catch (PipelineException) { throw; } catch (Exception inner) { throw new PipelineException(string.Format("Importer '{0}' had unexpected failure!", pipelineEvent.Importer), inner); } } else { var importContext = new PipelineImporterContext(this); importedObject = importer.Import(pipelineEvent.SourceFile, importContext); } // The pipelineEvent.Processor can be null or empty. In this case the // asset should be imported but not processed. if (string.IsNullOrEmpty(pipelineEvent.Processor)) return importedObject; var processor = CreateProcessor(pipelineEvent.Processor, pipelineEvent.Parameters); if (processor == null) throw new PipelineException("Failed to create processor '{0}'", pipelineEvent.Processor); // Make sure the input type is valid. if (!processor.InputType.IsAssignableFrom(importedObject.GetType())) { throw new PipelineException( string.Format("The type '{0}' cannot be processed by {1} as a {2}!", importedObject.GetType().FullName, pipelineEvent.Processor, processor.InputType.FullName)); } // Process the imported object. object processedObject; if (RethrowExceptions) { try { var processContext = new PipelineProcessorContext(this, pipelineEvent); processedObject = processor.Process(importedObject, processContext); } catch (PipelineException) { throw; } catch (InvalidContentException) { throw; } catch (Exception inner) { throw new PipelineException(string.Format("Processor '{0}' had unexpected failure!", pipelineEvent.Processor), inner); } } else { var processContext = new PipelineProcessorContext(this, pipelineEvent); processedObject = processor.Process(importedObject, processContext); } return processedObject; } public void CleanContent(string sourceFilepath, string outputFilepath = null) { // First try to load the event file. ResolveOutputFilepath(sourceFilepath, ref outputFilepath); string eventFilepath; var cachedEvent = LoadBuildEvent(outputFilepath, out eventFilepath); if (cachedEvent != null) { // Recursively clean additional (nested) assets. foreach (var asset in cachedEvent.BuildAsset) { string assetEventFilepath; var assetCachedEvent = LoadBuildEvent(asset, out assetEventFilepath); if (assetCachedEvent == null) { Logger.LogMessage("Cleaning {0}", asset); // Remove asset (.xnb file) from output folder. FileHelper.DeleteIfExists(asset); // Remove event file (.mgcontent file) from intermediate folder. FileHelper.DeleteIfExists(assetEventFilepath); continue; } CleanContent(string.Empty, asset); } // Remove related output files (non-XNB files) that were copied to the output folder. foreach (var asset in cachedEvent.BuildOutput) { Logger.LogMessage("Cleaning {0}", asset); FileHelper.DeleteIfExists(asset); } } Logger.LogMessage("Cleaning {0}", outputFilepath); // Remove asset (.xnb file) from output folder. FileHelper.DeleteIfExists(outputFilepath); // Remove event file (.mgcontent file) from intermediate folder. FileHelper.DeleteIfExists(eventFilepath); _pipelineBuildEvents.Remove(sourceFilepath); } private void WriteXnb(object content, PipelineBuildEvent pipelineEvent) { // Make sure the output directory exists. var outputFileDir = Path.GetDirectoryName(pipelineEvent.DestFile); Directory.CreateDirectory(outputFileDir); if (_compiler == null) _compiler = new ContentCompiler(); // Write the XNB. using (var stream = new FileStream(pipelineEvent.DestFile, FileMode.Create, FileAccess.Write, FileShare.None)) _compiler.Compile(stream, content, Platform, Profile, CompressContent, OutputDirectory, outputFileDir); // Store the last write time of the output XNB here // so we can verify it hasn't been tampered with. pipelineEvent.DestTime = File.GetLastWriteTime(pipelineEvent.DestFile); } /// /// Stores the pipeline build event (in memory) if no matching event is found. /// /// The pipeline build event. private void TrackPipelineBuildEvent(PipelineBuildEvent pipelineEvent) { List pipelineBuildEvents; bool eventsFound = _pipelineBuildEvents.TryGetValue(pipelineEvent.SourceFile, out pipelineBuildEvents); if (!eventsFound) { pipelineBuildEvents = new List(); _pipelineBuildEvents.Add(pipelineEvent.SourceFile, pipelineBuildEvents); } if (FindMatchingEvent(pipelineBuildEvents, pipelineEvent.DestFile, pipelineEvent.Importer, pipelineEvent.Processor, pipelineEvent.Parameters) == null) pipelineBuildEvents.Add(pipelineEvent); } /// /// Gets an automatic asset name, such as "AssetName_0". /// /// The source file name. /// The name of the content importer. Can be . /// The name of the content processor. Can be . /// The processor parameters. Can be . /// The asset name. public string GetAssetName(string sourceFileName, string importerName, string processorName, OpaqueDataDictionary processorParameters) { Debug.Assert(Path.IsPathRooted(sourceFileName), "Absolute path expected."); // Get source file name, which is used for lookup in _pipelineBuildEvents. sourceFileName = PathHelper.Normalize(sourceFileName); string relativeSourceFileName = PathHelper.GetRelativePath(ProjectDirectory, sourceFileName); List pipelineBuildEvents; if (_pipelineBuildEvents.TryGetValue(sourceFileName, out pipelineBuildEvents)) { // This source file has already been build. // --> Compare pipeline build events. ResolveImporterAndProcessor(sourceFileName, ref importerName, ref processorName); var matchingEvent = FindMatchingEvent(pipelineBuildEvents, null, importerName, processorName, processorParameters); if (matchingEvent != null) { // Matching pipeline build event found. string existingName = matchingEvent.DestFile; existingName = PathHelper.GetRelativePath(OutputDirectory, existingName); existingName = existingName.Substring(0, existingName.Length - 4); // Remove ".xnb". return existingName; } Logger.LogMessage(string.Format("Warning: Asset {0} built multiple times with different settings.", relativeSourceFileName)); } // No pipeline build event with matching settings found. // Get default asset name (= output file name relative to output folder without ".xnb"). string directoryName = Path.GetDirectoryName(relativeSourceFileName); string fileName = Path.GetFileNameWithoutExtension(relativeSourceFileName); string assetName = Path.Combine(directoryName, fileName); assetName = PathHelper.Normalize(assetName); return AppendAssetNameSuffix(assetName); } /// /// Determines whether the specified list contains a matching pipeline build event. /// /// The list of pipeline build events. /// Absolute path to the output file. Can be . /// The name of the content importer. Can be . /// The name of the content processor. Can be . /// The processor parameters. Can be . /// /// The matching pipeline build event, or . /// private PipelineBuildEvent FindMatchingEvent(List pipelineBuildEvents, string destFile, string importerName, string processorName, OpaqueDataDictionary processorParameters) { foreach (var existingBuildEvent in pipelineBuildEvents) { if ((destFile == null || existingBuildEvent.DestFile.Equals(destFile)) && existingBuildEvent.Importer == importerName && existingBuildEvent.Processor == processorName) { var defaultValues = GetProcessorDefaultValues(processorName); if (PipelineBuildEvent.AreParametersEqual(existingBuildEvent.Parameters, processorParameters, defaultValues)) { return existingBuildEvent; } } } return null; } /// /// Gets the asset name including a suffix, such as "_0". (The number is incremented /// automatically. /// /// /// The asset name without suffix (relative to output folder). /// /// The asset name with suffix. private string AppendAssetNameSuffix(string baseAssetName) { int index = 0; string assetName = baseAssetName + "_0"; while (IsAssetNameUsed(assetName)) { index++; assetName = baseAssetName + '_' + index; } return assetName; } /// /// Determines whether the specified asset name is already used. /// /// The asset name (relative to output folder). /// /// if the asset name is already used; otherwise, /// if the name is available. /// private bool IsAssetNameUsed(string assetName) { string destFile = Path.Combine(OutputDirectory, assetName + ".xnb"); return _pipelineBuildEvents.SelectMany(pair => pair.Value) .Select(pipelineEvent => pipelineEvent.DestFile) .Any(existingDestFile => destFile.Equals(existingDestFile, StringComparison.OrdinalIgnoreCase)); } } }