#region File Description
//-----------------------------------------------------------------------------
// ContentBuilder.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Build.BuildEngine;
#endregion
namespace WinFormsContentLoading
{
///
/// This class wraps the MSBuild functionality needed to build XNA Framework
/// content dynamically at runtime. It creates a temporary MSBuild project
/// in memory, and adds whatever content files you choose to this project.
/// It then builds the project, which will create compiled .xnb content files
/// in a temporary directory. After the build finishes, you can use a regular
/// ContentManager to load these temporary .xnb files in the usual way.
///
class ContentBuilder : IDisposable
{
#region Fields
// What importers or processors should we load?
const string xnaVersion = ", Version=3.0.0.0, PublicKeyToken=6d5c3888ef60e27d";
static string[] pipelineAssemblies =
{
"Microsoft.Xna.Framework.Content.Pipeline.FBXImporter" + xnaVersion,
"Microsoft.Xna.Framework.Content.Pipeline.XImporter" + xnaVersion,
"Microsoft.Xna.Framework.Content.Pipeline.TextureImporter" + xnaVersion,
"Microsoft.Xna.Framework.Content.Pipeline.EffectImporter" + xnaVersion,
};
// MSBuild objects used to dynamically build content.
Engine msBuildEngine;
Project msBuildProject;
ErrorLogger errorLogger;
// Temporary directories used by the content build.
string buildDirectory;
string processDirectory;
string baseDirectory;
// Generate unique directory names if there is more than one ContentBuilder.
static int directorySalt;
// Have we been disposed?
bool isDisposed;
#endregion
#region Properties
///
/// Gets the output directory, which will contain the generated .xnb files.
///
public string OutputDirectory
{
get { return Path.Combine(buildDirectory, "bin/Content"); }
}
#endregion
#region Initialization
///
/// Creates a new content builder.
///
public ContentBuilder()
{
CreateTempDirectory();
CreateBuildProject();
}
///
/// Finalizes the content builder.
///
~ContentBuilder()
{
Dispose(false);
}
///
/// Disposes the content builder when it is no longer required.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Implements the standard .NET IDisposable pattern.
///
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
isDisposed = true;
DeleteTempDirectory();
}
}
#endregion
#region MSBuild
///
/// Creates a temporary MSBuild content project in memory.
///
void CreateBuildProject()
{
string projectPath = Path.Combine(buildDirectory, "content.contentproj");
string outputPath = Path.Combine(buildDirectory, "bin");
// Create the build engine.
msBuildEngine = new Engine(RuntimeEnvironment.GetRuntimeDirectory());
// Hook up our custom error logger.
errorLogger = new ErrorLogger();
msBuildEngine.RegisterLogger(errorLogger);
// Create the build project.
msBuildProject = new Project(msBuildEngine);
msBuildProject.FullFileName = projectPath;
msBuildProject.SetProperty("XnaPlatform", "Windows");
msBuildProject.SetProperty("XnaFrameworkVersion", "v2.0");
msBuildProject.SetProperty("Configuration", "Release");
msBuildProject.SetProperty("OutputPath", outputPath);
// Register any custom importers or processors.
foreach (string pipelineAssembly in pipelineAssemblies)
{
msBuildProject.AddNewItem("Reference", pipelineAssembly);
}
// Include the standard targets file that defines
// how to build XNA Framework content.
msBuildProject.AddNewImport("$(MSBuildExtensionsPath)\\Microsoft\\XNA " +
"Game Studio\\v3.0\\Microsoft.Xna.GameStudio" +
".ContentPipeline.targets", null);
}
///
/// Adds a new content file to the MSBuild project. The importer and
/// processor are optional: if you leave the importer null, it will
/// be autodetected based on the file extension, and if you leave the
/// processor null, data will be passed through without any processing.
///
public void Add(string filename, string name, string importer, string processor)
{
BuildItem buildItem = msBuildProject.AddNewItem("Compile", filename);
buildItem.SetMetadata("Link", Path.GetFileName(filename));
buildItem.SetMetadata("Name", name);
if (!string.IsNullOrEmpty(importer))
buildItem.SetMetadata("Importer", importer);
if (!string.IsNullOrEmpty(processor))
buildItem.SetMetadata("Processor", processor);
}
///
/// Removes all content files from the MSBuild project.
///
public void Clear()
{
msBuildProject.RemoveItemsByName("Compile");
}
///
/// Builds all the content files which have been added to the project,
/// dynamically creating .xnb files in the OutputDirectory.
/// Returns an error message if the build fails.
///
public string Build()
{
// Clear any previous errors.
errorLogger.Errors.Clear();
// Build the project.
if (!msBuildProject.Build())
{
// If the build failed, return an error string.
return string.Join("\n", errorLogger.Errors.ToArray());
}
return null;
}
#endregion
#region Temp Directories
///
/// Creates a temporary directory in which to build content.
///
void CreateTempDirectory()
{
// Start with a standard base name:
//
// %temp%\WinFormsContentLoading.ContentBuilder
baseDirectory = Path.Combine(Path.GetTempPath(), GetType().FullName);
// Include our process ID, in case there is more than
// one copy of the program running at the same time:
//
// %temp%\WinFormsContentLoading.ContentBuilder\
int processId = Process.GetCurrentProcess().Id;
processDirectory = Path.Combine(baseDirectory, processId.ToString());
// Include a salt value, in case the program
// creates more than one ContentBuilder instance:
//
// %temp%\WinFormsContentLoading.ContentBuilder\\
directorySalt++;
buildDirectory = Path.Combine(processDirectory, directorySalt.ToString());
// Create our temporary directory.
Directory.CreateDirectory(buildDirectory);
PurgeStaleTempDirectories();
}
///
/// Deletes our temporary directory when we are finished with it.
///
void DeleteTempDirectory()
{
Directory.Delete(buildDirectory, true);
// If there are no other instances of ContentBuilder still using their
// own temp directories, we can delete the process directory as well.
if (Directory.GetDirectories(processDirectory).Length == 0)
{
Directory.Delete(processDirectory);
// If there are no other copies of the program still using their
// own temp directories, we can delete the base directory as well.
if (Directory.GetDirectories(baseDirectory).Length == 0)
{
Directory.Delete(baseDirectory);
}
}
}
///
/// Ideally, we want to delete our temp directory when we are finished using
/// it. The DeleteTempDirectory method (called by whichever happens first out
/// of Dispose or our finalizer) does exactly that. Trouble is, sometimes
/// these cleanup methods may never execute. For instance if the program
/// crashes, or is halted using the debugger, we never get a chance to do
/// our deleting. The next time we start up, this method checks for any temp
/// directories that were left over by previous runs which failed to shut
/// down cleanly. This makes sure these orphaned directories will not just
/// be left lying around forever.
///
void PurgeStaleTempDirectories()
{
// Check all subdirectories of our base location.
foreach (string directory in Directory.GetDirectories(baseDirectory))
{
// The subdirectory name is the ID of the process which created it.
int processId;
if (int.TryParse(Path.GetFileName(directory), out processId))
{
try
{
// Is the creator process still running?
Process.GetProcessById(processId);
}
catch (ArgumentException)
{
// If the process is gone, we can delete its temp directory.
Directory.Delete(directory, true);
}
}
}
}
#endregion
}
}