changeset 469:2ba1f24eb427

Re #359: Add "only contained" attribute to unit types * Add attribute to schema and load in factory Also: * Reformat line endings to be consistent
author IBBoard <dev@ibboard.co.uk>
date Sat, 14 Apr 2012 20:04:31 +0100
parents 95c1b68a600b
children 426b8c5e283c
files API/Factories/Xml/WarFoundryXmlRaceFactory.cs schemas/race.xsd
diffstat 2 files changed, 477 insertions(+), 475 deletions(-) [+]
line wrap: on
line diff
--- a/API/Factories/Xml/WarFoundryXmlRaceFactory.cs	Sat Apr 14 16:57:34 2012 +0100
+++ b/API/Factories/Xml/WarFoundryXmlRaceFactory.cs	Sat Apr 14 20:04:31 2012 +0100
@@ -1,111 +1,111 @@
-//  This file (WarFoundryXmlRaceFactory.cs) is a part of the IBBoard.WarFoundry.API project and is copyright 2009 IBBoard
-// 
-// The file and the library/program it is in are licensed and distributed, without warranty, under the GNU Affero GPL license, either version 3 of the License or (at your option) any later version. Please see COPYING for more information and the full license.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Xml;
-using IBBoard.Xml;
-using IBBoard.IO;
-using IBBoard.Limits;
-using IBBoard.CustomMath;
-using ICSharpCode.SharpZipLib.Zip;
+//  This file (WarFoundryXmlRaceFactory.cs) is a part of the IBBoard.WarFoundry.API project and is copyright 2009 IBBoard
+// 
+// The file and the library/program it is in are licensed and distributed, without warranty, under the GNU Affero GPL license, either version 3 of the License or (at your option) any later version. Please see COPYING for more information and the full license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using IBBoard.Xml;
+using IBBoard.IO;
+using IBBoard.Limits;
+using IBBoard.CustomMath;
+using ICSharpCode.SharpZipLib.Zip;
 using IBBoard.WarFoundry.API.Objects;
 using IBBoard.WarFoundry.API.Objects.Requirement;
