// 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(); Dependencies = new List(); BuildAsset = new List(); BuildOutput = new List(); } /// /// Absolute path to the source file. /// public string SourceFile { get; set; } /// /// The date/time stamp of the source file. /// public DateTime SourceTime { get; set; } /// /// Absolute path to the output file. /// public string DestFile { get; set; } /// /// The date/time stamp of the destination file. /// public DateTime DestTime { get; set; } public string Importer { get; set; } /// /// The date/time stamp of the DLL containing the importer. /// public DateTime ImporterTime { get; set; } public string Processor { get; set; } /// /// The date/time stamp of the DLL containing the processor. /// 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 ParametersXml { get; set; } /// /// Gets or sets the dependencies. /// /// The dependencies. /// /// Dependencies are extra files that are required in addition to the . /// Dependencies are added using . Changes /// to the dependent file causes a rebuilt of the content. /// public List Dependencies { get; set; } /// /// Gets or sets the additional (nested) assets. /// /// The additional (nested) assets. /// /// /// Additional assets are built by using an and calling /// /// or . /// /// /// Examples: The mesh processor may build textures and effects in addition to the mesh. /// /// public List BuildAsset { get; set; } /// /// Gets or sets the related output files. /// /// The related output files. /// /// 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 . /// public List 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); } }; }