#region ================== Copyright (c) 2007 Pascal vd Heiden

/*
 * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
 * This program is released under GNU General Public License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 */

#endregion

#region ================== Namespaces

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Actions;
using CodeImp.DoomBuilder.Compilers;
using CodeImp.DoomBuilder.Config;
using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.GZBuilder.Data; //mxd
using CodeImp.DoomBuilder.GZBuilder.GZDoom; //mxd
using CodeImp.DoomBuilder.IO;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.VisualModes;
using CodeImp.DoomBuilder.Windows;

#endregion

namespace CodeImp.DoomBuilder 
{
	public sealed class MapManager : IDisposable
	{
		#region ================== Constants

		// Map header name in temporary file
		internal const string TEMP_MAP_HEADER = "TEMPMAP";
		internal const string BUILD_MAP_HEADER = "MAP01";
		public const string CONFIG_MAP_HEADER = "~MAP";
		private const int REPLACE_TARGET_MAP = -1; //mxd

		#endregion

		#region ================== Variables

		// Status
		private bool changed;
		private bool scriptschanged;
		private bool maploading; //mxd

		// Map information
		private string filetitle;
		private string filepathname;
		private string temppath;
		private string origmapconfigname; //mxd. Map configuration, which was used to open the map.

		// Main objects
		private MapSet map;
		private MapSetIO io;
		private MapOptions options;
		private ConfigurationInfo configinfo;
		private GameConfiguration config;
		private DataManager data;
		private D3DDevice graphics;
		private Renderer2D renderer2d;
		private Renderer3D renderer3d;
		private WAD tempwad;
		private GridSetup grid;
		private UndoManager undoredo;
		private CopyPasteManager copypaste;
		private Launcher launcher;
		private ThingsFilter thingsfilter;
		private ScriptEditorForm scriptwindow;
		private List<CompilerError> errors;
		private VisualCamera visualcamera;

		//mxd
		private Dictionary<string, ScriptItem> namedscripts;
		private Dictionary<int, ScriptItem> numberedscripts;
		private readonly HashSet<string> scriptincludes;

		// Disposing
		private bool isdisposed;

		#endregion

		#region ================== Properties

		public string FilePathName { get { return filepathname; } }
		public string FileTitle { get { return filetitle; } internal set { filetitle = value; } } //mxd. Added setter
		public string TempPath { get { return temppath; } }
		public MapOptions Options { get { return options; } }
		public MapSet Map { get { return map; } }
		public DataManager Data { get { return data; } }
		public bool IsChanged { get { return changed | CheckScriptChanged(); } set { changed |= value; if(!maploading) General.MainWindow.UpdateMapChangedStatus(); } }
		public bool IsDisposed { get { return isdisposed; } }
		internal D3DDevice Graphics { get { return graphics; } }
		public IRenderer2D Renderer2D { get { return renderer2d; } }
		public IRenderer3D Renderer3D { get { return renderer3d; } }
		internal Renderer2D CRenderer2D { get { return renderer2d; } }
		internal Renderer3D CRenderer3D { get { return renderer3d; } }
		public GameConfiguration Config { get { return config; } }
		public ConfigurationInfo ConfigSettings { get { return configinfo; } }
		public GridSetup Grid { get { return grid; } }
		public UndoManager UndoRedo { get { return undoredo; } }
		internal CopyPasteManager CopyPaste { get { return copypaste; } }
		public IMapSetIO FormatInterface { get { return io; } }
		internal Launcher Launcher { get { return launcher; } }
		public ThingsFilter ThingsFilter { get { return thingsfilter; } }
		internal List<CompilerError> Errors { get { return errors; } }
		internal ScriptEditorForm ScriptEditor { get { return scriptwindow; } }
		public VisualCamera VisualCamera { get { return visualcamera; } set { visualcamera = value; } }
		public bool IsScriptsWindowOpen { get { return (scriptwindow != null) && !scriptwindow.IsDisposed; } }

		//mxd. Map format
		public bool UDMF { get { return config.UDMF; } }
		public bool HEXEN { get { return config.HEXEN; } }
		public bool DOOM { get { return config.DOOM; } }
        public bool SRB2 { get { return config.SRB2; } }

		//mxd. Scripts
		internal Dictionary<string, ScriptItem> NamedScripts { get { return namedscripts; } }
		internal Dictionary<int, ScriptItem> NumberedScripts { get { return numberedscripts; } }
		internal HashSet<string> ScriptIncludes { get { return scriptincludes; } }

		public ViewMode ViewMode { get { return renderer2d.ViewMode; } }

		#endregion

		#region ================== Constructor / Disposer

		// Constructor
		internal MapManager() 
		{
			// We have no destructor
			GC.SuppressFinalize(this);

			// Create temporary path
			temppath = General.MakeTempDirname();
			Directory.CreateDirectory(temppath);
			General.WriteLogLine("Temporary directory:  " + temppath);

			// Basic objects
			grid = new GridSetup();
			undoredo = new UndoManager();
			copypaste = new CopyPasteManager();
			launcher = new Launcher(this);
			thingsfilter = new NullThingsFilter();
			errors = new List<CompilerError>();

			//mxd
			numberedscripts = new Dictionary<int, ScriptItem>();
			namedscripts = new Dictionary<string, ScriptItem>();
			scriptincludes = new HashSet<string>();
		}

		// Disposer
		public void Dispose() 
		{
			// Not already disposed?
			if(!isdisposed) 
			{
				// Let the plugins know
				General.Plugins.OnMapCloseBegin();

				// Stop processing
				General.MainWindow.StopProcessing();

				// Close script editor
				CloseScriptEditor(false);

				// Change to no mode
				General.Editing.ChangeMode((EditMode)null);

				// Unbind any methods
				General.Actions.UnbindMethods(this);

				// Dispose
				maploading = true; //mxd
				if(grid != null) grid.Dispose();
				if(launcher != null) launcher.Dispose();
				if(copypaste != null) copypaste.Dispose();
				if(undoredo != null) undoredo.Dispose();
				General.WriteLogLine("Unloading data resources...");
				if(data != null) data.Dispose();
				General.WriteLogLine("Closing temporary file...");
				if(tempwad != null) tempwad.Dispose();
				General.WriteLogLine("Unloading map data...");
				if(map != null) map.Dispose();
				General.WriteLogLine("Stopping graphics device...");
				if(renderer2d != null) renderer2d.Dispose();
				if(renderer3d != null) renderer3d.Dispose();
				if(graphics != null) graphics.Dispose();
				visualcamera = null;
				grid = null;
				launcher = null;
				copypaste = null;
				undoredo = null;
				data = null;
				tempwad = null;
				map = null;
				renderer2d = null;
				renderer3d = null;
				graphics = null;

				// We may spend some time to clean things up here
				GC.Collect();
				GC.WaitForPendingFinalizers(); //mxd
				GC.Collect(); //mxd

				// Remove temp file
				General.WriteLogLine("Removing temporary directory...");
				try
				{
					Directory.Delete(temppath, true);
				} 
				catch(Exception e) 
				{
					General.WriteLogLine(e.GetType().Name + ": " + e.Message);
					General.WriteLogLine("Failed to remove temporary directory!");
				}

				// Let the plugins know
				General.Plugins.OnMapCloseEnd();

				// Done
				isdisposed = true;
			}
		}

		#endregion

		#region ================== New / Open

