587 lines
22 KiB
C#
587 lines
22 KiB
C#
// MonoGame - Copyright (C) The MonoGame Team
|
|
// This file is subject to the terms and conditions defined in
|
|
// file 'LICENSE.txt', which is part of this source code package.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Xml;
|
|
using MGCB;
|
|
using Microsoft.Xna.Framework.Content.Pipeline;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using PathHelper = MonoGame.Framework.Content.Pipeline.Builder.PathHelper;
|
|
|
|
namespace MonoGame.Tools.Pipeline
|
|
{
|
|
public class PipelineProjectParser
|
|
{
|
|
#region Other Data
|
|
|
|
private readonly PipelineProject _project;
|
|
private readonly IContentItemObserver _observer;
|
|
private readonly OpaqueDataDictionary _processorParams = new OpaqueDataDictionary();
|
|
|
|
private string _processor;
|
|
|
|
#endregion
|
|
|
|
#region CommandLineParameters
|
|
|
|
[CommandLineParameter(
|
|
Name = "outputDir",
|
|
ValueName = "directoryPath",
|
|
Description = "The directory where all content is written.")]
|
|
public string OutputDir { set { _project.OutputDir = value; } }
|
|
|
|
[CommandLineParameter(
|
|
Name = "intermediateDir",
|
|
ValueName = "directoryPath",
|
|
Description = "The directory where all intermediate files are written.")]
|
|
public string IntermediateDir { set { _project.IntermediateDir = value; } }
|
|
|
|
[CommandLineParameter(
|
|
Name = "reference",
|
|
ValueName = "assemblyNameOrFile",
|
|
Description = "Adds an assembly reference for resolving content importers, processors, and writers.")]
|
|
public List<string> References
|
|
{
|
|
set { _project.References = value; }
|
|
get { return _project.References; }
|
|
}
|
|
|
|
[CommandLineParameter(
|
|
Name = "platform",
|
|
ValueName = "targetPlatform",
|
|
Description = "Set the target platform for this build. Defaults to Windows.")]
|
|
public TargetPlatform Platform { set { _project.Platform = value; } }
|
|
|
|
[CommandLineParameter(
|
|
Name = "profile",
|
|
ValueName = "graphicsProfile",
|
|
Description = "Set the target graphics profile for this build. Defaults to HiDef.")]
|
|
public GraphicsProfile Profile { set { _project.Profile = value; } }
|
|
|
|
[CommandLineParameter(
|
|
Name = "config",
|
|
ValueName = "string",
|
|
Description = "The optional build config string from the build system.")]
|
|
public string Config { set { _project.Config = value; } }
|
|
|
|
#pragma warning disable 414
|
|
|
|
// Allow a MGCB file containing the /rebuild parameter to be imported without error
|
|
[CommandLineParameter(
|
|
Name = "rebuild",
|
|
ValueName = "bool",
|
|
Description = "Forces a rebuild of the project.")]
|
|
public bool Rebuild { set { _rebuild = value; } }
|
|
private bool _rebuild;
|
|
|
|
// Allow a MGCB file containing the /clean parameter to be imported without error
|
|
[CommandLineParameter(
|
|
Name = "clean",
|
|
ValueName = "bool",
|
|
Description = "Removes intermediate and output files.")]
|
|
public bool Clean { set { _clean = value; } }
|
|
private bool _clean;
|
|
|
|
#pragma warning restore 414
|
|
|
|
[CommandLineParameter(
|
|
Name = "compress",
|
|
ValueName = "bool",
|
|
Description = "Content files can be compressed for smaller file sizes.")]
|
|
public bool Compress { set { _project.Compress = value; } }
|
|
|
|
[CommandLineParameter(
|
|
Name = "importer",
|
|
ValueName = "className",
|
|
Description = "Defines the class name of the content importer for reading source content.")]
|
|
public string Importer;
|
|
|
|
[CommandLineParameter(
|
|
Name = "processor",
|
|
ValueName = "className",
|
|
Description = "Defines the class name of the content processor for processing imported content.")]
|
|
public string Processor
|
|
{
|
|
get { return _processor; }
|
|
set
|
|
{
|
|
_processor = value;
|
|
_processorParams.Clear();
|
|
}
|
|
}
|
|
|
|
[CommandLineParameter(
|
|
Name = "processorParam",
|
|
ValueName = "name=value",
|
|
Description = "Defines a parameter name and value to set on a content processor.")]
|
|
public void AddProcessorParam(string nameAndValue)
|
|
{
|
|
var keyAndValue = nameAndValue.Split('=');
|
|
if (keyAndValue.Length != 2)
|
|
{
|
|
// Do we error out or something?
|
|
return;
|
|
}
|
|
|
|
_processorParams.Remove(keyAndValue[0]);
|
|
_processorParams.Add(keyAndValue[0], keyAndValue[1]);
|
|
}
|
|
|
|
[CommandLineParameter(
|
|
Name = "build",
|
|
ValueName = "sourceFile",
|
|
Description = "Build the content source file using the previously set switches and options.")]
|
|
public void OnBuild(string sourceFile)
|
|
{
|
|
AddContent(sourceFile, false);
|
|
}
|
|
|
|
[CommandLineParameter(
|
|
Name = "launchDebugger",
|
|
ValueName = "sourceFile")]
|
|
public void OnDebug()
|
|
{
|
|
_project.LaunchDebugger = true;
|
|
}
|
|
|
|
public bool AddContent(string sourceFile, bool skipDuplicates)
|
|
{
|
|
string link = null;
|
|
|
|
if (sourceFile.Contains(";"))
|
|
{
|
|
var split = sourceFile.Split(';');
|
|
sourceFile = split[0];
|
|
|
|
if (split.Length > 0)
|
|
link = split[1];
|
|
}
|
|
|
|
// Make sure the source file is relative to the project.
|
|
var projectDir = ProjectDirectory + Path.DirectorySeparatorChar;
|
|
|
|
sourceFile = PathHelper.GetRelativePath(projectDir, sourceFile);
|
|
|
|
// Do we have a duplicate?
|
|
var previous = _project.ContentItems.FindIndex(e => string.Equals(e.OriginalPath, sourceFile, StringComparison.InvariantCultureIgnoreCase));
|
|
if (previous != -1)
|
|
{
|
|
if (skipDuplicates)
|
|
return false;
|
|
|
|
// Replace the duplicate.
|
|
_project.ContentItems.RemoveAt(previous);
|
|
}
|
|
|
|
// Create the item for processing later.
|
|
var item = new ContentItem
|
|
{
|
|
Observer = _observer,
|
|
BuildAction = BuildAction.Build,
|
|
OriginalPath = sourceFile,
|
|
DestinationPath = string.IsNullOrEmpty(link) ? sourceFile : link,
|
|
ImporterName = Importer,
|
|
ProcessorName = Processor,
|
|
ProcessorParams = new OpaqueDataDictionary()
|
|
};
|
|
_project.ContentItems.Add(item);
|
|
|
|
// Copy the current processor parameters blind as we
|
|
// will validate and remove invalid parameters during
|
|
// the build process later.
|
|
foreach (var pair in _processorParams)
|
|
item.ProcessorParams.Add(pair.Key, pair.Value);
|
|
|
|
return true;
|
|
}
|
|
|
|
[CommandLineParameter(
|
|
Name = "copy",
|
|
ValueName = "sourceFile",
|
|
Description = "Copy the content source file verbatim to the output directory.")]
|
|
public void OnCopy(string sourceFile)
|
|
{
|
|
string link = null;
|
|
|
|
if (sourceFile.Contains(";"))
|
|
{
|
|
var split = sourceFile.Split(';');
|
|
sourceFile = split[0];
|
|
|
|
if (split.Length > 0)
|
|
link = split[1];
|
|
}
|
|
|
|
// Make sure the source file is relative to the project.
|
|
var projectDir = ProjectDirectory + Path.DirectorySeparatorChar;
|
|
|
|
sourceFile = PathHelper.GetRelativePath(projectDir, sourceFile);
|
|
|
|
// Remove duplicates... keep this new one.
|
|
var previous = _project.ContentItems.FirstOrDefault(e => e.OriginalPath.Equals(sourceFile));
|
|
if (previous != null)
|
|
_project.ContentItems.Remove(previous);
|
|
|
|
// Create the item for processing later.
|
|
var item = new ContentItem
|
|
{
|
|
BuildAction = BuildAction.Copy,
|
|
OriginalPath = sourceFile,
|
|
DestinationPath = string.IsNullOrEmpty(link) ? sourceFile : link,
|
|
ProcessorParams = new OpaqueDataDictionary()
|
|
};
|
|
_project.ContentItems.Add(item);
|
|
|
|
// Copy the current processor parameters blind as we
|
|
// will validate and remove invalid parameters during
|
|
// the build process later.
|
|
foreach (var pair in _processorParams)
|
|
item.ProcessorParams.Add(pair.Key, pair.Value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
public PipelineProjectParser(IContentItemObserver observer, PipelineProject project)
|
|
{
|
|
_observer = observer;
|
|
_project = project;
|
|
}
|
|
|
|
public void OpenProject(string projectFilePath, MGBuildParser.ErrorCallback errorCallback)
|
|
{
|
|
_project.ContentItems.Clear();
|
|
|
|
// Store the file name for saving later.
|
|
_project.OriginalPath = projectFilePath;
|
|
|
|
var parser = new MGBuildParser(this);
|
|
parser.Title = "Pipeline";
|
|
|
|
if (errorCallback != null)
|
|
parser.OnError += errorCallback;
|
|
|
|
var commands = new string[]
|
|
{
|
|
string.Format("/@:{0}", projectFilePath),
|
|
};
|
|
parser.Parse(commands);
|
|
}
|
|
|
|
public void SaveProject()
|
|
{
|
|
using (var io = File.CreateText(_project.OriginalPath))
|
|
SaveProject(io, null);
|
|
}
|
|
|
|
public void SaveProject(TextWriter io, Func<ContentItem, bool> filterItem)
|
|
{
|
|
const string lineFormat = "/{0}:{1}";
|
|
const string processorParamFormat = "{0}={1}";
|
|
string line;
|
|
|
|
line = FormatDivider("Global Properties");
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "outputDir", _project.OutputDir);
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "intermediateDir", _project.IntermediateDir);
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "platform", _project.Platform);
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "config", _project.Config);
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "profile", _project.Profile);
|
|
io.WriteLine(line);
|
|
|
|
line = string.Format(lineFormat, "compress", _project.Compress);
|
|
io.WriteLine(line);
|
|
|
|
if (_project.LaunchDebugger)
|
|
io.WriteLine("/launchdebugger");
|
|
|
|
line = FormatDivider("References");
|
|
io.WriteLine(line);
|
|
|
|
foreach (var i in _project.References)
|
|
{
|
|
line = string.Format(lineFormat, "reference", i);
|
|
io.WriteLine(line);
|
|
}
|
|
|
|
line = FormatDivider("Content");
|
|
io.WriteLine(line);
|
|
|
|
// Sort the items alphabetically to ensure a consistent output
|
|
// and better mergability of the resulting MGCB file.
|
|
var sortedItems = _project.ContentItems.OrderBy(c => c.OriginalPath, StringComparer.InvariantCulture);
|
|
|
|
foreach (var i in sortedItems)
|
|
{
|
|
// Reject any items that don't pass the filter.
|
|
if (filterItem != null && filterItem(i))
|
|
continue;
|
|
|
|
// Wrap content item lines with a begin comment line
|
|
// to make them more cohesive (for version control).
|
|
line = string.Format("#begin {0}", i.OriginalPath);
|
|
io.WriteLine(line);
|
|
|
|
if (i.BuildAction == BuildAction.Copy)
|
|
{
|
|
string path = i.OriginalPath;
|
|
if (i.OriginalPath != i.DestinationPath)
|
|
path += ";" + i.DestinationPath;
|
|
line = string.Format(lineFormat, "copy", path);
|
|
io.WriteLine(line);
|
|
io.WriteLine();
|
|
}
|
|
else
|
|
{
|
|
|
|
// Write importer.
|
|
{
|
|
line = string.Format(lineFormat, "importer", i.ImporterName);
|
|
io.WriteLine(line);
|
|
}
|
|
|
|
// Write processor.
|
|
{
|
|
line = string.Format(lineFormat, "processor", i.ProcessorName);
|
|
io.WriteLine(line);
|
|
}
|
|
|
|
// Write processor parameters.
|
|
{
|
|
if (i.Processor == PipelineTypes.MissingProcessor)
|
|
{
|
|
// Could still be missing the real processor.
|
|
// If so, write the string parameters from import.
|
|
foreach (var j in i.ProcessorParams)
|
|
{
|
|
line = string.Format(lineFormat, "processorParam", string.Format(processorParamFormat, j.Key, j.Value));
|
|
io.WriteLine(line);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, write only values which are defined by the real processor.
|
|
foreach (var j in i.Processor.Properties)
|
|
{
|
|
object value = null;
|
|
if (i.ProcessorParams.ContainsKey(j.Name))
|
|
value = i.ProcessorParams[j.Name];
|
|
|
|
// JCF: I 'think' writting an empty string for null would be appropriate but to be on the safe side
|
|
// im just not writting the value at all.
|
|
if (value != null)
|
|
{
|
|
var converter = PipelineTypes.FindConverter(value.GetType());
|
|
var valueStr = converter.ConvertTo(null, CultureInfo.InvariantCulture, value, typeof(string));
|
|
line = string.Format(lineFormat, "processorParam", string.Format(processorParamFormat, j.Name, valueStr));
|
|
io.WriteLine(line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
string buildValue = i.OriginalPath;
|
|
if (i.OriginalPath != i.DestinationPath)
|
|
buildValue += ";" + i.DestinationPath;
|
|
line = string.Format(lineFormat, "build", buildValue);
|
|
io.WriteLine(line);
|
|
io.WriteLine();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ImportProject(string projectFilePath)
|
|
{
|
|
_project.OriginalPath = projectFilePath.Remove(projectFilePath.LastIndexOf('.')) + ".mgcb";
|
|
|
|
using (var io = XmlReader.Create(File.OpenText(projectFilePath)))
|
|
{
|
|
while (io.Read())
|
|
{
|
|
if (io.NodeType == XmlNodeType.Element)
|
|
{
|
|
var buildAction = io.LocalName;
|
|
if (buildAction.Equals("Reference"))
|
|
{
|
|
string include, hintPath;
|
|
ReadIncludeReference(io, out include, out hintPath);
|
|
|
|
if (!string.IsNullOrEmpty(hintPath) &&
|
|
hintPath.IndexOf("microsoft", StringComparison.CurrentCultureIgnoreCase) == -1 &&
|
|
hintPath.IndexOf("monogamecontentprocessors", StringComparison.CurrentCultureIgnoreCase) == -1)
|
|
{
|
|
_project.References.Add(hintPath);
|
|
}
|
|
}
|
|
else if (buildAction.Equals("Content") || buildAction.Equals("None"))
|
|
{
|
|
string include, copyToOutputDirectory;
|
|
ReadIncludeContent(io, out include, out copyToOutputDirectory);
|
|
|
|
if (!string.IsNullOrEmpty(copyToOutputDirectory) && !copyToOutputDirectory.Equals("Never"))
|
|
{
|
|
var sourceFilePath = Path.GetDirectoryName(projectFilePath);
|
|
sourceFilePath += "\\" + include;
|
|
|
|
OnCopy(sourceFilePath);
|
|
}
|
|
}
|
|
else if (buildAction.Equals("Compile"))
|
|
{
|
|
string include, name, importer, processor;
|
|
string[] processorParams;
|
|
ReadIncludeCompile(io, out include, out name, out importer, out processor, out processorParams);
|
|
|
|
Importer = importer;
|
|
Processor = processor;
|
|
if (processorParams != null)
|
|
{
|
|
foreach (var i in processorParams)
|
|
AddProcessorParam(i);
|
|
}
|
|
|
|
var sourceFilePath = Path.GetDirectoryName(projectFilePath);
|
|
sourceFilePath += "\\" + include;
|
|
|
|
OnBuild(sourceFilePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private string ProjectDirectory
|
|
{
|
|
get
|
|
{
|
|
return _project.Location;
|
|
}
|
|
}
|
|
|
|
private void ReadIncludeReference(XmlReader io, out string include, out string hintPath)
|
|
{
|
|
include = io.GetAttribute("Include").Unescape();
|
|
hintPath = null;
|
|
|
|
if (!io.IsEmptyElement)
|
|
{
|
|
var depth = io.Depth;
|
|
for (io.Read(); io.Depth != depth; io.Read())
|
|
{
|
|
// process sub nodes
|
|
if (io.IsStartElement("HintPath"))
|
|
{
|
|
io.Read();
|
|
hintPath = io.Value.Unescape();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadIncludeContent(XmlReader io, out string include, out string copyToOutputDirectory)
|
|
{
|
|
copyToOutputDirectory = null;
|
|
include = io.GetAttribute("Include").Unescape();
|
|
|
|
if (!io.IsEmptyElement)
|
|
{
|
|
var depth = io.Depth;
|
|
for (io.Read(); io.Depth != depth; io.Read())
|
|
{
|
|
// process sub nodes here.
|
|
|
|
if (io.IsStartElement())
|
|
{
|
|
switch (io.LocalName)
|
|
{
|
|
case "CopyToOutputDirectory":
|
|
io.Read();
|
|
copyToOutputDirectory = io.Value.Unescape();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadIncludeCompile(XmlReader io,
|
|
out string include,
|
|
out string name,
|
|
out string importer,
|
|
out string processor,
|
|
out string[] processorParams)
|
|
{
|
|
name = null;
|
|
importer = null;
|
|
processor = null;
|
|
|
|
include = io.GetAttribute("Include").Unescape();
|
|
var parameters = new List<string>();
|
|
|
|
if (!io.IsEmptyElement)
|
|
{
|
|
var depth = io.Depth;
|
|
for (io.Read(); io.Depth != depth; io.Read())
|
|
{
|
|
// process sub nodes here.
|
|
|
|
if (io.IsStartElement())
|
|
{
|
|
switch (io.LocalName)
|
|
{
|
|
case "Name":
|
|
io.Read();
|
|
name = io.Value.Unescape();
|
|
break;
|
|
case "Importer":
|
|
io.Read();
|
|
importer = io.Value.Unescape();
|
|
break;
|
|
case "Processor":
|
|
io.Read();
|
|
processor = io.Value.Unescape();
|
|
break;
|
|
default:
|
|
if (io.LocalName.Contains("ProcessorParameters_"))
|
|
{
|
|
var line = io.LocalName.Replace("ProcessorParameters_", "");
|
|
line += "=";
|
|
io.Read();
|
|
line += io.Value;
|
|
parameters.Add(line.Unescape());
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
processorParams = parameters.ToArray();
|
|
}
|
|
|
|
private string FormatDivider(string label)
|
|
{
|
|
var commentFormat = Environment.NewLine + "#----------------------------------------------------------------------------#" + Environment.NewLine;
|
|
|
|
label = " " + label + " ";
|
|
var src = commentFormat.Length / 2 - label.Length / 2;
|
|
var dst = src + label.Length;
|
|
|
|
return commentFormat.Substring(0, src) + label + commentFormat.Substring(dst);
|
|
}
|
|
}
|
|
}
|