Monthly Archives: May 2012

Working with iOS Property Lists using T4

One of the disadvantages on working with on a multi platform project, and one of the other platforms is iOS, is that iOS is considered the main game and everyone else a port. This is our case, so the actual game design and tweaking is being done in a bunch of Property Lists.

So the first thing we have to do is import these files and read them. The property list is nothing more then a XML file with a bunch of properties in it.

Because these files will constantly change throughout the project, and the game designer can decide to add or remove properties from them, I decided that instead of just reading the XML I should also generate a strongly type C# Class representing what I read. This allows me to get compiler errors if someone removes a property I’m using, if someone changes the structure of an array or if someone decides to change a type of a property, because I’m working with a strongly typed class I will get compiler errors warning me of the issue.

You can download the full solution here: PListReader

The solution: Convert the Property Lists to a C# Class

To solve my Property List issues I decided to write a T4 template, that I point to a folder with all the .plist files and it will generate a C# class. The project is setup like this:

plist1

All the Property Lists are in the Configuration folder, the PList.tt is the T4 template, the Plist.cs is the C# class and Program.cs as some prototyping code that I left there because it may be useful to test new template functionality.

To exemplify I will show an example Property List and the resulting C# Class. An example .plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Structures</key>
	<array>
		<string>well</string>
		<string>toolshed</string>
		<string>storageshed</string>
		<string>field</string>
	</array>
	<key>Seeds</key>
	<array/>
	<key>Clients</key>
	<array/>
	<key>Season</key>
	<integer>0</integer>
	<key>Score</key>
	<integer>1000</integer>
	<key>ScoreExpert</key>
	<integer>1500</integer>
	<key>Time</key>
	<integer>120</integer>
</dict>
</plist>

And the generated C# class:

public static class Level0101
{
	public static string[] Structures { get {
												return new string[] {
													"well",
													"toolshed",
													"storageshed",
													"field",
												};
											} }

	public static int Season { get { return 0 ; } }
	public static int Score { get { return 1000 ; } }
	public static int ScoreExpert { get { return 1500 ; } }
	public static int Time { get { return 120 ; } }
}

Details of the T4 Template

I built the template for the features the game designer is currently using, so I skipped on a lot of Property types that can exist in a Property List. This template is a starting point and not finished work.

The complexity of the template comes from the array type in Property Lists, because the arrays come be of N dimension and because they can contain a mix of types there’s a lot of inspection going on to determine the dimension and if the array is just one type, or several. If we have a mix of several types inside the array we just say it’s an object array.

Of the possible key types in Property Lists, the designer is only using string and integer, so those are the only two that the template looks at.

On the top of the template, the folder where all the .plist files are located is defined, this needs to be changed for each specific scenario.

string[] filepaths = Directory.GetFiles(System.IO.Path.GetDirectoryName(this.Host.TemplateFile) + "\\Configuration", "*.plist");

Reading through a Property List file

The main part of the template, iterates through all the files, reads the contents of each file and parses all the keys:

<#
	foreach(var file in filepaths)
	{
#>
	// From File: <#= file.Substring(file.LastIndexOf("\\") + 1) #>
	public static class <#= FilePath2Class(file) #>
	{
<#
	    XElement plist = XElement.Load(file);
	    XElement plistDic = plist.Elements().Single();

        using (IEnumerator<XElement> enumerator = plistDic.Elements().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                if (enumerator.Current.Name != "key") continue;

                string keyName = enumerator.Current.Value;
                enumerator.MoveNext();
                XName typeName = enumerator.Current.Name;
                string typeValue = enumerator.Current.Value;

                if (typeName.ToString() == "array")
                {
					WriteArray(enumerator.Current, keyName);
                }
				else
				{
					WriteType(typeName.ToString(), keyName, typeValue);
				}
            }
        }
#>
	}

<#
	}
#>

Contains a helper function to convert the file name into a class name, it’s not perfect and sometimes will break naming conventions:

string FilePath2Class(string filePath)
{
	var final = filePath.Substring(filePath.LastIndexOf("\\") + 1);
	final = final.Substring(0, final.Length - 6).Replace("_", "");
	final = final[0].ToString().ToUpper() + final.Substring(1);
	return final;
}

When we encounter a key that’s not an array, we use a helper method to write it:

	void WriteType(string type, string name, string typeValue)
	{
		if(type == "string")
		{
#>
		public static string <#= name #> { get { return "<#= typeValue #>" ; } }
<#+
		}
		if(type == "integer")
		{
#>
		public static int <#= name #> { get { return <#= typeValue #> ; } }
<#+
		}
	}

Reading through arrays

Write arrays is composed of 4 methods, one that write an array (the header of the array):

	void WriteArray(XElement element, string key)
	{

		IEnumerable<XElement> descendants = element.Elements();
		XElement firstChild = descendants.FirstOrDefault();
		
		if(firstChild == null) return; // Empty Array
		
		XElement firstTypeIterator = element;
		string firstType = firstType = firstTypeIterator.Name.ToString();
		int depth = 0;

		while(firstType == "array")
		{
			depth++;
			firstTypeIterator = firstTypeIterator.Elements().FirstOrDefault();
			firstType = firstTypeIterator.Name.ToString();
		}

		string arrayType = GetArrayType(element, firstType);
		if(arrayType == "integer") arrayType = "int"; // int normalization, should be done as a method!

#>
		public static <#= arrayType #><#= WriteArrayDimension(depth) #> <#= key #> { get {
													return new <#= arrayType #><#= WriteArrayDimension(depth) #> {
<#+
		WriteArraySet(element);
#>													};
												} }

<#+
	}

