#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.Data.Scripting; using CodeImp.DoomBuilder.Editing; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.GZBuilder.Data; //mxd using CodeImp.DoomBuilder.IO; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.Rendering; using CodeImp.DoomBuilder.VisualModes; using CodeImp.DoomBuilder.Windows; using CodeImp.DoomBuilder.ZDoom.Scripting; #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 WADReader tempwadreader; private GridSetup grid; private UndoManager undoredo; private CopyPasteManager copypaste; private Launcher launcher; private ThingsFilter thingsfilter; private ScriptEditorForm scriptwindow; private List errors; private VisualCamera visualcamera; //mxd private Dictionary namedscripts; private Dictionary numberedscripts; // 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 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; } } internal WADReader TemporaryMapFile { get { return tempwadreader; } } //mxd //mxd. Map format public bool UDMF { get { return config.UDMF; } } public bool HEXEN { get { return config.HEXEN; } } public bool DOOM { get { return config.DOOM; } } //mxd. Scripts internal Dictionary NamedScripts { get { return namedscripts; } } internal Dictionary NumberedScripts { get { return numberedscripts; } } 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(); //mxd numberedscripts = new Dictionary(); namedscripts = new Dictionary(StringComparer.OrdinalIgnoreCase); } // 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(tempwadreader != null) tempwadreader.Dispose(); //mxd 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; tempwadreader = null; //mxd 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(); //mxd. Check if default script compiler is required if(string.IsNullOrEmpty(configinfo.DefaultScriptCompiler)) { foreach(MapLumpInfo info in config.MapLumps.Values) { if(info.ScriptBuild) { General.ErrorLogger.Add(ErrorType.Error, "\"DefaultScriptCompiler\" property is not set in \"" + configinfo + "\" game configuration. The editor may fail to compile ACC scripts."); break; } } } // Create map data map = new MapSet(); // Create temp wadfile DataLocation templocation = new DataLocation(DataLocation.RESOURCE_WAD, General.MakeTempFilename(temppath), false, false, false); //mxd General.WriteLogLine("Creating temporary file: " + templocation.location); #if DEBUG tempwadreader = new WADReader(templocation, false, true); #else try { tempwadreader = new WADReader(templocation, false, true); } 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, tempwadreader.WadFile, this); // Create required lumps General.WriteLogLine("Creating map data structures..."); tempwadreader.WadFile.Insert(TEMP_MAP_HEADER, 0, 0); io.Write(map, TEMP_MAP_HEADER, 1); CreateRequiredLumps(tempwadreader.WadFile, 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(StringComparer.OrdinalIgnoreCase); //mxd numberedscripts = new Dictionary(); //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 DataLocation templocation = new DataLocation(DataLocation.RESOURCE_WAD, General.MakeTempFilename(temppath), false, false, false); //mxd General.WriteLogLine("Creating temporary file: " + templocation.location); #if DEBUG tempwadreader = new WADReader(templocation, false, true); #else try { tempwadreader = new WADReader(templocation, false, true); } 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, tempwadreader.WadFile, 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. Flip linedefs with only back side int flipsdone = MapSet.FlipBackwardLinedefs(map.Linedefs); if(flipsdone > 0) General.WriteLogLine(flipsdone + " single-sided linedefs were flipped."); // Update structures options.ApplyGridSettings(); map.UpdateConfiguration(); map.SnapAllToAccuracy(); map.Update(); thingsfilter.Update(); //mxd. Update script names LoadACS(); //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 DataLocation templocation = new DataLocation(DataLocation.RESOURCE_WAD, General.MakeTempFilename(temppath), false, false, false); //mxd General.WriteLogLine("Creating temporary file: " + templocation.location); if(tempwadreader != null) tempwadreader.Dispose(); #if DEBUG tempwadreader = new WADReader(templocation, false, true); #else try { tempwadreader = new WADReader(templocation, false, true); } 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, tempwadreader.WadFile, 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); // Sector textures may've been changed data.UpdateUsedTextures(); // This will update DataManager.mapinfo only data.ReloadMapInfoPartial(); // Skybox may've been changed data.SetupSkybox(); // Update script names LoadACS(); // Restore selection groups options.ReadSelectionGroups(); if(General.Editing.Mode != null) { if(General.Editing.Mode is ClassicMode) { ClassicMode mode = (ClassicMode)General.Editing.Mode; mode.OnRedoEnd(); // Center map in screen or on stored coordinates if(options.ViewPosition.IsFinite() && !float.IsNaN(options.ViewScale)) mode.CenterOnCoordinates(options.ViewPosition, options.ViewScale); else mode.CenterInScreen(); } else if(General.Editing.Mode is VisualMode) { VisualMode mode = (VisualMode)General.Editing.Mode; // This will rebuild blockmap, among the other things General.Editing.Mode.OnReloadResources(); // Update camera position if(options.ViewPosition.IsFinite()) mode.CenterOnCoordinates(options.ViewPosition); } } // 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, tempwadreader.WadFile, 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, tempwadreader.WadFile, 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 /// /// This exports the structures from memory into a WAD file with the current map format. /// public bool ExportToFile(string filepathname) { General.Plugins.OnMapSaveBegin(SavePurpose.Testing); bool result = SaveMap(filepathname, SavePurpose.Testing); General.Plugins.OnMapSaveEnd(SavePurpose.Testing); return result; } /// /// This writes the map structures to the temporary file. /// 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.Type == config.Start3DModeThingType) { // We're not using SetFlag here, this doesn't have to be undone. // Please note that this is totally exceptional! List flagkeys = new List(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 int initialsidescount = outputset.Sidedefs.Count; //mxd General.MainWindow.DisplayStatus(StatusType.Busy, "Compressing sidedefs..."); outputset.CompressSidedefs(); // Check if it still doesnt 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!" + Environment.NewLine + Environment.NewLine + "Sidedefs before compresion: " + initialsidescount + Environment.NewLine + "Sidedefs after compresion: " + outputset.Sidedefs.Count + " (" + (outputset.Sidedefs.Count - io.MaxSidedefs) + " sidedefs above the limit)", 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, tempwadreader.WadFile.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; bool fileexists = File.Exists(newfilepathname); //mxd General.WriteLogLine("Saving map to file: " + newfilepathname); //mxd. Official IWAD check... if(fileexists) { WAD hashtest = new WAD(newfilepathname, true); if(hashtest.IsOfficialIWAD) { General.WriteLogLine("Map saving aborted: attempt to modify an official IWAD"); General.ShowErrorMessage("Official IWADs should not be modified.\nConsider making a PWAD instead", MessageBoxButtons.OK); return false; } hashtest.Dispose(); } // 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 or there are compiler errors (mxd) // (not when only the map changed) if((localscriptschanged || errors.Count > 0) && !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, false); // 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); //mxd. Compress temp file... tempwadreader.WadFile.Compress(); } else { // Check if we have nodebuilder lumps includenodes = VerifyNodebuilderLumps(tempwadreader.WadFile, TEMP_MAP_HEADER); } //mxd. Target file is read-only? if(fileexists) { FileInfo info = new FileInfo(newfilepathname); if(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 if(fileexists) { FileLockChecker.FileLockCheckResult checkresult = FileLockChecker.CheckFile(newfilepathname); if(!string.IsNullOrEmpty(checkresult.Error)) { if(checkresult.Processes.Count > 0) { string rest = Environment.NewLine + "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(fileexists) { // 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; } } // Backup existing file, if any if(File.Exists(newfilepathname + ".backup3")) File.Delete(newfilepathname + ".backup3"); if(File.Exists(newfilepathname + ".backup2")) File.Move(newfilepathname + ".backup2", newfilepathname + ".backup3"); if(File.Exists(newfilepathname + ".backup1")) File.Move(newfilepathname + ".backup1", newfilepathname + ".backup2"); 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); } // [ZZ] We really want to tell apart saving into current archive from saving into a new one. // Treat "save as" into the current archive as normal save. bool isSaveAs = (purpose == SavePurpose.AsNewFile) && (newfilepathname != filepathname); // On Save AS we have to copy the previous file to the new file if(isSaveAs) { // 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 // [ZZ] The original code here would do some weird trickery with tempfiles. // I'm just finding the map lumps and deleting them on later stages. 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(tempwadreader.WadFile, 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(true); } 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 /// /// 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. /// 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); //mxd. Compress temp file... tempwadreader.WadFile.Compress(); 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 : tempwadreader.WadFile.Filename); //mxd. RemoveUnneededLumps(tempwadreader.WadFile, TEMP_MAP_HEADER, true); // Copy lumps to buildwad General.WriteLogLine("Copying map lumps to temporary build file..."); CopyLumpsByType(tempwadreader.WadFile, 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, tempwadreader.WadFile, 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 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 = tempwadreader.WadFile.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 = tempwadreader.WadFile.Lumps.Count; // Remove the lump if it already exists int li = tempwadreader.WadFile.FindLumpIndex(lumpname); if(li > -1) { insertindex = li; tempwadreader.WadFile.RemoveAt(li); } // Insert new lump Lump l = tempwadreader.WadFile.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 (tempwadreader.WadFile.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 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, false); } else { // Move insert index insertindex = targetindex; } } } target.WriteHeaders(); //mxd } //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 requiredLumps = new List(); foreach(KeyValuePair 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; string lumpname = group.Key; if(lumpname == CONFIG_MAP_HEADER) lumpname = mapname; requiredLumps.Add(lumpname); } //Remove lumps, which are not required List toRemove = new List(); foreach(Lump lump in target.Lumps) if(!requiredLumps.Contains(lump.Name)) toRemove.Add(lump); foreach(Lump lump in toRemove) target.Remove(lump); } // 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 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, false); 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."); } } } } target.WriteHeaders(); //mxd target.Compress(); // [ZZ] } } // 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 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 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("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; // Load the window? if(scriptwindow == null) scriptwindow = new ScriptEditorForm(); // Window not yet visible? if(!scriptwindow.Visible) scriptwindow.Show(); scriptwindow.TopMost = General.Settings.ScriptOnTop; //mxd 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 if(scriptwindow != null) { 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) { // Compile it now success &= tempwadreader.CompileLump(lumpinfo.Name, lumpinfo.Script, errors); } //mxd. Is this ACS script? else if(lumpinfo.ScriptBuild) { // Compile it using selected script compiler ScriptConfiguration cfg = General.GetScriptConfiguration(ScriptType.ACS); if(cfg != null) { success &= tempwadreader.CompileLump(lumpinfo.Name, cfg, errors); } else { General.ErrorLogger.Add(ErrorType.Error, "Unable to compile \"" + lumpinfo.Name + "\" script lump: unable to find suitable Script Configuration!"); success = false; } } } return success; } //mxd. Update script numbers and names, collect loose ACS files. private void LoadACS() { /////////////////////////////////////////// // Step 1: Update script numbers and names /////////////////////////////////////////// General.Map.Data.ScriptResources[ScriptType.ACS] = new HashSet(); // Find SCRIPTS lump and parse it 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)) { string error = "Unable to compile lump \"" + maplumpinfo.Name + "\". Unable to find required script compiler configuration (\"" + General.Map.Options.ScriptCompiler + "\")."; General.ErrorLogger.Add(ErrorType.Error, error); return; } 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 && scriptconfig.ScriptType == ScriptType.ACS) { // Get script names AcsParserSE parser = new AcsParserSE { IsMapScriptsLump = true, IgnoreErrors = true, OnInclude = delegate(AcsParserSE se, string includefile, AcsParserSE.IncludeType includetype) { TextResourceData includedata = General.Map.Data.GetTextResourceData(includefile); if(includedata == null) return false; // Fial return se.Parse(includedata, true, includetype, false); } }; //INFO: CompileLump() prepends lumpname with "?" to distinguish between temporary files and files compiled in place DataLocation location = new DataLocation { location = tempwadreader.WadFile.Filename, type = DataLocation.RESOURCE_WAD }; TextResourceData trd = new TextResourceData(stream, location, "?SCRIPTS"); if(parser.Parse(trd, scriptconfig.Compiler.Files, true, AcsParserSE.IncludeType.NONE, false)) { // Add to text resource list General.Map.Data.ScriptResources[parser.ScriptType].UnionWith(parser.ScriptResources.Values); // Update the names UpdateScriptNames(parser); } else { // Clear collections namedscripts.Clear(); numberedscripts.Clear(); } // Log errors, if any if(parser.HasError) parser.LogError(); // Done here break; } } } /////////////////////////////////////////// // Step 2: Try to load unused ACS files /////////////////////////////////////////// Dictionary> existingacsfiles = new Dictionary>(); // Gather already parsed files... foreach(ScriptResource sr in General.Map.Data.ScriptResources[ScriptType.ACS]) { if(!existingacsfiles.ContainsKey(sr.Resource.Location.location)) existingacsfiles.Add(sr.Resource.Location.location, new HashSet()); existingacsfiles[sr.Resource.Location.location].Add(sr.Filename); } HashSet looseacsfiles = new HashSet(); foreach(DataReader dr in General.Map.Data.Containers) { //TODO: implement for WADs if(dr is PK3StructuredReader) { PK3StructuredReader pkr = (PK3StructuredReader)dr; string[] acsfiles = pkr.GetFilesWithExt("", "acs", true); foreach(string acsfile in acsfiles) { if(!existingacsfiles.ContainsKey(pkr.Location.location) || !existingacsfiles[pkr.Location.location].Contains(acsfile)) { TextResourceData trd = new TextResourceData(dr, dr.LoadFile(acsfile), acsfile, true); looseacsfiles.Add(new ScriptResource(trd, ScriptType.ACS)); } } } } // Add to the main collection General.Map.Data.ScriptResources[ScriptType.ACS].UnionWith(looseacsfiles); } //mxd internal void UpdateScriptNames(AcsParserSE parser) { if(parser.HasError) { // Clear collections namedscripts.Clear(); numberedscripts.Clear(); } else { // Add to collections namedscripts = new Dictionary(parser.NamedScripts.Count, StringComparer.OrdinalIgnoreCase); numberedscripts = new Dictionary(parser.NumberedScripts.Count); // Sort script names parser.NamedScripts.Sort(ScriptItem.SortByName); parser.NumberedScripts.Sort(ScriptItem.SortByIndex); foreach(ScriptItem item in parser.NamedScripts) { if(!namedscripts.ContainsKey(item.Name.ToLowerInvariant())) namedscripts.Add(item.Name.ToLowerInvariant(), item); } foreach(ScriptItem item in parser.NumberedScripts) { if(!numberedscripts.ContainsKey(item.Index)) numberedscripts.Add(item.Index, item); } } } //mxd public bool ScriptNumberExists(int scriptnumber) { return numberedscripts.ContainsKey(scriptnumber); } //mxd public bool ScriptNameExists(string scriptname) { return namedscripts.ContainsKey(scriptname); } #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(true); 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(bool clearerrors) //mxd. clearerrors flag { // 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? if(clearerrors) 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, options.StrictPatches, 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(); //mxd. Also Check appropriate button on interface General.MainWindow.CheckEditModeButton(General.Editing.Mode.EditModeButtonName); } //mxd. Update script names LoadACS(); //mxd. Script Editor may need updating... if(scriptwindow != null) scriptwindow.OnReloadResources(); // Reset status General.MainWindow.DisplayStatus(oldstatus); Cursor.Current = oldcursor; } // 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(); //mxd. Clear errors General.ErrorLogger.Clear(); // 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, tempwadreader.WadFile, this); //mxd. Some lumps may've become unneeded during map format conversion. if(oldiotype != io.GetType()) RemoveUnneededLumps(tempwadreader.WadFile, TEMP_MAP_HEADER, false); // Create required lumps if they don't exist yet CreateRequiredLumps(tempwadreader.WadFile, 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(false); // Update interface General.MainWindow.SetupInterface(); General.MainWindow.UpdateThingsFilters(); General.MainWindow.UpdateLinedefColorPresets(); //mxd General.MainWindow.UpdateInterface(); // 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 [BeginAction("snapvertstogrid")] private void SnapSelectedMapElementsToGrid() { // Get selected elements ICollection verts = map.GetSelectedVertices(true); ICollection lines = map.GetSelectedLinedefs(true); // Sector lines are auto-selected when a sector is selected ICollection things = map.GetSelectedThings(true); // Get vertices from selection Dictionary vertstosnap = new Dictionary(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 message = new List(); 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; } //mxd private int SnapVertices(IEnumerable verts) { int snappedCount = 0; List movedVerts = new List(); List movedLines = new List(); //snap them all! foreach(Vertex v in verts) { Vector2D pos = v.Position; v.SnapToGrid(); if(v.Position.x != pos.x || v.Position.y != pos.y) { 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 blockmap = new BlockMap(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 changedSectors = new List(); 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 things) { int snappedCount = 0; //snap them all! foreach(Thing t in things) { Vector2D pos = t.Position; t.SnapToGrid(); if(t.Position.x != pos.x || t.Position.y != pos.y) snappedCount++; } return snappedCount; } #endregion } }