using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Globalization;
using System.Reflection;
using System.Threading;
using HakInstaller.Utilities;
using NWN;
using NWN.FileTypes;
using NWN.FileTypes.Gff;
namespace HakInstaller
{
///
/// This class stores the data for a hif conflict.
///
public class HifConflict
{
#region public properties/methods
///
/// Gets the name of the module in conflict
///
public string Module { get { return module; } }
///
/// Gets the name of the hif in conflict
///
public string Hif { get { return hif; } }
///
/// Gets the version of the hif installed in the module
///
public float InstalledVersion { get { return installedVersion; } }
///
/// Gets the version of the hif that is going to be installed
///
public float CurrentVersion { get { return currentVersion; } }
///
/// Class constructor.
///
/// The module in conflict
/// The hif in conflict
/// The version of the hif installed in the module
/// The version of the hif that is going to be installed
public HifConflict(string module, string hif, float installedVersion, float currentVersion)
{
this.module = module;
this.hif = hif;
this.installedVersion = installedVersion;
this.currentVersion = currentVersion;
}
///
/// Override of ToString() that returns a nice formatted value.
///
///
public override string ToString()
{
System.Text.StringBuilder b = new System.Text.StringBuilder();
b.AppendFormat("'{0}' contains '{1}'{2}{3}", module, hif,
0 == installedVersion ? "" : " version ",
0 == installedVersion ? "" : installedVersion.ToString("0.00"));
return b.ToString();
}
#endregion
#region private fields/properties/methods
private string module;
private string hif;
private float installedVersion;
private float currentVersion;
#endregion
}
///
/// This class defines a type safe collection of HifConflict objects.
///
public class HifConflictCollection: CollectionBase
{
#region public properties/methods
///
/// Indexer to index into the collection to get FileConflict objects.
///
public HifConflict this[int index]
{ get { return InnerList[index] as HifConflict; } }
///
/// Default constructor
///
public HifConflictCollection()
{}
///
/// Adds a conflict to the collection.
///
///
public void Add(HifConflict conflict)
{
InnerList.Add(conflict);
}
#endregion
}
///
/// This class stores the data for a file conflict, and how to resolve the
/// conflict.
///
public class FileConflict
{
#region public properties/methods
public string ModuleFile { get { return moduleFile; } }
public string HakFile { get { return hakFile; } }
public string FileName { get { return Path.GetFileName(moduleFile); } }
public bool ReplaceFile
{
get { return replaceFile; }
set { replaceFile = value; }
}
///
/// Class constructor
///
/// The module file in conflict
/// The hak file in conflict
public FileConflict(string moduleFile, string hakFile)
{
this.moduleFile = moduleFile;
this.hakFile = hakFile;
this.replaceFile = true;
}
///
/// Replace ToString() with something more reasonable.
///
/// The FileName property
public override string ToString()
{
return FileName;
}
#endregion
#region private fields/properties/methods
private string moduleFile;
private string hakFile;
private bool replaceFile;
#endregion
}
///
/// This class defines a type save collection of FileConflict objects.
///
public class FileConflictCollection: CollectionBase
{
#region public properties/methods
///
/// Indexer to index into the collection to get FileConflict objects.
///
public FileConflict this[int index]
{ get { return InnerList[index] as FileConflict; } }
///
/// Default constructor
///
public FileConflictCollection()
{}
///
/// Adds a conflict to the collection.
///
///
public void Add(FileConflict conflict)
{
InnerList.Add(conflict);
}
///
/// Generates a copy of the FileConflictCollection
///
///
public FileConflictCollection Clone()
{
FileConflictCollection copy = new FileConflictCollection();
foreach (object o in InnerList)
copy.InnerList.Add(o);
return copy;
}
///
/// Removes a conflict from the collection.
///
/// The conflict to remove
public void Remove(FileConflict conflict)
{
InnerList.Remove(conflict);
}
#endregion
}
///
/// This class contains the information needed to describe an overwrite warning.
/// This warns the user that one file is going to get overwritten by another,
/// but they have no choice in which overwrites which (as opposed to a FileConflict
/// where they do).
///
public class OverwriteWarning
{
#region public properties/methods
///
/// Gets the file being overwritten.
///
public string File { get { return file; } }
///
/// Gets the source who's file is being overwritten.
///
public string Source { get { return source; } }
///
/// gets the file that is overwriting the source.
///
public string Replacer { get { return replacer; } }
///
/// Class constructor.
///
/// The file getting overwritten
/// The source hak/module/erf
/// The overwriting hak/module/erf
public OverwriteWarning(string file, string source, string replacer)
{
this.file = file;
this.source = source;
this.replacer = replacer;
}
///
/// Override of ToString() to give back a formatted value.
///
///
public override string ToString()
{
System.Text.StringBuilder b = new System.Text.StringBuilder();
b.AppendFormat("{0} will overwrite {1} in {2}", replacer, file, source);
return b.ToString();
}
#endregion
#region private fields/properties/methods
private string file;
private string source;
private string replacer;
#endregion
}
///
/// This class defines a type safe collection of OverwriteWarning objects.
///
public class OverwriteWarningCollection: CollectionBase
{
#region public properties/methods
///
/// Indexer to get the index'th OverwriteWarning.
///
public OverwriteWarning this[int index]
{
get { return InnerList[index] as OverwriteWarning; }
}
///
/// Adds an OverwriteWarning.
///
///
public void Add(OverwriteWarning warning)
{
InnerList.Add(warning);
}
///
/// Makes a clone of the collection
///
/// The clone
public OverwriteWarningCollection Clone()
{
OverwriteWarningCollection copy = new OverwriteWarningCollection();
foreach (object o in InnerList)
copy.InnerList.Add(o);
return copy;
}
///
/// Removes an overwrite warning from the collection
///
/// The warning to remove
public void Remove(OverwriteWarning warning)
{
InnerList.Remove(warning);
}
#endregion
}
///
/// This enum defines the different types of warnings that
///
public enum OverwriteWarningType
{
HifsOverwritesModule,
ModuleOverwritesHifs
}
///
/// Interface used to display progress information for the install.
///
public interface IHakInstallProgress
{
#region properties/methods
///
/// Gets whether the user cancelled the install.
///
bool IsCancelled { get; }
///
/// Gets/sets the number of steps for the progress bar.
///
int ProgressSteps { get; set; }
///
/// Advances the progress bar 1 step.
///
void Step();
///
/// Sets the currently displayed progress message.
///
/// Format string
/// Message arguments
void SetMessage(string format, params object[] args);
///
/// This methods should ask the user for confirmation of replacing
/// the listed files in the module with files from sources in the
/// hif files, as this operation may break the module.
///
/// The list of file conflicts. The method
/// may alter the list to indicate what the resolution of the conflict
/// should be on a file by file basis.
/// true if the files should be replaced, false if adding
/// the hak(s) to the module should be aborted
bool ShouldReplaceFiles(FileConflictCollection conflicts);
///
/// This method should ask the user for confirmation of overwriting
/// the listed files. If fatal is true then there is no confirmation,
/// it is just an informational message that the operation must be aborted.
///
/// The list of warnings
/// True if the warnings are fatal
/// The type of overwrite being confirmed
/// True if the operation should proceed
bool ShouldOverwrite(OverwriteWarningCollection warnings, bool fatal,
OverwriteWarningType type);
///
/// Displays an error message to the user.
///
/// The error message to display
void DisplayErrorMessage(string error);
///
/// Displays a message to the user.
///
/// The message to display
void DisplayMessage(string message);
#endregion
}
///
/// Exception used by the hak installer to cancel the install, when
/// the IHakInstallProgress.IsCancelled property returns true.
///
internal class InstallCancelledException: Exception
{
#region public properties/methods
public InstallCancelledException() {}
#endregion
}
///
/// Delegate for methods that handle the setting of properties.
///
internal delegate void PropertyHandler(Erf module, object source, string property,
StringCollection values);
///
/// This attribute is used to tag methods that handle module properties
/// in the HakInstaller.
///
[AttributeUsage(AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
internal class PropertyHandlerAttribute: Attribute
{
#region public properties/methods
///
/// Gets the name of the object containing the property.
///
public string Object { get { return obj; } }
///
/// Gets the name of the property.
///
public string Property { get { return property; } }
///
/// Class constructor
///
/// The object containing the property
/// The name of the property
public PropertyHandlerAttribute(string obj, string property)
{
this.obj = obj;
this.property = property;
}
#endregion
#region public static methods
///
/// Gets the handler collection for the specified object. This collection
/// contains all of the methods tagged with the [PropertyHandler] attribute.
///
/// The source object for which to get the
/// collection
/// The property handler collection
public static ObjectProperyHandlerDictionary GetHandlerCollection(object sourceObject)
{
ObjectProperyHandlerDictionary objects = new ObjectProperyHandlerDictionary();
// Get all of the methods of the type and look for ones that have the [DBQuery] attached.
Type sourceType = sourceObject.GetType();
MethodInfo[] methods = sourceType.GetMethods (
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance);
foreach (MethodInfo method in methods)
{
// Try to get the method's [PropertyHandler] attribute. If it
// doesn't have one then ignore it.
Attribute[] atts = method.GetCustomAttributes (
typeof (PropertyHandlerAttribute), false) as Attribute[];
if (null == atts || 0 == atts.Length) continue;
// Create a delegate for the method.
PropertyHandler handler = (PropertyHandler) (method.IsStatic ?
PropertyHandler.CreateDelegate (typeof (PropertyHandler), method) :
PropertyHandler.CreateDelegate(typeof (PropertyHandler), sourceObject, method.Name));
// Add the handler once for each property it handles. Handlers may
// handle more than one property thus the attribute array may have
// multiple entries.
foreach (Attribute att in atts)
{
PropertyHandlerAttribute handlerAtt = (PropertyHandlerAttribute) att;
// Look up the handler dictionary for the object, if there isn't one
// yet then create on.
PropertyHandlerDictionary handlers = objects[handlerAtt.Object];
if (null == handlers)
{
handlers = new PropertyHandlerDictionary(handlerAtt.Object);
objects.Add(handlerAtt.Object, handlers);
}
handlers.Add(handlerAtt.Property, handler);
}
}
return objects;
}
#endregion
#region private fields/properties/methods
private string obj;
private string property;
#endregion
}
///
/// This class defines a dictionary of PropertyHandler objects,
/// allowing for the handler for a particular property to be looked up.
///
internal class PropertyHandlerDictionary: DictionaryBase
{
#region public properties/methods
///
/// Gets the object for which this collection contains property handlers.
///
public string Object { get { return obj; } }
///
/// Indexer to look up a handler for a property.
///
public PropertyHandler this[string property]
{
get { return InnerHashtable[property.ToLower()] as PropertyHandler; }
}
///
/// Default constructor
///
/// The type of object that this dictionary
/// contains handlers for
public PropertyHandlerDictionary(string obj)
{
this.obj = obj;
}
///
/// Adds a handler to the collection
///
/// The property the handler handles
/// The handler
public void Add(string property, PropertyHandler handler)
{
InnerHashtable.Add(property.ToLower(), handler);
}
#endregion
#region private fields/properties/methods
private string obj;
#endregion
}
///
/// This class defines a dictionary collectoin of PropertyHandlerDictionary
/// objects, each of those handling properties for a particular object.
///
internal class ObjectProperyHandlerDictionary: DictionaryBase
{
#region public properties/methods
///
/// Indexer to look up a handler dictionary for an object.
///
public PropertyHandlerDictionary this[string obj]
{
get { return InnerHashtable[obj.ToLower()] as PropertyHandlerDictionary; }
}
///
/// Default constructor
///
public ObjectProperyHandlerDictionary()
{
}
///
/// Adds a handler to the collection
///
/// The object that the handlers handle properties for
/// The handler collection
public void Add(string obj, PropertyHandlerDictionary handlers)
{
InnerHashtable.Add(obj.ToLower(), handlers);
}
#endregion
}
///
/// Summary description for HakInstaller.
///
public class HakInstaller
{
#region public static methods
///
/// This method checks to see if any of the proposed hifs are installed on any of the
/// given modules, returning a collection of conflicts that can be displayed to the user.
///
/// The list of hifs that the user wants to add to the modules
/// The list of modules
/// A collection containing the list of conflicts, or null if there are no
/// conflicts
public static HifConflictCollection CheckInstalledHifs(HakInfo[] proposedHifs, string[] modules)
{
// Force the thread to use the invariant culture to make the install
// code work on foreign language versions of windows.
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
try
{
HifConflictCollection conflicts = null;
foreach (string module in modules)
{
// Load the module info data and create an object for it.
MemoryStream stream = Erf.GetFile(NWNInfo.GetFullFilePath(module), ModuleInfo.FileName);
if (null == stream) NWN.FileTypes.Tools.NWNLogger.Log(10, "HakInstaller.CheckInstalledHifs, Erf.GetFile() returned null!!!");
ModuleInfo info = new ModuleInfo(stream);
// Load the installed hifs in module if any.
string[] installedHifs;
float[] installedVersions;
info.GetInstalledHakInfos(out installedHifs, out installedVersions);
// Create a StringCollection of the proposed hifs so we can use IndexOf(),
// then check to see if there are any hif conflicts, if there are then
// add them to the conflict list.
StringCollection proposedHifsColl = new StringCollection();
foreach (HakInfo hif in proposedHifs)
proposedHifsColl.Add(Path.GetFileNameWithoutExtension(hif.Name).ToLower());
for (int i = 0; i < installedHifs.Length; i++)
{
if (proposedHifsColl.Contains(installedHifs[i].ToLower()))
{
HifConflict conflict = new HifConflict(module,
installedHifs[i], installedVersions[i], 0);
if (null == conflicts) conflicts = new HifConflictCollection();
conflicts.Add(conflict);
}
}
}
return conflicts;
}
finally
{
Thread.CurrentThread.CurrentCulture = currentCulture;
}
}
///
/// Installs the listed haks (defined by hif files) on the listed
/// modules.
///
/// The list of haks to add
/// The list of modules to add the haks to
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
public static void InstallHaks(HakInfo[] hifs, string[] modules,
IHakInstallProgress progress)
{
// Force the thread to use the invariant culture to make the install
// code work on foreign language versions of windows.
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
try
{
// If no progress was given then use a dummy one which does nothing.
if (null == progress) progress = new DummyProgress();
// Invoke the private method on the singleton to do all the real work.
Singleton.DoInstall(hifs, modules, progress);
}
finally
{
Thread.CurrentThread.CurrentCulture = currentCulture;
}
}
///
/// Installs the listed haks (defined by hif files) on the module.
///
/// The list of haks to add
/// The module to add the haks to
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
public static void InstallHaks(HakInfo[] hifs, string moduleFile,
IHakInstallProgress progress)
{
// Force the thread to use the invariant culture to make the install
// code work on foreign language versions of windows.
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
try
{
// If no progress was given then use a dummy one which does nothing.
if (null == progress) progress = new DummyProgress();
// Invoke the private method on the singleton to do all the real work.
Singleton.DoInstall(hifs, new string[] { moduleFile }, progress);
}
finally
{
Thread.CurrentThread.CurrentCulture = currentCulture;
}
}
#endregion
#region module property handlers
///
/// Property handler for all of the module event properties. It sets
/// the specified events on the appropriate event handler, creating
/// another script to execute all scripts in the chain if there are
/// multiple scripts on the event.
///
/// The module being modified
/// The source object, a ModuleInfo object in this case
/// The property to set
/// The collection containing the property values
[PropertyHandler("Module", "OnAcquireItem")]
[PropertyHandler("Module", "OnActivateItem")]
[PropertyHandler("Module", "OnClientEnter")]
[PropertyHandler("Module", "OnClientLeave")]
[PropertyHandler("Module", "OnCutsceneAbort")]
[PropertyHandler("Module", "OnHeartbeat")]
[PropertyHandler("Module", "OnModuleLoad")]
[PropertyHandler("Module", "OnModuleStart")]
[PropertyHandler("Module", "OnPlayerChat")]
[PropertyHandler("Module", "OnPlayerDeath")]
[PropertyHandler("Module", "OnPlayerDying")]
[PropertyHandler("Module", "OnPlayerEquipItem")]
[PropertyHandler("Module", "OnPlayerLevelUp")]
[PropertyHandler("Module", "OnPlayerRest")]
[PropertyHandler("Module", "OnPlayerUnEquipItem")]
[PropertyHandler("Module", "OnPlayerRespawn")]
[PropertyHandler("Module", "OnUnaquireItem")]
[PropertyHandler("Module", "OnUserDefined")]
private void Module_SetEvent(Erf module, object source, string property,
StringCollection values)
{
ModuleInfo moduleInfo = (ModuleInfo) source;
// Get the current event handler on the module, if any.
string currentEvent = moduleInfo[property];
// Get the total number of event handlers.
int count = values.Count + (string.Empty == currentEvent ? 0 : 1);
// If there is only 1 handler then this is easy, just set the handler.
if (1 == count)
{
moduleInfo[property] = values[0];
return;
}
// There are multiple events, we must build a new script that
// invokes the events.
string newScript = CreateExecuteScript(module, property, currentEvent, values);
moduleInfo[property] = newScript;
}
///
/// Property handler for the cache property, it adds all of the scripts to
/// the module's cache list.
///
/// The module being modified
/// The source object, a ModuleInfo object in this case
/// The property to set
/// The collection containing the property values
[PropertyHandler("Module", "Cache")]
private void Module_ScriptCache(Erf module, object source, string property,
StringCollection values)
{
ModuleInfo moduleInfo = (ModuleInfo) source;
// Copy the haks to a flat array and add them to the module.
string[] scripts = new string[values.Count];
for (int i = 0; i < scripts.Length; i++)
scripts[i] = (values[i].ToLower());
moduleInfo.AddToCache(scripts);
}
///
/// Property handler for the module's custom tlk property. It sets
/// the first string in the collection to be the module's custom tlk.
///
/// The module being modified
/// The source object, a ModuleInfo object in this case
/// The property to set
/// The collection containing the property values
[PropertyHandler("Module", "CustomTlk")]
private void Module_CustomTlk(Erf module, object source, string property,
StringCollection values)
{
// If we have a merge tlk then do not set the custom tlk, the
// conflict resolution code for tlk's did that already.
if (string.Empty != mergeTlk) return;
string tlk = Path.GetFileNameWithoutExtension(values[0].ToLower());
// If the tlk is being set to the same tlk then do nothing.
ModuleInfo moduleInfo = (ModuleInfo) source;
if (0 == string.Compare(moduleInfo.CustomTlk, tlk, true, CultureInfo.InvariantCulture)) return;
// Check to see if the module already has a custom tlk file, if it does
// then we are dead; we cannot change it w/o breaking the module but the
// hak won't run w/o it's custom tlk either.
if (string.Empty != moduleInfo.CustomTlk)
throw new NWNException(
"The module {0} already contains a custom tlk {1}, hak cannot be added",
moduleInfo.Name, values[0]);
moduleInfo.CustomTlk = tlk;
}
///
/// Property handler for the module's hak property. It adds all of the haks
/// to the module.
///
/// The module being modified
/// The source object, a ModuleInfo object in this case
/// The property to set
/// The collection containing the property values
[PropertyHandler("Module", "Hak")]
private void Module_Hak(Erf module, object source, string property,
StringCollection values)
{
ModuleInfo moduleInfo = (ModuleInfo) source;
// Copy the haks to a flat array and add them to the module.
string[] haks = new string[values.Count];
for (int i = 0; i < haks.Length; i++)
haks[i] = Path.GetFileNameWithoutExtension(values[i].ToLower());
moduleInfo.AddHaks(haks);
}
///
/// Property handler for the module's hak property. It adds all of the haks
/// to the module.
///
/// The module being modified
/// The source object, a ModuleInfo object in this case
/// The property to set
/// The collection containing the property values
[PropertyHandler("Module", "Areas")]
private void Module_Areas(Erf module, object source, string property,
StringCollection values)
{
ModuleInfo moduleInfo = (ModuleInfo) source;
// Copy the haks to a flat array and add them to the module.
string[] areas = new string[values.Count];
for (int i = 0; i < areas.Length; i++)
areas[i] = values[i].ToLower();
moduleInfo.AddAreas(areas);
}
#endregion
#region private nested classes
///
/// This class implements a null IHakInstallProgress implementation, i.e.
/// all of the properties and methods on the interface are no-ops.
///
private class DummyProgress: IHakInstallProgress
{
#region implementation
///
/// Gets whether the user cancelled the install.
///
bool IHakInstallProgress.IsCancelled { get { return false;} }
///
/// Gets/sets the number of steps for the progress bar.
///
int IHakInstallProgress.ProgressSteps { get { return steps; } set { steps = value;} }
///
/// Advances the progress bar 1 step.
///
void IHakInstallProgress.Step() {}
///
/// Sets the currently displayed progress message.
///
/// Format string
/// Message arguments
void IHakInstallProgress.SetMessage(string format, params object[] args) {}
///
/// This methods should ask the user for confirmation of replacing
/// the listed files in the module with files from sources in the
/// hif files, as this operation may break the module.
///
/// The list of replaced files
/// true if the files should be replaced, false if adding
/// the hak(s) to the module should be aborted
bool IHakInstallProgress.ShouldReplaceFiles(FileConflictCollection conflicts)
{
return true;
}
///
/// This method should ask the user for confirmation of overwriting
/// the listed files. If fatal is true then there is no confirmation,
/// it is just an informational message that the operation must be aborted.
///
/// The list of warnings
/// True if the warnings are fatal
/// The type of overwrite being confirmed
/// True if the operation should proceed
bool IHakInstallProgress.ShouldOverwrite(OverwriteWarningCollection warnings,
bool fatal, OverwriteWarningType type)
{
return true;
}
///
/// Displays an error message to the user.
///
/// The error message to display
void IHakInstallProgress.DisplayErrorMessage(string error) {}
///
/// Displays a message to the user.
///
/// The message to display
void IHakInstallProgress.DisplayMessage(string message) {}
private int steps = 0;
#endregion
}
#endregion
#region private fields/properties/methods
private string currentTempDir;
private string mergeTlk;
private string mergeHak;
private ObjectProperyHandlerDictionary objects;
///
/// Default constructor, private to force singleton implementation.
///
private HakInstaller()
{
objects = PropertyHandlerAttribute.GetHandlerCollection(this);
}
///
/// Gets the number of progress steps required given a source list of
/// hifs and modules.
///
/// The list of hifs
/// The list of modules
/// The number of progress steps required.
private int GetProgressCount(HakInfo[] hifs, string[] modules)
{
// Count the number of erfs and files in the erfs.
int fileCount, erfCount;
CountAddedFiles(hifs, out fileCount, out erfCount);
// Start with 3 steps per module, to load, decompress, and save.
int count = modules.Length * 3;
// Add one step per hif to load them.
count += hifs.Length;
// Add 2 steps per erf, one to load one to decompress.
count += erfCount * 2;
// Add a number of steps to add each file to each module.
count += modules.Length * fileCount;
// Add 1 steps for each (module, hif) pair to wire up events.
count += modules.Length * hifs.Length;
return count;
}
///
/// Counts the number of files in the hifs that will be added to a
/// module, to allow for progress bar movement.
///
/// The list of hifs
/// Returns the number of files in the erfs.
/// Returns the number of erf files.
private void CountAddedFiles(HakInfo[] hifs, out int fileCount, out int erfCount)
{
fileCount = 0;
erfCount = 0;
// Now loop through all of the haks and add them to the module.
foreach (HakInfo hif in hifs)
{
erfCount += hif.Erfs.Count;
foreach (string erf in hif.Erfs)
fileCount += Erf.GetFileCount(Path.Combine(NWNInfo.GetPathForFile(erf), erf));
}
}
///
/// Decompresses an ERF file, returning the temp director that the
/// file is decompressed to.
///
/// The erf to decompress
/// The string collection in which to place
/// the temp directory.
/// The temp directory that the ERF was decompressed to, this
/// is also added to the StringCollection
private string Decompress (Erf erf, StringCollection tempDirs)
{
string tempDir = erf.FileName + ".Temp";
tempDirs.Add(tempDir);
erf.Decompress(tempDir);
return tempDir;
}
///
/// Loads a collection of hifs into memory.
///
/// The hifs to load
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
/// An array of HakInfo objects representing the hifs
private HakInfo[] LoadHifs(string[] hifs, IHakInstallProgress progress)
{
// Loop through the array of hif files loading them into HakInfo objects.
HakInfo[] hakInfos = new HakInfo[hifs.Length];
for (int i = 0; i < hifs.Length; i++)
{
// Load the hif file, stepping the progress bar.
Progress(progress, true, "Reading {0}", hifs[i]);
if (progress.IsCancelled) throw new InstallCancelledException();
hakInfos[i] = new HakInfo(Path.Combine(NWNInfo.HakInfoPath, hifs[i]));
}
return hakInfos;
}
///
/// Decompresses all of the ERF files in the hak info objects, returning
/// a collection of the temp directories they are in.
///
/// The array of hak infos for which to decompress
/// the erfs
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
/// A string collection of all of the directories in which the
/// ERF's have been decompressed.
private StringCollection DecompressHifErfs(HakInfo[] hakInfos,
IHakInstallProgress progress)
{
StringCollection tempDirs = new StringCollection();
foreach (HakInfo hakInfo in hakInfos)
{
// Add any erf files to the module.
foreach (string erf in hakInfo.Erfs)
{
// Load the erf.
Progress(progress, true, "Loading {0}", erf);
if (progress.IsCancelled) throw new InstallCancelledException();
Erf hakErf = Erf.Load(Path.Combine(NWNInfo.GetPathForFile(erf), erf));
// Decompress the erf into it's own temporary directory, saving the
// directory for later cleanup.
Progress(progress, true, "Decompressing {0}", erf);
Decompress(hakErf, tempDirs);
}
}
return tempDirs;
}
///
/// Method to display a progress message. It displays the message,
/// steps the progress bar, and checks to see if the operation has
/// been cancelled throwing a InstallCancelledException if it has.
///
/// The progress object
/// If true steps the progress bar
/// Message format string
/// Message arguments
private void Progress(IHakInstallProgress progress, bool step,
string format, params object[] args)
{
// If the operation has been cancelled then abort.
if (progress.IsCancelled) throw new InstallCancelledException();
// Display the message and step the progress bar.
progress.SetMessage(format, args);
if (step) progress.Step();
}
#endregion
#region private methods to deal with conflict resolution
///
/// This method creates a conflict collection from the module.
///
///
///
private FileConflictCollection CreateConflictCollection(Erf module)
{
FileConflictCollection conflicts = new FileConflictCollection();
StringCollection replacedFiles = module.ReplacedFiles;
foreach (string file in replacedFiles)
{
// Generate the full path of the module file, which has the same name
// but is decompressed to the current temp directory.
string moduleFile = Path.Combine(currentTempDir, Path.GetFileName(file));
// Create the conflict object and add it to the collection.
FileConflict conflict = new FileConflict(moduleFile, file);
conflicts.Add(conflict);
}
return conflicts;
}
///
/// This function checks for tlk conflicts, checking to see if the module
/// and hifs have tlk files. If there are multiple tlk files it will attempt
/// to generate a merge tlk file, if this cannot be done it will display an
/// error message and throw an InstallCancelledException to cancel the install.
///
/// The hak infos being added to the module
/// The module
/// The module info
/// The object implemening the progress interface
private void CheckForTlkConflicts(HakInfo[] hakInfos,
Erf module, ModuleInfo moduleInfo,
IHakInstallProgress progress)
{
// Create a tlk string collection and add the module's tlk if it has one.
StringCollection tlks = new StringCollection();
if (string.Empty != moduleInfo.CustomTlk)
tlks.Add(moduleInfo.CustomTlk.ToLower() + ".tlk");
// Add all of the tlk's from all of the HIFs.
foreach (HakInfo hif in hakInfos)
{
StringCollection hifTlks = hif.ModuleProperties["customtlk"];
if (null != hifTlks && hifTlks.Count > 0)
{
// Loop through the tlk's individually to exclude duplicates.
foreach (string hifTlk in hifTlks)
{
string lower = hifTlk.ToLower();
if (!tlks.Contains(lower)) tlks.Add(lower);
}
}
}
// If we have less than 2 tlks there is no conflict to resolve.
if (tlks.Count < 2) return;
// We have 2 or more tlk files, create a conflict resolver to
// build a merge tlk file.
ConflictResolver resolver = new ConflictResolver(progress);
string[] tlkStrings = new string[tlks.Count];
tlks.CopyTo(tlkStrings, 0);
mergeTlk = resolver.ResolveTlkConflict(module, tlkStrings);
// If we don't get a merge tlk back from the conflict resolver then we couldn't
// resolve the conflict. This is a fatal error so display an error message and
// cancel the install.
if (string.Empty == mergeTlk)
{
progress.DisplayErrorMessage("The module and custom content contain tlk files " +
"that cannot be merged. The module update will be aborted.");
throw new InstallCancelledException();
}
// Save the merge tlk as the module's custom tlk.
moduleInfo.CustomTlk = Path.GetFileNameWithoutExtension(mergeTlk.ToLower());
}
///
/// This function checks for hak conflicts, checking to see if any files
/// in the hifs will overwrite files in the module or vica versa. If
/// overwrites will happen, it prompts the user to see if we should continue,
/// throwing an InstallCancelledException() if the user chooses to cancel.
///
/// The hak infos being added to the module
/// The decompressed erfs
/// The module
/// The module info
/// The object implemening the progress interface
private void CheckForHakConflicts(HakInfo[] hakInfos,
StringCollection decompressedErfs, Erf module, ModuleInfo moduleInfo,
IHakInstallProgress progress)
{
// Create a hashtable for fast lookup and add all of the files in all
// of the decompressed erf's to it.
Hashtable hifErfHash = new Hashtable(10000);
foreach(string directory in decompressedErfs)
{
// Strip the ".temp" off the end of the name.
string erf = Path.GetFileNameWithoutExtension(directory);
string[] files = Directory.GetFiles(directory);
foreach (string file in files)
{
// Only add the ERF file if it's not already there. We assume that
// the ERF's in the HIF play well together so we ignore duplicates.
string key = Path.GetFileName(file).ToLower();
if ("exportinfo.gff" != key && !hifErfHash.Contains(key)) hifErfHash.Add(key, erf.ToLower());
}
}
// Build a list of all of the added haks.
StringCollection hakInfoHaks = new StringCollection();
foreach (HakInfo hakInfo in hakInfos)
{
StringCollection temp = hakInfo.ModuleProperties["hak"] as StringCollection;
if (null != temp)
{
foreach (string tempString in temp)
hakInfoHaks.Add(tempString.ToLower());
}
}
// Add all of the files in all of the haks to the hash table.
Hashtable hifHakHash = new Hashtable(10000);
foreach (string hakName in hakInfoHaks)
{
Erf hak = Erf.Load(NWNInfo.GetFullFilePath(hakName));
StringCollection files = hak.Files;
foreach (string file in files)
try
{
string key = file.ToLower();
string hakNameLower = hakName.ToLower();
hifHakHash.Add(key, hakNameLower);
}
catch (ArgumentException)
{}
}
// At this point we have built a lookup hash table that contains every
// file going into the module (either directly in an erf or indirectly
// in a hak). Now we need to loop through all of the files in the
// module (and all of it's haks) and check to see if any of them are
// going to get overwritten. At this point we have several cases.
// 1. Module content is going to get replaced by erf content. We
// do not handle that case now, we wait until the end and allow
// the user to selectivly overwrite whatever they wish.
// 2. Module content is going to get replaced by hak content. We must
// warn the user that module files will not be used and the module
// may not work.
// 3. Module hak content is going to get replaced by hak content. Same
// as above.
// 4. Module hak content is going to overwrite erf content from the hif.
// In this case the hif's content is the content that is going to be
// ignored, again the user has to be warned.
OverwriteWarningCollection hakWarnings = new OverwriteWarningCollection();
OverwriteWarningCollection erfWarnings = new OverwriteWarningCollection();
string moduleFileName = Path.GetFileName(module.FileName);
// Loop through all of the files in the module checking to see if files in
// added haks will overwrite them.
StringCollection moduleFiles = module.Files;
foreach (string file in moduleFiles)
{
string source = hifHakHash[file.ToLower()] as string;
if (null != source)
hakWarnings.Add(new OverwriteWarning(file.ToLower(), moduleFileName, source));
}
// Loop through all of the files in the module's haks checking to see if
// files in the added haks will overwrite them or if they will overwrite
// files in added erf's.
StringCollection moduleHaks = moduleInfo.Haks;
foreach (string moduleHak in moduleHaks)
{
// Check to see if the hak in the module is one of the haks being added (this is
// a no-op condition which will result in 100% duplicates, no need to check it).
string hak = moduleHak + ".hak";
if (hakInfoHaks.Contains(hak.ToLower())) continue;
Erf erf = Erf.Load(NWNInfo.GetFullFilePath(hak));
StringCollection hakFiles = erf.Files;
foreach (string file in hakFiles)
{
// If the file is in the hak hash then it is going to be
// overwritten by the hif's haks.
string key = file.ToLower();
string source = hifHakHash[key] as string;
if (null != source)
hakWarnings.Add(new OverwriteWarning(key,
Path.GetFileName(erf.FileName.ToLower()), source));
// If the file is in the erf hash then it will overwrite the
// hif's erf.
source = hifErfHash[key] as string;
if (null != source)
erfWarnings.Add(new OverwriteWarning(key, source,
Path.GetFileName(erf.FileName.ToLower())));
}
}
// We have built the list of conflicts, before asking the user try to resolve the
// conflicts as we may be able to generate a merge hak to resolve some of them.
if (hakWarnings.Count > 0)
{
ConflictResolver resolver = new ConflictResolver(progress);
mergeHak = resolver.ResolveConflicts(hakInfos, module, moduleInfo, hakWarnings);
}
// We have finished checking for files that are going to get overwritten.
// If we have any warnings to issue to the user then do so now.
if (hakWarnings.Count > 0 &&
!progress.ShouldOverwrite(hakWarnings, false, OverwriteWarningType.HifsOverwritesModule))
throw new InstallCancelledException();
if (erfWarnings.Count > 0 &&
!progress.ShouldOverwrite(erfWarnings, false, OverwriteWarningType.ModuleOverwritesHifs))
throw new InstallCancelledException();
}
#endregion
#region private methods to do the install work.
///
/// Installs the listed haks (defined by hif files) on the module.
///
/// The array of loaded hif files to install.
/// The string collection of decompressed
/// erf files from the hifs. The values in here should be the
/// temp directories in which the erfs have been decompressed to. If this
/// collection is empty then the erfs will be decompressed to temp directories
/// and the directories returned in this collection. It will be the caller's
/// responsibility to delete them.
/// The module to add the haks to
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
private void DoInstall(HakInfo[] hakInfos, StringCollection decompressedErfs,
string moduleFile, IHakInstallProgress progress)
{
mergeHak = string.Empty;
mergeTlk = string.Empty;
StringCollection tempDirs = new StringCollection();
try
{
// Load the module file.
Progress(progress, true, "Loading {0}", moduleFile);
if (progress.IsCancelled) return;
Erf module = Erf.Load(NWNInfo.GetFullFilePath(moduleFile));
// Decompress the module to a temp directory.
Progress(progress, true, "Decompressing {0}", moduleFile);
currentTempDir = Decompress(module, tempDirs);
// Load the moduleInfo for the module.
ModuleInfo moduleInfo = new ModuleInfo(currentTempDir);
// Check for any tlk file conflicts and abort the install if they
// cannot be resolved.
CheckForTlkConflicts(hakInfos, module, moduleInfo, progress);
// Check for any file conflicts and make sure it is OK with the user.
Progress(progress, false, "Checking for overwrites");
CheckForHakConflicts(hakInfos, decompressedErfs, module, moduleInfo,
progress);
// Add any erf files to the module.
foreach (string erfDir in decompressedErfs)
{
// Add each of the erf files to the module.
string[] files = Directory.GetFiles(erfDir);
foreach (string file in files)
{
// Add the file to the module.
Progress(progress, true, "Adding {1} to\n{0}", moduleFile, Path.GetFileName(file));
module.AddFile(Path.Combine(erfDir, file), true);
// If the file is an area file then add it to the module's area list.
if (0 == string.Compare(".are", Path.GetExtension(file), true, CultureInfo.InvariantCulture))
moduleInfo.AddAreas(new string[] { Path.GetFileNameWithoutExtension(file) });
}
}
// If we are overwriting files in the module then warn the user that
// they are doing so and give them a chance to abort.
if (module.ReplacedFiles.Count > 0)
{
// Create a conflict collection and ask the user what to do. If
// they cancel then trow a cancel exception to abort adding the
// hak(s).
FileConflictCollection conflicts = CreateConflictCollection(module);
if (!progress.ShouldReplaceFiles(conflicts))
throw new InstallCancelledException();
// The user may have chosen to keep some of the original module
// files, un-add any file that they chose not to replace.
foreach (FileConflict conflict in conflicts)
if (!conflict.ReplaceFile)
module.RemoveFileFromAddedList(conflict.HakFile);
}
// Now loop through all of the haks and make any module changes
// required.
foreach (HakInfo hakInfo in hakInfos)
{
// Set all of the module properties.
Progress(progress, true, "Setting module properties for {0}", moduleFile);
PropertyHandlerDictionary handlers = objects["Module"];
foreach (DictionaryEntry entry in hakInfo.ModuleProperties)
{
if (progress.IsCancelled) throw new InstallCancelledException();
// Resolve the DictionaryEntry to native data.
string property = (string) entry.Key;
StringCollection values = (StringCollection) entry.Value;
if (0 == values.Count) continue;
// Look up the handler for the property, throwing an exception
// if we don't find one, then invoke it.
PropertyHandler handler = handlers[property];
if (null == handler)
throw new InvalidOperationException("Unknown module property " + property);
handler(module, moduleInfo, property, values);
}
}
// If we have a merge hak then add it now so it goes to the top of
// the hak list.
if (string.Empty != mergeHak)
{
StringCollection mergeHakCollection = new StringCollection();
mergeHakCollection.Add(mergeHak);
Module_Hak(module, moduleInfo, "hak", mergeHakCollection);
}
// Build string arrays of the hif names and versions of all of the HakInfo
// objects we added to the module, then update the module's installed
// HakInfo property. This is a custom property used by this tool to
// keep track of what is installed on a module.
string[] hifs = new string[hakInfos.Length];
float[] versions = new float[hakInfos.Length];
for (int i = 0; i < hakInfos.Length; i++)
{
hifs[i] = hakInfos[i].Name;
versions[i] = hakInfos[i].Version;
}
moduleInfo.AddInstalledHakInfos(hifs, versions);
// Save the changes to the module info file.
moduleInfo.Save();
// Backup the old module file before saving.
// changed to not use the same extension as the toolset
string backupName = Path.Combine(NWNInfo.GetPathForFile(moduleFile),
Path.GetFileNameWithoutExtension(moduleFile) + ".prc.BackupMod");
// if a backup already exists, this means the module installer has been
// used before. We don't want to overwrite it, otherwise the original is lost
if(!File.Exists(backupName))
File.Copy(NWNInfo.GetFullFilePath(moduleFile), backupName, false);
// Recreate the module file with our changed files.
Progress(progress, true, "Saving {0}", moduleFile);
module.RecreateFile();
// If we created merge files then display a message to the user
// telling them what we did.
if (string.Empty != mergeHak || string.Empty != mergeTlk)
{
string files = "\r\n\r\n\t";
if (string.Empty != mergeHak) files += NWN.NWNInfo.GetPartialFilePath(mergeHak);
if (string.Empty != mergeTlk)
{
if (string.Empty != files) files += "\r\n\t";
files += NWN.NWNInfo.GetPartialFilePath(mergeTlk);
}
progress.DisplayMessage(string.Format(
"There were conflicts between the custom content you are trying to add and " +
"the files already used by the module '{0}'. Merge files were created to resolve " +
"these conflicts, you should delete these files when you are finished " +
"with your module." + files, Path.GetFileNameWithoutExtension(module.FileName)));
}
}
catch (Exception e)
{
// Delete any merge files we created, the install is failing.s
if (string.Empty != mergeTlk) File.Delete(NWN.NWNInfo.GetFullFilePath(mergeTlk));
if (string.Empty != mergeHak) File.Delete(NWN.NWNInfo.GetFullFilePath(mergeHak));
// If the exception isn't an InstallCancelledException then throw it.
// InstallCancelledExceptions are thrown to abort the install, we want to eat those.
if (!(e is InstallCancelledException)) throw;
}
finally
{
// Always clean up temp dirs no matter what.
foreach (string dir in tempDirs)
try
{
if (Directory.Exists(dir)) Directory.Delete(dir, true);
}
catch{}
}
}
///
/// Installs the listed haks (defined by hif files) on the listed
/// modules.
///
/// The list of haks to add
/// The list of modules to add the haks to
/// An interface used to an object used to display
/// progress information, or null if no progress information is desired
private void DoInstall(HakInfo[] hifs, string[] modules,
IHakInstallProgress progress)
{
StringCollection tempDirs = null;
try
{
// Calcualte the number of steps needed for the progress bar. The
// hard coded numbers are based on the number of step calls in
// DoInstall().
progress.ProgressSteps = GetProgressCount(hifs, modules);
// Load the hif files and decompress them to temp directories.
tempDirs = DecompressHifErfs(hifs, progress);
// Now apply the hifs to each module in turn.
foreach (string module in modules)
DoInstall(hifs, tempDirs, module, progress);
}
finally
{
// Always clean up temp dirs no matter what.
if (null != tempDirs)
{
foreach (string dir in tempDirs)
try
{
if (Directory.Exists(dir)) Directory.Delete(dir, true);
}
catch{}
}
}
}
#endregion
#region private methods to generate merge scripts
///
/// This method creates a script that calls ExecuteScript() to invoke
/// multiple scripts. It is designed to allow multiple scripts to wire up
/// to an event handler. The script is created and compiled and both
/// the NSS and NCS files are added to the module.
///
/// The module to modify
/// The property to which the scripts are being attached
/// The original script on the property, or
/// string.Empty if none
/// A list of other scripts to execute
/// The ResRef of the newly created script
private string CreateExecuteScript(Erf module, string property,
string originalScript, StringCollection otherScripts)
{
// If the original script and the otherScripts are the same then
// there is nothing to do, just return originalScript as the RefRef
if (1 == otherScripts.Count &&
0 == string.Compare(otherScripts[0], originalScript, true, CultureInfo.InvariantCulture))
return originalScript.ToLower();
// Build the file name and full name of the script file.
string substring = property.Length > 12 ? property.Substring(0, 12) : property;
string sourceName = "hif_" + substring + ".nss";
string fullSourceName = Path.Combine(currentTempDir, sourceName);
System.Text.StringBuilder b = new System.Text.StringBuilder();
// Check to see if the original script is one of our generated scripts
// (the name will start with "hif_" if it is). If so then we need to
// open the file and read the list of scripts currently being called
// and add them to the list of scripts to call.
StringCollection scriptsToExecute = new StringCollection();
bool createScript =
0 != string.Compare(originalScript, Path.GetFileNameWithoutExtension(sourceName), true, CultureInfo.InvariantCulture);
if (!createScript)
{
// Read the list of scripts currently being executed from the hif_
// script file.
string[] scripts = null;
using (StreamReader reader = new StreamReader(fullSourceName))
{
// Read the first line, strip the comment prefix off, and
// split the line into all of the scripts that the script
// executes.
string line = reader.ReadLine();
line = line.Trim();
line = line.Substring(3, line.Length - 3);
scripts = line.Split(',');
}
// Add all of the scripts currently in the file, and then add
// all of the scripts in the otherScripts collection if they aren't
// already there.
scriptsToExecute.AddRange(scripts);
foreach (string script in otherScripts)
if (!scriptsToExecute.Contains(script)) scriptsToExecute.Insert(0, script);
}
else
{
// Add all of the other scripts to our execute list, then add the
// original script if there was one.
// modified by fluffyamoeba 2008-06-16
// swapped so original script is executed last
foreach (string script in otherScripts)
scriptsToExecute.Add(script);
if (string.Empty != originalScript) scriptsToExecute.Add(originalScript);
}
// Create the script file.
using (StreamWriter writer = new StreamWriter(fullSourceName, false,
System.Text.Encoding.ASCII))
{
// Make the first line be a list of the scripts being executed
// so we can do updates to the file later.
b.Length = 0;
foreach (string script in scriptsToExecute)
{
if (b.Length > 0) b.Append(",");
b.Append(script);
}
writer.WriteLine("// {0}", b.ToString());
// Write out a comment header.
writer.WriteLine("/////////////////////////////////////////////////////////////////////");
writer.WriteLine("//");
writer.WriteLine("// This script has been auto-generated by HakInstaller to call");
writer.WriteLine("// multiple handlers for the {0} event.", property);
writer.WriteLine("//");
writer.WriteLine("/////////////////////////////////////////////////////////////////////");
writer.WriteLine("");
writer.WriteLine("void main()");
writer.WriteLine("{");
// Add an execute line for each script in the collection.
foreach (string script in scriptsToExecute)
writer.WriteLine(" ExecuteScript(\"{0}\", OBJECT_SELF);", script);
writer.WriteLine("}");
writer.Flush();
writer.Close();
}
// Build the name of the obj file.
string objName = Path.GetFileNameWithoutExtension(sourceName) + ".ncs";
string fullObjName = Path.Combine(currentTempDir, objName);
// Generate the compiler command line.
string compiler = Path.Combine(NWNInfo.ToolsPath, "clcompile.exe");
b.Length = 0;
b.AppendFormat("\"{0}\" \"{1}\"", fullSourceName, currentTempDir);
// Start the compiler process and wait for it.
ProcessStartInfo info = new ProcessStartInfo();
info.FileName = compiler;
info.Arguments = b.ToString();
info.CreateNoWindow = true;
info.WindowStyle = ProcessWindowStyle.Hidden;
Process process = Process.Start(info);
process.WaitForExit();
// If the compiler didn't work then we have a problem.
if (0 != process.ExitCode)
throw new NWNException("Could not run the NWN script compiler");
// Add the source and object files to the module, if we have created new
// files. If the original script was a hif_ script that we just changed
// then the file is already part of the module, no need to add it.
if (createScript)
{
module.AddFile(fullSourceName, true);
module.AddFile(fullObjName, true);
}
// Return the ResRef for the new script file.
return Path.GetFileNameWithoutExtension(sourceName).ToLower();
}
#endregion
#region private static fields/properties/methods
private static HakInstaller singleton;
///
/// Gets the singleton HakInstaller object.
///
private static HakInstaller Singleton
{
get
{
// Create the singleton if we haven't already, and return it.
if (null == singleton) singleton = new HakInstaller();
return singleton;
}
}
#endregion
}
}