(ded4a3e0a) v0.9.0.7
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Builder.Convertors
|
||||
{
|
||||
public class StringToColorConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string))
|
||||
return true;
|
||||
|
||||
return base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
if (destinationType != typeof (string))
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
|
||||
var color = (Color)value;
|
||||
return string.Format("{0},{1},{2},{3}", color.R, color.G, color.B, color.A);
|
||||
}
|
||||
|
||||
public override bool CanConvertFrom (ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof (string))
|
||||
return true;
|
||||
|
||||
return base.CanConvertFrom (context, sourceType);
|
||||
}
|
||||
|
||||
public override object ConvertFrom (ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
|
||||
{
|
||||
if (value.GetType () == typeof (string)) {
|
||||
string[] values = ((string)value).Split(new char[] {','},StringSplitOptions.None);
|
||||
if (values.Length == 4)
|
||||
{
|
||||
var r = int.Parse(values[0].Trim());
|
||||
var g = int.Parse(values[1].Trim());
|
||||
var b = int.Parse(values[2].Trim());
|
||||
var a = int.Parse(values[3].Trim());
|
||||
return new Microsoft.Xna.Framework.Color(r, g, b, a);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(string.Format("Could not convert from string({0}) to Color, expected format is 'r,g,b,a'", value));
|
||||
}
|
||||
}
|
||||
|
||||
return base.ConvertFrom (context, culture, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public static class FileHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks deletes a file from disk without throwing exceptions.
|
||||
/// </summary>
|
||||
public static void DeleteIfExists(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// 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 Microsoft.Xna.Framework.Content.Pipeline;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public static class PathHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The/universal/standard/directory/seperator.
|
||||
/// </summary>
|
||||
public const char DirectorySeparator = '/';
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path string normalized to the/universal/standard.
|
||||
/// </summary>
|
||||
public static string Normalize(string path)
|
||||
{
|
||||
return path.Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a directory path string normalized to the/universal/standard
|
||||
/// with a trailing seperator.
|
||||
/// </summary>
|
||||
public static string NormalizeDirectory(string path)
|
||||
{
|
||||
return path.Replace('\\', '/').TrimEnd('/') + '/';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path string normalized to the\Windows\standard.
|
||||
/// </summary>
|
||||
public static string NormalizeWindows(string path)
|
||||
{
|
||||
return path.Replace('/', '\\');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path relative to the base path.
|
||||
/// </summary>
|
||||
/// <param name="basePath">The path to make relative to. Must end with directory seperator.</param>
|
||||
/// <param name="path">The path to be made relative to the basePath.</param>
|
||||
/// <returns>The relative path or the original string if it is not absolute or cannot be made relative.</returns>
|
||||
public static string GetRelativePath(string basePath, string path)
|
||||
{
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
|
||||
return path;
|
||||
|
||||
uri = new Uri(basePath).MakeRelativeUri(uri);
|
||||
var str = Uri.UnescapeDataString(uri.ToString());
|
||||
|
||||
return Normalize(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.Xna.Framework.Content.Pipeline;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public class PipelineBuildEvent
|
||||
{
|
||||
private static readonly OpaqueDataDictionary EmptyParameters = new OpaqueDataDictionary();
|
||||
public static readonly string Extension = ".mgcontent";
|
||||
|
||||
public PipelineBuildEvent()
|
||||
{
|
||||
SourceFile = string.Empty;
|
||||
DestFile = string.Empty;
|
||||
Importer = string.Empty;
|
||||
Processor = string.Empty;
|
||||
Parameters = new OpaqueDataDictionary();
|
||||
ParametersXml = new List<Pair>();
|
||||
Dependencies = new List<string>();
|
||||
BuildAsset = new List<string>();
|
||||
BuildOutput = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the source file.
|
||||
/// </summary>
|
||||
public string SourceFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date/time stamp of the source file.
|
||||
/// </summary>
|
||||
public DateTime SourceTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the output file.
|
||||
/// </summary>
|
||||
public string DestFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date/time stamp of the destination file.
|
||||
/// </summary>
|
||||
public DateTime DestTime { get; set; }
|
||||
|
||||
public string Importer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date/time stamp of the DLL containing the importer.
|
||||
/// </summary>
|
||||
public DateTime ImporterTime { get; set; }
|
||||
|
||||
public string Processor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date/time stamp of the DLL containing the processor.
|
||||
/// </summary>
|
||||
public DateTime ProcessorTime { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public OpaqueDataDictionary Parameters { get; set; }
|
||||
|
||||
public class Pair
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
[XmlElement("Parameters")]
|
||||
public List<Pair> ParametersXml { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dependencies.
|
||||
/// </summary>
|
||||
/// <value>The dependencies.</value>
|
||||
/// <remarks>
|
||||
/// Dependencies are extra files that are required in addition to the <see cref="SourceFile"/>.
|
||||
/// Dependencies are added using <see cref="ContentProcessorContext.AddDependency"/>. Changes
|
||||
/// to the dependent file causes a rebuilt of the content.
|
||||
/// </remarks>
|
||||
public List<string> Dependencies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the additional (nested) assets.
|
||||
/// </summary>
|
||||
/// <value>The additional (nested) assets.</value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Additional assets are built by using an <see cref="ExternalReference{T}"/> and calling
|
||||
/// <see cref="ContentProcessorContext.BuildAndLoadAsset{TInput,TOutput}(ExternalReference{TInput},string)"/>
|
||||
/// or <see cref="ContentProcessorContext.BuildAsset{TInput,TOutput}(ExternalReference{TInput},string)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Examples: The mesh processor may build textures and effects in addition to the mesh.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public List<string> BuildAsset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the related output files.
|
||||
/// </summary>
|
||||
/// <value>The related output files.</value>
|
||||
/// <remarks>
|
||||
/// Related output files are non-XNB files that are included in addition to the XNB files.
|
||||
/// Related output files need to be copied to the output folder by a content processor and
|
||||
/// registered by calling <see cref="ContentProcessorContext.AddOutputFile"/>.
|
||||
/// </remarks>
|
||||
public List<string> BuildOutput { get; set; }
|
||||
|
||||
public static PipelineBuildEvent Load(string filePath)
|
||||
{
|
||||
var fullFilePath = Path.GetFullPath(filePath);
|
||||
var deserializer = new XmlSerializer(typeof (PipelineBuildEvent));
|
||||
PipelineBuildEvent pipelineEvent;
|
||||
try
|
||||
{
|
||||
using (var textReader = new StreamReader(fullFilePath))
|
||||
pipelineEvent = (PipelineBuildEvent) deserializer.Deserialize(textReader);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Repopulate the parameters from the serialized state.
|
||||
foreach (var pair in pipelineEvent.ParametersXml)
|
||||
pipelineEvent.Parameters.Add(pair.Key, pair.Value);
|
||||
pipelineEvent.ParametersXml.Clear();
|
||||
|
||||
return pipelineEvent;
|
||||
}
|
||||
|
||||
public void Save(string filePath)
|
||||
{
|
||||
var fullFilePath = Path.GetFullPath(filePath);
|
||||
// Make sure the directory exists.
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullFilePath) + Path.DirectorySeparatorChar);
|
||||
|
||||
// Convert the parameters into something we can serialize.
|
||||
ParametersXml.Clear();
|
||||
foreach (var pair in Parameters)
|
||||
ParametersXml.Add(new Pair { Key = pair.Key, Value = ConvertToString(pair.Value) });
|
||||
|
||||
// Serialize our state.
|
||||
var serializer = new XmlSerializer(typeof (PipelineBuildEvent));
|
||||
using (var textWriter = new StreamWriter(fullFilePath, false, new UTF8Encoding(false)))
|
||||
serializer.Serialize(textWriter, this);
|
||||
}
|
||||
|
||||
public bool NeedsRebuild(PipelineManager manager, PipelineBuildEvent cachedEvent)
|
||||
{
|
||||
// If we have no previously cached build event then we cannot
|
||||
// be sure that the state hasn't changed... force a rebuild.
|
||||
if (cachedEvent == null)
|
||||
return true;
|
||||
|
||||
// Verify that the last write time of the source file matches
|
||||
// what we recorded when it was built. If it is different
|
||||
// that means someone modified it and we need to rebuild.
|
||||
var sourceWriteTime = File.GetLastWriteTime(SourceFile);
|
||||
if (cachedEvent.SourceTime != sourceWriteTime)
|
||||
return true;
|
||||
|
||||
// Do the same test for the dest file.
|
||||
var destWriteTime = File.GetLastWriteTime(DestFile);
|
||||
if (cachedEvent.DestTime != destWriteTime)
|
||||
return true;
|
||||
|
||||
// If the source file is newer than the dest file
|
||||
// then it must have been updated and needs a rebuild.
|
||||
if (sourceWriteTime >= destWriteTime)
|
||||
return true;
|
||||
|
||||
// Are any of the dependancy files newer than the dest file?
|
||||
foreach (var depFile in cachedEvent.Dependencies)
|
||||
{
|
||||
if (File.GetLastWriteTime(depFile) >= destWriteTime)
|
||||
return true;
|
||||
}
|
||||
|
||||
// This shouldn't happen... but if the source or dest files changed
|
||||
// then force a rebuild.
|
||||
if (cachedEvent.SourceFile != SourceFile ||
|
||||
cachedEvent.DestFile != DestFile)
|
||||
return true;
|
||||
|
||||
// Did the importer assembly change?
|
||||
if (manager.GetImporterAssemblyTimestamp(cachedEvent.Importer) > cachedEvent.ImporterTime)
|
||||
return true;
|
||||
|
||||
// Did the importer change?
|
||||
if (cachedEvent.Importer != Importer)
|
||||
return true;
|
||||
|
||||
// Did the processor assembly change?
|
||||
if (manager.GetProcessorAssemblyTimestamp(cachedEvent.Processor) > cachedEvent.ProcessorTime)
|
||||
return true;
|
||||
|
||||
// Did the processor change?
|
||||
if (cachedEvent.Processor != Processor)
|
||||
return true;
|
||||
|
||||
// Did the parameters change?
|
||||
var defaultValues = manager.GetProcessorDefaultValues(Processor);
|
||||
if (!AreParametersEqual(cachedEvent.Parameters, Parameters, defaultValues))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool AreParametersEqual(OpaqueDataDictionary parameters0, OpaqueDataDictionary parameters1, OpaqueDataDictionary defaultValues)
|
||||
{
|
||||
Debug.Assert(defaultValues != null, "defaultValues must not be empty.");
|
||||
Debug.Assert(EmptyParameters != null && EmptyParameters.Count == 0);
|
||||
|
||||
// Same reference or both null?
|
||||
if (parameters0 == parameters1)
|
||||
return true;
|
||||
|
||||
if (parameters0 == null)
|
||||
parameters0 = EmptyParameters;
|
||||
if (parameters1 == null)
|
||||
parameters1 = EmptyParameters;
|
||||
|
||||
// Are both dictionaries empty?
|
||||
if (parameters0.Count == 0 && parameters1.Count == 0)
|
||||
return true;
|
||||
|
||||
// Compare the values with the second dictionary or
|
||||
// the default values.
|
||||
if (parameters0.Count < parameters1.Count)
|
||||
{
|
||||
var dummy = parameters0;
|
||||
parameters0 = parameters1;
|
||||
parameters1 = dummy;
|
||||
}
|
||||
|
||||
// Compare parameters0 with parameters1 or defaultValues.
|
||||
foreach (var pair in parameters0)
|
||||
{
|
||||
object value0 = pair.Value;
|
||||
object value1;
|
||||
|
||||
// Search for matching parameter.
|
||||
if (!parameters1.TryGetValue(pair.Key, out value1) && !defaultValues.TryGetValue(pair.Key, out value1))
|
||||
return false;
|
||||
|
||||
if (!AreEqual(value0, value1))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare parameters which are only in parameters1 with defaultValues.
|
||||
foreach (var pair in parameters1)
|
||||
{
|
||||
if (parameters0.ContainsKey(pair.Key))
|
||||
continue;
|
||||
|
||||
object defaultValue;
|
||||
if (!defaultValues.TryGetValue(pair.Key, out defaultValue))
|
||||
return false;
|
||||
|
||||
if (!AreEqual(pair.Value, defaultValue))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AreEqual(object value0, object value1)
|
||||
{
|
||||
// Are values equal or both null?
|
||||
if (Equals(value0, value1))
|
||||
return true;
|
||||
|
||||
// Is one value null?
|
||||
if (value0 == null || value1 == null)
|
||||
return false;
|
||||
|
||||
// Values are of different type: Compare string representation.
|
||||
if (ConvertToString(value0) != ConvertToString(value1))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ConvertToString(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
var typeConverter = TypeDescriptor.GetConverter(value.GetType());
|
||||
return typeConverter.ConvertToInvariantString(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// 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 Microsoft.Xna.Framework.Content.Pipeline;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public class PipelineBuildLogger : ContentBuildLogger
|
||||
{
|
||||
public override void LogMessage(string message, params object[] messageArgs)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine(string.Format(message, messageArgs));
|
||||
}
|
||||
|
||||
public override void LogImportantMessage(string message, params object[] messageArgs)
|
||||
{
|
||||
// TODO: How do i make it high importance?
|
||||
System.Diagnostics.Trace.WriteLine(string.Format(message, messageArgs));
|
||||
}
|
||||
|
||||
public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs)
|
||||
{
|
||||
var msg = string.Format(message, messageArgs);
|
||||
var fileName = GetCurrentFilename(contentIdentity);
|
||||
System.Diagnostics.Trace.WriteLine(string.Format("{0}: {1}", fileName, msg));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// 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 Microsoft.Xna.Framework.Content.Pipeline;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public class PipelineImporterContext : ContentImporterContext
|
||||
{
|
||||
private readonly PipelineManager _manager;
|
||||
|
||||
public PipelineImporterContext(PipelineManager manager)
|
||||
{
|
||||
_manager = manager;
|
||||
}
|
||||
|
||||
public override string IntermediateDirectory { get { return _manager.IntermediateDirectory; } }
|
||||
public override string OutputDirectory { get { return _manager.OutputDirectory; } }
|
||||
public override ContentBuildLogger Logger { get { return _manager.Logger; } }
|
||||
|
||||
public override void AddDependency(string filename)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// 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.IO;
|
||||
using Microsoft.Xna.Framework.Content.Pipeline;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
public class PipelineProcessorContext : ContentProcessorContext
|
||||
{
|
||||
private readonly PipelineManager _manager;
|
||||
|
||||
private readonly PipelineBuildEvent _pipelineEvent;
|
||||
|
||||
public PipelineProcessorContext(PipelineManager manager, PipelineBuildEvent pipelineEvent)
|
||||
{
|
||||
_manager = manager;
|
||||
_pipelineEvent = pipelineEvent;
|
||||
}
|
||||
|
||||
public override TargetPlatform TargetPlatform { get { return _manager.Platform; } }
|
||||
public override GraphicsProfile TargetProfile { get { return _manager.Profile; } }
|
||||
|
||||
public override string BuildConfiguration { get { return _manager.Config; } }
|
||||
|
||||
public override string IntermediateDirectory { get { return _manager.IntermediateDirectory; } }
|
||||
public override string OutputDirectory { get { return _manager.OutputDirectory; } }
|
||||
public override string OutputFilename { get { return _pipelineEvent.DestFile; } }
|
||||
|
||||
public override OpaqueDataDictionary Parameters { get { return _pipelineEvent.Parameters; } }
|
||||
|
||||
public override ContentBuildLogger Logger { get { return _manager.Logger; } }
|
||||
|
||||
public override ContentIdentity SourceIdentity { get { return new ContentIdentity(_pipelineEvent.SourceFile); } }
|
||||
|
||||
public override void AddDependency(string filename)
|
||||
{
|
||||
_pipelineEvent.Dependencies.AddUnique(filename);
|
||||
}
|
||||
|
||||
public override void AddOutputFile(string filename)
|
||||
{
|
||||
_pipelineEvent.BuildOutput.AddUnique(filename);
|
||||
}
|
||||
|
||||
public override TOutput Convert<TInput, TOutput>( TInput input,
|
||||
string processorName,
|
||||
OpaqueDataDictionary processorParameters)
|
||||
{
|
||||
var processor = _manager.CreateProcessor(processorName, processorParameters);
|
||||
var processContext = new PipelineProcessorContext(_manager, new PipelineBuildEvent { Parameters = processorParameters } );
|
||||
var processedObject = processor.Process(input, processContext);
|
||||
|
||||
// Add its dependencies and built assets to ours.
|
||||
_pipelineEvent.Dependencies.AddRangeUnique(processContext._pipelineEvent.Dependencies);
|
||||
_pipelineEvent.BuildAsset.AddRangeUnique(processContext._pipelineEvent.BuildAsset);
|
||||
|
||||
return (TOutput)processedObject;
|
||||
}
|
||||
|
||||
public override TOutput BuildAndLoadAsset<TInput, TOutput>( ExternalReference<TInput> sourceAsset,
|
||||
string processorName,
|
||||
OpaqueDataDictionary processorParameters,
|
||||
string importerName)
|
||||
{
|
||||
var sourceFilepath = PathHelper.Normalize(sourceAsset.Filename);
|
||||
|
||||
// The processorName can be null or empty. In this case the asset should
|
||||
// be imported but not processed. This is, for example, necessary to merge
|
||||
// animation files as described here:
|
||||
// http://blogs.msdn.com/b/shawnhar/archive/2010/06/18/merging-animation-files.aspx.
|
||||
bool processAsset = !string.IsNullOrEmpty(processorName);
|
||||
_manager.ResolveImporterAndProcessor(sourceFilepath, ref importerName, ref processorName);
|
||||
|
||||
var buildEvent = new PipelineBuildEvent
|
||||
{
|
||||
SourceFile = sourceFilepath,
|
||||
Importer = importerName,
|
||||
Processor = processAsset ? processorName : null,
|
||||
Parameters = _manager.ValidateProcessorParameters(processorName, processorParameters),
|
||||
};
|
||||
|
||||
var processedObject = _manager.ProcessContent(buildEvent);
|
||||
|
||||
// Record that we processed this dependent asset.
|
||||
_pipelineEvent.Dependencies.AddUnique(sourceFilepath);
|
||||
|
||||
return (TOutput)processedObject;
|
||||
}
|
||||
|
||||
public override ExternalReference<TOutput> BuildAsset<TInput, TOutput>( ExternalReference<TInput> sourceAsset,
|
||||
string processorName,
|
||||
OpaqueDataDictionary processorParameters,
|
||||
string importerName,
|
||||
string assetName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetName))
|
||||
assetName = _manager.GetAssetName(sourceAsset.Filename, importerName, processorName, processorParameters);
|
||||
|
||||
// Build the content.
|
||||
var buildEvent = _manager.BuildContent(sourceAsset.Filename, assetName, importerName, processorName, processorParameters);
|
||||
|
||||
// Record that we built this dependent asset.
|
||||
_pipelineEvent.BuildAsset.AddUnique(buildEvent.DestFile);
|
||||
|
||||
return new ExternalReference<TOutput>(buildEvent.DestFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// 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.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
static public class TypeExtensions
|
||||
{
|
||||
public static Color ToColor(this System.Drawing.Color color)
|
||||
{
|
||||
return new Color(color.R, color.G, color.B, color.A);
|
||||
}
|
||||
|
||||
public static Vector3 ToVector3(this System.Drawing.Color color)
|
||||
{
|
||||
return new Vector3(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f);
|
||||
}
|
||||
|
||||
public static void AddUnique<T>(this List<T> list, T item)
|
||||
{
|
||||
if (!list.Contains(item))
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
public static void AddRangeUnique<T>(this List<T> dstList, List<T> list)
|
||||
{
|
||||
foreach (var i in list)
|
||||
{
|
||||
if (!dstList.Contains(i))
|
||||
dstList.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// 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.Drawing;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MonoGame.Framework.Content.Pipeline.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for serializing color types with the XmlSerializer.
|
||||
/// </summary>
|
||||
public class XmlColor
|
||||
{
|
||||
private Color _color;
|
||||
|
||||
public XmlColor()
|
||||
{
|
||||
}
|
||||
|
||||
public XmlColor(Color c)
|
||||
{
|
||||
_color = c;
|
||||
}
|
||||
|
||||
public static implicit operator Color(XmlColor x)
|
||||
{
|
||||
return x._color;
|
||||
}
|
||||
|
||||
public static implicit operator XmlColor(Color c)
|
||||
{
|
||||
return new XmlColor(c);
|
||||
}
|
||||
|
||||
public static string FromColor(Color color)
|
||||
{
|
||||
if (color.IsNamedColor)
|
||||
return color.Name;
|
||||
return string.Format("{0}, {1}, {2}, {3}", color.R, color.G, color.B, color.A);
|
||||
}
|
||||
|
||||
public static Color ToColor(string value)
|
||||
{
|
||||
if (!value.Contains(","))
|
||||
return Color.FromName(value);
|
||||
|
||||
int r, g, b, a;
|
||||
var colors = value.Split(',');
|
||||
int.TryParse(colors.Length > 0 ? colors[0] : string.Empty, out r);
|
||||
int.TryParse(colors.Length > 1 ? colors[1] : string.Empty, out g);
|
||||
int.TryParse(colors.Length > 2 ? colors[2] : string.Empty, out b);
|
||||
int.TryParse(colors.Length > 3 ? colors[3] : string.Empty, out a);
|
||||
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
|
||||
[XmlText]
|
||||
public string Default
|
||||
{
|
||||
get { return FromColor(_color); }
|
||||
set { _color = ToColor(value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user