		// Initializes for a new map
		internal bool InitializeNewMap(MapOptions options) 
		{
#if DEBUG
			DebugConsole.Clear();
#endif
			
			// Apply settings
			this.filetitle = options.CurrentName + ".wad";
			this.filepathname = String.Empty;
			this.maploading = true; //mxd
			this.changed = false;
			this.options = options;

			General.WriteLogLine("Creating new map '" + options.CurrentName + "' with configuration '" + options.ConfigFile + "'");

			// Initiate graphics
			General.WriteLogLine("Initializing graphics device...");
			graphics = new D3DDevice(General.MainWindow.Display);
			if(!graphics.Initialize()) return false;

			// Create renderers
			renderer2d = new Renderer2D(graphics);
			renderer3d = new Renderer3D(graphics);

			// Load game configuration
			General.WriteLogLine("Loading game configuration...");
			configinfo = General.GetConfigurationInfo(options.ConfigFile);
			config = new GameConfiguration(configinfo.Configuration); //mxd
			origmapconfigname = configinfo.Filename;//mxd
			configinfo.ApplyDefaults(config);
			General.Editing.UpdateCurrentEditModes();

			// Create map data
			map = new MapSet();

			// Create temp wadfile
			string tempfile = General.MakeTempFilename(temppath);
			General.WriteLogLine("Creating temporary file: " + tempfile);
#if DEBUG
			tempwad = new WAD(tempfile);
#else
			try { tempwad = new WAD(tempfile); }
			catch(Exception e)
			{
				General.ShowErrorMessage("Error while creating a temporary wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
				return false;
			}
#endif

			// Read the map from temp file
			General.WriteLogLine("Initializing map format interface " + config.FormatInterface + "...");
			io = MapSetIO.Create(config.FormatInterface, tempwad, this);

			// Create required lumps
			General.WriteLogLine("Creating map data structures...");
			tempwad.Insert(TEMP_MAP_HEADER, 0, 0);
			io.Write(map, TEMP_MAP_HEADER, 1);
			CreateRequiredLumps(tempwad, TEMP_MAP_HEADER);

			// Load data manager
			General.WriteLogLine("Loading data resources...");
			data = new DataManager();
			data.Load(configinfo.Resources, options.Resources);

			// Update structures
			options.ApplyGridSettings();
			map.UpdateConfiguration();
			map.Update();
			thingsfilter.Update();

			namedscripts = new Dictionary<string, ScriptItem>(); //mxd
			numberedscripts = new Dictionary<int, ScriptItem>(); //mxd

			// Bind any methods
			General.Actions.BindMethods(this);

			// Set defaults
			this.visualcamera = new VisualCamera();
			General.Editing.ChangeMode(configinfo.StartMode);
			ClassicMode cmode = (General.Editing.Mode as ClassicMode);
			if(cmode != null) cmode.SetZoom(0.5f);
			renderer2d.SetViewMode((ViewMode)General.Settings.DefaultViewMode);
			General.Settings.SetDefaultThingFlags(config.DefaultThingFlags);

			// Success
			this.changed = false;
			this.maploading = false; //mxd
			General.WriteLogLine("Map creation done");
			General.MainWindow.UpdateMapChangedStatus(); //mxd
			return true;
		}

		// Initializes for an existing map
		internal bool InitializeOpenMap(string filepathname, MapOptions options) 
		{
			WAD mapwad;

#if DEBUG
			DebugConsole.Clear();
#endif

			// Apply settings
			this.filetitle = Path.GetFileName(filepathname);
			this.filepathname = filepathname;
			this.changed = false;
			this.maploading = true; //mxd
			this.options = options;

			General.WriteLogLine("Opening map '" + options.CurrentName + "' with configuration '" + options.ConfigFile + "'");

			// Initiate graphics
			General.WriteLogLine("Initializing graphics device...");
			graphics = new D3DDevice(General.MainWindow.Display);
			if(!graphics.Initialize()) return false;

			// Create renderers
			renderer2d = new Renderer2D(graphics);
			renderer3d = new Renderer3D(graphics);

			// Load game configuration
			General.WriteLogLine("Loading game configuration...");
			configinfo = General.GetConfigurationInfo(options.ConfigFile);
			config = new GameConfiguration(configinfo.Configuration);
			origmapconfigname = configinfo.Filename;//mxd
			configinfo.ApplyDefaults(config);
			General.Editing.UpdateCurrentEditModes();

			// Create map data
			map = new MapSet();

			// Create temp wadfile
			string tempfile = General.MakeTempFilename(temppath);
			General.WriteLogLine("Creating temporary file: " + tempfile);
#if DEBUG
			tempwad = new WAD(tempfile);
#else
			try { tempwad = new WAD(tempfile); } catch(Exception e) 
			{
				General.ShowErrorMessage("Error while creating a temporary wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
				return false;
			}
#endif

			// Now open the map file
			General.WriteLogLine("Opening source file: " + filepathname);
#if DEBUG
			mapwad = new WAD(filepathname, true);
#else
			try { mapwad = new WAD(filepathname, true); } catch(Exception e) 
			{
				General.ShowErrorMessage("Error while opening source wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
				return false;
			}
#endif

			// Copy the map lumps to the temp file
			General.WriteLogLine("Copying map lumps to temporary file...");
			CopyLumpsByType(mapwad, options.CurrentName, tempwad, TEMP_MAP_HEADER, REPLACE_TARGET_MAP, true, true, true, true);

			// Close the map file
			mapwad.Dispose();

			//mxd. Create MapSet
			bool maprestored;
			if(!CreateMapSet(map, filepathname, options, out maprestored)) return false;

			// Load data manager
			General.WriteLogLine("Loading data resources...");
			data = new DataManager();
			DataLocation maplocation = new DataLocation(DataLocation.RESOURCE_WAD, filepathname, options.StrictPatches, false, false);
			data.Load(configinfo.Resources, options.Resources, maplocation);

			// Remove unused sectors
			map.RemoveUnusedSectors(true);

			//mxd. Translate to long or short texture names.
			bool nameschanged = map.TranslateTextureNames(config.UseLongTextureNames, false);
			grid.TranslateBackgroundName(config.UseLongTextureNames);

			//mxd. Sector textures may've been changed 
			if(nameschanged) data.UpdateUsedTextures();

			// Update structures
			options.ApplyGridSettings();
			map.UpdateConfiguration();
			map.SnapAllToAccuracy();
			map.Update();
			thingsfilter.Update();

			//mxd. Update includes list and script names
			UpdateScriptNames(true);

			//mxd. Restore selection groups
			options.ReadSelectionGroups();

			// Bind any methods
			General.Actions.BindMethods(this);

			// Set defaults
			this.visualcamera = new VisualCamera();
			General.Editing.ChangeMode(configinfo.StartMode);
			renderer2d.SetViewMode((ViewMode)General.Settings.DefaultViewMode);
			General.Settings.SetDefaultThingFlags(config.DefaultThingFlags);

			// Center map in screen
			//if(General.Editing.Mode is ClassicMode) (General.Editing.Mode as ClassicMode).CenterInScreen();

			// Success
			this.changed = maprestored; //mxd
			this.maploading = false; //mxd
			General.WriteLogLine("Map loading done");
			General.MainWindow.UpdateMapChangedStatus(); //mxd
			return true;
		}

		//mxd. This switches to another map in the same wad 
		internal bool InitializeSwitchMap(MapOptions options)
		{
#if DEBUG
			DebugConsole.Clear();
#endif
			
			this.changed = false;
			this.maploading = true;
			this.options = options;

			// Create map data
			MapSet newmap = new MapSet();
			WAD mapwad;

			// Create temp wadfile
			string tempfile = General.MakeTempFilename(temppath);
			General.WriteLogLine("Creating temporary file: " + tempfile);
			if(tempwad != null) tempwad.Dispose();

#if DEBUG
			tempwad = new WAD(tempfile);
#else
			try { tempwad = new WAD(tempfile); } catch(Exception e) 
			{
				General.ShowErrorMessage("Error while creating a temporary wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
				return false;
			}
#endif

			// Now open the map file
			General.WriteLogLine("Opening source file: " + filepathname);
			
#if DEBUG
			mapwad = new WAD(filepathname, true);
#else
			try { mapwad = new WAD(filepathname, true); } catch(Exception e) 
			{
				General.ShowErrorMessage("Error while opening source wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
				return false;
			}
#endif

			// Copy the map lumps to the temp file
			General.WriteLogLine("Copying map lumps to temporary file...");
			CopyLumpsByType(mapwad, options.CurrentName, tempwad, TEMP_MAP_HEADER, REPLACE_TARGET_MAP, true, true, true, true);

			// Close the map file
			mapwad.Dispose();

			// Create MapSet
			bool maprestored;
			if(!CreateMapSet(newmap, filepathname, options, out maprestored)) return false;

			// And switch to it
			ChangeMapSet(newmap);

			// Translate texture names
			map.TranslateTextureNames(config.UseLongTextureNames, false);
			grid.TranslateBackgroundName(config.UseLongTextureNames);

			// Sector textures may've been changed 
			data.UpdateUsedTextures();

            // This will update DataManager.mapinfo only
            data.ReloadMapInfoPartial();

            // Skybox may've been changed
            data.SetupSkybox();

            // Update includes list and script names
            UpdateScriptNames(true);

			// Restore selection groups
			options.ReadSelectionGroups();

			// Center map in screen or on stored coordinates
			ClassicMode mode = General.Editing.Mode as ClassicMode;
			if(mode != null) 
			{
				mode.OnRedoEnd();

				if(options.ViewPosition.IsFinite() && !float.IsNaN(options.ViewScale))
					mode.CenterOnCoordinates(options.ViewPosition, options.ViewScale);
				else
					mode.CenterInScreen();
			}

			// Success
			this.changed = maprestored;
			this.maploading = false;
			General.WriteLogLine("Map switching done");
			General.MainWindow.UpdateMapChangedStatus();
			return true;
		}

		//mxd
		private bool CreateMapSet(MapSet newmap, string filepathname, MapOptions options, out bool maprestored)
		{
			maprestored = false;
			string wadname = Path.GetFileNameWithoutExtension(filepathname);
			if(!string.IsNullOrEmpty(wadname))
			{
				string hash = MurmurHash2.Hash(wadname + options.LevelName + File.GetLastWriteTime(filepathname)).ToString();
				string backuppath = Path.Combine(General.MapRestorePath, wadname + "." + hash + ".restore");

				// Backup exists and it's newer than the map itself?
				if(File.Exists(backuppath) && File.GetLastWriteTime(backuppath) > File.GetLastWriteTime(filepathname))
				{
					if(General.ShowWarningMessage("Looks like your previous editing session has gone terribly wrong." + Environment.NewLine
						   + "Would you like to restore the map from the backup?", MessageBoxButtons.YesNo) == DialogResult.Yes)
					{
						General.WriteLogLine("Initializing map format interface " + config.FormatInterface + "...");
						io = MapSetIO.Create(config.FormatInterface, tempwad, this);
						General.WriteLogLine("Restoring map from '" + backuppath + "'...");
						
#if DEBUG
						// Restore map
						newmap.Deserialize(SharpCompressHelper.DecompressStream(new MemoryStream(File.ReadAllBytes(backuppath))));
#else
						try
						{
							// Restore map
							newmap.Deserialize(SharpCompressHelper.DecompressStream(new MemoryStream(File.ReadAllBytes(backuppath))));

							// Delete the backup
							File.Delete(backuppath);
						}
						catch(Exception e) 
						{
							General.ErrorLogger.Add(ErrorType.Error, "Unable to restore the map data structures from the backup. " + e.GetType().Name + ": " + e.Message);
							General.ShowErrorMessage("Unable to restore the map data structures from the backup.", MessageBoxButtons.OK);
							return false;
						}	
#endif
						maprestored = true;
					}
				}
			}

			// Read the map from temp file
			if(!maprestored)
			{
				newmap.BeginAddRemove();
				General.WriteLogLine("Initializing map format interface " + config.FormatInterface + "...");
				io = MapSetIO.Create(config.FormatInterface, tempwad, this);
				General.WriteLogLine("Reading map data from file...");
#if DEBUG
				newmap = io.Read(newmap, TEMP_MAP_HEADER);
#else
				try { newmap = io.Read(newmap, TEMP_MAP_HEADER); } catch(Exception e) 
				{
					General.ErrorLogger.Add(ErrorType.Error, "Unable to read the map data with the specified configuration. " + e.GetType().Name + ": " + e.Message);
					General.ShowErrorMessage("Unable to read the map data with the specified configuration." + Environment.NewLine + Environment.NewLine + e.Message, MessageBoxButtons.OK);
					return false;
				}
#endif
				newmap.EndAddRemove();
			}

			return true;
		}

		#endregion

		#region ================== Save

		/// <summary>
		/// This exports the structures from memory into a WAD file with the current map format.
		/// </summary>
		public bool ExportToFile(string filepathname) 
		{
            General.Plugins.OnMapSaveBegin(SavePurpose.Testing);
            bool result = SaveMap(filepathname, SavePurpose.Testing);
            General.Plugins.OnMapSaveEnd(SavePurpose.Testing);
            return result;
        }

        /// <summary>
        /// This writes the map structures to the temporary file.
        /// </summary>
        private bool WriteMapToTempFile()
        {
            StatusInfo oldstatus = General.MainWindow.Status;

            // Make a copy of the map data
            MapSet outputset = map.Clone();

            // Remove all flags from all 3D Start things
            foreach (Thing t in outputset.Things)
            {
                if (t.SRB2Type == config.Start3DModeThingType)
                {
                    // We're not using SetFlag here, this doesn't have to be undone.
                    // Please note that this is totally exceptional!
                    List<string> flagkeys = new List<string>(t.Flags.Keys);
                    foreach (string k in flagkeys) t.Flags[k] = false;
                }
            }

            // Do we need sidedefs compression?
            if (map.Sidedefs.Count > io.MaxSidedefs)
            {
                // Compress sidedefs
                General.MainWindow.DisplayStatus(StatusType.Busy, "Compressing sidedefs...");
                outputset.CompressSidedefs();

                // Check if it still doesnt fit
                if (outputset.Sidedefs.Count > io.MaxSidedefs)
                {
                    // Problem! Can't save the map like this!
                    General.ShowErrorMessage("Unable to save the map: There are too many unique sidedefs!", MessageBoxButtons.OK);
                    General.MainWindow.DisplayStatus(oldstatus);
                    return false;
                }
            }

            // Check things
            if (map.Things.Count > io.MaxThings)
            {
                General.ShowErrorMessage("Unable to save the map: There are too many things!", MessageBoxButtons.OK);
                General.MainWindow.DisplayStatus(oldstatus);
                return false;
            }

            // Check sectors
            if (map.Sectors.Count > io.MaxSectors)
            {
                General.ShowErrorMessage("Unable to save the map: There are too many sectors!", MessageBoxButtons.OK);
                General.MainWindow.DisplayStatus(oldstatus);
                return false;
            }

            // Check linedefs
            if (map.Linedefs.Count > io.MaxLinedefs)
            {
                General.ShowErrorMessage("Unable to save the map: There are too many linedefs!", MessageBoxButtons.OK);
                General.MainWindow.DisplayStatus(oldstatus);
                return false;
            }

            // Check vertices
            if (map.Vertices.Count > io.MaxVertices)
            {
                General.ShowErrorMessage("Unable to save the map: There are too many vertices!", MessageBoxButtons.OK);
                General.MainWindow.DisplayStatus(oldstatus);
                return false;
            }

            // TODO: Check for more limitations

            // Write to temporary file
            General.WriteLogLine("Writing map data structures to file...");
            int index = Math.Max(0, tempwad.FindLumpIndex(TEMP_MAP_HEADER));
            io.Write(outputset, TEMP_MAP_HEADER, index);
            outputset.Dispose();

            General.MainWindow.DisplayStatus(oldstatus);
            return true;
        }

        // Initializes for an existing map
        internal bool SaveMap(string newfilepathname, SavePurpose purpose) 
		{
			string settingsfile;
			WAD targetwad = null;
			bool includenodes;

			General.WriteLogLine("Saving map to file: " + newfilepathname);

			//mxd. Official IWAD check...
			WAD hashtest = new WAD(newfilepathname, true);
			if(hashtest.IsOfficialIWAD)
			{
				General.WriteLogLine("Map saving aborted: attempt to modify official IWAD");
				General.ShowErrorMessage("Official IWADs should not be modified.\nConsider making a PWAD instead", MessageBoxButtons.OK);
				return false;
			}
			else
			{
				hashtest.Dispose();
				hashtest = null;
			}

			// Scripts changed?
			bool localscriptschanged = CheckScriptChanged();

			// If the scripts window is open, save the scripts first
			if(IsScriptsWindowOpen) scriptwindow.Editor.ImplicitSave();

			// Only recompile scripts when the scripts have changed
			// (not when only the map changed)
			if(localscriptschanged && !CompileScriptLumps()) 
			{
				// Compiler failure
				if(errors.Count > 0)
					General.ShowErrorMessage("Error while compiling scripts: " + errors[0].description, MessageBoxButtons.OK);
				else
					General.ShowErrorMessage("Unknown compiler error while compiling scripts!", MessageBoxButtons.OK);
			}

			// Show script window if there are any errors and we are going to test the map
			// and always update the errors on the scripts window.
			if((errors.Count > 0) && (scriptwindow == null) && (purpose == SavePurpose.Testing)) ShowScriptEditor();
			if(scriptwindow != null) scriptwindow.Editor.ShowErrors(errors);

			// Only write the map and rebuild nodes when the actual map has changed
			// (not when only scripts have changed)
			if(changed) 
			{
                // Write the current map structures to the temp file
                if (!WriteMapToTempFile()) return false;

                // Get the corresponding nodebuilder
                string nodebuildername = (purpose == SavePurpose.Testing) ? configinfo.NodebuilderTest : configinfo.NodebuilderSave;

                // Build the nodes
                StatusInfo oldstatus = General.MainWindow.Status;
                General.MainWindow.DisplayStatus(StatusType.Busy, "Building map nodes...");
				includenodes = (!string.IsNullOrEmpty(nodebuildername) && BuildNodes(nodebuildername, true));
				General.MainWindow.DisplayStatus(oldstatus);
			}
			else 
			{
				// Check if we have nodebuilder lumps
				includenodes = VerifyNodebuilderLumps(tempwad, TEMP_MAP_HEADER);
			}

			//mxd. Target file is read-only?
			FileInfo info = new FileInfo(newfilepathname);
			if(info.Exists && info.IsReadOnly)
			{
				if(General.ShowWarningMessage("Unable to save the map: target file is read-only.\nRemove read-only flag and save the map anyway?", MessageBoxButtons.YesNo) == DialogResult.Yes)
				{
					General.WriteLogLine("Removing read-only flag from the map file...");
					try
					{
						info.IsReadOnly = false;
					}
					catch(Exception e)
					{
						General.ShowErrorMessage("Failed to remove read-only flag from \"" + filepathname + "\":" + Environment.NewLine + Environment.NewLine + e.Message, MessageBoxButtons.OK);
						General.WriteLogLine("Failed to remove read-only flag from \"" + filepathname + "\":" + e.Message);
						return false;
					}
				}
				else
				{
					General.WriteLogLine("Map saving cancelled...");
					return false;
				}
			}

			// Suspend data resources
			data.Suspend();

			//mxd. Check if the target file is locked
			FileLockChecker.FileLockCheckResult checkresult = FileLockChecker.CheckFile(newfilepathname);
			if(!string.IsNullOrEmpty(checkresult.Error))
			{
				if(checkresult.Processes.Count > 0)
				{
					string rest = "Press 'Retry' to close " + (checkresult.Processes.Count > 1 ? "all processes" : "the process") 
						+ " and retry." + Environment.NewLine + "Press 'Cancel' to cancel saving.";
					
					if(General.ShowErrorMessage(checkresult.Error + rest, MessageBoxButtons.RetryCancel) == DialogResult.Retry)
					{
						// Close all processes
						foreach(Process process in checkresult.Processes)
						{
							try
							{
								if(!process.HasExited) process.Kill();
							}
							catch(Exception e)
							{
								General.ShowErrorMessage("Failed to close " + Path.GetFileName(process.MainModule.FileName) + ":" + Environment.NewLine + Environment.NewLine + e.Message, MessageBoxButtons.OK);
								data.Resume();
								General.WriteLogLine("Map saving failed: failed to close " + Path.GetFileName(process.MainModule.FileName));
								return false;
							}
						}

						// Retry
						data.Resume();
						General.WriteLogLine("Map saving restarted...");
						return SaveMap(newfilepathname, purpose);
					}
					else
					{
						data.Resume();
						General.WriteLogLine("Map saving cancelled...");
						return false;
					}
				}
				else
				{
					General.ShowErrorMessage(checkresult.Error, MessageBoxButtons.OK);
					data.Resume();
					General.WriteLogLine("Map saving failed: " + checkresult.Error);
					return false;
				}
			}

            // Determine original map name
            string origmapname = (!string.IsNullOrEmpty(options.PreviousName) && purpose != SavePurpose.IntoFile) ? options.PreviousName : options.CurrentName;
			string origwadfile = string.Empty; //mxd
			int mapheaderindex = REPLACE_TARGET_MAP; //mxd. Lump index of the map file header in the source WAD

			try 
			{
				if(File.Exists(newfilepathname)) 
				{
					// mxd. Check if target wad already has a map with the same name
					if(purpose == SavePurpose.IntoFile) 
					{
						WAD wad = new WAD(newfilepathname, true);
						int mapindex = wad.FindLumpIndex(origmapname);
						wad.Dispose();

						if(mapindex != -1 && MessageBox.Show(General.MainWindow, "Target file already contains map '" + origmapname + "'\nDo you want to replace it?", "Map already exists!", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No) 
						{
							data.Resume();
							General.WriteLogLine("Map saving cancelled...");
							return false;
						}
					}

                    int backups = General.Settings.MaxBackups;
                    if (backups > 0)
                    {
                        // Backup existing file, if any
                        if (File.Exists(newfilepathname + ".backup" + backups)) File.Delete(newfilepathname + ".backup" + backups);
                        for (int i = backups - 1; i >= 1; i--)
                        {
                            if (File.Exists(newfilepathname + ".backup" + i)) File.Move(newfilepathname + ".backup" + i, newfilepathname + ".backup" + (i+1));
                        }
                        File.Copy(newfilepathname, newfilepathname + ".backup1");
                    }
                }

				// Except when saving INTO another file,
				// kill the target file if it is different from source file
				if((purpose != SavePurpose.IntoFile) && (newfilepathname != filepathname)) 
				{
					// Kill target file
					if(File.Exists(newfilepathname)) File.Delete(newfilepathname);

					// Kill .dbs settings file
					settingsfile = newfilepathname.Substring(0, newfilepathname.Length - 4) + ".dbs";
					if(File.Exists(settingsfile)) File.Delete(settingsfile);
				}

				// On Save AS we have to copy the previous file to the new file
				if((purpose == SavePurpose.AsNewFile) && (!String.IsNullOrEmpty(filepathname))) 
				{
					// Copy if original file still exists
					if(File.Exists(filepathname)) File.Copy(filepathname, newfilepathname, true);
				}

				// If the target file exists, we need to rebuild it
				if(File.Exists(newfilepathname)) 
				{
					// Move the target file aside
					origwadfile = newfilepathname + ".temp";
					File.Move(newfilepathname, origwadfile);

					// Open original file
					WAD origwad = new WAD(origwadfile, true);

					// Create new target file
					targetwad = new WAD(newfilepathname) { IsIWAD = origwad.IsIWAD }; //mxd. Let's preserve wad type

					// Copy all lumps, except the original map
					GameConfiguration origcfg; //mxd
					if(origmapconfigname == configinfo.Filename) 
					{
						origcfg = config;
					} 
					else 
					{
						ConfigurationInfo ci = General.GetConfigurationInfo(origmapconfigname);
						origcfg = new GameConfiguration(ci.Configuration);
					}

					mapheaderindex = CopyAllLumpsExceptMap(origwad, targetwad, origcfg, origmapname);

					// Close original file and delete it
					origwad.Dispose();
					File.Delete(origwadfile);
				}
				else 
				{
					// Create new target file
					targetwad = new WAD(newfilepathname);
				}
			} 
			catch(Exception e) 
			{
				General.ShowErrorMessage("Unable to write the map to target file \"" + newfilepathname + "\":\n" + e.Message, MessageBoxButtons.OK);
				if(!string.IsNullOrEmpty(origwadfile) && File.Exists(origwadfile))
				{
					//mxd. Clean-up
					if(File.Exists(newfilepathname))
					{
						//mxd. We MAY've just deleted the map from the target file. Let's pretend this never happened
						if(targetwad != null) targetwad.Dispose();
						File.Delete(newfilepathname);
						File.Move(origwadfile, newfilepathname);
					}
					else
					{
						File.Delete(origwadfile); 
					}
				}

				data.Resume();
				General.WriteLogLine("Map saving failed: " + e.Message);
				return false;
			} 

			// Copy map lumps to target file
			CopyLumpsByType(tempwad, TEMP_MAP_HEADER, targetwad, origmapname, mapheaderindex, true, true, includenodes, true);

			// mxd. Was the map renamed?
			if(options.LevelNameChanged) 
			{
				if(purpose != SavePurpose.IntoFile) 
				{
					General.WriteLogLine("Changing map name from '" + options.PreviousName + "' to '" + options.CurrentName + "'");

					// Find the map header in target
					int index = targetwad.FindLumpIndex(options.PreviousName);
					if(index > -1) 
					{
						// Rename the map lump name
						targetwad.Lumps[index].Rename(options.CurrentName);
					} 
					else 
					{
						// Houston, we've got a problem!
						General.ShowErrorMessage("Error renaming map lump name: the original map lump could not be found!", MessageBoxButtons.OK);
						options.CurrentName = options.PreviousName;
					}
				}

				options.PreviousName = String.Empty;
			}

			// Done with the target file
			targetwad.Dispose();

			// Resume data resources
			data.Resume();

			// Not saved for testing purpose?
			if(purpose != SavePurpose.Testing) 
			{
				// Saved in a different file?
				if(newfilepathname != filepathname) 
				{
					// Keep new filename
					filepathname = newfilepathname;
					filetitle = Path.GetFileName(filepathname);

					// Reload resources
					ReloadResources();
				}

				try 
				{
					// Open or create the map settings
					settingsfile = newfilepathname.Substring(0, newfilepathname.Length - 4) + ".dbs";
					options.WriteConfiguration(settingsfile);
				} 
				catch (Exception e) 
				{
					// Warning only
					General.ErrorLogger.Add(ErrorType.Warning, "Could not write the map settings configuration file. " + e.GetType().Name + ": " + e.Message);
				}

				// Changes saved
				changed = false;
				scriptschanged = false;
			}

			// Success!
			General.WriteLogLine("Map saving done");
			General.MainWindow.UpdateMapChangedStatus(); //mxd
			return true;
		}

		//mxd. Don't save the map if it was not changed
		internal bool MapSaveRequired(string newfilepathname, SavePurpose purpose)
		{
			return (changed || scriptschanged || CheckScriptChanged() || options.LevelNameChanged || newfilepathname != filepathname || purpose != SavePurpose.Normal);
		}

		//mxd. Saves .dbs file
		internal bool SaveSettingsFile(string newfilepathname)
		{
			try 
			{
				string settingsfile = newfilepathname.Substring(0, newfilepathname.Length - 4) + ".dbs";
				options.WriteConfiguration(settingsfile);
			} 
			catch(Exception e) 
			{
				// Warning only
				General.ErrorLogger.Add(ErrorType.Warning, "Could not write the map settings configuration file. " + e.GetType().Name + ": " + e.Message);
				return false;
			}

			return true;
		}

		//mxd
		internal void SaveMapBackup()
		{
			if(isdisposed || map == null || map.IsDisposed || string.IsNullOrEmpty(filepathname) || options == null)
			{
				General.WriteLogLine("Map backup saving failed: required structures already disposed...");
				return;
			}

#if !DEBUG
			try
			{
#endif
				string wadname = Path.GetFileNameWithoutExtension(filepathname);
				if(!string.IsNullOrEmpty(wadname))
				{
					// Make backup file path
					if(!Directory.Exists(General.MapRestorePath)) Directory.CreateDirectory(General.MapRestorePath);
					string hash = MurmurHash2.Hash(wadname + options.LevelName + File.GetLastWriteTime(filepathname)).ToString();
					string backuppath = Path.Combine(General.MapRestorePath, wadname + "." + hash + ".restore");

					// Export map
					MemoryStream ms = map.Serialize();
					ms.Seek(0, SeekOrigin.Begin);
					File.WriteAllBytes(backuppath, SharpCompressHelper.CompressStream(ms).ToArray());

					// Log it
					General.WriteLogLine("Map backup saved to '" + backuppath + "'");
				}
				else
				{
					// Log it
					General.WriteLogLine("Map backup saving failed: invalid map WAD name");
				}
#if !DEBUG
			}
			catch(Exception e)
			{
				// Log it
				General.WriteLogLine("Map backup saving failed: " + e.Source + ": " + e.Message);
			}
#endif
		}

        #endregion

        #region ================== Nodebuild

        /// <summary>
        /// This stores the current structures in memory to the temporary file and rebuilds the nodes.
        /// The 'nodebuildername' must be a valid nodebuilder configuration profile.
        /// Returns True on success, False when failed.
        /// </summary>
        public bool RebuildNodes(string nodebuildername, bool failaswarning)
        {
            bool result;

            // Write the current map structures to the temp file
            if (!WriteMapToTempFile()) return false;

            // Build the nodes
            StatusInfo oldstatus = General.MainWindow.Status;
            General.MainWindow.DisplayStatus(StatusType.Busy, "Building map nodes...");
            if (!string.IsNullOrEmpty(nodebuildername))
                result = BuildNodes(nodebuildername, failaswarning);
            else
                result = false;
            General.MainWindow.DisplayStatus(oldstatus);

            return result;
        }

        // This builds the nodes in the temproary file with the given configuration name
        private bool BuildNodes(string nodebuildername, bool failaswarning) 
		{
			bool lumpscomplete = false;
			WAD buildwad;

			// Find the nodebuilder
			NodebuilderInfo nodebuilder = General.GetNodebuilderByName(nodebuildername);
			if(nodebuilder == null) 
			{
				// Problem! Can't find that nodebuilder!
				General.ShowWarningMessage("Unable to build the nodes: The configured nodebuilder cannot be found.\nPlease check your game configuration settings!", MessageBoxButtons.OK);
				return false;
			}
			else 
			{
				// Create the compiler interface that will run the nodebuilder
				// This automatically creates a temporary directory for us
				Compiler compiler = nodebuilder.CreateCompiler();

				// Make temporary filename
				string tempfile1 = General.MakeTempFilename(compiler.Location);

				// Make the temporary WAD file
				General.WriteLogLine("Creating temporary build file: " + tempfile1);
#if DEBUG
				buildwad = new WAD(tempfile1);
#else
				try { buildwad = new WAD(tempfile1); }
				catch(Exception e)
				{
					General.ShowErrorMessage("Error while creating a temporary wad file:\n" + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
					return false;
				}
#endif

				// Determine source file
				string sourcefile = (filepathname.Length > 0 ? filepathname : tempwad.Filename);

				//mxd.
				RemoveUnneededLumps(tempwad, TEMP_MAP_HEADER, true);

				// Copy lumps to buildwad
				General.WriteLogLine("Copying map lumps to temporary build file...");
				CopyLumpsByType(tempwad, TEMP_MAP_HEADER, buildwad, BUILD_MAP_HEADER, REPLACE_TARGET_MAP, true, false, false, true);

				// Close buildwad
				buildwad.Dispose();

				// Does the nodebuilder require an output file?
				string tempfile2;
				if(nodebuilder.HasSpecialOutputFile) 
				{
					// Make a temporary output file for the nodebuilder
					tempfile2 = General.MakeTempFilename(compiler.Location);
					General.WriteLogLine("Temporary output file: " + tempfile2);
				}
				else 
				{
					// Output file is same as input file
					tempfile2 = tempfile1;
				}

				// Run the nodebuilder
				compiler.Parameters = nodebuilder.Parameters;
				compiler.InputFile = Path.GetFileName(tempfile1);
				compiler.OutputFile = Path.GetFileName(tempfile2);
				compiler.SourceFile = sourcefile;
				compiler.WorkingDirectory = Path.GetDirectoryName(tempfile1);
				if(compiler.Run()) 
				{
					// Open the output file
					try { buildwad = new WAD(tempfile2); } 
					catch(Exception e) 
					{
						General.WriteLogLine(e.GetType().Name + " while reading build wad file: " + e.Message);
						buildwad = null;
					}

					if(buildwad != null) 
					{
						// Output lumps complete?
						lumpscomplete = VerifyNodebuilderLumps(buildwad, BUILD_MAP_HEADER);
					}

					if(lumpscomplete) 
					{
						// Copy nodebuilder lumps to temp file
						General.WriteLogLine("Copying nodebuilder lumps to temporary file...");
						CopyLumpsByType(buildwad, BUILD_MAP_HEADER, tempwad, TEMP_MAP_HEADER, REPLACE_TARGET_MAP, false, false, true, false);
					}
					else 
					{
						//mxd. collect errors
						string compilererrors = String.Empty;
						foreach(CompilerError e in compiler.Errors)
							compilererrors += Environment.NewLine + e.description;

						// Nodebuilder did not build the lumps!
						if(failaswarning)
							General.ShowWarningMessage("Unable to build the nodes: The nodebuilder failed to build the expected data structures.\nThe map will be saved without the nodes." + (compiler.Errors.Length > 0 ? Environment.NewLine + compilererrors : String.Empty), MessageBoxButtons.OK);
						else
							General.ShowErrorMessage("Unable to build the nodes: The nodebuilder failed to build the expected data structures." + (compiler.Errors.Length > 0 ? Environment.NewLine + compilererrors : String.Empty), MessageBoxButtons.OK);
					}

					// Done with the build wad
					if(buildwad != null) buildwad.Dispose();
				}
				else //mxd
				{
					//collect errors
					string compilererrors = String.Empty;
					foreach(CompilerError e in compiler.Errors)
						compilererrors += Environment.NewLine + e.description;

					// Nodebuilder did not build the lumps!
					General.ShowErrorMessage("Unable to build the nodes: The nodebuilder failed to build the expected data structures" + (compiler.Errors.Length > 0 ? ":" + Environment.NewLine + compilererrors : "."), MessageBoxButtons.OK);
				}

				// Clean up
				compiler.Dispose();

                // Let the plugins know
                if (lumpscomplete) General.Plugins.OnMapNodesRebuilt();

                // Return result
                return lumpscomplete;
			}
		}

		// This verifies if the nodebuilder lumps exist in a WAD file
		private bool VerifyNodebuilderLumps(WAD wad, string mapheader) 
		{
			bool lumpscomplete = false;

			// Find the map header in source
			int srcindex = wad.FindLumpIndex(mapheader);
			
			if(srcindex > -1) 
			{
				// Go for all the map lump names
				lumpscomplete = true;
				
				foreach(KeyValuePair<string, MapLumpInfo> group in config.MapLumps) 
				{
					// Check if this lump should exist
					if(group.Value.NodeBuild && !group.Value.AllowEmpty && group.Value.Required) 
					{
						//mxd
						string lumpname = group.Key;
						if(lumpname.Contains(CONFIG_MAP_HEADER)) lumpname = lumpname.Replace(CONFIG_MAP_HEADER, mapheader);
						
						// Find the lump in the source
						if(wad.FindLump(lumpname, srcindex, srcindex + config.MapLumps.Count + 2) == null) 
						{
							// Missing a lump!
							lumpscomplete = false;
							break;
						}
					}
				}
			}

			return lumpscomplete;
		}

		#endregion

		#region ================== Lumps

		// This returns a copy of the requested lump stream data
		// This is copied from the temp wad file and returns null when the lump is not found
		public MemoryStream GetLumpData(string lumpname) 
		{
			Lump l = tempwad.FindLump(lumpname);
			if(l != null) 
			{
				l.Stream.Seek(0, SeekOrigin.Begin);
				return new MemoryStream(l.Stream.ReadAllBytes());
			}
			return null;
		}

		// This writes a copy of the data to a lump in the temp file
		public void SetLumpData(string lumpname, MemoryStream lumpdata) 
		{
			int insertindex = tempwad.Lumps.Count;

			// Remove the lump if it already exists
			int li = tempwad.FindLumpIndex(lumpname);
			if(li > -1)
			{
				insertindex = li;
				tempwad.RemoveAt(li);
			}

			// Insert new lump
			Lump l = tempwad.Insert(lumpname, insertindex, (int)lumpdata.Length);
			l.Stream.Seek(0, SeekOrigin.Begin);
			lumpdata.WriteTo(l.Stream);

			//mxd. Mark the map as changed (will also update the title)
			IsChanged = true;
		}

		// This checks if the specified lump exists in the temp file
		public bool LumpExists(string lumpname) 
		{
			return (tempwad.FindLumpIndex(lumpname) > -1);
		}

		// This creates empty lumps for those required
		private void CreateRequiredLumps(WAD target, string mapname) 
		{
			// Find the map header in target
			int headerindex = target.FindLumpIndex(mapname);
			if(headerindex == -1) 
			{
				// If this header doesnt exists in the target
				// then insert at the end of the target
				headerindex = target.Lumps.Count;
			}

			// Begin inserting at target header index
			int insertindex = headerindex;

			// Go for all the map lump names
			foreach(KeyValuePair<string, MapLumpInfo> group in config.MapLumps)
			{
				// Check if this lump is required
				if(group.Value.Required) 
				{
					// Get the lump name
					string lumpname = (group.Key.Contains(CONFIG_MAP_HEADER) ? group.Key.Replace(CONFIG_MAP_HEADER, mapname) : group.Key); //mxd

					// Check if the lump is missing at the target
					int targetindex = FindSpecificLump(target, lumpname, headerindex, mapname, config.MapLumps);
					if(targetindex == -1) 
					{
						// Determine target index
						insertindex++;
						if(insertindex > target.Lumps.Count) insertindex = target.Lumps.Count;

						// Create new, emtpy lump
						General.WriteLogLine(lumpname + " is required! Created empty lump.");
						target.Insert(lumpname, insertindex, 0);
					}
					else
					{
						// Move insert index
						insertindex = targetindex;
					}
				}
			}
		}

		//mxd. This is called on tempwad, which should only have the current map inside it.
		private void RemoveUnneededLumps(WAD target, string mapname, bool glnodesonly) 
		{
			//Get the list of lumps required by current map format
			List<string> requiredLumps = new List<string>();
			foreach(KeyValuePair<string, MapLumpInfo> group in config.MapLumps) 
			{
				//this lump well be recreated by a nodebuilder when saving the map 
				//(or it won't be if the new map format or nodebuilder doesn't require / build this lump, 
				//so it will just stay there, possibly messing things up)
				if(group.Value.NodeBuild && (!glnodesonly || group.Key.ToUpperInvariant().StartsWith("GL_"))) continue;

                //SRB2: Skip BLOCKMAP and REJECT to prevent outdated data from persisting.
                if (General.Map.SRB2 && (group.Key.ToUpperInvariant() == "BLOCKMAP" || group.Key.ToUpperInvariant() == "REJECT")) continue;

				string lumpname = group.Key;
				if(lumpname == CONFIG_MAP_HEADER) lumpname = mapname;
				requiredLumps.Add(lumpname);
			}

			//Remove lumps, which are not required
			List<Lump> toRemove = new List<Lump>();
			foreach(Lump lump in target.Lumps)
				if(!requiredLumps.Contains(lump.Name)) toRemove.Add(lump);

			foreach(Lump lump in toRemove) target.Remove(lump);
		}

		// This copies all lumps, except those of a specific map. mxd. Returns the index of skipped map's header lump
		private static int CopyAllLumpsExceptMap(WAD source, WAD target, GameConfiguration mapconfig, string sourcemapname) 
		{
			// Go for all lumps
			bool skipping = false;
			int headerpos = REPLACE_TARGET_MAP; //mxd
			for(int i = 0; i < source.Lumps.Count; i++)
			{
				Lump srclump = source.Lumps[i];
				
				// Check if we should stop skipping lumps here
				if(skipping) 
				{
					//mxd
					string srclumpname = srclump.Name;
					if(srclumpname.Contains(sourcemapname)) srclumpname = srclumpname.Replace(sourcemapname, CONFIG_MAP_HEADER);

					if(!mapconfig.MapLumps.ContainsKey(srclumpname)) 
					{
						// Stop skipping
						skipping = false;
					}
				}

				// Check if we should start skipping lumps here
				//TODO: I see a big, but kinda esoteric problem here if the source has several maps with the same name (mxd)
				if(!skipping && headerpos == REPLACE_TARGET_MAP && srclump.Name == sourcemapname) 
				{
					// We have encountered the map header, start skipping!
					skipping = true;
					headerpos = i;
				}

				// Not skipping this lump?
				if(!skipping) 
				{
					// Copy lump over!
					Lump tgtlump = target.Insert(srclump.Name, target.Lumps.Count, srclump.Length);
					srclump.CopyTo(tgtlump);
				}
			}

			return headerpos;
		}

		// This copies specific map lumps from one WAD to another
		private void CopyLumpsByType(WAD source, string sourcemapname,
									 WAD target, string targetmapname,
									 int targetheaderinsertindex, //mxd
									 bool copyrequired, bool copyblindcopy,
									 bool copynodebuild, bool copyscript) 
		{
			// Find the map header in target (mxd. Or use the provided one)
			bool replacetargetmap = (targetheaderinsertindex == REPLACE_TARGET_MAP); //mxd
			int tgtheaderindex = (replacetargetmap ? target.FindLumpIndex(targetmapname) : targetheaderinsertindex); //mxd
			if(tgtheaderindex == -1) 
			{
				// If this header doesnt exists in the target
				// then insert at the end of the target
				tgtheaderindex = target.Lumps.Count;
			}

			// Begin inserting at target header index
			int targetindex = tgtheaderindex;

			// Find the map header in source
			int srcheaderindex = source.FindLumpIndex(sourcemapname);
			if(srcheaderindex > -1) 
			{
				// Go for all the map lump names
				foreach(KeyValuePair<string, MapLumpInfo> group in config.MapLumps) 
				{
					// Check if this lump should be copied
					if((group.Value.Required && copyrequired) || (group.Value.BlindCopy && copyblindcopy) ||
					   (group.Value.NodeBuild && copynodebuild) || ((group.Value.Script != null || group.Value.ScriptBuild) && copyscript)) 
					{
						// Get the lump name
						string srclumpname = (group.Key.Contains(CONFIG_MAP_HEADER) ? group.Key.Replace(CONFIG_MAP_HEADER, sourcemapname) : group.Key); //mxd
						string tgtlumpname = (group.Key.Contains(CONFIG_MAP_HEADER) ? group.Key.Replace(CONFIG_MAP_HEADER, targetmapname) : group.Key); //mxd

						// Find the lump in the source
						int sourceindex = FindSpecificLump(source, srclumpname, srcheaderindex, sourcemapname, config.MapLumps);
						if(sourceindex > -1) 
						{
							//mxd. Don't do this when inserting a map (SaveMap() removes the old version of the map before calling CopyLumpsByType())
							if(replacetargetmap)
							{
								// Remove lump at target
								int lumpindex = RemoveSpecificLump(target, tgtlumpname, tgtheaderindex, targetmapname, config.MapLumps);

								// Determine target index
								// When original lump was found and removed then insert at that position
								// otherwise insert after last insertion position
								if(lumpindex > -1) targetindex = lumpindex; else targetindex++;
							}
							if(targetindex > target.Lumps.Count) targetindex = target.Lumps.Count;

							// Copy the lump to the target
							//General.WriteLogLine(srclumpname + " copying as " + tgtlumpname);
							Lump lump = source.Lumps[sourceindex];
							Lump newlump = target.Insert(tgtlumpname, targetindex, lump.Length);
							lump.CopyTo(newlump);

							//mxd. We still need to increment targetindex...
							if(!replacetargetmap) targetindex++;
						}
						else 
						{
							// We don't want to bother the user with this. There are a lot of lumps in
							// the game configs that are trivial and don't need to be found.
							if(group.Value.Required) 
							{
								General.ErrorLogger.Add(ErrorType.Warning, group.Key + " (required lump) should be read but was not found in the WAD file.");
							}
						}
					}
				}
			}
		}

		// This finds a lump within the range of known lump names
		// Returns -1 when the lump cannot be found
		private static int FindSpecificLump(WAD source, string lumpname, int mapheaderindex, string mapheadername, Dictionary<string, MapLumpInfo> maplumps) 
		{
			// Use the configured map lump names to find the specific lump within range,
			// because when an unknown lump is met, this search must stop.

			// Go for all lumps in order to find the specified lump
			for(int i = 0; i < maplumps.Count + 1; i++) 
			{
				// Still within bounds?
				if((mapheaderindex + i) < source.Lumps.Count) 
				{
					// Check if this is a known lump name
					string srclumpname = source.Lumps[mapheaderindex + i].Name; //mxd
					if(srclumpname.Contains(mapheadername)) srclumpname = srclumpname.Replace(mapheadername, CONFIG_MAP_HEADER);

					if(maplumps.ContainsKey(srclumpname)) //mxd
					{
						// Is this the lump we are looking for?
						if(source.Lumps[mapheaderindex + i].Name == lumpname) 
						{
							// Return this index
							return mapheaderindex + i;
						}
					}
					else
					{
						// Unknown lump hit, abort search
						break;
					}
				}
			}

			// Nothing found
			return -1;
		}

		// This removes a specific lump and returns the position where the lump was removed
		// Returns -1 when the lump could not be found
		internal static int RemoveSpecificLump(WAD source, string lumpname, int mapheaderindex, string mapheadername, Dictionary<string, MapLumpInfo> maplumps) 
		{
			// Find the specific lump index
			int lumpindex = FindSpecificLump(source, lumpname, mapheaderindex, mapheadername, maplumps);
			if(lumpindex > -1) 
			{
				// Remove this lump
				//General.WriteLogLine(lumpname + " removed");
				source.RemoveAt(lumpindex);
			}
			/*else 
			{
				// Lump not found
				General.ErrorLogger.Add(ErrorType.Warning, lumpname + " should be removed but was not found!");
			}*/

			// Return result
			return lumpindex;
		}

		#endregion

		#region ================== Selection Groups

		// This adds selection to a group
		private void AddSelectionToGroup(int groupindex) 
		{
			General.Interface.SetCursor(Cursors.WaitCursor);

			// Make selection
			map.AddSelectionToGroup(groupindex); //mxd. switched groupmask to groupindex

			General.Interface.DisplayStatus(StatusType.Action, "Assigned selection to group " + (groupindex + 1));
			General.Interface.SetCursor(Cursors.Default);
		}

		// This selects a group
		private void SelectGroup(int groupindex)
		{
			// Select
			int groupmask = 0x01 << groupindex;
			map.SelectVerticesByGroup(groupmask);
			map.SelectLinedefsByGroup(groupmask);
			map.SelectSectorsByGroup(groupmask);
			map.SelectThingsByGroup(groupmask);

			// Redraw to show selection
			General.Interface.DisplayStatus(StatusType.Action, "Selected group " + (groupindex + 1));
			General.Interface.RedrawDisplay();
		}

		//mxd. This clears a group
		private void ClearGroup(int groupindex)
		{
			General.Interface.SetCursor(Cursors.WaitCursor);

			// Clear group
			map.ClearGroup(0x01 << groupindex);

			General.Interface.DisplayStatus(StatusType.Action, "Cleared group " + (groupindex + 1));
			General.Interface.SetCursor(Cursors.Default);
		}

		// Select actions
		[BeginAction("selectgroup1")]
		internal void SelectGroup1() { SelectGroup(0); }
		[BeginAction("selectgroup2")]
		internal void SelectGroup2() { SelectGroup(1); }
		[BeginAction("selectgroup3")]
		internal void SelectGroup3() { SelectGroup(2); }
		[BeginAction("selectgroup4")]
		internal void SelectGroup4() { SelectGroup(3); }
		[BeginAction("selectgroup5")]
		internal void SelectGroup5() { SelectGroup(4); }
		[BeginAction("selectgroup6")]
		internal void SelectGroup6() { SelectGroup(5); }
		[BeginAction("selectgroup7")]
		internal void SelectGroup7() { SelectGroup(6); }
		[BeginAction("selectgroup8")]
		internal void SelectGroup8() { SelectGroup(7); }
		[BeginAction("selectgroup9")]
		internal void SelectGroup9() { SelectGroup(8); }
		[BeginAction("selectgroup10")]
		internal void SelectGroup10() { SelectGroup(9); }

		// Assign actions
		[BeginAction("assigngroup1")]
		internal void AssignGroup1() { AddSelectionToGroup(0); }
		[BeginAction("assigngroup2")]
		internal void AssignGroup2() { AddSelectionToGroup(1); }
		[BeginAction("assigngroup3")]
		internal void AssignGroup3() { AddSelectionToGroup(2); }
		[BeginAction("assigngroup4")]
		internal void AssignGroup4() { AddSelectionToGroup(3); }
		[BeginAction("assigngroup5")]
		internal void AssignGroup5() { AddSelectionToGroup(4); }
		[BeginAction("assigngroup6")]
		internal void AssignGroup6() { AddSelectionToGroup(5); }
		[BeginAction("assigngroup7")]
		internal void AssignGroup7() { AddSelectionToGroup(6); }
		[BeginAction("assigngroup8")]
		internal void AssignGroup8() { AddSelectionToGroup(7); }
		[BeginAction("assigngroup9")]
		internal void AssignGroup9() { AddSelectionToGroup(8); }
		[BeginAction("assigngroup10")]
		internal void AssignGroup10() { AddSelectionToGroup(9); }

		//mxd. Clear actions
		[BeginAction("cleargroup1")]
		internal void ClearGroup1() { ClearGroup(0); }
		[BeginAction("cleargroup2")]
		internal void ClearGroup2() { ClearGroup(1); }
		[BeginAction("cleargroup3")]
		internal void ClearGroup3() { ClearGroup(2); }
		[BeginAction("cleargroup4")]
		internal void ClearGroup4() { ClearGroup(3); }
		[BeginAction("cleargroup5")]
		internal void ClearGroup5() { ClearGroup(4); }
		[BeginAction("cleargroup6")]
		internal void ClearGroup6() { ClearGroup(5); }
		[BeginAction("cleargroup7")]
		internal void ClearGroup7() { ClearGroup(6); }
		[BeginAction("cleargroup8")]
		internal void ClearGroup8() { ClearGroup(7); }
		[BeginAction("cleargroup9")]
		internal void ClearGroup9() { ClearGroup(8); }
		[BeginAction("cleargroup10")]
		internal void ClearGroup10() { ClearGroup(9); }

        #endregion

        #region ================== [mxd] GZDB actions

        [BeginAction("gztogglemodels")]
        internal void ToggleModelsRenderingMode()
        {
            switch (General.Settings.GZDrawModelsMode)
            {
                case ModelRenderMode.NONE:
                    General.Settings.GZDrawModelsMode = ModelRenderMode.SELECTION;
                    General.MainWindow.DisplayStatus(StatusType.Action, "Models rendering mode: SELECTION ONLY");
                    break;

                case ModelRenderMode.SELECTION:
                    General.Settings.GZDrawModelsMode = ModelRenderMode.ACTIVE_THINGS_FILTER;
                    General.MainWindow.DisplayStatus(StatusType.Action, "Models rendering mode: ACTIVE THINGS FILTER ONLY");
                    break;

                case ModelRenderMode.ACTIVE_THINGS_FILTER:
                    General.Settings.GZDrawModelsMode = ModelRenderMode.ALL;
                    General.MainWindow.DisplayStatus(StatusType.Action, "Models rendering mode: ALL");
                    break;

                case ModelRenderMode.ALL:
                    General.Settings.GZDrawModelsMode = ModelRenderMode.NONE;
                    General.MainWindow.DisplayStatus(StatusType.Action, "Models rendering mode: NONE");
                    break;
            }

            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztogglelights")]
        internal void ToggleLightsRenderingMode()
        {
            if (General.Editing.Mode is ClassicMode)
            {
                switch (General.Settings.GZDrawLightsMode)
                {
                    case LightRenderMode.NONE:
                        General.Settings.GZDrawLightsMode = LightRenderMode.ALL;
                        General.MainWindow.DisplayStatus(StatusType.Action, "Dynamic lights rendering mode: ALL");
                        break;

                    default:
                        General.Settings.GZDrawLightsMode = LightRenderMode.NONE;
                        General.MainWindow.DisplayStatus(StatusType.Action, "Dynamic lights rendering mode: NONE");
                        break;
                }
            }
            else
            {
                switch (General.Settings.GZDrawLightsMode)
                {
                    case LightRenderMode.NONE:
                        General.Settings.GZDrawLightsMode = LightRenderMode.ALL;
                        General.MainWindow.DisplayStatus(StatusType.Action, "Dynamic lights rendering mode: ALL");
                        break;

                    case LightRenderMode.ALL:
                        General.Settings.GZDrawLightsMode = LightRenderMode.ALL_ANIMATED;
                        General.MainWindow.DisplayStatus(StatusType.Action, "Dynamic lights rendering mode: ANIMATED");
                        break;

                    case LightRenderMode.ALL_ANIMATED:
                        General.Settings.GZDrawLightsMode = LightRenderMode.NONE;
                        General.MainWindow.DisplayStatus(StatusType.Action, "Dynamic lights rendering mode: NONE");
                        break;
                }
            }

            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztogglefog")]
        internal void ToggleFog()
        {
            General.Settings.GZDrawFog = !General.Settings.GZDrawFog;
            General.MainWindow.DisplayStatus(StatusType.Action, "Fog rendering is " + (General.Settings.GZDrawFog ? "ENABLED" : "DISABLED") + ".");
            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztogglesky")]
        internal void ToggleSky()
        {
            General.Settings.GZDrawSky = !General.Settings.GZDrawSky;
            General.MainWindow.DisplayStatus(StatusType.Action, "Sky rendering is " + (General.Settings.GZDrawSky ? "ENABLED" : "DISABLED") + ".");
            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztogglefx")]
        internal void ToggleFx()
        {
            int on = 0;
            on += General.Settings.GZDrawFog ? 1 : -1;
            on += General.Settings.GZDrawSky ? 1 : -1;
            on += General.Settings.GZDrawLightsMode != LightRenderMode.NONE ? 1 : -1;
            on += General.Settings.GZDrawModelsMode != ModelRenderMode.NONE ? 1 : -1;

            bool enable = (on < 0);

            General.Settings.GZDrawFog = enable;
            General.Settings.GZDrawSky = enable;
            General.Settings.GZDrawLightsMode = (enable ? LightRenderMode.ALL : LightRenderMode.NONE);
            General.Settings.GZDrawModelsMode = (enable ? ModelRenderMode.ALL : ModelRenderMode.NONE);
            General.MainWindow.DisplayStatus(StatusType.Action, "Advanced effects are " + (enable ? "ENABLED" : "DISABLED") + ".");

            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztoggleeventlines")]
        internal void ToggleEventLines()
        {
            General.Settings.GZShowEventLines = !General.Settings.GZShowEventLines;
            General.MainWindow.DisplayStatus(StatusType.Action, "Event lines are " + (General.Settings.GZShowEventLines ? "ENABLED" : "DISABLED") + ".");
            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gztogglevisualvertices")]
        internal void ToggleVisualVertices()
        {
            General.Settings.GZShowVisualVertices = !General.Settings.GZShowVisualVertices;
            General.MainWindow.DisplayStatus(StatusType.Action, "Visual vertices are " + (General.Settings.GZShowVisualVertices ? "ENABLED" : "DISABLED") + ".");
            General.MainWindow.RedrawDisplay();
            General.MainWindow.UpdateGZDoomPanel();
        }

        [BeginAction("gzreloadmodeldef")]
        internal void ReloadModeldef()
        {
            data.ReloadModeldef();
        }

        [BeginAction("gzreloadgldefs")]
        internal void ReloadGldefs()
        {
            data.ReloadGldefs();
        }

        #endregion

        #region ================== Script Editing

        // Show the script editor
        [BeginAction("openscripteditor")]
		internal void ShowScriptEditor() 
		{
			Cursor.Current = Cursors.WaitCursor;

			if(scriptwindow == null) 
			{
				// Load the window
				scriptwindow = new ScriptEditorForm();
			}

			// Window not yet visible?
			if(!scriptwindow.Visible) 
			{
				// Show the window
				if(General.Settings.ScriptOnTop) 
				{
					if(scriptwindow.Visible && (scriptwindow.Owner == null)) scriptwindow.Hide();
					scriptwindow.Show(General.MainWindow);
				}
				else 
				{
					if(scriptwindow.Visible && (scriptwindow.Owner != null)) scriptwindow.Hide();
					scriptwindow.Show();
				}
			}

			if(scriptwindow.WindowState == FormWindowState.Minimized) scriptwindow.WindowState = FormWindowState.Normal; //mxd
			scriptwindow.Activate();
			scriptwindow.Focus();
			Cursor.Current = Cursors.Default;
		}

		// This asks the user to save changes in script files
		// Returns false when cancelled by the user
		internal bool AskSaveScriptChanges() 
		{
			// Window open?
			if(scriptwindow != null) 
			{
				// Ask to save changes
				// This also saves implicitly
				return scriptwindow.AskSaveAll();
			}

			// No problems
			return true;
		}

		// This applies the changed status for internal scripts
		internal void ApplyScriptChanged() 
		{
			// Remember if lumps are changed
			scriptschanged |= scriptwindow.Editor.CheckImplicitChanges();
		}

		// Close the script editor
		// Specify true for the closing parameter when
		// the window is already in the closing process
		internal void CloseScriptEditor(bool closing) 
		{
			if(scriptwindow != null) 
			{
				if(!scriptwindow.IsDisposed) 
				{
					// Remember what files were open
					scriptwindow.Editor.WriteOpenFilesToConfiguration();

					// Close now
					if(!closing) scriptwindow.Close();
				}

				// Done
				scriptwindow = null;
			}
		}

		// This checks if the scripts are changed
		private bool CheckScriptChanged() 
		{
			if(scriptwindow != null) 
			{
				// Check if scripts are changed			
				return scriptschanged || scriptwindow.Editor.CheckImplicitChanges();
			}
	
			return scriptschanged;
		}

		// This compiles all lumps that require compiling and stores the results
		// Returns true when our code worked properly (even when the compiler returned errors)
		private bool CompileScriptLumps() 
		{
			bool success = true;
			errors.Clear();

			// Go for all the map lumps
			foreach(MapLumpInfo lumpinfo in config.MapLumps.Values) 
			{
				// Is this a script lump?
				if(lumpinfo.Script != null || lumpinfo.ScriptBuild) 
				{
					// Compile it now
					success &= CompileLump(lumpinfo.Name, false);
				}
			}

			return success;
		}

		// This compiles a script lump and returns any errors that may have occurred
		// Returns true when our code worked properly (even when the compiler returned errors)
		internal bool CompileLump(string lumpname, bool clearerrors) 
		{
			//mxd. Boilerplate
			if(!config.MapLumps.ContainsKey(lumpname))
			{
				General.ShowErrorMessage("Unable to compile lump '" + lumpname + "'. This lump is not defined in the current game configuration.", MessageBoxButtons.OK);
				return false;
			}
			
			string inputfile;
			Compiler compiler;
			string reallumpname = lumpname;

			//mxd. Does lump require compiling?
			ScriptConfiguration scriptconfig;
			if(config.MapLumps[lumpname].ScriptBuild) 
			{
				//mxd. More boilderplate
				if(!General.CompiledScriptConfigs.ContainsKey(General.Map.Options.ScriptCompiler))
				{
					General.ShowErrorMessage("Unable to compile lump '" + lumpname + "'. Unable to find required script compiler configuration ('" + General.Map.Options.ScriptCompiler + "').", MessageBoxButtons.OK);
					return false;
				}
				
				scriptconfig = General.CompiledScriptConfigs[General.Map.Options.ScriptCompiler];
			} 
			else 
			{
				scriptconfig = config.MapLumps[lumpname].Script;
			}
			if(scriptconfig.Compiler == null) return true;

			// Find the lump
			if(lumpname == CONFIG_MAP_HEADER) reallumpname = TEMP_MAP_HEADER;
			Lump lump = tempwad.FindLump(reallumpname);
			if(lump == null) throw new Exception("No such lump in temporary wad file '" + reallumpname + "'.");

			// Determine source file
			string sourcefile = (filepathname.Length > 0 ? filepathname : tempwad.Filename);

			// New list of errors
			if(clearerrors) errors.Clear();

			// Determine the script configuration to use
			try 
			{
				// Initialize compiler
				compiler = scriptconfig.Compiler.Create();
			} 
			catch(Exception e) 
			{
				// Fail
				errors.Add(new CompilerError("Unable to initialize compiler. " + e.GetType().Name + ": " + e.Message));
				return false;
			}

			try 
			{
				// Write lump data to temp script file in compiler's temp directory
				inputfile = General.MakeTempFilename(compiler.Location, "tmp");
				lump.Stream.Seek(0, SeekOrigin.Begin);
				BinaryReader reader = new BinaryReader(lump.Stream);
				File.WriteAllBytes(inputfile, reader.ReadBytes((int)lump.Stream.Length));
			} 
			catch(Exception e) 
			{
				// Fail
				compiler.Dispose();
				errors.Add(new CompilerError("Unable to write script to working file. " + e.GetType().Name + ": " + e.Message));
				return false;
			}

			// Make random output filename
			string outputfile = General.MakeTempFilename(compiler.Location, "tmp");

			// Run compiler
			compiler.Parameters = scriptconfig.Parameters;
			compiler.InputFile = Path.GetFileName(inputfile);
			compiler.OutputFile = Path.GetFileName(outputfile);
			compiler.SourceFile = sourcefile;
			compiler.WorkingDirectory = Path.GetDirectoryName(inputfile);

			//mxd
			if(scriptconfig.ScriptType == ScriptType.ACS)
			{
				compiler.Includes = General.Map.ScriptIncludes;
				compiler.CopyIncludesToWorkingDirectory = true;
			}

			if(compiler.Run()) 
			{
				// Process errors
				foreach(CompilerError e in compiler.Errors) 
				{
					CompilerError newerror = e;

					// If the error's filename equals our temporary file,
					// use the lump name instead and prefix it with ?
					if(string.Compare(e.filename, inputfile, true) == 0)
						newerror.filename = "?" + reallumpname;

					errors.Add(newerror);
				}

				// No errors?
				if(compiler.Errors.Length == 0) 
				{
					// Output file exists?
					if(File.Exists(outputfile)) 
					{
						// Copy output file data into a lump?
						if(!string.IsNullOrEmpty(scriptconfig.ResultLump)) 
						{
							// Do that now then
							byte[] filedata;

							try 
							{
								filedata = File.ReadAllBytes(outputfile);
							} 
							catch(Exception e) 
							{
								// Fail
								compiler.Dispose();
								errors.Add(new CompilerError("Unable to read compiler output file. " + e.GetType().Name + ": " + e.Message));
								return false;
							}

							// Store data
							MemoryStream stream = new MemoryStream(filedata);
							SetLumpData(scriptconfig.ResultLump, stream);
						}
					}
				}

				// Clean up
				compiler.Dispose();

				// Done
				return true;
			}

			// Fail
			compiler.Dispose();
			errors.Clear(); //mxd
			return false;
		}

		// This clears all compiler errors
		/*internal void ClearCompilerErrors() 
		{
			errors.Clear();
		}*/

		//mxd. Update includes list and script names
		internal List<CompilerError> UpdateScriptNames(bool logerrors)
		{
			List<CompilerError> compilererrors = UpdateScriptNames();
			if(logerrors && compilererrors.Count > 0)
			{
				//INFO: CompileLump() prepends lumpname with "?" to distinguish between temporary files and files compiled in place
				//INFO: also, error.linenumber is zero-based
				foreach(CompilerError error in compilererrors)
				{
					General.ErrorLogger.Add(ErrorType.Error, "ACS error in '" + (error.filename.StartsWith("?") ? error.filename.Replace("?", String.Empty) : error.filename)
						+ (error.linenumber != CompilerError.NO_LINE_NUMBER ? "', line " + (error.linenumber + 1) : "'") 
						+ ". " + error.description + ".");
				}
			}

			return compilererrors;
		}

		//mxd. Update includes list and script names
		internal List<CompilerError> UpdateScriptNames()
		{
			List<ScriptItem> namedscriptslist = new List<ScriptItem>();
			List<ScriptItem> numberedscriptslist = new List<ScriptItem>();
			List<string> scripincludeslist = new List<string>();
			List<CompilerError> compilererrors = new List<CompilerError>();

			// Load the script lumps
			foreach(MapLumpInfo maplumpinfo in config.MapLumps.Values) 
			{
				// Is this a script lump?
				if((maplumpinfo.ScriptBuild || maplumpinfo.Script != null) && maplumpinfo.Name == "SCRIPTS") 
				{
					ScriptConfiguration scriptconfig;
					if(maplumpinfo.ScriptBuild)
					{
						//mxd. More boilderplate
						if(!General.CompiledScriptConfigs.ContainsKey(General.Map.Options.ScriptCompiler))
						{
							compilererrors.Add(new CompilerError("Unable to compile lump '" + maplumpinfo.Name + "'. Unable to find required script compiler configuration ('" + General.Map.Options.ScriptCompiler + "')."));
							return compilererrors;
						}

						scriptconfig = General.CompiledScriptConfigs[General.Map.Options.ScriptCompiler];
					}
					else
					{
						scriptconfig = maplumpinfo.Script;
					}
					
					// Load the lump data
					MemoryStream stream = GetLumpData(maplumpinfo.Name);
					if(stream != null && stream.Length > 0 && scriptconfig != null && scriptconfig.Compiler != null)
					{
						// Get script names
						AcsParserSE parser = new AcsParserSE { OnInclude = (se, path, includetype) => se.Parse(General.Map.Data.LoadFile(path), path, true, includetype, false) };

						//INFO: CompileLump() prepends lumpname with "?" to distinguish between temporary files and files compiled in place
						if(parser.Parse(stream, "?SCRIPTS", scriptconfig.Compiler.Files, true, AcsParserSE.IncludeType.NONE, false))
						{
							// Add them to arrays
							namedscriptslist.AddRange(parser.NamedScripts);
							numberedscriptslist.AddRange(parser.NumberedScripts);
							scripincludeslist.AddRange(parser.Includes);
						}
						
						// Check for errors
						if(parser.HasError)
						{
							compilererrors.Add(new CompilerError(parser.ErrorDescription, parser.ErrorSource, parser.ErrorLine));
							break;
						}
                    }
				}
			}

			// Add to collections
			scriptincludes.Clear();
			if(compilererrors.Count == 0)
			{
				namedscripts = new Dictionary<string, ScriptItem>(namedscriptslist.Count);
				numberedscripts = new Dictionary<int, ScriptItem>(numberedscriptslist.Count);

				// Sort script names
				namedscriptslist.Sort(ScriptItem.SortByName);
				numberedscriptslist.Sort(ScriptItem.SortByIndex);

				foreach(ScriptItem item in namedscriptslist)
					if(!namedscripts.ContainsKey(item.Name.ToLowerInvariant())) namedscripts.Add(item.Name.ToLowerInvariant(), item);
				foreach(ScriptItem item in numberedscriptslist)
					if(!numberedscripts.ContainsKey(item.Index)) numberedscripts.Add(item.Index, item);
				foreach(string include in scripincludeslist)
					if(!scriptincludes.Contains(include)) scriptincludes.Add(include);
			}
			else
			{
				// Clear collections
				namedscripts.Clear();
				numberedscripts.Clear();
			}

			return compilererrors;
		}

		#endregion

		#region ================== Methods

		// This updates everything after the configuration or settings have been changed
		internal void UpdateConfiguration() 
		{
			// Update map
			map.UpdateConfiguration();

			// Update settings
			renderer3d.CreateProjection();
			renderer3d.UpdateVertexHandle(); //mxd

			// Things filters
			General.MainWindow.UpdateThingsFilters();
		}

		// This changes thing filter
		public void ChangeThingFilter(ThingsFilter newfilter) 
		{
			// We have a special filter for null
			if(newfilter == null) newfilter = new NullThingsFilter();

			// Deactivate old filter
			if(thingsfilter != null) thingsfilter.Deactivate();

			// Change
			thingsfilter = newfilter;

			// Activate filter
			thingsfilter.Activate();

			// Update interface
			General.MainWindow.ReflectThingsFilter();

			// Redraw
			General.MainWindow.RedrawDisplay();
		}

		// This sets a new mapset for editing
		private void ChangeMapSet(MapSet newmap) 
		{
			// Let the plugin and editing mode know
			General.Plugins.OnMapSetChangeBegin();
			if(General.Editing.Mode != null) General.Editing.Mode.OnMapSetChangeBegin();
			this.visualcamera.Sector = null;

			// Can't have a selection in an old map set
			map.ClearAllSelected();

			// Reset surfaces
			renderer2d.Surfaces.Reset();

			// Apply
			map.Dispose();
			map = newmap;
			map.UpdateConfiguration();
			map.SnapAllToAccuracy();
			map.Update();
			thingsfilter.Update();

			// Let the plugin and editing mode know
			General.Plugins.OnMapSetChangeEnd();
			if(General.Editing.Mode != null) General.Editing.Mode.OnMapSetChangeEnd();
		}

		// This reloads resources
		[BeginAction("reloadresources")]
		internal void DoReloadResource() 
		{
			//mxd. Get rid of old errors
			General.ErrorLogger.Clear();
			
			// Set this to false so we can see if errors are added
			General.ErrorLogger.IsErrorAdded = false;

#if DEBUG 
			DebugConsole.Clear();
#endif

			ReloadResources();

			if(General.ErrorLogger.IsErrorAdded)
			{
				// Show any errors if preferred
				General.MainWindow.DisplayStatus(StatusType.Warning, "There were errors during resources loading!");
				if(General.Settings.ShowErrorsWindow) General.MainWindow.ShowErrors();
			}
			else
			{
				General.MainWindow.DisplayReady();
			}

		}

		internal void ReloadResources() 
		{
			// Keep old display info
			StatusInfo oldstatus = General.MainWindow.Status;
			Cursor oldcursor = Cursor.Current;

			// Show status
			General.MainWindow.DisplayStatus(StatusType.Busy, "Reloading data resources...");
			Cursor.Current = Cursors.WaitCursor;

			// Clean up
			data.Dispose();
			data = null;
			config = null;
			configinfo = null;
			GC.Collect();
			GC.WaitForPendingFinalizers();
			GC.Collect(); //mxd

			// Clear errors
			General.ErrorLogger.Clear();

			// Reload game configuration
			General.WriteLogLine("Reloading game configuration...");
			configinfo = General.GetConfigurationInfo(options.ConfigFile);
			config = new GameConfiguration(configinfo.Configuration); //mxd
			General.Editing.UpdateCurrentEditModes();

			// Reload data resources
			General.WriteLogLine("Reloading data resources...");
			data = new DataManager();
			if(!string.IsNullOrEmpty(filepathname)) 
			{
				DataLocation maplocation = new DataLocation(DataLocation.RESOURCE_WAD, filepathname, false, false, false);
				data.Load(configinfo.Resources, options.Resources, maplocation);
			}
			else 
			{
				data.Load(configinfo.Resources, options.Resources);
			}

			// Apply new settings to map elements
			map.UpdateConfiguration();

			// Re-link the background image
			grid.LinkBackground();

			// Inform all plugins that the resources are reloaded
			General.Plugins.ReloadResources();

			// Inform editing mode that the resources are reloaded
			if(General.Editing.Mode != null) General.Editing.Mode.OnReloadResources();

			// Reset status
			General.MainWindow.DisplayStatus(oldstatus);
			Cursor.Current = oldcursor;

			//mxd. Update includes list and script names
			UpdateScriptNames(true);
		}

		// Game Configuration action
		[BeginAction("mapoptions")]
		internal void ShowMapOptions() 
		{
			// Cancel volatile mode, if any
			General.Editing.DisengageVolatileMode();

			// Show map options dialog
			MapOptionsForm optionsform = new MapOptionsForm(options, false);
			if(optionsform.ShowDialog(General.MainWindow) == DialogResult.OK) 
			{
				// Update interface
				//General.MainWindow.UpdateInterface();

				// Stop data manager
				data.Dispose();

				// Apply new options
				this.options = optionsform.Options;

				// Load new game configuration
				General.WriteLogLine("Loading game configuration '" + options.ConfigFile + "'...");
				configinfo = General.GetConfigurationInfo(options.ConfigFile);
				Type oldiotype = io.GetType(); //mxd

                //mxd. Step 1 of hackish way to translate SP/MP thing flags to Hexen / UDMF formats...
                //TODO: add proper Doom -> Hexen thing flags translation to the configs?
                if (oldiotype == typeof(DoomMapSetIO) && configinfo.FormatInterface != "doommapsetio")
                {
                    // Translate to UDMF using Doom things flags translation table
                    foreach (Thing t in General.Map.Map.Things) t.TranslateToUDMF();
				}

				config = new GameConfiguration(configinfo.Configuration); //mxd
				configinfo.ApplyDefaults(config);
				General.Editing.UpdateCurrentEditModes();

				// Setup new map format IO
				General.WriteLogLine("Initializing map format interface " + config.FormatInterface + "...");
				io = MapSetIO.Create(config.FormatInterface, tempwad, this);

				//mxd. Some lumps may've become unneeded during map format conversion. 
				if(oldiotype != io.GetType())
					RemoveUnneededLumps(tempwad, TEMP_MAP_HEADER, false);

				// Create required lumps if they don't exist yet
				CreateRequiredLumps(tempwad, TEMP_MAP_HEADER);

				// Let the plugins know
				General.Plugins.MapReconfigure();

				//mxd. Update linedef color presets and flags if required
				if(oldiotype == typeof(UniversalMapSetIO) && !(io is UniversalMapSetIO)) 
				{
					foreach(Linedef l in General.Map.Map.Linedefs) l.TranslateFromUDMF();
					foreach(Thing t in General.Map.Map.Things) t.TranslateFromUDMF();
				}
                else if (oldiotype == typeof(DoomMapSetIO))
                {
                    if (io is UniversalMapSetIO)
                    {
                        //Thing flags were already translated in Setp 1...
                        //TODO: linedef actions will require the same handling...
                        foreach (Linedef l in General.Map.Map.Linedefs) l.TranslateToUDMF(oldiotype);
                    }
                    else if (io is HexenMapSetIO)
                    {
                        // Step 2 of hackish way to translate SP/MP thing flags to Hexen map format...
                        foreach (Thing t in General.Map.Map.Things) t.TranslateFromUDMF();
                    }
                }
                else if (oldiotype != typeof(UniversalMapSetIO) && io is UniversalMapSetIO)
                {
                    foreach (Linedef l in General.Map.Map.Linedefs) l.TranslateToUDMF(oldiotype);
                    foreach (Thing t in General.Map.Map.Things) t.TranslateToUDMF();
                }

                // Drop all arguments
                if (oldiotype != typeof(DoomMapSetIO) && io is DoomMapSetIO)
                {

                    foreach (Linedef l in General.Map.Map.Linedefs)
                        for (int i = 0; i < l.Args.Length; i++) l.Args[i] = 0;
                    foreach (Thing t in General.Map.Map.Things)
                        for (int i = 0; i < t.Args.Length; i++) t.Args[i] = 0;
                }

                map.UpdateCustomLinedefColors();

                // Reload resources
                ReloadResources();

                // Update interface
                General.MainWindow.SetupInterface();
				General.MainWindow.UpdateThingsFilters();
				General.MainWindow.UpdateLinedefColorPresets(); //mxd
				General.MainWindow.UpdateInterface();

				//mxd. Translate texture names
				bool nameschanged = map.TranslateTextureNames(config.UseLongTextureNames, false);
				grid.TranslateBackgroundName(config.UseLongTextureNames);
				
				//mxd. Sector textures may've been changed 
				if(nameschanged) data.UpdateUsedTextures();

				// Done
				General.MainWindow.DisplayReady();
				General.MainWindow.RedrawDisplay(); //mxd
			}

			// Done
			optionsform.Dispose();
		}

		// This shows the things filters setup
		[BeginAction("thingsfilterssetup")]
		internal void ShowThingsFiltersSetup() 
		{
			new ThingsFiltersForm().ShowDialog(General.MainWindow);
		}

		//mxd. This shows the linedef color presets window
		[BeginAction("linedefcolorssetup")]
		internal void ShowLinedefColorsSetup()
		{
			// Show things filter dialog
			new LinedefColorPresetsForm().ShowDialog(General.MainWindow);
		}

		// This returns true is the given type matches
		public bool IsType(Type t) 
		{
			return io.GetType() == t;
		}

		//mxd
		public SizeF GetTextSize(string text, float scale) 
		{
			return graphics.Font.GetTextSize(text, scale);
		}

		//mxd
		[BeginAction("snapvertstogrid")]
		private void SnapSelectedMapElementsToGrid() 
		{
			// Get selected elements
			ICollection<Vertex> verts = map.GetSelectedVertices(true);
			ICollection<Linedef> lines = map.GetSelectedLinedefs(true); // Sector lines are auto-selected when a sector is selected
			ICollection<Thing> things = map.GetSelectedThings(true);

			// Get vertices from selection
			Dictionary<int, Vertex> vertstosnap = new Dictionary<int, Vertex>(verts.Count);
			foreach(Vertex v in verts) vertstosnap.Add(v.Index, v);
			foreach(Linedef l in lines)
			{
				if(!vertstosnap.ContainsKey(l.Start.Index)) vertstosnap.Add(l.Start.Index, l.Start);
				if(!vertstosnap.ContainsKey(l.End.Index)) vertstosnap.Add(l.End.Index, l.End);
			}

			// Anything to snap?
			if(vertstosnap.Count == 0 && things.Count == 0) 
			{
				General.Interface.DisplayStatus(StatusType.Warning, "Select any map element first!");
				return;
			}

			// Make undo
			undoredo.CreateUndo("Snap map elements to grid");

			// Do the snapping
			Cursor.Current = Cursors.AppStarting;

			// Snap vertices?
			int snappedverts = (vertstosnap.Count > 0 ? SnapVertices(vertstosnap.Values) : 0);

			// Snap things?..
			int snappedthings = (things.Count > 0 ? SnapThings(things) : 0);

			// Assemble status message
			List<string> message = new List<string>();
			if(snappedverts > 0) message.Add(snappedverts + " vertices");
			if(snappedthings > 0) message.Add(snappedthings + " things");

			// Map changed?
			if(message.Count > 0) 
			{
				// Display status
				General.Interface.DisplayStatus(StatusType.Info, "Snapped " + string.Join(" and ", message.ToArray()));

				// Warn the user
				/*if(snappedverts > 0) 
				{
					MessageBox.Show("Snapped " + snappedverts + " vertices to grid." + Environment.NewLine +
					                "It's a good idea to run Map Analysis Mode now.");
				}*/

				// Invoke clear selection to update sector highlight overlay
				General.Actions.InvokeAction("builder_clearselection");

				// Update cached values
				General.Map.Map.Update();

				// Map is changed
				General.Map.IsChanged = true;
			}
			else 
			{
				// Display status
				General.Interface.DisplayStatus(StatusType.Info, "Selected map elements were already on the grid.");

				// Withdraw undo
				undoredo.WithdrawUndo();
			}

			// Done
			General.Interface.RedrawDisplay();
			Cursor.Current = Cursors.Default;
		}

		//JBR
		[BeginAction("snapvertsto1mp")]
		private void SnapSelectedMapElementsTo1mp()
		{
			// Get selected elements
			ICollection<Vertex> verts = map.GetSelectedVertices(true);
			ICollection<Linedef> lines = map.GetSelectedLinedefs(true); // Sector lines are auto-selected when a sector is selected
			ICollection<Thing> things = map.GetSelectedThings(true);

			// Get vertices from selection
			Dictionary<int, Vertex> vertstosnap = new Dictionary<int, Vertex>(verts.Count);
			foreach (Vertex v in verts) vertstosnap.Add(v.Index, v);
			foreach (Linedef l in lines)
			{
				if (!vertstosnap.ContainsKey(l.Start.Index)) vertstosnap.Add(l.Start.Index, l.Start);
				if (!vertstosnap.ContainsKey(l.End.Index)) vertstosnap.Add(l.End.Index, l.End);
			}

			// Anything to snap?
			if (vertstosnap.Count == 0 && things.Count == 0)
			{
				General.Interface.DisplayStatus(StatusType.Warning, "Select any map element first!");
				return;
			}

			// Make undo
			undoredo.CreateUndo("Force snap map elements to 1mp");

			// Do the snapping
			Cursor.Current = Cursors.AppStarting;

			// Snap vertices?
			int snappedverts = (vertstosnap.Count > 0 ? SnapVertices(vertstosnap.Values, false, true) : 0);

			// Snap things?..
			int snappedthings = (things.Count > 0 ? SnapThings(things, false, true) : 0);

			// Assemble status message
			List<string> message = new List<string>();
			if (snappedverts > 0) message.Add(snappedverts + " vertices");
			if (snappedthings > 0) message.Add(snappedthings + " things");

			// Display status
			General.Interface.DisplayStatus(StatusType.Info, "Force snapped " + string.Join(" and ", message.ToArray()));

			// Invoke clear selection to update sector highlight overlay
			//General.Actions.InvokeAction("builder_clearselection");

			// Update cached values
			General.Map.Map.Update();

			// Map is changed
			General.Map.IsChanged = true;

			// Done
			General.Interface.RedrawDisplay();
			Cursor.Current = Cursors.Default;
		}

		//mxd
		private int SnapVertices(IEnumerable<Vertex> verts, bool snapToGrid = true, bool addAll = false) //JBR Added snapToGrid & addAll
		{
			int snappedCount = 0;
			List<Vertex> movedVerts = new List<Vertex>();
			List<Linedef> movedLines = new List<Linedef>();

			//snap them all!
			foreach(Vertex v in verts) 
			{
				Vector2D pos = v.Position;
				if (snapToGrid) v.SnapToGrid();
				else v.Move(GridSetup.SnappedToGrid(v.Position, 1f, 1f));

				if (v.Position.x != pos.x || v.Position.y != pos.y || addAll) 
				{
					snappedCount++;
					movedVerts.Add(v);
					foreach(Linedef l in v.Linedefs) 
					{
						if(!movedLines.Contains(l)) movedLines.Add(l);
					}
				}
			}

			//Create blockmap
			RectangleF area = MapSet.CreateArea(General.Map.Map.Vertices);
			BlockMap<BlockEntry> blockmap = new BlockMap<BlockEntry>(area);
			blockmap.AddVerticesSet(General.Map.Map.Vertices);

			//merge overlapping vertices using teh power of BLOCKMAP!!!11
			foreach(Vertex v in movedVerts) 
			{
				BlockEntry block = blockmap.GetBlockAt(v.Position);
				if(block == null) continue;

				foreach(Vertex blockVert in block.Vertices) 
				{
					if(blockVert.IsDisposed || blockVert.Index == v.Index || blockVert.Position != v.Position)
						continue;

					foreach(Linedef l in blockVert.Linedefs) 
					{
						if(!movedLines.Contains(l)) movedLines.Add(l);
					}
					v.Join(blockVert);
					break;
				}
			}

			// Update cached values of lines because we may need their length/angle
			General.Map.Map.Update(true, false);

			General.Map.Map.BeginAddRemove();
			MapSet.RemoveLoopedLinedefs(movedLines);
			MapSet.JoinOverlappingLines(movedLines);
			General.Map.Map.EndAddRemove();

			//get changed sectors
			List<Sector> changedSectors = new List<Sector>();
			foreach(Linedef l in movedLines) 
			{
				if(l == null || l.IsDisposed) continue;
				if(l.Front != null && l.Front.Sector != null && !changedSectors.Contains(l.Front.Sector))
					changedSectors.Add(l.Front.Sector);
				if(l.Back != null && l.Back.Sector != null && !changedSectors.Contains(l.Back.Sector))
					changedSectors.Add(l.Back.Sector);
			}

			// Now update area of sectors
			General.Map.Map.Update(false, true);

			//fix invalid sectors
			foreach(Sector s in changedSectors) 
			{
				if(s.BBox.IsEmpty) 
				{
					s.Dispose();
				} 
				else if(s.Sidedefs.Count < 3) 
				{
					bool merged = false;
					foreach(Sidedef side in s.Sidedefs) 
					{
						if(side.Other != null && side.Other.Sector != null) 
						{
							s.Join(side.Other.Sector);
							merged = true;
							break;
						}
					}

					//oh well, I don't know what else I can do here...
					if(!merged) s.Dispose();
				}
			}

			return snappedCount;
		}

		//mxd
		private static int SnapThings(IEnumerable<Thing> things, bool snapToGrid = true, bool addAll = false) //JBR Added snapToGrid & addAll
		{
			int snappedCount = 0;

			//snap them all!
			foreach(Thing t in things) 
			{
				Vector2D pos = t.Position;
				if (snapToGrid) t.SnapToGrid();
				else t.Move(GridSetup.SnappedToGrid(t.Position, 1f, 1f));
				if (t.Position.x != pos.x || t.Position.y != pos.y || addAll) snappedCount++;
			}

			return snappedCount;
		}

		#endregion
	}
}