comparison api/Factories/Xml/WarFoundryXmlFactory.cs @ 52:64ef178c18aa

Re #10 - Refactor for readability * Break WarFoundryXMLFactory out in to GameSystem, Race and Army factories * Create factory utils classes with methods from WarFoundryXMLFactory for getting node lists etc
author IBBoard <dev@ibboard.co.uk>
date Mon, 30 Mar 2009 19:44:03 +0000
parents b271a2252758
children 3ea0ab04352b
comparison
equal deleted inserted replaced
51:b271a2252758 52:64ef178c18aa
24 /// The WarFoundryXmlFactory loads WarFoundry classes from the native "XML in a zip" file format. Files are validated using the schema for the file type, so structurally invalid files should be identified at initial load. 24 /// The WarFoundryXmlFactory loads WarFoundry classes from the native "XML in a zip" file format. Files are validated using the schema for the file type, so structurally invalid files should be identified at initial load.
25 /// </summary> 25 /// </summary>
26 public class WarFoundryXmlFactory : AbstractNativeWarFoundryFactory 26 public class WarFoundryXmlFactory : AbstractNativeWarFoundryFactory
27 { 27 {
28 private static WarFoundryXmlFactory factory; 28 private static WarFoundryXmlFactory factory;
29 private XmlReaderSettings settings; 29 private WarFoundryXmlGameSystemFactory gameSystemFactory;
30 private XmlNamespaceManager nsManager; 30 private WarFoundryXmlRaceFactory raceFactory;
31 private Dictionary<IWarFoundryObject, XmlDocument> extraData = new Dictionary<IWarFoundryObject, XmlDocument>(); 31 private WarFoundryXmlArmyFactory armyFactory;
32 32
33 public static AbstractNativeWarFoundryFactory GetFactory() 33 public static AbstractNativeWarFoundryFactory GetFactory()
34 { 34 {
35 if (factory == null) 35 if (factory == null)
36 { 36 {
40 return factory; 40 return factory;
41 } 41 }
42 42
43 private WarFoundryXmlFactory() : base() 43 private WarFoundryXmlFactory() : base()
44 { 44 {
45 //Hide constructor 45 gameSystemFactory = new WarFoundryXmlGameSystemFactory(this);
46 raceFactory = new WarFoundryXmlRaceFactory(this);
47 armyFactory = new WarFoundryXmlArmyFactory();
46 } 48 }
47 49
48 protected override bool CheckCanFindArmyFileContent(ZipFile file) 50 protected override bool CheckCanFindArmyFileContent(ZipFile file)
49 { 51 {
50 return file.FindEntry("data.armyx", true) > -1; 52 return file.FindEntry("data.armyx", true) > -1;
66 } 68 }
67 69
68 protected override Army CreateArmyFromStream (ZipFile file, Stream dataStream) 70 protected override Army CreateArmyFromStream (ZipFile file, Stream dataStream)
69 { 71 {
70 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.ARMY_ELEMENT); 72 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.ARMY_ELEMENT);
71 return CreateArmyFromElement(file, elem); 73 return armyFactory.CreateArmyFromElement(file, elem);
72 } 74 }
73 75
74 private XmlElement GetRootElementFromStream(Stream stream, WarFoundryXmlElementName elementName) 76 private XmlElement GetRootElementFromStream(Stream stream, WarFoundryXmlElementName elementName)
75 { 77 {
76 XmlDocument doc = CreateXmlDocumentFromStream(stream); 78 XmlDocument doc = WarFoundryXmlFactoryUtils.CreateXmlDocumentFromStream(stream);
77 XmlElement elem = (XmlElement)doc.LastChild; 79 XmlElement elem = (XmlElement)doc.LastChild;
78 80
79 if (!elem.LocalName.Equals(elementName.Value)) 81 if (!elem.LocalName.Equals(elementName.Value))
80 { 82 {
81 throw new InvalidFileException(String.Format("Root element of XML was not valid. Expected {0} but got {1}", elementName.Value, elem.Name)); 83 throw new InvalidFileException(String.Format("Root element of XML was not valid. Expected {0} but got {1}", elementName.Value, elem.Name));
82 } 84 }
83 85
84 return elem; 86 return elem;
85 }
86
87 private XmlDocument CreateXmlDocumentFromStream(Stream stream)
88 {
89 XmlDocument doc = new XmlDocument();
90 XmlReader reader = XmlReader.Create(stream, GetReaderSettings());
91
92 try
93 {
94 doc.Load(reader);
95 }
96 //Don't catch XMLSchemaExceptions - let them get thrown out
97 finally
98 {
99 reader.Close();
100 }
101
102 return doc;
103 }
104
105 /// <summary>
106 /// Lazy-getter for XML reader settings. May throw a <see cref="InvalidDataException"/> if there is a problem with the translation schema.
107 /// </summary>
108 /// <returns>
109 /// A <see cref="XmlReaderSettings"/> with the default values for validating the translation document against the translation schema
110 /// </returns>
111 private XmlReaderSettings GetReaderSettings()
112 {
113 if (settings == null)
114 {
115 settings = new XmlReaderSettings();
116 settings.ValidationType = ValidationType.Schema;
117 settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
118 settings.ProhibitDtd = true;
119 settings.ValidationEventHandler+= new ValidationEventHandler(ValidationEventMethod);
120 XmlSchemaSet cache = new XmlSchemaSet();
121 string path = IBBoard.Constants.ExecutablePath + "/dtds/";
122 string nsBase = "http://ibboard.co.uk/warfoundry/";
123 AddSchemaToCache(cache, nsBase + "core", path + "warfoundry-core.xsd");
124 AddSchemaToCache(cache, nsBase + "cats", path + "warfoundry-cats.xsd");
125 AddSchemaToCache(cache, nsBase + "race", path + "race.xsd");
126 AddSchemaToCache(cache, nsBase + "system", path + "system.xsd");
127 AddSchemaToCache(cache, nsBase + "army", path + "army.xsd");
128 settings.Schemas.Add(cache);
129 }
130
131 return settings;
132 }
133
134 private void ValidationEventMethod(object sender, ValidationEventArgs e)
135 {
136 throw new InvalidDataException("Problem validating against schema for WarFoundry data: " + e.Exception.Message, e.Exception);
137 }
138
139 private void AddSchemaToCache(XmlSchemaSet cache, string xmlNamespace, string schemaLocation)
140 {
141 try
142 {
143 cache.Add(xmlNamespace, schemaLocation);
144 }
145 catch (IOException ex)
146 {
147 LogNotifier.Warn(GetType(), "Problem reading schema: " + ex.Message, ex);
148 }
149 catch (XmlSchemaException ex)
150 {
151 LogNotifier.Warn(GetType(), "Problem validating schema for WarFoundry data: " + ex.Message, ex);
152 }
153 catch (XmlException ex)
154 {
155 LogNotifier.Warn(GetType(), "Problem reading data for schema: " + ex.Message, ex);
156 }
157 }
158
159 private Army CreateArmyFromElement(ZipFile file, XmlElement elem)
160 {
161 string name = elem.GetAttribute("name");
162 string systemID = elem.GetAttribute("gameSystem");
163 GameSystem system = WarFoundryLoader.GetDefault().GetGameSystem(systemID);
164 string raceID = elem.GetAttribute("race");
165 Race race = WarFoundryLoader.GetDefault().GetRace(system, raceID);
166 int points = XmlTools.GetIntValueFromAttribute(elem, "maxPoints");
167 Army army = new Army(race, name, points, file);
168 //TODO: Complete loading of army
169 return army;
170 } 87 }
171 88
172 protected override Stream GetGameSystemDataStream (ZipFile file) 89 protected override Stream GetGameSystemDataStream (ZipFile file)
173 { 90 {
174 return file.GetInputStream(file.FindEntry("data.systemx", true)); 91 return file.GetInputStream(file.FindEntry("data.systemx", true));
176 93
177 protected override GameSystem CreateGameSystemFromStream (ZipFile file, Stream dataStream) 94 protected override GameSystem CreateGameSystemFromStream (ZipFile file, Stream dataStream)
178 { 95 {
179 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.SYSTEM_ELEMENT); 96 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.SYSTEM_ELEMENT);
180 LogNotifier.Debug(GetType(), "Create GameSystem"); 97 LogNotifier.Debug(GetType(), "Create GameSystem");
181 return CreateSystemFromElement(file, elem); 98 return gameSystemFactory.CreateSystemFromElement(file, elem);
182 }
183
184 private GameSystem CreateSystemFromElement(ZipFile file, XmlElement elem)
185 {
186 string id = elem.GetAttribute("id");
187 string name = elem.GetAttribute("name");
188 GameSystem system = new GameSystem(id, name, this);
189 StoreExtraData(system, elem);
190 return system;
191 }
192
193 private void StoreExtraData(WarFoundryStagedLoadingObject wfObject, XmlElement elem)
194 {
195 extraData[wfObject] = elem.OwnerDocument;
196 } 99 }
197 100
198 protected override Stream GetRaceDataStream (ZipFile file) 101 protected override Stream GetRaceDataStream (ZipFile file)
199 { 102 {
200 return file.GetInputStream(file.FindEntry("data.racex", true)); 103 return file.GetInputStream(file.FindEntry("data.racex", true));
202 105
203 protected override Race CreateRaceFromStream (ZipFile file, Stream dataStream) 106 protected override Race CreateRaceFromStream (ZipFile file, Stream dataStream)
204 { 107 {
205 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.RACE_ELEMENT); 108 XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.RACE_ELEMENT);
206 LogNotifier.Debug(GetType(), "Create Race"); 109 LogNotifier.Debug(GetType(), "Create Race");
207 return CreateRaceFromElement(file, elem); 110 return raceFactory.CreateRaceFromElement(file, elem);
208 }
209
210 private Race CreateRaceFromElement(ZipFile file, XmlElement elem)
211 {
212 string id = elem.GetAttribute("id");
213 string subid = elem.GetAttribute("subid");
214 string systemID = elem.GetAttribute("system");
215 string name = elem.GetAttribute("name");
216 Race race = new Race(id, subid, name, systemID, this);
217 StoreExtraData(race, elem);
218 return race;
219 }
220
221 public XmlDocument GetExtraData(IWarFoundryObject obj)
222 {
223 XmlDocument extra = null;
224 extraData.TryGetValue(obj, out extra);
225 return extra;
226 }
227
228 private XmlNamespaceManager GetNamespaceManager()
229 {
230 if (nsManager == null)
231 {
232 nsManager = new XmlNamespaceManager(new NameTable());
233 nsManager.AddNamespace("core", "http://ibboard.co.uk/warfoundry/core");
234 nsManager.AddNamespace("cat", "http://ibboard.co.uk/warfoundry/cats");
235 nsManager.AddNamespace("race", "http://ibboard.co.uk/warfoundry/race");
236 nsManager.AddNamespace("system", "http://ibboard.co.uk/warfoundry/system");
237 nsManager.AddNamespace("army", "http://ibboard.co.uk/warfoundry/army");
238 }
239
240 return nsManager;
241 }
242
243 private XmlNodeList SelectNodes(XmlNode element, string xpathQuery)
244 {
245 return element.SelectNodes(xpathQuery, GetNamespaceManager());
246 }
247
248 private XmlNode SelectSingleNode(XmlNode element, string xpathQuery)
249 {
250 return element.SelectSingleNode(xpathQuery, GetNamespaceManager());
251 }
252
253 private XmlElement SelectSingleElement(XmlNode element, string xpathQuery)
254 {
255 XmlNode node = SelectSingleNode(element, xpathQuery);
256 return (node is XmlElement) ? (XmlElement) node : null;
257 } 111 }
258 112
259 public override void CompleteLoading(IWarFoundryStagedLoadObject obj) 113 public override void CompleteLoading(IWarFoundryStagedLoadObject obj)
260 { 114 {
261 LogNotifier.DebugFormat(GetType(), "Complete loading of {0} with ID {1}", obj.GetType().Name, obj.ID); 115 LogNotifier.DebugFormat(GetType(), "Complete loading of {0} with ID {1}", obj.GetType().Name, obj.ID);
262 116
263 if (obj is GameSystem) 117 if (obj is GameSystem)
264 { 118 {
265 CompleteLoading((GameSystem)obj); 119 gameSystemFactory.CompleteLoading((GameSystem)obj);
266 } 120 }
267 else if (obj is Race) 121 else if (obj is Race)
268 { 122 {
269 CompleteLoading((Race)obj); 123 raceFactory.CompleteLoading((Race)obj);
270 } 124 }
271 }
272
273 public void CompleteLoading(GameSystem system)
274 {
275 if (!CanCompleteLoading(system))
276 {
277 return;
278 }
279
280 system.SetAsLoading();
281 XmlDocument extraData = GetExtraData(system);
282 LoadCategoriesForSystem(system, extraData);
283 XmlElement statsElem = SelectSingleElement(extraData, "/system:system/system:sysStatsList");
284 string defaultStatsID = statsElem.GetAttribute("defaultStats");
285 LoadSystemStatsForSystem(system, extraData);
286 system.StandardSystemStatsID = defaultStatsID;
287 XmlElement systemElement = SelectSingleElement(extraData, "/system:system");
288 system.WarnOnError = XmlTools.GetBoolValueFromAttribute(systemElement, "warn");
289 system.AllowAllies = XmlTools.GetBoolValueFromAttribute(systemElement, "allowAllies");
290 LogNotifier.DebugFormat(GetType(), "Completed loading of GameSystem with ID {0}", system.ID);
291 LogNotifier.DebugFormat(GetType(), "GameSystem with ID {0} default stats: {1}", system.ID, system.StandardSystemStatsID);
292 system.SetAsFullyLoaded();
293 }
294
295 private bool CanCompleteLoading(IWarFoundryStagedLoadObject obj)
296 {
297 bool canLoad = true;
298
299 if (obj.IsFullyLoaded)
300 {
301 LogNotifier.DebugFormat(GetType(), "Object of type {0} with ID {1} is already fully loaded", obj.GetType().Name, obj.ID);
302 canLoad = false;
303 }
304 else if (obj.IsLoading)
305 {
306 LogNotifier.WarnFormat(GetType(), "Object of type {0} with ID {1} is already being loaded", obj.GetType().Name, obj.ID);
307 canLoad = false;
308 }
309
310 return canLoad;
311 }
312
313 private void LoadCategoriesForSystem(GameSystem system, XmlNode elem)
314 {
315 foreach (XmlElement cat in SelectNodes(elem, "/system:system/system:categories/cat:cat"))
316 {
317 system.AddCategory(CreateCategoryFromElement(cat));
318 }
319 }
320
321 private Category CreateCategoryFromElement(XmlElement elem)
322 {
323 string id = elem.GetAttribute("id");
324 string name = elem.GetAttribute("name");
325 Category cat = new Category(id, name);
326 cat.MaximumPercentage = XmlTools.GetIntValueFromAttribute(elem, "maxPercentage");
327 cat.MinimumPercentage = XmlTools.GetIntValueFromAttribute(elem, "minPercentage");
328 cat.MaximumPoints = XmlTools.GetIntValueFromAttribute(elem, "maxPoints");
329 cat.MinimumPoints = XmlTools.GetIntValueFromAttribute(elem, "minPoints");
330 return cat;
331 }
332
333 private void LoadSystemStatsForSystem(GameSystem system, XmlNode elem)
334 {
335 foreach (XmlElement stats in SelectNodes(elem, "/system:system/system:sysStatsList/system:sysStats"))
336 {
337 SystemStats sysStats = CreateSystemStatsFromElement(stats);
338 system.AddSystemStats(sysStats);
339 }
340 }
341
342 private SystemStats CreateSystemStatsFromElement(XmlElement elem)
343 {
344 List<StatSlot> slots = new List<StatSlot>();
345 string id = elem.GetAttribute("id");
346
347 foreach (XmlElement slot in elem.ChildNodes)
348 {
349 StatSlot statSlot = new StatSlot(slot.GetAttribute("name"));
350 slots.Add(statSlot);
351 }
352
353 return new SystemStats(id, slots.ToArray());
354 }
355
356 public void CompleteLoading(Race race)
357 {
358 if (!CanCompleteLoading(race))
359 {
360 return;
361 }
362
363 race.SetAsLoading();
364 XmlDocument extraData = GetExtraData(race);
365
366 foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:units/race:unit"))
367 {
368 UnitType type = CreateUnitTypeFromElement(node, race, race.GameSystem);
369 race.AddUnitType(type);
370 }
371
372 foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:categories/cat:cat"))
373 {
374 race.AddCategory(CreateCategoryFromElement(node));
375 }
376
377 foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:equipment/cat:equipmentItem"))
378 {
379 EquipmentItem item = CreateEquipmentItemFromElement(node, race);
380 race.AddEquipmentItem(item);
381 }
382
383 foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:abilities/cat:ability"))
384 {
385 Ability ability = CreateAbilityFromElement(node, race);
386 race.AddAbility(ability);
387 }
388
389 race.SetAsFullyLoaded();
390 LogNotifier.DebugFormat(GetType(), "Completed loading of Race with ID {0}", race.ID);
391 }
392
393 private UnitType CreateUnitTypeFromElement(XmlElement elem, Race parentRace, GameSystem system)
394 {
395 string id = elem.GetAttribute("id");
396 string name = elem.GetAttribute("typeName");
397 UnitType type = new UnitType(id, name, parentRace);
398 type.MaxNumber = XmlTools.GetIntValueFromAttribute(elem, "maxNum");
399 type.MinNumber = XmlTools.GetIntValueFromAttribute(elem, "minNum");
400 type.MaxSize = XmlTools.GetIntValueFromAttribute(elem, "maxSize");
401 type.MinSize = XmlTools.GetIntValueFromAttribute(elem, "minSize");
402 type.BaseSize = XmlTools.GetIntValueFromAttribute(elem, "baseSize");
403 type.CostPerTrooper = XmlTools.GetIntValueFromAttribute(elem, "points");
404 type.BaseUnitCost = XmlTools.GetIntValueFromAttribute(elem, "unitPoints");
405 string mainCatID = elem.GetAttribute("cat");
406 Category cat = parentRace.GetCategory(mainCatID);
407
408 if (cat == null)
409 {
410 throw new InvalidDataException(String.Format("Attribute 'cat' of UnitType {0} (value: {1}) did not reference a valid category", id, mainCatID));
411 }
412
413 type.MainCategory = cat;
414 XmlElement statsElement = SelectSingleElement(elem, "/race:race/race:units/race:unit/race:stats");
415 type.UnitStats = ParseUnitStats(statsElement, system);
416 //TODO: Add unit requirements
417 LogNotifier.Debug(GetType(), "Loaded "+type.Name);
418 return type;
419 }
420
421 private Stats ParseUnitStats(XmlElement elem, GameSystem system)
422 {
423 List<Stat> statsList = new List<Stat>();
424 String statsID = elem.GetAttribute("statSet");
425 SystemStats statsSet;
426
427 if (statsID == "")
428 {
429 statsSet = system.StandardSystemStats;
430 }
431 else
432 {
433 statsSet = system.GetSystemStatsForID(statsID);
434 }
435
436 Stats stats = new Stats(statsSet);
437
438 foreach (XmlElement stat in elem.ChildNodes)
439 {
440 String statID = stat.GetAttribute("name");
441 StatSlot slot = statsSet[statID];
442
443 if (slot!=null)
444 {
445 statsList.Add(new Stat(slot, stat.InnerText));
446 }
447 else
448 {
449 throw new InvalidFileException("The stat "+statID+" was not found in stats set "+statsID);
450 }
451 }
452
453 stats.SetStats(statsList);
454
455 return stats;
456 }
457
458 private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, Race race)
459 {
460 string id = elem.GetAttribute("id");
461 string name = elem.GetAttribute("name");
462 double cost = 0, min = 0, max = 0;
463 ArmourType armourType;
464
465 try
466 {
467 cost = XmlTools.GetDoubleValueFromAttribute(elem, "cost");
468 }
469 catch(FormatException ex)
470 {
471 throw new InvalidFileException("Attribute 'cost' of equipment item "+id+" was not a valid number", ex);
472 }
473
474 try
475 {
476 armourType = (ArmourType)Enum.Parse(typeof(ArmourType), elem.GetAttribute("armourType"));
477 }
478 catch(ArgumentException ex)
479 {
480 throw new InvalidFileException("Attribute 'armourType' of equipment "+id+" was not a valid value from the enumeration", ex);
481 }
482
483 //TODO: Parse equipment stats if there are any
484
485 return new EquipmentItem(id, name, cost, min, max, armourType, race);
486 }
487
488 private Ability CreateAbilityFromElement(XmlElement elem, Race race)
489 {
490 string id = elem.GetAttribute("id");
491 string name = elem.GetAttribute("name");
492 Ability ability = new Ability(id, name);
493 XmlNode node = elem.SelectSingleNode("description", GetNamespaceManager());
494 ability.Description = (node == null) ? "" : node.InnerText;
495 return ability;
496 } 125 }
497 } 126 }
498 } 127 }