view api/WarFoundryLoader.cs @ 68:10d14a7051d5

Re #50 - Complete core loading of WarFoundry XML files * Start to restructure loading so that we can use pre-existing objects * Break unit loading in to methods Also: * Pad stats list with nulls because setting capacity doesn't let you set arbitrary indexes * Add GameSystem property to UnitType
author IBBoard <dev@ibboard.co.uk>
date Sat, 25 Apr 2009 19:18:11 +0000
parents 422ddd5fedd1
children 3ea0ab04352b
line wrap: on
line source

// This file (WarFoundryLoader.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 under the GNU LGPL license, either version 3 of the License or (at your option) any later version. Please see COPYING.LGPL for more information and the full license.

using System;
using System.Collections.Generic;
using System.IO;
using IBBoard.Collections;
using IBBoard.IO;
using IBBoard.Logging;
using IBBoard.WarFoundry.API.Factories;
using IBBoard.WarFoundry.API.Objects;
using ICSharpCode.SharpZipLib.Zip;

namespace IBBoard.WarFoundry.API
{
	public class WarFoundryLoader
	{		
		private static WarFoundryLoader loader;
		
		/// <summary>
		/// Gets the default <see cref="WarFoundryLoader"/> used to load WarFoundry data files.
		/// </summary>
		/// <returns>
		/// The default <see cref="WarFoundryLoader"/>
		/// </returns>
		public static WarFoundryLoader GetDefault()
		{
			if (loader == null)
			{
				loader = new WarFoundryLoader();
			}
			
			return loader;
		}
		
		private ICollection<DirectoryInfo> directories;
		private ICollection<INativeWarFoundryFactory> factories;
		private ICollection<INonNativeWarFoundryFactory> nonNativeFactories;
		private Dictionary<string, GameSystem> systemsTable;
		private Dictionary<string, Dictionary<string, Dictionary<string, Race>>> racesTable; //Keys are: System, Race, SubRace
		private Dictionary<IWarFoundryFactory, SimpleSet<IWarFoundryObject>> loadedObjects;
		
		protected WarFoundryLoader()
		{
			directories = new List<DirectoryInfo>();
			factories = new List<INativeWarFoundryFactory>();
			nonNativeFactories = new List<INonNativeWarFoundryFactory>();
			loadedObjects = new Dictionary<IWarFoundryFactory,SimpleSet<IWarFoundryObject>>();
		}
		
		/// <summary>
		/// Adds a directory to the collection of directories that will be searched for WarFoundry data files.
		/// </summary>
		/// <param name="directory">
		/// The <see cref="DirectoryInfo"/> to add to the list for searching for data files
		/// </param>
		public void AddLoadDirectory(DirectoryInfo directory)
		{
			if (!directories.Contains(directory))
			{
				directories.Add(directory);
			}
		}
		
		/// <summary>
		/// Removes a directory from the collection of directories that will be searched for WarFoundry data files.
		/// </summary>
		/// <param name="directory">
		/// A <see cref="DirectoryInfo"/>
		/// </param>
		public void RemoveLoadDirectory(DirectoryInfo directory)
		{
			if (directories.Contains(directory))
			{
				directories.Remove(directory);
			}
		}
		
		/// <summary>
		/// Registers a <see cref="INativeWarFoundryFactory"/> as a factory that can parse native data files.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INativeWarFoundryFactory"/> to register to parse native data files.
		/// </param>
		public void RegisterFactory(INativeWarFoundryFactory factory)
		{
			if (!factories.Contains(factory))
			{
				factories.Add(factory);
			}
		}
		
		/// <summary>
		/// Unregisters a <see cref="INativeWarFoundryFactory"/> so that it will no longer be used to try to parse native data files.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INativeWarFoundryFactory"/> to remove from the collection of factories that are used to try to parse native data files.
		/// </param>
		public void UnregisterFactory(INativeWarFoundryFactory factory)
		{
			if (factories.Contains(factory))
			{
				factories.Remove(factory);
			}
		}
		
		/// <summary>
		/// Registers a <see cref="INonNativeWarFoundryFactory"/> so that it will be used to try to parse non-native data files from other applications.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INonNativeWarFoundryFactory"/> to register to parse non-native data files.
		/// </param>
		public void RegisterNonNativeFactory(INonNativeWarFoundryFactory factory)
		{
			if (!nonNativeFactories.Contains(factory))
			{
				nonNativeFactories.Add(factory);
			}
		}
		
