Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
937 lines (791 sloc) 38.6 KB
// 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<ImporterInfo> _importers;
[DebuggerDisplay("ProcessorInfo: {type.Name}")]
private struct ProcessorInfo
{
public ContentProcessorAttribute attribute;
public Type type;
public DateTime assemblyTimestamp;
};
private List<ProcessorInfo> _processors;
private List<Type> _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<string, List<PipelineBuildEvent>> _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<string, OpaqueDataDictionary> _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<string> Assemblies { get; private set; }
/// <summary>
/// The current target graphics profile for which all content is built.
/// </summary>
public GraphicsProfile Profile { get; set; }
/// <summary>
/// The current target platform for which all content is built.
/// </summary>
public TargetPlatform Platform { get; set; }
/// <summary>
/// The build configuration passed thru to content processors.
/// </summary>
public string Config { get; set; }
/// <summary>
/// Gets or sets if the content is compressed.
/// </summary>
public bool CompressContent { get; set; }
/// <summary>
/// If true exceptions thrown from within an importer or processor are caught and then
/// thrown from the context. Default value is true.
/// </summary>
public bool RethrowExceptions { get; set; }
public PipelineManager(string projectDir, string outputDir, string intermediateDir)
{
_pipelineBuildEvents = new Dictionary<string, List<PipelineBuildEvent>>();
_processorDefaultValues = new Dictionary<string, OpaqueDataDictionary>();
RethrowExceptions = true;
Assemblies = new List<string>();
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<TType, TTypeConverter> ()
{
TypeDescriptor.AddAttributes (typeof (TType), new TypeConverterAttribute (typeof (TTypeConverter)));
}
private void RegisterCustomConverters ()
{
AssignTypeConverter<Microsoft.Xna.Framework.Color, StringToColorConverter> ();
}
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<ImporterInfo>();
_processors = new List<ProcessorInfo>();
_writers = new List<Type>();
// 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<Type> types = new List<Type>();
foreach (var item in _importers)
{
types.Add(item.type);
}
return types.ToArray();
}
public Type[] GetProcessorTypes()
{
if (_processors == null)
ResolveAssemblies();
List<Type> types = new List<Type>();
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;
}
/// <summary>
/// Gets the default values for the content processor parameters.
/// </summary>
/// <param name="processorName">The name of the content processor.</param>
/// <returns>
/// A dictionary containing the default value for each parameter. Returns
/// <see langword="null"/> if the content processor has not been created yet.
/// </returns>
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);
}
/// <summary>
/// Stores the pipeline build event (in memory) if no matching event is found.
/// </summary>
/// <param name="pipelineEvent">The pipeline build event.</param>
private void TrackPipelineBuildEvent(PipelineBuildEvent pipelineEvent)
{
List<PipelineBuildEvent> pipelineBuildEvents;
bool eventsFound = _pipelineBuildEvents.TryGetValue(pipelineEvent.SourceFile, out pipelineBuildEvents);
if (!eventsFound)
{
pipelineBuildEvents = new List<PipelineBuildEvent>();
_pipelineBuildEvents.Add(pipelineEvent.SourceFile, pipelineBuildEvents);
}
if (FindMatchingEvent(pipelineBuildEvents, pipelineEvent.DestFile, pipelineEvent.Importer, pipelineEvent.Processor, pipelineEvent.Parameters) == null)
pipelineBuildEvents.Add(pipelineEvent);
}
/// <summary>
/// Gets an automatic asset name, such as "AssetName_0".
/// </summary>
/// <param name="sourceFileName">The source file name.</param>
/// <param name="importerName">The name of the content importer. Can be <see langword="null"/>.</param>
/// <param name="processorName">The name of the content processor. Can be <see langword="null"/>.</param>
/// <param name="processorParameters">The processor parameters. Can be <see langword="null"/>.</param>
/// <returns>The asset name.</returns>
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<PipelineBuildEvent> 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);
}
/// <summary>
/// Determines whether the specified list contains a matching pipeline build event.
/// </summary>
/// <param name="pipelineBuildEvents">The list of pipeline build events.</param>
/// <param name="destFile">Absolute path to the output file. Can be <see langword="null"/>.</param>
/// <param name="importerName">The name of the content importer. Can be <see langword="null"/>.</param>
/// <param name="processorName">The name of the content processor. Can be <see langword="null"/>.</param>
/// <param name="processorParameters">The processor parameters. Can be <see langword="null"/>.</param>
/// <returns>
/// The matching pipeline build event, or <see langword="null"/>.
/// </returns>
private PipelineBuildEvent FindMatchingEvent(List<PipelineBuildEvent> 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;
}
/// <summary>
/// Gets the asset name including a suffix, such as "_0". (The number is incremented
/// automatically.
/// </summary>
/// <param name="baseAssetName">
/// The asset name without suffix (relative to output folder).
/// </param>
/// <returns>The asset name with suffix.</returns>
private string AppendAssetNameSuffix(string baseAssetName)
{
int index = 0;
string assetName = baseAssetName + "_0";
while (IsAssetNameUsed(assetName))
{
index++;
assetName = baseAssetName + '_' + index;
}
return assetName;
}
/// <summary>
/// Determines whether the specified asset name is already used.
/// </summary>
/// <param name="assetName">The asset name (relative to output folder).</param>
/// <returns>
/// <see langword="true"/> if the asset name is already used; otherwise,
/// <see langword="false"/> if the name is available.
/// </returns>
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));
}
}
}
You can’t perform that action at this time.