937 lines
39 KiB
C#
937 lines
39 KiB
C#
// 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));
|
|
}
|
|
}
|
|
}
|