		/// <summary>
		/// Unregisters a <see cref="INonNativeWarFoundryFactory"/> so that it will no longer be used to try to parse non-native data files from other applications.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INonNativeWarFoundryFactory"/> to remove from the collection of factories that are used to try to parse non-native data files.
		/// </param>
		public void UnregisterNonNativeFactory(INonNativeWarFoundryFactory factory)
		{
			if (nonNativeFactories.Contains(factory))
			{
				nonNativeFactories.Remove(factory);
			}
		}
		
		/// <summary>
		/// Loads all of the data files in the registered directories.
		/// </summary>
		/// <returns>
		/// A <see cref="Dictionary"/> of files that failed to load mapped against the message that their failure returned
		/// </returns>
		public List<FileLoadFailure> LoadFiles()
		{
			LogNotifier.Debug(GetType(), "Load files");
			PrepareForFileLoad();
			Dictionary<FileInfo, IWarFoundryFactory> loadableRaces = new Dictionary<FileInfo, IWarFoundryFactory>();
			Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems = new Dictionary<FileInfo, IWarFoundryFactory>();
			List<FileLoadFailure> failedLoads = FillLoadableFiles(loadableRaces, loadableGameSystems);
			failedLoads.AddRange(LoadGameSystems(loadableGameSystems));
			failedLoads.AddRange(LoadRaces(loadableRaces));

			LogNotifier.Debug(GetType(), failedLoads.Count + " failed file loads");
			
			return failedLoads;
		}
		
		protected void PrepareForFileLoad()
		{
			//Just set up blank dictionaries for now - may try different and more complex handling in future
			systemsTable = new Dictionary<string,GameSystem>();
			racesTable = new Dictionary<string,Dictionary<string,Dictionary<string,Race>>>();
		}

		private List<FileLoadFailure> FillLoadableFiles(Dictionary<FileInfo, IWarFoundryFactory> loadableRaces, Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems)
		{			
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			
			foreach (DirectoryInfo directory in directories)
			{				
				if (directory.Exists)
				{
					List<FileLoadFailure> directoryFails = FillLoadableFilesForDirectory(loadableRaces, loadableGameSystems, directory);
					fails.AddRange(directoryFails);
				}
				else
				{
					LogNotifier.WarnFormat(GetType(), "Load for {0} failed because directory didn't exist", directory.FullName);
				}
			}
			
			return fails;
		}

		private List<FileLoadFailure> FillLoadableFilesForDirectory(Dictionary<FileInfo, IWarFoundryFactory> loadableRaces, Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems, DirectoryInfo directory)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			LogNotifier.Debug(GetType(), "Load from "+directory.FullName);
		
			foreach (FileInfo file in directory.GetFiles())
			{
				IWarFoundryFactory factory = GetGameSystemLoadingFactoryForFile(file);
				
				if (factory != null)
				{
					loadableGameSystems.Add(file, factory);
				}
				else
				{
					factory = GetRaceLoadingFactoryForFile(file);
	
					if (factory!=null)
					{
						loadableRaces.Add(file, factory);
					}
					else
					{
						FileLoadFailure failure = new FileLoadFailure(file, "File not handled as a Race or Game System definition: {0}", "FileNotHandled");
						fails.Add(failure);
						LogNotifier.Info(GetType(), failure.Message);
					}
				}
			}

			return fails;
		}

