diff --git a/Source/Core/Editing/UndoManager.cs b/Source/Core/Editing/UndoManager.cs
index 9862c8e7f5677e9a5186534fcd514e5ccc65b921..29c5fd3497488cc42f3f3d93bb546b7b9fa66671 100644
--- a/Source/Core/Editing/UndoManager.cs
+++ b/Source/Core/Editing/UndoManager.cs
@@ -43,7 +43,7 @@ namespace CodeImp.DoomBuilder.Editing
 		#region ================== Constants
 
 		// Maximum undo/redo levels
-		private const int MAX_UNDO_LEVELS = 1000;
+		private const int MAX_UNDO_LEVELS = 2000;
 		
 		// Default stream capacity
 		private const int STREAM_CAPACITY = 1000;
@@ -315,11 +315,12 @@ namespace CodeImp.DoomBuilder.Editing
 		private void FinishRecording()
 		{
 			// End current recording
-			if(stream != null)
+			if((stream != null) && (ss != null))
 			{
 				propsrecorded = null;
 				ss.wInt(commandswritten);
 				ss.End();
+				ss = null;
 			}
 		}
 		
@@ -419,7 +420,27 @@ namespace CodeImp.DoomBuilder.Editing
 		#endregion
 		
 		#region ================== Public Methods
