(ded4a3e0a) v0.9.0.7

This commit is contained in:
Joonas Rikkonen
2019-06-25 16:00:44 +03:00
parent e5ae622c77
commit 4a51db77b5
1777 changed files with 421528 additions and 917 deletions
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
};
}
@@ -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));
}
}
}
@@ -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)
{
}
}
}
@@ -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));
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
@@ -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); }
}
}
}