|
|
Custom Content Pipeline to process an XML file?
Last post 04-30-2008, 4:19 AM by phil. 12 replies.
-
06-18-2007, 6:56 AM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Custom Content Pipeline to process an XML file?
Hi,
I'm desiging a Tower Defence engine/game (http://tdengine.pbwiki.com/) and am looking to store the level/map data in an XML file.
I have managed to set one up so it looks like this and is parsed using a standard XMLTextReader...
*I can't work out how to post code...
The problem is that someone could edit the XML File accidentely/deliberately and change the game...
I'm curous to know if there is someway to make it using custom content pipelines so that it's a binary file that can't be edited (maybe with the extension .tdl (tower defence level))
Many thanks,
AP
|
|
-
06-18-2007, 2:41 PM |
|
|
Re: Custom Content Pipeline to process an XML file?
Sure, you could do that. You would need to either write an importer to read your XML format, or arrange it in such a way that it can be read using the standard built-in XML Importer. You probably wouldn't need any processor at all, so you could set that to "No Processing Required". Then you write a ContentTypeWriter to save your data into XML format, and a ContentTypeReader to load it back in to the game. The font sample on this site is probably a good place to start. Ignore everything to do with the processor, and just look at the format of the XML data it reads in, and how the ContentTypeWriter and ContentTypeReader work.
-- XNA Framework Developer blog - homepage
|
|
-
06-19-2007, 8:18 AM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
Ok, i have looked at a number of examples including the font sample and have written a processor, ContentTypeReader, ContentImporter, and ContentTypeWriter...
As I understand it... the custom importer and things below are run at buildtime...
It first calls the custom importer I have written...
[ContentImporter(".tdl", DefaultProcessor="LP_LevelProcessor")]
public class LP_LevelImporter : ContentImporter
{
public override LP_LevelDescription Import(string filename, ContentImporterContext context)
{
LP_LevelDescription description = new LP_LevelDescription();
int currentRow = 0;
FileStream stream = new FileStream("Content/" + filename, FileMode.Open, FileAccess.Read, FileShare.Read);
XmlReader reader = new XmlTextReader(stream);
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.LocalName.Equals("Name"))
{
description.Name = reader.ReadString();
}
if (reader.LocalName.Equals("row"))
{
parseRow(reader.ReadString(), currentRow, ref description);
currentRow++;
}
if (reader.LocalName.Equals("path1"))
{
parsePath(reader.ReadString(), 1, ref description);
}
if (reader.LocalName.Equals("path2"))
{
parsePath(reader.ReadString(), 2, ref description);
}
if (reader.LocalName.Equals("entry1"))
{
parseEntry(reader.ReadString(), 1, ref description);
}
if (reader.LocalName.Equals("entry2"))
{
parseEntry(reader.ReadString(), 2, ref description);
}
if (reader.LocalName.Equals("exit1"))
{
parseExit(reader.ReadString(), 1, ref description);
}
if (reader.LocalName.Equals("exit2"))
{
parseExit(reader.ReadString(), 2, ref description);
}
}
}
return description;
}
which then calls the content processor... (in my case the processor just passes on the data as it's already in the required format...
[ContentProcessor]
public class LP_LevelProcessor : ContentProcessor
{
public override LP_LevelDescription Process(LP_LevelDescription input, ContentProcessorContext context)
{
return input;
}
}
which then calls the writer, which writes the content to a binary xnb file...
[ContentTypeWriter]
public class LP_LevelWriter : ContentTypeWriter
{
protected override void Write(ContentWriter output, LP_LevelDescription value)
{
output.WriteObject(value.Name);
output.WriteObject(value.Grid);
output.WriteObject(value.Path1);
output.WriteObject(value.Path2);
output.WriteObject(value.Entries);
output.WriteObject(value.Exits);
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
return "TD_Engine.Utilities.LevelPipeline.TD_LevelReader, TD_Engine, " + "Version=1.0.0.0, Culture=neutral";
}
public override string GetRuntimeType(TargetPlatform targetPlatform)
{
return "TD_Engine.Game_Objects.Levels.TD_Level, TD_Engine, Version=1.0.0.0, Culture=neutral";
}
}
then at runtime when I call
TD_Level level = content.Load("Content/Levels/Level1");
it calls the Content reader class...
public class TD_LevelReader : ContentTypeReader
{
protected override TD_Level Read(ContentReader input, TD_Level existingInstance)
{
string name = input.ReadObject();
int[,] grid = input.ReadObject();
LinkedList path1 = input.ReadObject>();
LinkedList path2 = input.ReadObject>();
Point[] entries = input.ReadObject();
Point[] exits = input.ReadObject();
return new TD_Level(name, grid, path1, path2, entries, exits);
}
}
... now I think I am majorly confused...
Any help would be appreciated...
btw, how do I format the code so it's coloured and indented so XML files show the tages and not just the data?
Thanks,
AP
|
|
-
06-19-2007, 2:04 PM |
|
|
Re: Custom Content Pipeline to process an XML file?
That code all looks sane to me. What problem are you having with it? btw. you don't actually need to make a custom processor if you just want to pass the data through unchanged: the built-in PassThroughProcessor class (which shows up as "No Processing Required" in the VS properties pane) can be used for this. To insert formatted code, use the Insert Code Snippet icon (bottom right of all the icons above the post editor pane)
-- XNA Framework Developer blog - homepage
|
|
-
06-19-2007, 8:54 PM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
Right, it appears I need to use IE to see the buttons, Opera doesn't show any formatting buttons or options.
Thanks for the tip on the processor.
My problem was that that it wasn't building but I realised that it was because of my poor coding and had left a NullReference lying around... Stupid!
Now I have a problem where I can't serialize a multidimensional array. Unfortunately the Grid in this bit of code...
protected override void Write(ContentWriter output, LP_LevelDescription value) { output.WriteObject(value.Name); output.WriteObject(value.Grid); output.WriteObject(value.Path1); output.WriteObject(value.Path2); output.WriteObject(value.Entries); output.WriteObject(value.Exits); }
is a 32,20 grid, for holding all of the tile numbers...
Finally, I can post the XML format...
<?xml version="1.0" encoding="utf-8" ?> <Level> <!-- This the name of the level/map.--> <Name>Test</Name> <!--This is the grid the will be used to load the tiles.--> <Grid> <row>00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> <row>00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00</row> </Grid> <!--The two paths that the critters can travel along to. The first points the last points must correspond with the entrys and exits.--> <Paths> <path1>01,01;01,02;</path1> <path2>03,14;12,10;</path2> </Paths> <!--The two entry points--> <Entries> <entry1>0,2</entry1> <entry2>12,14</entry2> </Entries> <!--The two exits--> <Exits> <exit1>0,29</exit1> <exit2>32,0</exit2> </Exits> </Level>
Any suggestions?
Thanks
AP
|
|
-
06-20-2007, 12:11 AM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
Woot! Victory is mine...
I got it to work once I did a little bit of jumping through some hoops, i'll list the problems I had and how I fixed them for other people to read.
1. Cannot serialise a multi-dimensional array...
Fixed but splitting the 2D array into lots of little 1D arrays then putting it back together in the reader.
In the writer, this is how it works...
protected override void Write(ContentWriter output, TDP_LevelDescription value) { output.WriteObject(value.Name); for (int i = 0; i < value.Grid.GetLength(1); i++) { output.WriteObject(getSingleArray(ref value.Grid,i)); } output.WriteObject(value.Path1); output.WriteObject(value.Path2); output.WriteObject(value.Entries); output.WriteObject(value.Exits); }
private int[] getSingleArray(ref int[,] grid, int row) { int[] array = new int[32];
for (int i = 0; i < grid.GetLength(0); i++) { array[i] = grid[i,row]; }
return array; }
Then in the reader...
protected override TD_Level Read(ContentReader input, TD_Level existingInstance) { int[,] grid = new int[32, 20]; string name = input.ReadObject<string>();
for (int i = 0; i < grid.GetLength(1); i++) { addRow(ref grid, input.ReadObject<int[]>(), i); }
LinkedList<Point> path1 = input.ReadObject<LinkedList<Point>>();
LinkedList<Point> path2 = input.ReadObject<LinkedList<Point>>();
Point[] entries = input.ReadObject<Point[]>();
Point[] exits = input.ReadObject<Point[]>();
return new TD_Level(name, grid, path1, path2, entries, exits); }
private void addRow(ref int[,] multiArray, int[] array, int row) { for (int i = 0; i < array.GetLength(0); i++) { multiArray[i, row] = array[i]; } }
2. Cannot serialise a LinkedList<Point>
Fixed by creating two new classes, A ContentTypeWriter and ContentTypeReader, designed specifically for this case...
By converting the LinkedList to an array before serialisation, it's easy and simple to make this work...
First, the writer...
[ContentTypeWriter] public class TDP_LinkedListWriter : ContentTypeWriter<LinkedList<Point>> { protected override void Write(ContentWriter output, LinkedList<Point> value) { Point[] array = new Point[value.Count]; value.CopyTo(array,0);
output.WriteObject(array); }
public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "TD_Engine.Pipeline.TD_LinkedListReader, TD_Engine, Version=1.0.0.0, Culture=neutral"; }
public override string GetRuntimeType(TargetPlatform targetPlatform) { return "System.Collections.Generics.LinkedList<Point>, System, Version=1.0.0.0, Culture=neutral"; } }
and the reader...
public class TD_LinkedListReader : ContentTypeReader<LinkedList<Point>> { protected override LinkedList<Point> Read(ContentReader input, LinkedList<Point> existingInstance) { LinkedList<Point> path = new LinkedList<Point>(input.ReadObject<Point[]>());
return path; } }
And there we go, nice and simple and easy to use...
I'll be posting on the Wiki Page about the custom pipeline soon as to show people how this is structured.
The wiki is at: http://tdengine.pbwiki.com
Thanks Shawn for your help,
AP
|
|
-
06-20-2007, 12:32 PM |
|
|
Re: Custom Content Pipeline to process an XML file?
Awesome! Glad to hear you got this working. Nice trick using the array as a helper for serializing the list. If there are any types that you think ought to be serializable by default (such as multidimensional arrays and LinkedList), please file a suggestion about these on the connect site. When deciding which types we should build in ContentTypeWriter and ContentTypeReader implementations for, we pretty much just had to guess which would be the most common and important, so it would be very interesting to get data on anything you are wanting to use that we missed.
-- XNA Framework Developer blog - homepage
|
|
-
06-20-2007, 9:48 PM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
It seems like the multidimensional array non-serialization is an issue with the language or whatever, not an ContentType thingo...
I tried to make the LinkedList ContentTypeWriter generic but it doesn't seem to link it, seems that you need specific types of ContentType classes...
But i'll file a suggestion on the connect site to see if you guys to do it backend :D
|
|
-
06-21-2007, 11:53 AM |
|
|
Re: Custom Content Pipeline to process an XML file?
There's definately a limitation that the Content Pipeline doesn't support multidimensional arrays: I remember writing some code to detect if you tried to use them and make sure we threw a nice exception message to explain that they aren't supported! I don't know how well they are supported elsewhere (in the XmlSerializer, etc) - never tried that myself. You should be able to make a generic ContentTypeWriter and ContentTypeReader pair. We had to do that internally for the List<T> and Dictionary<K,T> support, so I implemented this in a flexible way that you should be able to take advantage of. The syntax is: [ContentTypeWriter] class LinkedListWriter<T> : ContentTypeWriter<LinkedList<T>> { ... } The serialization infrastructure will spot that this is a generic, figure out the pattern of what T means, and use reflection to specialize this generic any time it needs to serialize a type that matches the pattern LinkedList<T>. Let me know if you can't get that working...
-- XNA Framework Developer blog - homepage
|
|
-
06-21-2007, 10:33 PM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
Hmm, ok I can make the Writer generic...
When I was doing it i forgot the <T> on the class name :S Didn't I feel stupid when I saw your code :D
I seem to have a problem though when I try and make the reader generic.
This is the current code for the Writer, this works fine!
[ContentTypeWriter] public class TDP_LinkedListWriter<T> : ContentTypeWriter<LinkedList<T>> { protected override void Write(ContentWriter output, LinkedList<T> value) { T[] array = new T[value.Count]; value.CopyTo(array,0);
output.WriteObject(array); }
public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "TD_Engine.Pipeline.TD_LinkedListReader<T>, TD_Engine, Version=1.0.0.0, Culture=neutral"; }
public override string GetRuntimeType(TargetPlatform targetPlatform) { return "System.Collections.Generics.LinkedList<T>, System, Version=1.0.0.0, Culture=neutral"; } }
I'm not sure if the <T> in the RuntimeReader and RuntimeType methods are legal...
Now when it call the content.Load<TD_Level>... i get this error
Microsoft.Xna.Framework.Content.ContentLoadException was unhandled Message="Error loading \"Levels\\Level1\". Cannot find ContentTypeReader TD_Engine.Pipeline.TD_LinkedListReader<T>, TD_Engine, Version=1.0.0.0, Culture=neutral." Source="Microsoft.Xna.Framework" StackTrace: at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.InstantiateTypeReader(String readerTypeName, ContentReader contentReader, ContentTypeReader& reader) at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.GetTypeReader(String readerTypeName, ContentReader contentReader, List`1& newTypeReaders) at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.ReadTypeManifest(Int32 typeCount, ContentReader contentReader) at Microsoft.Xna.Framework.Content.ContentReader.ReadHeader() at Microsoft.Xna.Framework.Content.ContentReader.ReadAsset[T]() at Microsoft.Xna.Framework.Content.ContentManager.ReadAsset[T](String assetName, Action`1 recordDisposableObject) at Microsoft.Xna.Framework.Content.ContentManager.Load[T](String assetName) at TD_Engine.TD_Game.LoadGraphicsContent(Boolean loadAllContent) in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\TD_Game.cs:line 94 at Microsoft.Xna.Framework.Game.Initialize() at TD_Engine.TD_Game.Initialize() in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\TD_Game.cs:line 77 at Microsoft.Xna.Framework.Game.Run() at TD_Engine.Program.Main(String[] args) in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\Program.cs:line 14
It can't seem to find the Reader now...
this is the readers code...
public class TD_LinkedListReader<T> : ContentTypeReader<LinkedList<T>> { protected override LinkedList<T> Read(ContentReader input, LinkedList<T> existingInstance) { LinkedList<T> path = new LinkedList<T>(input.ReadObject<T[]>());
return path; } }
I think it has comething to do with the use of the TD_Engine.Pipeline.TD_LinkedListReader<T> in the GetRuntimeReader in the writer... I suspect it is becuase the reader changes from <T> to <Point> and it's still looking for <T>.
Is this the issue or something I have completely overlooked?
|
|
-
06-22-2007, 1:26 PM |
|
|
Re: Custom Content Pipeline to process an XML file?
For a generic type, GetRuntimeReader will should return something like: LinkedListReader`1[System.Int32] The `1 bit is the mangling the CLR includes to identify generic type names (1 would change if it had something other than 1 generic type argument). The typename inside the brackets needs to be expanded out, not just the T placeholder. You can use typeof(T).AssemblyQualifiedName to look this up. For a 100% robust implementation, you will also want to handle the possibility that the runtime version of T might not be the same as the design time version. For instance if you were serializing a LinkedList<Texture2DContent>, at runtime you would want to load this into a LinkedList<Texture2D>. You can handle types that change to something different when loaded by looking up the ContentTypeWriter for your T class, and calling its GetRuntimeType method. In your ContentTypeWriter: - Override the Initialize method, and call compiler.GetTypeWriter(typeof(T)) to look up the ContentTypeWriter for T. Store this somewhere you can get at it later.
- In your GetRuntimeReader method, return "LinkedListReader`1[" + writerForTypeT.GetRuntimeType(targetPlatform) + "]".
Or you can not bother, in which case your writer will still work fine for most types, and just not be able to hande those crazy graphics types that are different at design time and runtime.
-- XNA Framework Developer blog - homepage
|
|
-
06-23-2007, 3:56 AM |
-
AP Erebus
-
-
-
Joined on 06-11-2007
-
-
Posts 8
-
-
|
Re: Custom Content Pipeline to process an XML file?
Thanks for all you help :D
I tried to use the typeof(T).AssemblyQualifiedName but it still threw an error...
this is the GetRuntimeReader method...
public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "TD_Engine.Pipeline.TD_LinkedListReader`1[" + typeof(T).AssemblyQualifiedName + "] , TD_Engine, Version=1.0.0.0, Culture=neutral"; }
but i get this error
Microsoft.Xna.Framework.Content.ContentLoadException was unhandled Message="Error loading \"Levels\\Level1\". Cannot find ContentTypeReader TD_Engine.Pipeline.TD_LinkedListReader`1[Microsoft.Xna.Framework.Point, Microsoft.Xna.Framework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d] , TD_Engine, Version=1.0.0.0, Culture=neutral." Source="Microsoft.Xna.Framework" StackTrace: at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.InstantiateTypeReader(String readerTypeName, ContentReader contentReader, ContentTypeReader& reader) at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.GetTypeReader(String readerTypeName, ContentReader contentReader, List`1& newTypeReaders) at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.ReadTypeManifest(Int32 typeCount, ContentReader contentReader) at Microsoft.Xna.Framework.Content.ContentReader.ReadHeader() at Microsoft.Xna.Framework.Content.ContentReader.ReadAsset[T]() at Microsoft.Xna.Framework.Content.ContentManager.ReadAsset[T](String assetName, Action`1 recordDisposableObject) at Microsoft.Xna.Framework.Content.ContentManager.Load[T](String assetName) at TD_Engine.TD_Game.LoadGraphicsContent(Boolean loadAllContent) in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\TD_Game.cs:line 94 at Microsoft.Xna.Framework.Game.Initialize() at TD_Engine.TD_Game.Initialize() in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\TD_Game.cs:line 77 at Microsoft.Xna.Framework.Game.Run() at TD_Engine.Program.Main(String[] args) in D:\XNA\Projects\TD_Engine\TD_Engine (x86)\Program.cs:line 14
but when I only make it typeof(T) in the GetRuntimeReader is works... i'm quite happy that this works but I was wondering why the error came up?
Thanks,
AP
|
|
-
04-30-2008, 4:19 AM |
-
phil
-
-
-
Joined on 02-03-2008
-
-
Posts 94
-
-
|
Re: Custom Content Pipeline to process an XML file?
Shawn Hargreaves: The `1 bit is the mangling the CLR includes to identify generic type names (1 would change if it had something other than 1 generic type argument). The typename inside the brackets needs to be expanded out, not just the T placeholder. You can use typeof(T).AssemblyQualifiedName to look this up.
I just spent a good while debugging why this wasn't working on the XBOX. I had been using AssemblyQualifiedName, but it includes the "PublicKeyToken", which was different on windows (where the ContentTypeWriter is run) than on XBOX - so I was getting content load exceptions on the XBOX.
Changing it to FullName fixed it.
|
|
|
|
|