One that writes each component of the array:

	void WriteArraySet(XElement element)
	{
		foreach(XElement child in element.Elements())
		{
			string elementType = child.Name.ToString();
			if(elementType == "array")
			{
#>														{
<#+
				PushIndent("\t");
				WriteArraySet(child);
				PopIndent();
#>														},
<#+
			}
			else if(elementType == "string")
			{
#>														"<#= child.Value #>",
<#+
			}
			else if(elementType == "integer")
			{
#>														<#= child.Value #>,
<#+
			}
		}
	}

And two other helper methods, one for finding out the dimension of the array:

	string WriteArrayDimension(int size)
	{
		string result = "[";
		for(int i = 1; i < size; i++) result += ",";
		result += "]";
		return result;
	}

And finally one to determine what should be the type of the array

	string GetArrayType(XElement element, string type)
	{
		IEnumerable<XElement> descendants = element.Elements();
		XElement firstChild = descendants.FirstOrDefault();

		if(firstChild == null) return type; // Empty Array
		string firstType = firstChild.Name.ToString();

		foreach(XElement descendant in descendants)
		{
			if(descendant.Name.ToString() == "array")
			{
				string tmpType = GetArrayType(descendant, type);
				if(tmpType != type) return "object";
			}
			else
			{
				if(descendant.Name.ToString() != type) return "object";
			}
		}

		return type;
	}

The full Template

<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ Assembly Name="System.Xml.dll" #>
<#@ Assembly Name="System.Xml.Linq.dll" #>
<#@ Assembly Name="System.Windows.Forms.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#   
	string[] filepaths = Directory.GetFiles(System.IO.Path.GetDirectoryName(this.Host.TemplateFile) + "\\Configuration", "*.plist");
#>
namespace PListReader.Configuration
{
<#
	foreach(var file in filepaths)
	{
#>
	// From File: <#= file.Substring(file.LastIndexOf("\\") + 1) #>
	public static class <#= FilePath2Class(file) #>
	{
<#
	    XElement plist = XElement.Load(file);
	    XElement plistDic = plist.Elements().Single();

        using (IEnumerator<XElement> enumerator = plistDic.Elements().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                if (enumerator.Current.Name != "key") continue;

                string keyName = enumerator.Current.Value;
                enumerator.MoveNext();
                XName typeName = enumerator.Current.Name;
                string typeValue = enumerator.Current.Value;

                if (typeName.ToString() == "array")
                {
					WriteArray(enumerator.Current, keyName);
                }
				else
				{
					WriteType(typeName.ToString(), keyName, typeValue);
				}
            }
        }
#>
	}

<#
	}
#>
}
<#+
	string FilePath2Class(string filePath)
	{
		var final = filePath.Substring(filePath.LastIndexOf("\\") + 1);
		final = final.Substring(0, final.Length - 6).Replace("_", "");
		final = final[0].ToString().ToUpper() + final.Substring(1);
		return final;
	}
	
	void WriteType(string type, string name, string typeValue)
	{
		if(type == "string")
		{
#>
		public static string <#= name #> { get { return "<#= typeValue #>" ; } }
<#+
		}
		if(type == "integer")
		{
#>
		public static int <#= name #> { get { return <#= typeValue #> ; } }
<#+
		}
	}

	void WriteArray(XElement element, string key)
	{

		IEnumerable<XElement> descendants = element.Elements();
		XElement firstChild = descendants.FirstOrDefault();
		
		if(firstChild == null) return; // Empty Array
		
		XElement firstTypeIterator = element;
		string firstType = firstType = firstTypeIterator.Name.ToString();
		int depth = 0;

		while(firstType == "array")
		{
			depth++;
			firstTypeIterator = firstTypeIterator.Elements().FirstOrDefault();
			firstType = firstTypeIterator.Name.ToString();
		}

		string arrayType = GetArrayType(element, firstType);
		if(arrayType == "integer") arrayType = "int"; // int normalization, should be done as a method!

#>		public static <#= arrayType #><#= WriteArrayDimension(depth) #> <#= key #> { get {
													return new <#= arrayType #><#= WriteArrayDimension(depth) #> {
<#+
		WriteArraySet(element);
#>													};
												} }

<#+
	}
	
	string WriteArrayDimension(int size)
	{
		string result = "[";
		for(int i = 1; i < size; i++) result += ",";
		result += "]";
		return result;
	}

	void WriteArraySet(XElement element)
	{
		foreach(XElement child in element.Elements())
		{
			string elementType = child.Name.ToString();
			if(elementType == "array")
			{
#>														{
<#+
				PushIndent("\t");
				WriteArraySet(child);
				PopIndent();
#>														},
<#+
			}
			else if(elementType == "string")
			{
#>														"<#= child.Value #>",
<#+
			}
			else if(elementType == "integer")
			{
#>														<#= child.Value #>,
<#+
			}
		}
	}
	
	string GetArrayType(XElement element, string type)
	{
		IEnumerable<XElement> descendants = element.Elements();
		XElement firstChild = descendants.FirstOrDefault();

		if(firstChild == null) return type; // Empty Array
		string firstType = firstChild.Name.ToString();

		foreach(XElement descendant in descendants)
		{
			if(descendant.Name.ToString() == "array")
			{
				string tmpType = GetArrayType(descendant, type);
				if(tmpType != type) return "object";
			}
			else
			{
				if(descendant.Name.ToString() != type) return "object";
			}
		}

		return type;
	}
#>

Conclusion

One of the reasons why this doesn’t feel like final work is because it isn’t. In the end I went with a different solution, a lot more complex then this one, but one that is clean and will remain very effective through the rest of the development of the Game: I wrote a DSL that both generates Property Lists and C# code to fully define, and in some cases handle some logic, a game level.