Search code examples
c#xnaxna-4.0content-pipeline

Loading Textures from a reference to built content in an XML file


Goal

I'm trying to load a custom class that contains a Texture2D from an xml file using the default importer (XML Content), with no processor.


Approach

Lots of research online and a lot of dealing with other errors lead me to this XML:

<?xml version="1.0" encoding="utf-16"?>
<XnaContent xmlns:Components="Entities.Components">
  <Asset Type="EntitiesContentPipeline.EntityTemplateContent">
    <Name>entity name</Name>
    <TestTexture>
      <Reference>#External1</Reference>
    </TestTexture>
  </Asset>
  <ExternalReferences>
    <ExternalReference ID="#External1" TargetType="Microsoft.Xna.Framework.Graphics.Texture2D">C:\Documents and Settings\GDuckett\My Documents\Visual Studio 2010\Projects\Gravitron\Gravitron\Gravitron\bin\x86\Debug\Content\Bullet.xnb</ExternalReference>
  </ExternalReferences>
</XnaContent>

Yes, i don't like the hard-coded path either, but if i can get this working without a custom reader and or writer for each type containing a Texture2D i can live with it.

Below is my content version of the class (used by the pipeline):

[ContentSerializerRuntimeType("Entities.Content.EntityTemplate, Entities")]
public class EntityTemplateContent
{
    public string Name;
    public ExternalReference<Texture2D> TestTexture;

    public EntityTemplateContent()
    {

    }
}

Below is my runtime version:

public class EntityTemplate
{
    public string Name;
    public Texture2D TestTexture;

    public EntityTemplate()
    {

    }
}

Problem

If i try and do var test = Content.Load<EntityTemplate>("BulletTemplate"); below is the error i get:

Error loading "Bullet". ContentTypeReader Microsoft.Xna.Framework.Content.Texture2DReader, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553 conflicts with existing handler Microsoft.Xna.Framework.Content.ReflectiveReader`1[[Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553]], Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553 for type Microsoft.Xna.Framework.Graphics.Texture2D.

It looks like the runtime reader found 2 readers for dealing with a Texture2D asset, the ReflectiveReader<Texture2D> reader and the Texture2DReader.


Question

How can i solve this problem, so i end up with an object correctly populated, with the Texture2D property referencing a loaded texture?

Note: I don't want to add another string property and create a method on my object called LoadContent or something. I'd like to have Content.Load be the only thing i need to call.

I also want to avoid writing my own readers / writers for every type that contains a Texture2D property.

Ideally I want to avoid creating a wrapper class for Texture2D, or a subclass but if there's no alternative then I'm happy for a solution that does this.


Solution

  • The exact error message was caused by having another Texture2D field in the content object.

    The overall problem of getting a reference to a runtime type from an ExternalReference<T> in a content type was solved with the below.

    Currently it's really a proof-of-concept class, as such it works for the classes I've thrown at it so far, but will probably crash with anything more complicated.

    It uses reflection to convert any fields or properties of the input that are ExternalReference<T>'s into built versions of the type requested by creating an appropriate version of ContentProcessorContext.BuildAsset<T,T> and invoking it. It recurses down the object tree to do the same for references to other objects.

    [ContentProcessor(DisplayName = "ExternalRefObjectContentProcessor")]
    public class ExternalRefObjectContentProcessor : ContentProcessor<object, object>
    {
        private void ReplaceReferences(object input, ContentProcessorContext context)
        {
            Func<ExternalReference<object>, string, object> BuildAssetMethodTemplate = context.BuildAsset<object, object>;
            var BuildAssetMethod = BuildAssetMethodTemplate.Method.GetGenericMethodDefinition();
    
            foreach (var field in input.GetType().GetFields().Where(f => !f.IsStatic && !f.IsLiteral))
            {
                Type fieldType = field.FieldType;
                object fieldValue = field.GetValue(input);
    
                if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(ExternalReference<>))
                {
                    var GenericBuildMethod = BuildAssetMethod.MakeGenericMethod(fieldType.GetGenericArguments().First(), fieldType.GetGenericArguments().First());
    
                    object BuiltObject;
    
                    try
                    {
                        BuiltObject = GenericBuildMethod.Invoke(context, new object[] { fieldValue, null });
                    }
                    catch (Exception Ex)
                    {
                        throw Ex.InnerException;
                    }
    
                    field.SetValue(input, BuiltObject);
                }
                else if (fieldValue is IEnumerable && !(fieldValue is string))
                {
                    foreach (var item in (fieldValue as IEnumerable))
                    {
                        ReplaceReferences(item, context);
                    }
                }
                else if (fieldValue != null && !(fieldValue is string))
                {
                    ReplaceReferences(fieldValue, context);
                }
            }
    
            foreach (var property in input.GetType().GetProperties().Where(p => p.CanRead && p.CanWrite))
            {
                Type propertyType = property.PropertyType;
                object propertyValue = property.GetValue(input, null);
    
                if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(ExternalReference<>))
                {
                    var GenericBuildMethod = BuildAssetMethod.MakeGenericMethod(propertyType.GetGenericArguments().First(), propertyType.GetGenericArguments().First());
    
                    object BuiltObject;
    
                    try
                    {
                        BuiltObject = GenericBuildMethod.Invoke(context, new object[] { property.GetValue(input, null), null });
                    }
                    catch (Exception Ex)
                    {
                        throw Ex.InnerException;
                    }
                    property.SetValue(input, BuiltObject, null);
                }
                else if (propertyValue is IEnumerable && !(propertyValue is string))
                {
                    foreach (var item in (propertyValue as IEnumerable))
                    {
                        ReplaceReferences(item, context);
                    }
                }
                else if (propertyValue != null && !(propertyValue is string))
                {
                    ReplaceReferences(propertyValue, context);
                }
            }
        }
    
        public override object Process(object input, ContentProcessorContext context)
        {
            ReplaceReferences(input, context);
    
            return input;
        }
    }