-
+		
+		// This makes a list of the undo levels in order they will be undone
+		public List<UndoSnapshot> GetUndoList()
+		{
+			List<UndoSnapshot> list = new List<UndoSnapshot>(undos.Count + 1);
+			if(!isundosnapshot && (snapshot != null))
+				list.Add(snapshot);
+			list.AddRange(undos);
+			return list;
+		}
+		
+		// This makes a list of the redo levels in order they will be undone
+		public List<UndoSnapshot> GetRedoList()
+		{
+			List<UndoSnapshot> list = new List<UndoSnapshot>(redos.Count + 1);
+			if(isundosnapshot && (snapshot != null))
+				list.Add(snapshot);
+			list.AddRange(redos);
+			return list;
+		}
+		
 		// This clears all redos
 		public void ClearAllRedos()
 		{
@@ -545,10 +566,17 @@ namespace CodeImp.DoomBuilder.Editing
 		// This performs an undo
 		[BeginAction("undo")]
 		public void PerformUndo()
+		{
+			PerformUndo(1);
+		}
+		
+		// This performs one or more undo levels
+		public void PerformUndo(int levels)
 		{
 			UndoSnapshot u = null;
 			Cursor oldcursor = Cursor.Current;
 			Cursor.Current = Cursors.WaitCursor;
+			int levelsundone = 0;
 			
 			// Anything to undo?
 			if((undos.Count > 0) || ((snapshot != null) && !isundosnapshot))
@@ -563,63 +591,84 @@ namespace CodeImp.DoomBuilder.Editing
 						// This returns false when mode was not volatile
 						if(!General.CancelVolatileMode())
 						{
-							FinishRecording();
-							
-							if(isundosnapshot)
+							// Go for all levels to undo
+							for(int lvl = 0; lvl < levels; lvl++)
 							{
-								if(snapshot != null)
+								FinishRecording();
+								
+								if(isundosnapshot)
 								{
-									// This snapshot was made by a previous call to this
-									// function and should go on the redo list
-									lock(redos)
+									if(snapshot != null)
 									{
-										// The current top of the stack can now be written to disk
-										// because it is no longer the next immediate redo level
-										if(redos.Count > 0) redos[0].StoreOnDisk = true;
-										
-										// Put it on the stack
-										redos.Insert(0, snapshot);
-										LimitUndoRedoLevel(redos);
+										// This snapshot was made by a previous call to this
+										// function and should go on the redo list
+										lock(redos)
+										{
+											// The current top of the stack can now be written to disk
+											// because it is no longer the next immediate redo level
+											if(redos.Count > 0) redos[0].StoreOnDisk = true;
+											
+											// Put it on the stack
+											redos.Insert(0, snapshot);
+											LimitUndoRedoLevel(redos);
+										}
 									}
 								}
-							}
-							else
-							{
-								// The snapshot can be undone immediately and it will
-								// be recorded for the redo list
-								if(snapshot != null)
-									u = snapshot;
-							}
-							
-							// No immediate snapshot to undo? Then get the next one from the stack
-							if(u == null)
-							{
-								lock(undos)
+								else
+								{
+									// The snapshot can be undone immediately and it will
+									// be recorded for the redo list
+									if(snapshot != null)
+										u = snapshot;
+								}
+								
+								// No immediate snapshot to undo? Then get the next one from the stack
+								if(u == null)
 								{
-									// Get undo snapshot
-									u = undos[0];
-									undos.RemoveAt(0);
-									
-									// Make the current top of the stack load into memory
-									// because it just became the next immediate undo level
-									if(undos.Count > 0) undos[0].StoreOnDisk = false;
+									lock(undos)
+									{
+										if(undos.Count > 0)
+										{
+											// Get undo snapshot
+											u = undos[0];
+											undos.RemoveAt(0);
+											
+											// Make the current top of the stack load into memory
+											// because it just became the next immediate undo level
+											if(undos.Count > 0) undos[0].StoreOnDisk = false;
+										}
+										else
+										{
+											// Nothing more to undo
+											break;
+										}
+									}
 								}
+								
+								General.WriteLogLine("Performing undo \"" + u.Description + "\", Ticket ID " + u.TicketID + "...");
+								
+								if(levels == 1)
+									General.Interface.DisplayStatus(StatusType.Action, u.Description + " undone.");
+								
+								// Make a snapshot for redo
+								StartRecording(u.Description);
+								isundosnapshot = true;
+								
+								// Reset grouping
+								lastgroupplugin = null;
+
+								// Play back the stream in reverse
+								MemoryStream data = u.GetStream();
+								PlaybackStream(data);
+								data.Dispose();
+								
+								// Done with this snapshot
+								u = null;
+								levelsundone++;
 							}
 							
-							General.WriteLogLine("Performing undo \"" + u.Description + "\", Ticket ID " + u.TicketID + "...");
-							General.Interface.DisplayStatus(StatusType.Action, u.Description + " undone.");
-							
-							// Make a snapshot for redo
-							StartRecording(u.Description);
-							isundosnapshot = true;
-							
-							// Reset grouping
-							lastgroupplugin = null;
-
-							// Play back the stream in reverse
-							MemoryStream data = u.GetStream();
-							PlaybackStream(data);
-							data.Dispose();
+							if(levels > 1)
+								General.Interface.DisplayStatus(StatusType.Action, "Undone " + levelsundone + " changes.");
 							
 							// Remove selection
 							General.Map.Map.ClearAllSelected();
@@ -649,10 +698,16 @@ namespace CodeImp.DoomBuilder.Editing
 		// This performs a redo
 		[BeginAction("redo")]
 		public void PerformRedo()
+		{
+			PerformRedo(1);
+		}
+		
+		public void PerformRedo(int levels)
 		{
 			UndoSnapshot r = null;
 			Cursor oldcursor = Cursor.Current;
 			Cursor.Current = Cursors.WaitCursor;
+			int levelsundone = 0;
 			
 			// Anything to redo?
 			if((redos.Count > 0) || ((snapshot != null) && isundosnapshot))
@@ -664,89 +719,108 @@ namespace CodeImp.DoomBuilder.Editing
 					if(General.Editing.Mode.OnRedoBegin())
 					{
 						// Cancel volatile mode, if any
-						General.CancelVolatileMode();
-
-						FinishRecording();
-						
-						if(isundosnapshot)
-						{
-							// This snapshot was started by PerformUndo, which means
-							// it can directly be used to redo to previous undo
-							if(snapshot != null)
-								r = snapshot;
-						}
-						else
+						// This returns false when mode was not volatile
+						if(!General.CancelVolatileMode())
 						{
-							if(snapshot != null)
+							// Go for all levels to undo
+							for(int lvl = 0; lvl < levels; lvl++)
 							{
-								// This snapshot was made by a previous call to this
-								// function and should go on the undo list
-								lock(undos)
+								FinishRecording();
+								
+								if(isundosnapshot)
 								{
-									// The current top of the stack can now be written to disk
-									// because it is no longer the next immediate undo level
-									if(undos.Count > 0) undos[0].StoreOnDisk = true;
-
-									// Put it on the stack
-									undos.Insert(0, snapshot);
-									LimitUndoRedoLevel(undos);
+									// This snapshot was started by PerformUndo, which means
+									// it can directly be used to redo to previous undo
+									if(snapshot != null)
+										r = snapshot;
+								}
+								else
+								{
+									if(snapshot != null)
+									{
+										// This snapshot was made by a previous call to this
+										// function and should go on the undo list
+										lock(undos)
+										{
+											// The current top of the stack can now be written to disk
+											// because it is no longer the next immediate undo level
+											if(undos.Count > 0) undos[0].StoreOnDisk = true;
+
+											// Put it on the stack
+											undos.Insert(0, snapshot);
+											LimitUndoRedoLevel(undos);
+										}
+									}
 								}
-							}
-						}
-						
-						// No immediate snapshot to redo? Then get the next one from the stack
-						if(r == null)
-						{
-							lock(redos)
-							{
-								// Get redo snapshot
-								r = redos[0];
-								redos.RemoveAt(0);
 								
-								// Make the current top of the stack load into memory
-								// because it just became the next immediate undo level
-								if(redos.Count > 0) redos[0].StoreOnDisk = false;
-							}
-						}
+								// No immediate snapshot to redo? Then get the next one from the stack
+								if(r == null)
+								{
+									lock(redos)
+									{
+										if(redos.Count > 0)
+										{
+											// Get redo snapshot
+											r = redos[0];
+											redos.RemoveAt(0);
+											
+											// Make the current top of the stack load into memory
+											// because it just became the next immediate undo level
+											if(redos.Count > 0) redos[0].StoreOnDisk = false;
+										}
+										else
+										{
+											// Nothing more to redo
+											break;
+										}
+									}
+								}
 
-						General.WriteLogLine("Performing redo \"" + r.Description + "\", Ticket ID " + r.TicketID + "...");
-						General.Interface.DisplayStatus(StatusType.Action, r.Description + " redone.");
+								General.WriteLogLine("Performing redo \"" + r.Description + "\", Ticket ID " + r.TicketID + "...");
+								
+								if(levels == 1)
+									General.Interface.DisplayStatus(StatusType.Action, r.Description + " redone.");
 
-						StartRecording(r.Description);
-						isundosnapshot = false;
-						
-						// Reset grouping
-						lastgroupplugin = null;
+								StartRecording(r.Description);
+								isundosnapshot = false;
+								
+								// Reset grouping
+								lastgroupplugin = null;
 
-						// Play back the stream in reverse
-						MemoryStream data = r.GetStream();
-						PlaybackStream(data);
-						data.Dispose();
-						
-						// Remove selection
-						General.Map.Map.ClearAllSelected();
-
-						// Update map
-						General.Map.Map.Update();
-						foreach(Thing t in General.Map.Map.Things) if(t.Marked) t.UpdateConfiguration();
-						General.Map.ThingsFilter.Update();
-						General.Map.Data.UpdateUsedTextures();
-						General.MainWindow.RedrawDisplay();
-						
-						// Done
-						General.Editing.Mode.OnRedoEnd();
-						General.Plugins.OnRedoEnd();
+								// Play back the stream in reverse
+								MemoryStream data = r.GetStream();
+								PlaybackStream(data);
+								data.Dispose();
+							}
+							
+							if(levels > 1)
+								General.Interface.DisplayStatus(StatusType.Action, "Redone " + levelsundone + " changes.");
+							
+							// Remove selection
+							General.Map.Map.ClearAllSelected();
+
+							// Update map
+							General.Map.Map.Update();
+							foreach(Thing t in General.Map.Map.Things) if(t.Marked) t.UpdateConfiguration();
+							General.Map.ThingsFilter.Update();
+							General.Map.Data.UpdateUsedTextures();
+							General.MainWindow.RedrawDisplay();
+							
+							// Done
+							General.Editing.Mode.OnRedoEnd();
+							General.Plugins.OnRedoEnd();
 
-						// Update interface
-						dobackgroundwork = true;
-						General.MainWindow.UpdateInterface();
+							// Update interface
+							dobackgroundwork = true;
+							General.MainWindow.UpdateInterface();
+						}
 					}
 				}
 			}
 			
 			Cursor.Current = oldcursor;
 		}
-
+		
 		#endregion
 		
 		#region ================== Record and Playback