-using IBBoard.WarFoundry.API.Factories.Requirement;
-
-namespace IBBoard.WarFoundry.API.Factories.Xml
-{
-	/// <summary>
-	/// A sub-factory for loading WarFoundry Race XML files
-	/// </summary>
-	public class WarFoundryXmlRaceFactory : IRaceFactory<XmlDocument, XmlElement>
-	{
-		private Dictionary<Race, XmlDocument> extraData = new Dictionary<Race, XmlDocument>();
+using IBBoard.WarFoundry.API.Factories.Requirement;
+
+namespace IBBoard.WarFoundry.API.Factories.Xml
+{
+	/// <summary>
+	/// A sub-factory for loading WarFoundry Race XML files
+	/// </summary>
+	public class WarFoundryXmlRaceFactory : IRaceFactory<XmlDocument, XmlElement>
+	{
+		private Dictionary<Race, XmlDocument> extraData = new Dictionary<Race, XmlDocument>();
 		private WarFoundryXmlLimitParser limitParser = new WarFoundryXmlLimitParser();
-		private WarFoundryXmlFactory mainFactory;
-		
-		public WarFoundryXmlRaceFactory(WarFoundryXmlFactory factory)
-		{
-			this.mainFactory = factory;
-		}
-		
-		private void StoreExtraData(Race wfObject, XmlElement elem)
-		{
-			extraData[wfObject] = elem.OwnerDocument;
-		}
-	
-		private XmlDocument GetExtraData(Race obj)
-		{
-			XmlDocument extra = null;
-			extraData.TryGetValue(obj, out extra);
-			return extra;
+		private WarFoundryXmlFactory mainFactory;
+		
+		public WarFoundryXmlRaceFactory(WarFoundryXmlFactory factory)
+		{
+			this.mainFactory = factory;
+		}
+		
+		private void StoreExtraData(Race wfObject, XmlElement elem)
+		{
+			extraData[wfObject] = elem.OwnerDocument;
+		}
+	
+		private XmlDocument GetExtraData(Race obj)
+		{
+			XmlDocument extra = null;
+			extraData.TryGetValue(obj, out extra);
+			return extra;
 		}
 
 		public Race CreateRace(XmlElement elem)
 		{
-			string id = elem.GetAttribute("id");
-			string subid = elem.GetAttribute("subid");
-			string systemID = elem.GetAttribute("system");
-			string name = elem.GetAttribute("name");
-            string armyDefaultName = elem.GetAttribute("defaultArmyName");
-			GameSystem gameSystem = WarFoundryLoader.GetDefault ().GetGameSystem (systemID);
-
-			if (gameSystem == null)
-			{
-				throw new InvalidFileException("Referenced game system, '"+systemID+"', did not exist");
-			}
-
-            Race race = new Race(id, subid, name, gameSystem, mainFactory);
-			race.ArmyDefaultName = armyDefaultName;
-			StoreExtraData(race, elem);
-			return race;
-		}
-		
-		public void CompleteLoading(Race race)
-		{
-			if (!WarFoundryXmlFactoryUtils.CanCompleteLoading(race))
-			{
-				return;
-			}
-			
-			race.SetAsLoading();			
-			XmlDocument extraData = GetExtraData(race);
-			
-			foreach (XmlElement node in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:categories/cat:cat"))
-			{
-				CreateCategoryFromElement(node, race);
-			}
-							
-			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:equipment/race:equipmentItem"))
-			{
-				CreateEquipmentItemFromElement(node, race);
-			}
-							
-			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:abilities/race:ability"))
-			{
-				CreateAbilityFromElement(node, race);
-			}
-							
-			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:memberTypes/race:memberType"))
-			{
-				CreateMemberTypeFromElement(node, race);
-			}
-			
-			foreach (XmlElement node in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:units/race:unit"))
-			{
-				GetUnitTypeForElement(node, race);
-			}
-			
-			race.SetAsFullyLoaded();
-		}
-
-		private Category CreateCategoryFromElement(XmlElement elem, Race parentRace)
-		{
-			Category cat = CategoryLoader.CreateFromElement(elem);
-			parentRace.AddCategory(cat);
-			return cat;
+			string id = elem.GetAttribute("id");
+			string subid = elem.GetAttribute("subid");
+			string systemID = elem.GetAttribute("system");
+			string name = elem.GetAttribute("name");
+            string armyDefaultName = elem.GetAttribute("defaultArmyName");
+			GameSystem gameSystem = WarFoundryLoader.GetDefault ().GetGameSystem (systemID);
+
+			if (gameSystem == null)
+			{
+				throw new InvalidFileException("Referenced game system, '"+systemID+"', did not exist");
+			}
+
+            Race race = new Race(id, subid, name, gameSystem, mainFactory);
+			race.ArmyDefaultName = armyDefaultName;
+			StoreExtraData(race, elem);
+			return race;
+		}
+		
+		public void CompleteLoading(Race race)
+		{
+			if (!WarFoundryXmlFactoryUtils.CanCompleteLoading(race))
+			{
+				return;
+			}
+			
+			race.SetAsLoading();			
+			XmlDocument extraData = GetExtraData(race);
+			
+			foreach (XmlElement node in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:categories/cat:cat"))
+			{
+				CreateCategoryFromElement(node, race);
+			}
+							
+			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:equipment/race:equipmentItem"))
+			{
+				CreateEquipmentItemFromElement(node, race);
+			}
+							
+			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:abilities/race:ability"))
+			{
+				CreateAbilityFromElement(node, race);
+			}
+							
+			foreach (XmlElement node  in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:memberTypes/race:memberType"))
+			{
+				CreateMemberTypeFromElement(node, race);
+			}
+			
+			foreach (XmlElement node in WarFoundryXmlFactoryUtils.SelectNodes(extraData, "/race:race/race:units/race:unit"))
+			{
+				GetUnitTypeForElement(node, race);
+			}
+			
+			race.SetAsFullyLoaded();
+		}
+
+		private Category CreateCategoryFromElement(XmlElement elem, Race parentRace)
+		{
+			Category cat = CategoryLoader.CreateFromElement(elem);
+			parentRace.AddCategory(cat);
+			return cat;
 		}
 
 		
@@ -124,275 +124,276 @@
 			}
 
 			return type;
-		}
-
-		private UnitType GetUnitTypeFromDocument(XmlDocument doc, string id, Race parentRace)
-		{
-			XmlElement unitWithId = WarFoundryXmlFactoryUtils.SelectSingleElement (doc, "/race:race/race:units/race:unit[@id='" + id + "']");
-			
-			if (unitWithId == null)
-			{
-				throw new InvalidFileException("Could not find unit with ID "+id);
-			}
-			
-			return GetUnitTypeForElement(unitWithId, parentRace);
-		}
-						
-		private UnitType GetUnitTypeForElement(XmlElement elem, Race parentRace)
-		{
-			string id = elem.GetAttribute("id");
-			UnitType type = parentRace.GetUnitType(id);
-
-			if (type==null)
-			{
-				type = CreateUnitTypeFromElement(elem, id, parentRace);
-			}
-			
-			return type;
-		}
-
-		private UnitType CreateUnitTypeFromElement(XmlElement elem, string id, Race parentRace)
-		{
-			string name = elem.GetAttribute("typeName");
-			UnitType type = new UnitType(id, name, parentRace);
-			LoadCoreValuesForUnitType(elem, type);
-			parentRace.AddUnitType(type);
-			LoadEquipmentSlotsForUnitType(elem, type);
-			LoadEquipmentForUnitType(elem, type);
-			LoadAbilitiesForUnitType(elem, type);
-			LoadContainedUnitsForUnitType(elem, type);
-			LoadRequirementsForUnitType(elem, type);
-			LoadExtraDataForUnitType(elem, type);
-			LoadNotesForUnitType(elem, type);
-			return type;
-		}
-
-		private void LoadCoreValuesForUnitType(XmlElement elem, UnitType type)
-		{
-			try
-			{
-				type.MaxNumber = XmlTools.GetIntValueFromAttribute(elem, "maxNum");
-				type.MinNumber = XmlTools.GetIntValueFromAttribute(elem, "minNum");
-				type.MaxSize = XmlTools.GetIntValueFromAttribute(elem, "maxSize");
-				type.MinSize = XmlTools.GetIntValueFromAttribute(elem, "minSize");
-				type.BaseSize = XmlTools.GetIntValueFromAttribute(elem, "baseSize");
-				type.CostPerTrooper = XmlTools.GetDoubleValueFromAttribute(elem, "points");
-				type.BaseUnitCost = XmlTools.GetDoubleValueFromAttribute(elem, "basePoints");
-			}
-			catch (FormatException ex)
-			{
-				throw new InvalidFileException(ex.Message, ex);
-			}
-
-			Race race = type.Race;
-			string mainCatID = elem.GetAttribute("cat");
-			Category cat = race.GetCategory(mainCatID);
-			
-			if (cat == null)
-			{
-				throw new InvalidFileException(String.Format("Category with ID '{1}' did not exist for UnitType '{0}'", type.Name, mainCatID));
-			}
-			
-			type.MainCategory = cat;
-			
-			XmlNodeList unitCategories = WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitCategories/race:unitCategory");
-			
-			foreach (XmlElement unitCategory in unitCategories)
-			{
-				string catID = unitCategory.GetAttribute("catID");
-				Category unitCat = race.GetCategory(catID);
-				
-				if (unitCat == null)
-				{
-					throw new InvalidFileException(String.Format("Category with ID '{1}' did not exist for UnitType '{0}'", type.Name, catID));
-				}
-				
-				type.AddCategory(unitCat);
-			}
-			
-			XmlElement statsElement = WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:stats");
-			
-			if (statsElement!=null)
-			{
-				Stats unitStats = ParseUnitStats(statsElement, type.GameSystem);
-				type.SetUnitStats(unitStats);
-			}
-			
-			XmlNodeList unitMemberReferences = WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitMembers/race:unitMember");
-			
-			foreach (XmlElement unitMemberRef in unitMemberReferences)
-			{
-				string typeID = unitMemberRef.GetAttribute("typeID");
-				UnitMemberType unitMemberType = race.GetUnitMemberType(typeID);
-				type.AddUnitMemberType(unitMemberType);
-			}
-		}
-
-		private void LoadEquipmentSlotsForUnitType(XmlElement elem, UnitType type)
-		{
-			foreach (XmlElement equipSlot in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:equipmentSlots/race:equipmentSlot"))
-			{
-				LoadEquipmentSlotForUnitType (type, equipSlot);
-			}
-		}
-
-		private void LoadEquipmentSlotForUnitType(UnitType type, XmlElement equipSlot)
-		{
-			string slotName = equipSlot.GetAttribute("name");
-			ILimit limit = GetMaxLimit(equipSlot);
-			
-			if (limit != null)
-			{
-				type.AddEquipmentSlot(slotName, limit);
-			}
-		}
-		
-		private ILimit GetMinLimit(XmlElement elem)
-		{
-			return limitParser.GetMinLimit(elem);
-		}
-
-		private ILimit GetMaxLimit(XmlElement elem)
-		{
-			return limitParser.GetMaxLimit(elem);
-		}
-
-		private void LoadEquipmentForUnitType(XmlElement elem, UnitType type)
-		{
-			foreach (XmlElement equip in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitEquipment/race:unitEquipmentItem"))
-			{
-				string id = equip.GetAttribute("id");
-				EquipmentItem equipItem = type.Race.GetEquipmentItem(id);
-				
-				if (equipItem!=null)
-				{
-					string mutexGroupString = equip.GetAttribute("exclusivityGroups");
-					string[] mutexGroups;
-
-					if (mutexGroupString == "")
-					{
-						mutexGroupString = equip.GetAttribute("exclusivityGroup");
-					}
-
-					if (mutexGroupString != "")
-					{
-						string[] groups = mutexGroupString.Split(',');
-						int groupCount = groups.Length;
-
-						for (int i = 0; i < groupCount; i++)
-						{
-							groups[i] = groups[i].Trim();
-						}
-
-						mutexGroups = groups;
-					}
-					else
-					{
-						mutexGroups = new string[0];
-					}
-
-					UnitEquipmentItem unitEquipItem = new UnitEquipmentItem(equipItem, type, mutexGroups);
-
-					string equipSlot = equip.GetAttribute("equipmentSlot");
-
-					if (equipSlot != "")
-					{
-						if (type.HasEquipmentSlot(equipSlot))
-						{
-							unitEquipItem.SlotName = equipSlot;
-						}
-						else
-						{
-							throw new InvalidFileException("Attribute 'equipmentSlot' of unit equipment item " + id + " for " + type.Name + " was not a valid slot name");
-						}
-					}
-
-					ILimit limit = GetMaxLimit(equip);
-
-					if (limit != null)
-					{
-						unitEquipItem.MaxLimit = limit;
-					}
-
-					limit = GetMinLimit(equip);
-
-					if (limit != null)
-					{
-						unitEquipItem.MinLimit = limit;
-					}
-					
-					unitEquipItem.RoundNumberUp = equip.GetAttribute("roundDirection").Equals("up");
-					
-					try
-					{
-						unitEquipItem.IsRequired = XmlTools.GetBoolValueFromAttribute(equip, "required");
-					}
-					catch(FormatException e)
-					{
-						throw new InvalidFileException("Attribute 'required' of unit equipment item " + id + " for " + type.Name + " was not a valid boolean", e);
-					}
-					
-					try
-					{
-						unitEquipItem.CostMultiplier = XmlTools.GetDoubleValueFromAttribute(equip, "costMultiplier");
-					}
-					catch (FormatException e)
-					{
-						throw new InvalidFileException("Attribute 'costMultiplier' of unit equipment item " + id + " for " + type.Name + " was not a valid decimal number", e);
-					}
-					
-					try
-					{
-						unitEquipItem.CostRoundType = (RoundType) Enum.Parse(typeof(RoundType), equip.GetAttribute("costRounding"));
-					}
-					catch (ArgumentException e)
-					{
-						throw new InvalidFileException("Attribute 'costRounding' of unit equipment item " + id + " for " + type.Name + " was not a valid rounding type", e);
-					}
-				}
-				else
-				{
-					throw new InvalidFileException("Equipment item with ID '" + id + "' was required by " + type.Name + " but was not found");
-				}
-			}		
-		}
-		
-		private void LoadAbilitiesForUnitType(XmlElement elem, UnitType type)
-		{
-			foreach (XmlElement abilityElem in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitAbilities/race:unitAbility"))
-			{
-				string id = abilityElem.GetAttribute("abilityID");
-				Ability ability = type.Race.GetAbility(id);
-				
-				if (ability == null)
-				{
-					throw new InvalidFileException("Ability for "+type.Name+ " with ID "+id+ " did not exist in race definition");
-				}
-
-				bool required = XmlTools.GetBoolValueFromAttribute(abilityElem, "required");
-				type.AddAbility(ability, required);
-			}
-		}
-		
-		private void LoadContainedUnitsForUnitType(XmlElement elem, UnitType type)
-		{
-			foreach (XmlElement containedUnitType in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:contains/race:containedUnit"))
-			{
-				string id = containedUnitType.GetAttribute("containedID");
-				UnitType containedType = GetUnitTypeFromDocument(elem.OwnerDocument, id, type.Race);
-
-				if (containedType!=null)
-				{
-					type.AddContainedUnitType(containedType);
-				}
-				else
-				{
-					throw new InvalidFileException("Unit type " + type.Name + " tried to contain undefined unit with ID "+id);
-				}
-			}
-		}
-
-		private void LoadRequirementsForUnitType(XmlElement elem, UnitType type)
+		}
+
+		private UnitType GetUnitTypeFromDocument(XmlDocument doc, string id, Race parentRace)
+		{
+			XmlElement unitWithId = WarFoundryXmlFactoryUtils.SelectSingleElement (doc, "/race:race/race:units/race:unit[@id='" + id + "']");
+			
+			if (unitWithId == null)
+			{
+				throw new InvalidFileException("Could not find unit with ID "+id);
+			}
+			
+			return GetUnitTypeForElement(unitWithId, parentRace);
+		}
+						
+		private UnitType GetUnitTypeForElement(XmlElement elem, Race parentRace)
+		{
+			string id = elem.GetAttribute("id");
+			UnitType type = parentRace.GetUnitType(id);
+
+			if (type==null)
+			{
+				type = CreateUnitTypeFromElement(elem, id, parentRace);
+			}
+			
+			return type;
+		}
+
+		private UnitType CreateUnitTypeFromElement(XmlElement elem, string id, Race parentRace)
+		{
+			string name = elem.GetAttribute("typeName");
+			UnitType type = new UnitType(id, name, parentRace);
+			LoadCoreValuesForUnitType(elem, type);
+			parentRace.AddUnitType(type);
+			LoadEquipmentSlotsForUnitType(elem, type);
+			LoadEquipmentForUnitType(elem, type);
+			LoadAbilitiesForUnitType(elem, type);
+			LoadContainedUnitsForUnitType(elem, type);
+			LoadRequirementsForUnitType(elem, type);
+			LoadExtraDataForUnitType(elem, type);
+			LoadNotesForUnitType(elem, type);
+			return type;
+		}
+
+		private void LoadCoreValuesForUnitType(XmlElement elem, UnitType type)
+		{
+			try
+			{
+				type.MaxNumber = XmlTools.GetIntValueFromAttribute(elem, "maxNum");
+				type.MinNumber = XmlTools.GetIntValueFromAttribute(elem, "minNum");
+				type.MaxSize = XmlTools.GetIntValueFromAttribute(elem, "maxSize");
+				type.MinSize = XmlTools.GetIntValueFromAttribute(elem, "minSize");
+				type.BaseSize = XmlTools.GetIntValueFromAttribute(elem, "baseSize");
+				type.CostPerTrooper = XmlTools.GetDoubleValueFromAttribute(elem, "points");
+				type.BaseUnitCost = XmlTools.GetDoubleValueFromAttribute(elem, "basePoints");
+				type.IsContainedOnly = XmlTools.GetBoolValueFromAttribute(elem, "containedOnly");
+			}
+			catch (FormatException ex)
+			{
+				throw new InvalidFileException(ex.Message, ex);
+			}
+
+			Race race = type.Race;
+			string mainCatID = elem.GetAttribute("cat");
+			Category cat = race.GetCategory(mainCatID);
+			
+			if (cat == null)
+			{
+				throw new InvalidFileException(String.Format("Category with ID '{1}' did not exist for UnitType '{0}'", type.Name, mainCatID));
+			}
+			
+			type.MainCategory = cat;
+			
+			XmlNodeList unitCategories = WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitCategories/race:unitCategory");
+			
+			foreach (XmlElement unitCategory in unitCategories)
+			{
+				string catID = unitCategory.GetAttribute("catID");
+				Category unitCat = race.GetCategory(catID);
+				
+				if (unitCat == null)
+				{
+					throw new InvalidFileException(String.Format("Category with ID '{1}' did not exist for UnitType '{0}'", type.Name, catID));
+				}
+				
+				type.AddCategory(unitCat);
+			}
+			
+			XmlElement statsElement = WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:stats");
+			
+			if (statsElement!=null)
+			{
+				Stats unitStats = ParseUnitStats(statsElement, type.GameSystem);
+				type.SetUnitStats(unitStats);
+			}
+			
+			XmlNodeList unitMemberReferences = WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitMembers/race:unitMember");
+			
+			foreach (XmlElement unitMemberRef in unitMemberReferences)
+			{
+				string typeID = unitMemberRef.GetAttribute("typeID");
+				UnitMemberType unitMemberType = race.GetUnitMemberType(typeID);
+				type.AddUnitMemberType(unitMemberType);
+			}
+		}
+
+		private void LoadEquipmentSlotsForUnitType(XmlElement elem, UnitType type)
+		{
+			foreach (XmlElement equipSlot in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:equipmentSlots/race:equipmentSlot"))
+			{
+				LoadEquipmentSlotForUnitType (type, equipSlot);
+			}
+		}
+
+		private void LoadEquipmentSlotForUnitType(UnitType type, XmlElement equipSlot)
+		{
+			string slotName = equipSlot.GetAttribute("name");
+			ILimit limit = GetMaxLimit(equipSlot);
+			
+			if (limit != null)
+			{
+				type.AddEquipmentSlot(slotName, limit);
+			}
+		}
+		
+		private ILimit GetMinLimit(XmlElement elem)
+		{
+			return limitParser.GetMinLimit(elem);
+		}
+
+		private ILimit GetMaxLimit(XmlElement elem)
+		{
+			return limitParser.GetMaxLimit(elem);
+		}
+
+		private void LoadEquipmentForUnitType(XmlElement elem, UnitType type)
+		{
+			foreach (XmlElement equip in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitEquipment/race:unitEquipmentItem"))
+			{
+				string id = equip.GetAttribute("id");
+				EquipmentItem equipItem = type.Race.GetEquipmentItem(id);
+				
+				if (equipItem!=null)
+				{
+					string mutexGroupString = equip.GetAttribute("exclusivityGroups");
+					string[] mutexGroups;
+
+					if (mutexGroupString == "")
+					{
+						mutexGroupString = equip.GetAttribute("exclusivityGroup");
+					}
+
+					if (mutexGroupString != "")
+					{
+						string[] groups = mutexGroupString.Split(',');
+						int groupCount = groups.Length;
+
+						for (int i = 0; i < groupCount; i++)
+						{
+							groups[i] = groups[i].Trim();
+						}
+
+						mutexGroups = groups;
+					}
+					else
+					{
+						mutexGroups = new string[0];
+					}
+
+					UnitEquipmentItem unitEquipItem = new UnitEquipmentItem(equipItem, type, mutexGroups);
+
+					string equipSlot = equip.GetAttribute("equipmentSlot");
+
+					if (equipSlot != "")
+					{
+						if (type.HasEquipmentSlot(equipSlot))
+						{
+							unitEquipItem.SlotName = equipSlot;
+						}
+						else
+						{
+							throw new InvalidFileException("Attribute 'equipmentSlot' of unit equipment item " + id + " for " + type.Name + " was not a valid slot name");
+						}
+					}
+
+					ILimit limit = GetMaxLimit(equip);
+
+					if (limit != null)
+					{
+						unitEquipItem.MaxLimit = limit;
+					}
+
+					limit = GetMinLimit(equip);
+
+					if (limit != null)
+					{
+						unitEquipItem.MinLimit = limit;
+					}
+					
+					unitEquipItem.RoundNumberUp = equip.GetAttribute("roundDirection").Equals("up");
+					
+					try
+					{
+						unitEquipItem.IsRequired = XmlTools.GetBoolValueFromAttribute(equip, "required");
+					}
+					catch(FormatException e)
+					{
+						throw new InvalidFileException("Attribute 'required' of unit equipment item " + id + " for " + type.Name + " was not a valid boolean", e);
+					}
+					
+					try
+					{
+						unitEquipItem.CostMultiplier = XmlTools.GetDoubleValueFromAttribute(equip, "costMultiplier");
+					}
+					catch (FormatException e)
+					{
+						throw new InvalidFileException("Attribute 'costMultiplier' of unit equipment item " + id + " for " + type.Name + " was not a valid decimal number", e);
+					}
+					
+					try
+					{
+						unitEquipItem.CostRoundType = (RoundType) Enum.Parse(typeof(RoundType), equip.GetAttribute("costRounding"));
+					}
+					catch (ArgumentException e)
+					{
+						throw new InvalidFileException("Attribute 'costRounding' of unit equipment item " + id + " for " + type.Name + " was not a valid rounding type", e);
+					}
+				}
+				else
+				{
+					throw new InvalidFileException("Equipment item with ID '" + id + "' was required by " + type.Name + " but was not found");
+				}
+			}		
+		}
+		
+		private void LoadAbilitiesForUnitType(XmlElement elem, UnitType type)
+		{
+			foreach (XmlElement abilityElem in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitAbilities/race:unitAbility"))
+			{
+				string id = abilityElem.GetAttribute("abilityID");
+				Ability ability = type.Race.GetAbility(id);
+				
+				if (ability == null)
+				{
+					throw new InvalidFileException("Ability for "+type.Name+ " with ID "+id+ " did not exist in race definition");
+				}
+
+				bool required = XmlTools.GetBoolValueFromAttribute(abilityElem, "required");
+				type.AddAbility(ability, required);
+			}
+		}
+		
+		private void LoadContainedUnitsForUnitType(XmlElement elem, UnitType type)
+		{
+			foreach (XmlElement containedUnitType in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:contains/race:containedUnit"))
+			{
+				string id = containedUnitType.GetAttribute("containedID");
+				UnitType containedType = GetUnitTypeFromDocument(elem.OwnerDocument, id, type.Race);
+
+				if (containedType!=null)
+				{
+					type.AddContainedUnitType(containedType);
+				}
+				else
+				{
+					throw new InvalidFileException("Unit type " + type.Name + " tried to contain undefined unit with ID "+id);
+				}
+			}
+		}
+
+		private void LoadRequirementsForUnitType(XmlElement elem, UnitType type)
 		{
 			foreach (XmlElement extraData in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:requirements/race:requirement"))
 			{
@@ -404,109 +405,109 @@
 					IRequirement req = reqFactory.CreateRequirement(type, data, this);
 					type.AddRequirement(req);
 				}
-			}
+			}
+		}
+		
+		private void LoadExtraDataForUnitType(XmlElement elem, UnitType type)
+		{
+			foreach (XmlElement extraData in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:extraData/race:data"))
+			{
+				string id = extraData.GetAttribute("id");
+				string data = extraData.InnerXml;
+				type.AddExtraData(id, data);
+			}
+		}
+		
+		private void LoadNotesForUnitType(XmlElement elem, UnitType type)
+		{
+			XmlNode node = WarFoundryXmlFactoryUtils.SelectSingleNode(elem, "race:notes");
+
+			if (node!=null)
+			{
+				type.Notes = node.InnerText;
+			}
+		}
+		
+		private Stats ParseUnitStats(XmlElement elem, GameSystem system)
+		{
+			if (elem == null)
+			{
+				return null;
+			}
+			
+			String statsID = elem.GetAttribute("statSet");
+			SystemStats statsSet;
+			
+			if (statsID == "")
+			{
+				statsSet = system.StandardSystemStats;
+			}
+			else
+			{
+				statsSet = system.GetSystemStatsForID(statsID);
+			}
+			
+			Stats stats = new Stats(statsSet);
+			
+			foreach (XmlElement stat in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:stat"))
+			{
+				String statName = stat.GetAttribute("name");
+				stats.SetStatValue(statName, stat.InnerText);
+			}
+			
+			return stats;
 		}
-		
-		private void LoadExtraDataForUnitType(XmlElement elem, UnitType type)
-		{
-			foreach (XmlElement extraData in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:extraData/race:data"))
-			{
-				string id = extraData.GetAttribute("id");
-				string data = extraData.InnerXml;
-				type.AddExtraData(id, data);
-			}
-		}
-		
-		private void LoadNotesForUnitType(XmlElement elem, UnitType type)
-		{
-			XmlNode node = WarFoundryXmlFactoryUtils.SelectSingleNode(elem, "race:notes");
-
-			if (node!=null)
-			{
-				type.Notes = node.InnerText;
-			}
-		}
-		
-		private Stats ParseUnitStats(XmlElement elem, GameSystem system)
-		{
-			if (elem == null)
-			{
-				return null;
-			}
-			
-			String statsID = elem.GetAttribute("statSet");
-			SystemStats statsSet;
-			
-			if (statsID == "")
-			{
-				statsSet = system.StandardSystemStats;
-			}
-			else
-			{
-				statsSet = system.GetSystemStatsForID(statsID);
-			}
-			
-			Stats stats = new Stats(statsSet);
-			
-			foreach (XmlElement stat in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:stat"))
-			{
-				String statName = stat.GetAttribute("name");
-				stats.SetStatValue(statName, stat.InnerText);
-			}
-			
-			return stats;
-		}
-		
-		private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, Race race)
-		{
-			string id = elem.GetAttribute("id");
-			EquipmentItem item = race.GetEquipmentItem(id);
-
-			if (item == null)
-			{
-				item = CreateEquipmentItemFromElement(elem, id, race);
-			}
-			
-			return item;
-		}
-
-		private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, string id, Race race)
-		{
-			string name = elem.GetAttribute("name");
-			EquipmentItem item = new EquipmentItem(id, name, race);
-			double cost = 0;
-			
-			try
-			{
-				cost = XmlTools.GetDoubleValueFromAttribute(elem, "cost");
-			}
-			catch(FormatException ex)
-			{
-				throw new InvalidFileException("Attribute 'cost' of equipment item "+id+" was not a valid number", ex);
-			}			
-						
-			//TODO: Parse equipment stats if there are any
-			item.Cost = cost;
-			race.AddEquipmentItem(item);			
-			return item;
-		}
-		
-		private Ability CreateAbilityFromElement(XmlElement elem, Race race)
-		{
-			string id = elem.GetAttribute("id");
-			string name = elem.GetAttribute("name");
-			Ability ability = new Ability(id, name);
-			XmlNode node = WarFoundryXmlFactoryUtils.SelectSingleNode(elem, "race:description");
-			ability.Description = (node == null) ? "" : node.InnerText;
-			race.AddAbility(ability);
-			return ability;
-		}		
-
-		private void CreateMemberTypeFromElement(XmlElement elem, Race race)
-		{
-			Stats stats = ParseUnitStats(WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:stats"), race.GameSystem);
-			UnitMemberType unitMemberType = new UnitMemberType(elem.GetAttribute("id"), elem.GetAttribute("name"), stats);
-			race.AddUnitMemberType(unitMemberType);
-		}
-	}
-}
+		
+		private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, Race race)
+		{
+			string id = elem.GetAttribute("id");
+			EquipmentItem item = race.GetEquipmentItem(id);
+
+			if (item == null)
+			{
+				item = CreateEquipmentItemFromElement(elem, id, race);
+			}
+			
+			return item;
+		}
+
+		private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, string id, Race race)
+		{
+			string name = elem.GetAttribute("name");
+			EquipmentItem item = new EquipmentItem(id, name, race);
+			double cost = 0;
+			
+			try
+			{
+				cost = XmlTools.GetDoubleValueFromAttribute(elem, "cost");
+			}
+			catch(FormatException ex)
+			{
+				throw new InvalidFileException("Attribute 'cost' of equipment item "+id+" was not a valid number", ex);
+			}			
+						
+			//TODO: Parse equipment stats if there are any
+			item.Cost = cost;
+			race.AddEquipmentItem(item);			
+			return item;
+		}
+		
+		private Ability CreateAbilityFromElement(XmlElement elem, Race race)
+		{
+			string id = elem.GetAttribute("id");
+			string name = elem.GetAttribute("name");
+			Ability ability = new Ability(id, name);
+			XmlNode node = WarFoundryXmlFactoryUtils.SelectSingleNode(elem, "race:description");
+			ability.Description = (node == null) ? "" : node.InnerText;
+			race.AddAbility(ability);
+			return ability;
+		}		
+
+		private void CreateMemberTypeFromElement(XmlElement elem, Race race)
+		{
+			Stats stats = ParseUnitStats(WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:stats"), race.GameSystem);
+			UnitMemberType unitMemberType = new UnitMemberType(elem.GetAttribute("id"), elem.GetAttribute("name"), stats);
+			race.AddUnitMemberType(unitMemberType);
+		}
+	}
+}
--- a/schemas/race.xsd	Sat Apr 14 16:57:34 2012 +0100
+++ b/schemas/race.xsd	Sat Apr 14 20:04:31 2012 +0100
@@ -273,6 +273,7 @@
 	<xs:attribute name="maxSize" type="core:positiveOrInfiniteInteger" default="-1"/>
 	<xs:attribute name="minNum" type="xs:nonNegativeInteger" default="0"/>
 	<xs:attribute name="maxNum" type="core:positiveOrInfiniteInteger" default="-1"/>
+	<xs:attribute name="containedOnly" type="xs:boolean" default="false" />
 	<xs:anyAttribute processContents="lax"/>
 </xs:complexType>