(ded4a3e0a) v0.9.0.7
This commit is contained in:
+936
@@ -0,0 +1,936 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user