		private IWarFoundryFactory GetGameSystemLoadingFactoryForFile(FileInfo file)
		{
			IWarFoundryFactory loadingFactory = null;
			
			foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
			{
				if (factory.CanHandleFileAsGameSystem(file))
				{
					loadingFactory = factory;
					break;
				}
			}

			if (loadingFactory == null)
			{
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileAsGameSystem(file))
					{
						loadingFactory = factory;
						break;
					}
				}
			}

			return loadingFactory;
		}

		private IWarFoundryFactory GetRaceLoadingFactoryForFile(FileInfo file)
		{
			IWarFoundryFactory loadingFactory = null;
			
			foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
			{
				if (factory.CanHandleFileAsRace(file))
				{
					loadingFactory = factory;
					break;
				}
			}

			if (loadingFactory == null)
			{
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileAsRace(file))
					{
						loadingFactory = factory;
						break;
					}
				}
			}

			return loadingFactory;
		}

		private List<FileLoadFailure> LoadGameSystems(Dictionary<FileInfo, IWarFoundryFactory> gameSystemFiles)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();

			
			foreach (FileInfo file in gameSystemFiles.Keys)
			{
				FileLoadFailure failure = null;
				
				try
				{
					bool loaded = LoadObject(file, gameSystemFiles[file]);
	
					if (!loaded)
					{
						failure = new FileLoadFailure(file, "FileLoadFailed", "Failed to load {0} as GameSystem using {1}");
					}
				}
				catch (Exception ex)
				{
					failure = new FileLoadFailure(file, null, ex.Message, null, ex);
				}
						
				if (failure!=null)
				{
					fails.Add(failure);
					LogNotifier.Warn(GetType(), failure.Message, failure.Exception);
				}
			}
			
			return fails;
		}

		private List<FileLoadFailure> LoadRaces(Dictionary<FileInfo, IWarFoundryFactory> raceFiles)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			
			foreach (FileInfo file in raceFiles.Keys)
			{
				FileLoadFailure failure = null;
				
				try
				{
					bool loaded = LoadObject(file, raceFiles[file]);
	
					if (!loaded)
					{
						failure = new FileLoadFailure(file, "FileLoadFailed", "Failed to load {0} as Race using {1}");
					}
				}
				catch (Exception ex)
				{
					failure = new FileLoadFailure(file, null, ex.Message, null, ex);
				}
						
				if (failure!=null)
				{
					fails.Add(failure);
					LogNotifier.Warn(GetType(), failure.Message, failure.Exception);
				}
			}
			
			return fails;
		}

		private bool LoadObject(FileInfo file, IWarFoundryFactory factory)
		{
			bool loaded = false;
			
			LogNotifier.DebugFormat(GetType(), "Loading {0} using {1}", file.FullName, factory.GetType().Name);
			ICollection<IWarFoundryObject> objects = factory.CreateObjectsFromFile(file);
			
			if (objects.Count > 0)
			{
				AddLoadedObjects(objects, factory);
				loaded = true;
			}

			return loaded;
		}
		
		
		/// <summary>
		/// Loads a single file through the registered WarFoundryFactories, if a factory exists that supports the file format.
		/// </summary>
		/// <param name="file">
		/// A <see cref="FileInfo"/> for the file to attempt to load
		/// </param>
		/// <returns>
		/// An ICollection of IWarFoundryObjects loaded from <code>file</code>
		/// </returns>
		public ICollection<IWarFoundryObject> LoadFile(FileInfo file)
		{
			ICollection<IWarFoundryObject> objs = null;
			IWarFoundryFactory loadFactory = null;
			
			try
			{
				objs = LoadFileWithNonNativeFactories(file, out loadFactory);
				
				if (objs == null)
				{
					objs = LoadFileWithNativeFactories(file, out loadFactory);
				}
			}
			catch (InvalidFileException ex)
			{
				LogNotifier.Error(GetType(), file.FullName+" failed to load", ex);
			}
				
			if (objs!=null)
			{
				AddLoadedObjects(objs, loadFactory);
			}
			else
			{
				objs = new List<IWarFoundryObject>();
			}

			return objs;
		}
		
		private ICollection<IWarFoundryObject> LoadFileWithNonNativeFactories(FileInfo file, out IWarFoundryFactory loadFactory)
		{
			ICollection<IWarFoundryObject> objs = null;
			loadFactory = null;
			
			if (nonNativeFactories.Count > 0)
			{
				LogNotifier.Debug(GetType(), "Attempting to load "+file.FullName+" as a non-native file");
				
				foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
				{
					bool canLoad = factory.CanHandleFileFormat(file);
					LogNotifier.Debug(GetType(), "Load using "+factory.GetType().FullName+"? " + (canLoad ? "yes" : "no"));
					
					if (canLoad)
					{
						objs = factory.CreateObjectsFromFile(file);
						
						if (objs!=null)
						{
							loadFactory = factory;
							break;
						}
					}			         
				}
			}
			
			return objs;
		}
		
		private ICollection<IWarFoundryObject> LoadFileWithNativeFactories(FileInfo file, out IWarFoundryFactory loadFactory)
		{
			ICollection<IWarFoundryObject> objs = null;
			loadFactory = null;
			
			if (factories.Count > 0)
			{
				LogNotifier.Debug(GetType(), "Attempting to load "+file.FullName+" as native file");
						
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileFormat(file))
					{
						objs = factory.CreateObjectsFromFile(file);
						
						if (objs!=null)
						{
							loadFactory = factory;
							break;
						}
					}
				}
			}
			
			return objs;
		}
		
		private void AddLoadedObjects(ICollection<IWarFoundryObject> loadedObjs, IWarFoundryFactory factory)
		{
			SimpleSet<IWarFoundryObject> objs;
			loadedObjects.TryGetValue(factory, out objs);
			
			if (objs == null)
			{
				objs = new SimpleSet<IWarFoundryObject>();
				loadedObjects.Add(factory, objs);
			}
				
			objs.AddRange(loadedObjs);
			StoreObjects(loadedObjs);
		}

		private void StoreObjects(ICollection<IWarFoundryObject> loadedObjects)
		{
			foreach (IWarFoundryObject loadedObject in loadedObjects)
			{
				if (loadedObject is GameSystem)
				{
					StoreGameSystem((GameSystem)loadedObject);
				}
				else if (loadedObject is Race)
				{
					StoreRace((Race)loadedObject);
				}
			}
		}
		
		protected void StoreGameSystem(GameSystem system)
		{
			string sysid = system.ID.ToLower();
					
			if (systemsTable.ContainsKey(sysid))
			{
				LogNotifier.WarnFormat(GetType(), "System {0} ({1}) has already been loaded. Duplicate file ({3}) discarded", system.Name, system.ID, system.SourceFile.FullName);
			}
			else
			{
				systemsTable.Add(sysid, (GameSystem)system);
			}
		}
		
		protected void StoreRace(Race race)
		{
			Dictionary<string, Dictionary<string, Race>> systemRaces;

			if (race.GameSystem == null)
			{
				throw new InvalidOperationException("Race cannot have null game system. Game system should be loaded before race.");
			}
			
			string systemID = race.GameSystem.ID.ToLower();
			racesTable.TryGetValue(systemID, out systemRaces);
			
			if (systemRaces==null)
			{
				systemRaces = new Dictionary<string,Dictionary<string,Race>>();
				racesTable.Add(systemID, systemRaces);
			}
			
			Dictionary<string, Race> subRaces;
			systemRaces.TryGetValue(race.ID.ToLower(), out subRaces);
			
			if (subRaces==null)
			{
				subRaces = new Dictionary<string,Race>();
				systemRaces.Add(race.ID.ToLower(), subRaces);
			}

			if (subRaces.ContainsKey(race.SubID.ToLower()))
			{
				LogNotifier.WarnFormat(GetType(), "Race {0} ({1} - {2}) for system {3} ({4}) has already been loaded. Duplicate file ({5}) discarded", race.Name, race.ID, race.SubID, race.GameSystem.ID, race.GameSystem.Name, race.SourceFile.Name);
				race = null;
			}
			else
			{
				subRaces.Add(race.SubID.ToLower(), race);
			}
		}
		
		/// <summary>
		/// Gets all <see cref="GameSystem"/>s that are currently available, determined by those that can be loaded with the current <see cref="IWarFoundryFactory"/>s. 
		/// </summary>
		/// <returns>
		/// An array of <see cref="GameSystem"/>s that are currently available.
		/// </returns>
		public GameSystem[] GetGameSystems()
		{
			if (systemsTable==null)
			{
				LoadFiles();
			}
			
			return DictionaryUtils.ToArray<string, GameSystem>(systemsTable);
		}

		/// <summary>
		/// Gets a single <see cref="GameSystem"/> with a given ID. 
		/// </summary>
		/// <param name="systemID">
		/// The ID of the <see cref="GameSystem"/> to get, as a <see cref="System.String"/>.
		/// </param>
		/// <returns>
		/// The <see cref="GameSystem"/> with the given ID, or <code>null</code> if one doesn't exist.
		/// </returns>
		public GameSystem GetGameSystem(string systemID)
		{
			if (systemsTable==null)
			{
				LoadFiles();
			}
			
			GameSystem system;
			systemsTable.TryGetValue(systemID.ToLower(), out system);
			return system;
		}

		/// <summary>
		/// Gets an array of the races for the specified <see cref="GameSystem"/>.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> to get the available races for.
		/// </param>
		/// <returns>
		/// An array of <see cref="Race"/>s for the <see cref="GameSystem"/>
		/// </returns>
		public Race[] GetRaces(GameSystem system)
		{
			return GetRaces(system.ID);
		}

		/// <summary>
		/// Gets an array of the races for a game system by ID.
		/// </summary>
		/// <param name="systemID">
		/// The <see cref="System.String"/> ID of the game system to get races for
		/// </param>
		/// <returns>
		/// An array of <see cref="Race"/>s for the specified game system
		/// </returns>
		public Race[] GetRaces(string systemID)
		{
			if (racesTable==null)
			{
				LoadFiles();
			}
			
			systemID = systemID.ToLower();
			Dictionary<string, Dictionary<string, Race>> system;
			racesTable.TryGetValue(systemID, out system);
			
			if (system==null)
			{
				return new Race[0];
			}

			int count = 0;

			foreach (Dictionary<string, Race> racesDict in system.Values)
			{
				count+= racesDict.Count;
			}

			Race[] races = new Race[count];
			int i = 0;

			foreach (string raceID in system.Keys)
			{
				foreach (string raceSubId in system[raceID].Keys)
				{
					races[i++] = GetRace(systemID, raceID, raceSubId);
				}
			}

			return races;
		}

		/// <summary>
		/// Gets a single race for a given <see cref="GameSystem"/> by ID of the race.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// A <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <returns>
		/// A <see cref="Race"/> with the specified ID from the <see cref="GameSystem"/>, or <code>null</code> if one doesn't exist.
		/// </returns>
		public Race GetRace(GameSystem system, string raceID)
		{
			return GetRace(system.ID, raceID);
		}

		/// <summary>
		/// Gets a single race for a given game system by ID of the game system and race.
		/// </summary>
		/// <param name="systemID">
		/// The <see cref="System.String"/> ID of the game system that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// The <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <returns>
		/// A <see cref="Race"/> with the specified ID from the game system with the specified ID, or <code>null</code> if there is no race or game system with those IDs.
		/// </returns>
		public Race GetRace(string systemID, string raceID)
		{
			return GetRace(systemID, raceID, "");
		}

		/// <summary>
		/// Gets a single race for a given <see cref="GameSystem"/> by the race's ID and sub-race ID.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// The <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <param name="raceSubID">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="Race"/>
		/// </returns>
		public Race GetRace(GameSystem system, string raceID, string raceSubID)
		{
			return GetRace(system.ID, raceID, raceSubID);
		}

		/// <summary>
		/// Gets a single race for a given game system by the game system's ID and the race's ID and sub-race ID.
		/// </summary>
		/// <param name="systemID">
		/// The <see cref="System.String"/> ID of the game system that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// The <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <param name="raceSubID">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="Race"/>
		/// </returns>
		public Race GetRace(string systemID, string raceID, string raceSubID)
		{
			if (racesTable==null)
			{
				LoadFiles();
			}
			
			Race race = null;
			
			systemID = systemID.ToLower();

			Dictionary<string, Dictionary<string, Race>> races;
			racesTable.TryGetValue(systemID, out races);

			if (races!=null)
			{
				Dictionary<string, Race> subraces;
				races.TryGetValue(raceID, out subraces);

				if (subraces!=null)
				{
					subraces.TryGetValue(raceSubID, out race);
				}
			}
			
			return race;
		}

		/// <summary>
		/// Gets the IDs of all of the game systems currently available.
		/// </summary>
		/// <returns>
		/// An array of <see cref="System.String"/>s representing the IDs of the game systems.
		/// </returns>
		public string[] GetGameSystemIDs()
		{
			if (systemsTable==null)
			{
				LoadFiles();
			}

			string[] keys = new string[systemsTable.Keys.Count];
			int i = 0;

			foreach (string key in systemsTable.Keys)
			{
				keys[i++] = key;
			}

			return keys;
		}
		
		/// <summary>
		/// Gets the IDs of all of the races of a specified game system.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> to get the available races for.
		/// </param>
		/// <returns>
		/// An array of <see cref="System.String"/>s representing the IDs of the races of the specified game system.
		/// </returns>
		public string[] GetSystemRaceIDs(GameSystem system)
		{
			return GetSystemRaceIDs(system.ID);
		}

		/// <summary>
		/// Gets the IDs of all of the races of a specified game system.
		/// </summary>
		/// <param name="systemID">
		/// The <see cref="System.String"/> ID of the game system to get the available races for.
		/// </param>
		/// <returns>
		/// An array of <see cref="System.String"/>s representing the IDs of the races of the specified game system.
		/// </returns>
		public string[] GetSystemRaceIDs(string systemID)
		{
			if (racesTable == null)
			{
				LoadFiles();
			}

			Dictionary<string, Dictionary<string, Race>> races = racesTable[systemID.ToLower()];

			if (races==null)
			{
				return new string[0];
			}
			else
			{
				string[] keys = new string[races.Keys.Count];
				int i = 0;

				foreach (string key in races.Keys)
				{
					keys[i++] = key;
				}

				return keys;
			}
		}
	}
}