view api/Factories/Xml/WarFoundryXmlRaceFactory.cs @ 219:f609bcf7035b

Fixes #222: decimal comma/point not handled correctly (again?) in costMultiplier * Replace two "type.Parse" calls with XmlTools calls All decimals in WarFoundry should use the decimal point (or "period" to Americans) rather than the decimal comma because that's what XML uses in its default type definitions
author IBBoard <dev@ibboard.co.uk>
date Sat, 28 Nov 2009 16:40:27 +0000
parents 89e26d51afc2
children 78f7456f6419
line wrap: on
line source

//  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;

namespace IBBoard.WarFoundry.API.Factories.Xml
{
	/// <summary>
	/// A sub-factory for loading WarFoundry Race XML files
	/// </summary>
	public class WarFoundryXmlRaceFactory : AbstractStagedLoadedSubFactory
	{
		private Dictionary<Race, XmlDocument> extraData = new Dictionary<Race, XmlDocument>();
		
		public WarFoundryXmlRaceFactory(WarFoundryXmlFactory factory) : base (factory)
		{
			//Do nothing special
		}
		
		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 CreateRaceFromElement(ZipFile file, XmlElement elem)
		{
			string id = elem.GetAttribute("id");
			string subid = elem.GetAttribute("subid");
			string systemID = elem.GetAttribute("system");
			string name = elem.GetAttribute("name");
			Race race = new Race(id, subid, name, WarFoundryLoader.GetDefault().GetGameSystem(systemID), mainFactory);
			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:units/race:unit"))
			{
				GetUnitTypeForElement(node, race);
			}
			
			race.SetAsFullyLoaded();
		}

		private Category CreateCategoryFromElement(XmlElement elem, Race parentRace)
		{
			Category cat = CreateCategoryFromElement(elem);
			parentRace.AddCategory(cat);
			return cat;
		}

		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);
			LoadEquipmentSlotsForUnitType(elem, type);
			LoadEquipmentForUnitType(elem, type);
			LoadAbilitiesForUnitType(elem, type);
			LoadContainedUnitsForUnitType(elem, type);
			LoadRequirementsForUnitType(elem, type);
			LoadExtraDataForUnitType(elem, type);
			LoadNotesForUnitType(elem, type);
			parentRace.AddUnitType(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, "unitPoints");
			}
			catch (FormatException ex)
			{
				throw new InvalidFileException(ex.Message, ex);
			}

			string mainCatID = elem.GetAttribute("cat");
			Category cat = type.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;
			XmlElement statsElement = WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:stats");
			Stats unitStats = ParseUnitStats(statsElement, type.GameSystem);
			type.SetUnitStats(unitStats);
		}

		private void LoadEquipmentSlotsForUnitType(XmlElement elem, UnitType type)
		{
			foreach (XmlElement equipSlot in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:equipmentSlots/race:equipmentSlot"))
			{
				LoadEquipmentSlotForUnitType (type, equipSlot);
			}
		}

		private static void LoadEquipmentSlotForUnitType (UnitType type, XmlElement equipSlot)
		{
			string slotName = equipSlot.GetAttribute ("name");
			AbstractLimit limit = GetMaxLimit (equipSlot);
			
			if (limit!=null)
			{
				type.AddEquipmentSlot (slotName, limit);
			}
		}

		private static AbstractLimit GetMaxLimit (XmlElement equipSlot)
		{
			XmlElement limitElem = WarFoundryXmlFactoryUtils.SelectSingleElement(equipSlot, "race:maxLimit/*[1]");
			return GetLimitFromElement(limitElem);
		}
		
		private static AbstractLimit GetLimitFromElement(XmlElement limitElem)
		{
			AbstractLimit limit = null;

			if (limitElem != null)
			{
				switch (limitElem.LocalName)
				{
					case "percentageLimit":
						double limitPercent = XmlTools.GetDoubleValueFromAttribute(limitElem, "limit");
						bool roundUp = limitElem.GetAttribute("round").Equals("up");
						limit = new SimpleRoundedPercentageLimit(limitPercent, roundUp);
						break;
					case "sizeConstrainedLimit":
						limit = new NumericSizeConstrainedLimit(XmlTools.GetIntValueFromAttribute(limitElem, "limit"));
						break;
					case "absoluteLimit":
						limit = new AbsoluteNumericLimit(XmlTools.GetIntValueFromAttribute(limitElem, "limit"));
						break;
					case "unitSizeLimit":
						limit = new SimpleRoundedPercentageLimit(100);
						break;
					default:
						//TODO: Warn of missing handler for when we've extended the limit list
						break;
				}
			}
			
			return limit;
		}


		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");
						}
					}

					AbstractLimit 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 static AbstractLimit GetMinLimit(XmlElement elem)
		{
			XmlElement limitElem = WarFoundryXmlFactoryUtils.SelectSingleElement(elem, "race:minLimit/*[1]");
			return GetLimitFromElement(limitElem);
		}
		
		private void LoadAbilitiesForUnitType(XmlElement elem, UnitType type)
		{
			foreach (XmlElement ability in WarFoundryXmlFactoryUtils.SelectNodes(elem, "race:unitAbilities/race:unitAbility"))
			{
				string id = ability.GetAttribute("abilityID");
				bool required = XmlTools.GetBoolValueFromAttribute(ability, "required");
				type.AddAbility(type.Race.GetAbility(id), 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)
		{
			//TODO: Load requirements
		}
		
		private void LoadExtraDataForUnitType(XmlElement elem, UnitType type)
		{
		}
		
		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)
		{
			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;
			ArmourType armourType;
			
			try
			{
				cost = XmlTools.GetDoubleValueFromAttribute(elem, "cost");
			}
			catch(FormatException ex)
			{
				throw new InvalidFileException("Attribute 'cost' of equipment item "+id+" was not a valid number", ex);
			}			
			
			try
			{
				armourType = (ArmourType)Enum.Parse(typeof(ArmourType), elem.GetAttribute("armourType"));
			}
			catch(ArgumentException ex)
			{
				throw new InvalidFileException("Attribute 'armourType' of equipment "+id+" was not a valid value from the enumeration", ex);
			}
			
			//TODO: Parse equipment stats if there are any
			item.Cost = cost;
			item.ItemArmourType = armourType;
			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 = elem.SelectSingleNode("description", WarFoundryXmlFactoryUtils.GetNamespaceManager());
			ability.Description = (node == null) ? "" : node.InnerText;
			race.AddAbility(ability);
			return ability;
		}
	}
}