Skip to content
Snippets Groups Projects
BaseVisualMode.cs 183 KiB
Newer Older

#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;
biwa's avatar
biwa committed
using System.Linq;
using System.Windows.Forms;
using CodeImp.DoomBuilder.BuilderModes.Interface;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Actions;
using CodeImp.DoomBuilder.VisualModes;
using CodeImp.DoomBuilder.Config;
MaxED's avatar
MaxED committed
using CodeImp.DoomBuilder.GZBuilder.Data;
MaxED's avatar
MaxED committed
using CodeImp.DoomBuilder.Types;
using CodeImp.DoomBuilder.Data;

#endregion

namespace CodeImp.DoomBuilder.BuilderModes
{
	[EditMode(DisplayName = "Visual Mode",
			  SwitchAction = "gzdbvisualmode", // Action name used to switch to this mode
			  ButtonImage = "VisualMode.png",	// Image resource name for the button
			  ButtonOrder = 1,					// Position of the button (lower is more to the left)
			  ButtonGroup = "001_visual",

	public class BaseVisualMode : VisualMode
	{
		#region ================== Constants
		// Object picking
		private const long PICK_INTERVAL = 80;
		private const long PICK_INTERVAL_PAINT_SELECT = 10; // biwa

		// Gravity
		private const float GRAVITY = -0.06f;
		
		#endregion
		
		#region ================== Variables
		private double cameraflooroffset = 41.0;		// same as in doom
		private double cameraceilingoffset = 10.0;
		
		// Object picking
		private VisualPickResult target;
		private long lastpicktime;
		private bool locktarget;
MaxED's avatar
MaxED committed
		private bool useSelectionFromClassicMode;//mxd
		private readonly Timer selectioninfoupdatetimer; //mxd
MaxED's avatar
MaxED committed

		// This keeps extra element info
		private Dictionary<Sector, SectorData> sectordata;
MaxED's avatar
MaxED committed
		private Dictionary<Thing, ThingData> thingdata;
		private Dictionary<Vertex, VertexData> vertexdata; //mxd
		//private Dictionary<Thing, EffectDynamicLight> lightdata; //mxd
		// This is true when a selection was made because the action is performed
		// on an object that was not selected. In this case the previous selection
		// is cleared and the targeted object is temporarely selected to perform
		// the action on. After the action is completed, the object is deselected.
		private bool singleselection;
		// We keep these to determine if we need to make a new undo level
		private bool selectionchanged;
		private VisualActionResult actionresult;
		// List of selected objects when an action is performed
		private List<IVisualEventReceiver> selectedobjects;
		//mxd. Used in Cut/PasteSelection actions
		private readonly List<ThingCopyData> copybuffer;
		private Type lasthighlighttype;
MaxED's avatar
MaxED committed

		// biwa. Info for paint selection
		protected bool paintselectpressed;
		protected Type paintselecttype = null;
		protected IVisualPickable highlighted; // biwa

MaxED's avatar
MaxED committed
		//mxd. Moved here from Tools
		private struct SidedefAlignJob
		{
			public Sidedef sidedef;

biwa's avatar
biwa committed
			public double offsetx;
			public double scaleX; //mxd
			public double scaleY; //mxd
MaxED's avatar
MaxED committed

			private Sidedef controlside; //mxd
				get
				{
					return controlside;
				}
				set
				{
					controlside = value;
					ceilingheight = (controlside.Index != sidedef.Index && controlside.Line.Args[1] == 0 ? controlside.Sector.FloorHeight : controlside.Sector.CeilHeight);
				}
			}

			private int ceilingheight; //mxd
			public int ceilingHeight { get { return ceilingheight; } } //mxd

MaxED's avatar
MaxED committed
			// When this is true, the previous sidedef was on the left of
			// this one and the texture X offset of this sidedef can be set
			// directly. When this is false, the length of this sidedef
			// must be subtracted from the X offset first.
			public bool forward;
		}
		#endregion
		
		#region ================== Properties

		public override object HighlightedObject
		{
			get
			{
				// Geometry picked?
				VisualGeometry vg = target.picked as VisualGeometry;
				if(vg != null)
					if(vg.Sidedef != null) return vg.Sidedef;
					if(vg.Sector != null) return vg.Sector;
				VisualThing vt = target.picked as VisualThing;
				if(vt != null) return vt.Thing;
		public object HighlightedTarget { get { return target.picked; } } //mxd
		public bool UseSelectionFromClassicMode { get { return useSelectionFromClassicMode; } } //mxd
MaxED's avatar
MaxED committed
		new public IRenderer3D Renderer { get { return renderer; } }
		
		public bool IsSingleSelection { get { return singleselection; } }
		public bool SelectionChanged { get { return selectionchanged; } set { selectionchanged |= value; } }
		public bool PaintSelectPressed { get { return paintselectpressed; } } // biwa
		public Type PaintSelectType { get { return paintselecttype; } set { paintselecttype = value; } } // biwa
		public IVisualPickable Highlighted { get { return highlighted; } } // biwa

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

		// Constructor
		public BaseVisualMode()
		{
			// Initialize
			this.gravity = new Vector3D(0.0f, 0.0f, 0.0f);
			this.selectedobjects = new List<IVisualEventReceiver>();
			this.copybuffer = new List<ThingCopyData>();
			this.selectioninfoupdatetimer = new Timer();
			selectioninfoupdatetimer.Interval = 100;
			selectioninfoupdatetimer.Tick += SelectioninfoupdatetimerOnTick;
			
			// We have no destructor
			GC.SuppressFinalize(this);
		}

		// Disposer
		public override void Dispose()
		{
			// Not already disposed?
			if(!isdisposed)
			{
				// Clean up
				selectioninfoupdatetimer.Dispose(); //mxd
				
				// Done
				base.Dispose();
			}
		}

		#endregion
		
		#region ================== Methods
		internal int CalculateBrightness(int level)
			return renderer.CalculateBrightness(level);
		//mxd. This calculates brightness level with doom-style shading
		internal int CalculateBrightness(int level, Sidedef sd) 
		{
			return renderer.CalculateBrightness(level, sd);
		}
		// This adds a selected object
		internal void AddSelectedObject(IVisualEventReceiver obj)
		{
			selectedobjects.Add(obj);
			selectionchanged = true;
			selectioninfoupdatetimer.Start(); //mxd
		}
		
		// This removes a selected object
		internal void RemoveSelectedObject(IVisualEventReceiver obj)
		{
			selectedobjects.Remove(obj);
			selectionchanged = true;
			selectioninfoupdatetimer.Start(); //mxd
		// This is called before an action is performed
		public void PreAction(int multiselectionundogroup)
			actionresult = new VisualActionResult();
			// If the action is not performed on a selected object, clear the
			// current selection and make a temporary selection for the target.
			if ((target.picked != null) && !target.picked.Selected && (BuilderPlug.Me.VisualModeClearSelection || (selectedobjects.Count == 0)))
				// Single object, no selection
				singleselection = true;

				// Only clear the selection if anything is selected, since it can be very time consuming on huge maps
				if(BuilderPlug.Me.VisualModeClearSelection && selectedobjects.Count > 0)
					ClearSelection();

				// Check if we should make a new undo level
				// We don't want to do this if this is the same action with the same
				// selection and the action wants to group the undo levels
				if((lastundogroup != multiselectionundogroup) || (lastundogroup == UndoGroup.None) ||
				   (multiselectionundogroup == UndoGroup.None) || selectionchanged)
				{
					// We want to create a new undo level, but not just yet
					lastundogroup = multiselectionundogroup;
					// We don't want to make a new undo level (changes will be combined)
					undocreated = true;
		// Called before an action is performed. This does not make an undo level
		private void PreActionNoChange()
		{
			actionresult = new VisualActionResult();
			singleselection = false;
			undocreated = false;
		// This is called after an action is performed
		private void PostAction()
			if(!string.IsNullOrEmpty(actionresult.displaystatus))
				General.Interface.DisplayStatus(StatusType.Action, actionresult.displaystatus);

			// Reset changed flags
			foreach(KeyValuePair<Sector, VisualSector> vs in allsectors)
				BaseVisualSector bvs = (BaseVisualSector)vs.Value;
				foreach(VisualFloor vf in bvs.ExtraFloors) vf.Changed = false;
				foreach(VisualCeiling vc in bvs.ExtraCeilings) vc.Changed = false;
MaxED's avatar
MaxED committed
				foreach(VisualFloor vf in bvs.ExtraBackFloors) vf.Changed = false; //mxd
				foreach(VisualCeiling vc in bvs.ExtraBackCeilings) vc.Changed = false; //mxd
				bvs.Floor.Changed = false;
				bvs.Ceiling.Changed = false;
			}
			selectionchanged = false;

			// Only clear the selection if anything is selected, since it can be very time consuming on huge maps
			if (singleselection && selectedobjects.Count > 0) ClearSelection();
			UpdateChangedObjects();
			ShowTargetInfo();
		}
		
		// This sets the result for an action
		public void SetActionResult(VisualActionResult result)
		{
			actionresult = result;
		}

		// This sets the result for an action
		public void SetActionResult(string displaystatus)
		{
			actionresult = new VisualActionResult {displaystatus = displaystatus};
		}
		
		// This creates an undo, when only a single selection is made
		// When a multi-selection is made, the undo is created by the PreAction function
		public int CreateUndo(string description, int group, int grouptag)
			if(!undocreated)
			{
				undocreated = true;

				if(singleselection)
					return General.Map.UndoRedo.CreateUndo(description, this, group, grouptag);
				return General.Map.UndoRedo.CreateUndo(description, this, UndoGroup.None, 0);
		}

		// This creates an undo, when only a single selection is made
		// When a multi-selection is made, the undo is created by the PreAction function
		public int CreateUndo(string description)
			return CreateUndo(description, UndoGroup.None, 0);
		}

		// This makes a list of the selected object
		{
			// Make list of selected objects
			selectedobjects = new List<IVisualEventReceiver>();
			foreach(KeyValuePair<Sector, VisualSector> vs in allsectors)
					BaseVisualSector bvs = (BaseVisualSector)vs.Value;
					if((bvs.Floor != null) && bvs.Floor.Selected) selectedobjects.Add(bvs.Floor);
					if((bvs.Ceiling != null) && bvs.Ceiling.Selected) selectedobjects.Add(bvs.Ceiling);
					
					// Also check extra floors
					if (bvs.ExtraFloors.Count > 0)
						foreach (VisualFloor vf in bvs.ExtraFloors)
							if (vf.Selected) selectedobjects.Add(vf);

					if (bvs.ExtraBackFloors.Count > 0)
						foreach (VisualFloor vf in bvs.ExtraBackFloors)
							if (vf.Selected) selectedobjects.Add(vf);

					// Also check extra ceilings
					if (bvs.ExtraCeilings.Count > 0)
						foreach (VisualCeiling vc in bvs.ExtraCeilings)
							if (vc.Selected) selectedobjects.Add(vc);

					if (bvs.ExtraBackCeilings.Count > 0)
						foreach (VisualCeiling vc in bvs.ExtraBackCeilings)
							if (vc.Selected) selectedobjects.Add(vc);

					foreach (Sidedef sd in vs.Key.Sidedefs)
						List<VisualGeometry> sidedefgeos = bvs.GetSidedefGeometry(sd);
						foreach(VisualGeometry sdg in sidedefgeos)
						{
							if(sdg.Selected) selectedobjects.Add((IVisualEventReceiver)sdg);
						}
			foreach(KeyValuePair<Thing, VisualThing> vt in allthings)
				if(vt.Value != null)
				{
					BaseVisualThing bvt = (BaseVisualThing)vt.Value;
					if(bvt.Selected) selectedobjects.Add(bvt);
				}
MaxED's avatar
MaxED committed

			//mxd
			if(General.Map.UDMF && General.Map.Config.VertexHeightSupport && General.Settings.GZShowVisualVertices) 
			{
				foreach(KeyValuePair<Vertex, VisualVertexPair> pair in vertices) 
				{
					if(pair.Value.CeilingVertex.Selected)
						selectedobjects.Add((BaseVisualVertex)pair.Value.CeilingVertex);
					if(pair.Value.FloorVertex.Selected)
						selectedobjects.Add((BaseVisualVertex)pair.Value.FloorVertex);
MaxED's avatar
MaxED committed
				}
			}
biwa's avatar
biwa committed
			if (General.Map.UDMF)
			{
				foreach (KeyValuePair<Sector, List<VisualSlope>> kvp in allslopehandles)
				{
biwa's avatar
biwa committed
					foreach (BaseVisualSlope handle in kvp.Value)
						if (handle.Selected) selectedobjects.Add(handle);
			//mxd
			UpdateSelectionInfo();
		//mxd. Need this to apply changes to 3d-floor even if control sector doesn't exist as BaseVisualSector
		internal BaseVisualSector CreateBaseVisualSector(Sector s) 
		{
			BaseVisualSector vs = new BaseVisualSector(this, s);
		// This creates a visual sector
		protected override VisualSector CreateVisualSector(Sector s)
		{
			BaseVisualSector vs = new BaseVisualSector(this, s);
biwa's avatar
biwa committed

		internal VisualSlope CreateVisualSlopeHandle(SectorLevel level, Sidedef sd, bool up)
		{
			VisualSidedefSlope handle = new VisualSidedefSlope(this, level, sd, up);

			if (!allslopehandles.ContainsKey(sd.Sector))
				allslopehandles.Add(sd.Sector, new List<VisualSlope>());

			if (!sidedefslopehandles.ContainsKey(sd.Sector))
				sidedefslopehandles.Add(sd.Sector, new List<VisualSlope>());

biwa's avatar
biwa committed
			allslopehandles[sd.Sector].Add(handle);
			sidedefslopehandles[sd.Sector].Add(handle);

			return handle;
		}

		internal VisualSlope CreateVisualSlopeHandle(SectorLevel level, Vertex v, Sector s, bool up)
		{
			VisualVertexSlope handle = new VisualVertexSlope(this, level, v, s, up);

			/*
			if (!allslopehandles.ContainsKey(level.sector))
				allslopehandles.Add(level.sector, new List<VisualSlope>());

			if (!vertexslopehandles.ContainsKey(level.sector))
				vertexslopehandles.Add(level.sector, new List<VisualSlope>());

			allslopehandles[level.sector].Add(handle);
			vertexslopehandles[level.sector].Add(handle);
			*/

			if (!allslopehandles.ContainsKey(s))
				allslopehandles.Add(s, new List<VisualSlope>());

			if (!vertexslopehandles.ContainsKey(s))
				vertexslopehandles.Add(s, new List<VisualSlope>());

			allslopehandles[s].Add(handle);
			vertexslopehandles[s].Add(handle);
biwa's avatar
biwa committed

			return handle;
		}

		// This creates a visual thing
		protected override VisualThing CreateVisualThing(Thing t)
		{
			BaseVisualThing vt = new BaseVisualThing(this, t);
			return vt.Setup() ? vt : null;
		// This locks the target so that it isn't changed until unlocked
		public void LockTarget()
		{
			locktarget = true;
		}
		
		// This unlocks the target so that is changes to the aimed geometry again
		public void UnlockTarget()
		{
			locktarget = false;
		}
		
		// This picks a new target, if not locked
		private void PickTargetUnlocked()
		{
			if(!locktarget) PickTarget();
		}
		
		// This picks a new target
		private void PickTarget()
		{
			// Find the object we are aiming at
			Vector3D start = General.Map.VisualCamera.Position;
			Vector3D delta = General.Map.VisualCamera.Target - General.Map.VisualCamera.Position;
			delta = delta.GetFixedLength(General.Settings.ViewDistance * PICK_RANGE);
			VisualPickResult newtarget = PickObject(start, start + delta);
			VisualSlope pickedhandle = null;
			
			// Should we update the info on panels?
			bool updateinfo = (newtarget.picked != target.picked);
			// Operating on slope handles is potentially expensive, so only do it it absolutely necessary (i.e. when a new slope handle was selected)
				if (target.picked is VisualSlope) // Old target
				{
					// Remove all smart pivot handles from being processed. There should only be exactly one, but better save than sorry
					List<VisualSlope> sph = new List<VisualSlope>();
					foreach (VisualSlope vs in usedslopehandles)
					{
						if(vs.SmartPivot && !(vs.Selected || vs.Pivot))
							sph.Add(vs);
						vs.SmartPivot = false;
					}

					foreach (VisualSlope vs in sph)
						usedslopehandles.Remove(vs);

					// Don't render old slope handle anymore
					if (!((VisualSlope)target.picked).Selected && !((VisualSlope)target.picked).Pivot)
						usedslopehandles.Remove((VisualSlope)target.picked);
				if(newtarget.picked is VisualSlope)
					usedslopehandles.Add((VisualSlope)newtarget.picked);
					pickedhandle = ((VisualSlope)newtarget.picked);
			// Apply new target
			target = newtarget;

			// Get the smart pivot handle for the targeted slope handle, so that it can be drawn. We have to do it after the current
			// target is set because otherwise it might get wrong results if the old target was a floor/ceiling
			if (pickedhandle != null)
			{
				VisualSlope handle = pickedhandle.GetSmartPivotHandle();
				if (handle != null)
				{
					handle.SmartPivot = true;
					usedslopehandles.Add(handle);
				}
			}

			// Show target info
			if (updateinfo)
				ShowTargetInfo();
		}

		// This shows the picked target information
		public void ShowTargetInfo()
		{
			// Any result?
			if(target.picked != null)
			{
				// Geometry picked?
				if(target.picked is VisualGeometry)
				{
					VisualGeometry pickedgeo = (VisualGeometry)target.picked;
					
					// Sidedef?
					if(pickedgeo is BaseVisualGeometrySidedef)
					{
						BaseVisualGeometrySidedef pickedsidedef = (BaseVisualGeometrySidedef)pickedgeo;
						General.Interface.ShowLinedefInfo(pickedsidedef.GetControlLinedef(), pickedsidedef.Sidedef); //mxd
					}
					// Sector?
					else if(pickedgeo is BaseVisualGeometrySector)
					{
						BaseVisualGeometrySector pickedsector = (BaseVisualGeometrySector)pickedgeo;
						bool isceiling = (pickedsector is VisualCeiling); //mxd
						General.Interface.ShowSectorInfo(pickedsector.Level.sector, isceiling, !isceiling);
						General.Interface.HideInfo();
MaxED's avatar
MaxED committed
				} 
MaxED's avatar
MaxED committed
				else if(target.picked is VisualThing) 
				{ 
					VisualThing pickedthing = (VisualThing)target.picked;
					General.Interface.ShowThingInfo(pickedthing.Thing);
MaxED's avatar
MaxED committed
				} 
				//mxd. Vertex picked?
				else if(target.picked is VisualVertex)
MaxED's avatar
MaxED committed
				{
					VisualVertex pickedvert = (VisualVertex)target.picked;
MaxED's avatar
MaxED committed
					General.Interface.ShowVertexInfo(pickedvert.Vertex);
		// This updates the VisualSectors and VisualThings that have their Changed property set
		private void UpdateChangedObjects()
			foreach(KeyValuePair<Sector, VisualSector> vs in allsectors)
				if(vs.Value != null)
				{
					BaseVisualSector bvs = (BaseVisualSector)vs.Value;
biwa's avatar
biwa committed
					if(bvs.Changed)
					{
						bvs.Rebuild();

						// Also update slope handles
						if (allslopehandles.ContainsKey(vs.Key))
							foreach (VisualSlope handle in allslopehandles[vs.Key])
biwa's avatar
biwa committed
								handle.Update();
biwa's avatar
biwa committed
					}
			foreach(KeyValuePair<Thing, VisualThing> vt in allthings)
				if(vt.Value != null)
				{
					BaseVisualThing bvt = (BaseVisualThing)vt.Value;
					if(bvt.Changed) bvt.Rebuild();
				}
MaxED's avatar
MaxED committed

			//mxd
			if(General.Map.UDMF && General.Map.Config.VertexHeightSupport) 
MaxED's avatar
MaxED committed
				foreach(KeyValuePair<Vertex, VisualVertexPair> pair in vertices)
					pair.Value.Update();
			}

			//mxd. Update event lines (still better than updating them on every frame redraw)
			renderer.SetEventLines(LinksCollector.GetHelperShapes(General.Map.ThingsFilter.VisibleThings, blockmap));
		protected override void MoveSelectedThings(Vector2D direction, bool absoluteposition) 
			List<VisualThing> visualthings = GetSelectedVisualThings(true);
			if(visualthings.Count == 0) return;
			PreAction(UndoGroup.ThingMove);
MaxED's avatar
MaxED committed

			Vector3D[] coords = new Vector3D[visualthings.Count];
			for(int i = 0; i < visualthings.Count; i++)
				coords[i] = visualthings[i].Thing.Position;
MaxED's avatar
MaxED committed

			Vector3D[] translatedcoords = TranslateCoordinates(coords, direction, absoluteposition);
			for(int i = 0; i < visualthings.Count; i++) 
				BaseVisualThing t = (BaseVisualThing)visualthings[i];
MaxED's avatar
MaxED committed

			// Things may've changed sectors...
			FillBlockMap();

MaxED's avatar
MaxED committed

		private static Vector3D[] TranslateCoordinates(Vector3D[] coordinates, Vector2D direction, bool absolutePosition) 
			if(coordinates.Length == 0) return null;
MaxED's avatar
MaxED committed

			direction.x = Math.Round(direction.x);
			direction.y = Math.Round(direction.y);
MaxED's avatar
MaxED committed

			Vector3D[] translatedCoords = new Vector3D[coordinates.Length];
MaxED's avatar
MaxED committed

			if(!absolutePosition) //...relatively (that's easy)
				int camAngle = (int)Math.Round(Angle2D.RadToDeg(General.Map.VisualCamera.AngleXY));
				int sector = General.ClampAngle(camAngle - 45) / 90;
				direction = direction.GetRotated(sector * Angle2D.PIHALF);
MaxED's avatar
MaxED committed

				for(int i = 0; i < coordinates.Length; i++)
					translatedCoords[i] = coordinates[i] + new Vector3D(direction);
MaxED's avatar
MaxED committed

MaxED's avatar
MaxED committed

			//...to specified location preserving relative positioning (that's harder)
			if(coordinates.Length == 1) //just move it there
				translatedCoords[0] = new Vector3D(direction.x, direction.y, coordinates[0].z);
				return translatedCoords;
			}
MaxED's avatar
MaxED committed

biwa's avatar
biwa committed
			double minX = coordinates[0].x;
			double maxX = minX;
			double minY = coordinates[0].y;
			double maxY = minY;
MaxED's avatar
MaxED committed

			//get bounding coordinates for selected things
			for(int i = 1; i < coordinates.Length; i++) 
MaxED's avatar
MaxED committed

MaxED's avatar
MaxED committed

			Vector2D selectionCenter = new Vector2D(minX + (maxX - minX) / 2, minY + (maxY - minY) / 2);
MaxED's avatar
MaxED committed

			for(int i = 0; i < coordinates.Length; i++)
				translatedCoords[i] = new Vector3D(Math.Round(direction.x - (selectionCenter.x - coordinates[i].x)), Math.Round(direction.y - (selectionCenter.y - coordinates[i].y)), Math.Round(coordinates[i].z));
MaxED's avatar
MaxED committed

		public override void UpdateSelectionInfo() 
			int numWalls = 0;
			int numFloors = 0;
			int numCeilings = 0;
			int numThings = 0;
			int numVerts = 0;

			foreach(IVisualEventReceiver obj in selectedobjects) 
			{

				if(obj is BaseVisualThing) numThings++;
				else if(obj is BaseVisualVertex) numVerts++;
				else if(obj is VisualCeiling) numCeilings++;
				else if(obj is VisualFloor)	numFloors++;
				else if(obj is VisualMiddleSingle || obj is VisualMiddleDouble || obj is VisualLower || obj is VisualUpper || obj is VisualMiddle3D || obj is VisualMiddleBack)
					numWalls++;
			}

			List<string> results = new List<string>();
			if(numWalls > 0) results.Add(numWalls + (numWalls > 1 ? " sidedefs" : " sidedef"));
			if(numFloors > 0) results.Add(numFloors + (numFloors > 1 ? " floors" : " floor"));
			if(numCeilings > 0) results.Add(numCeilings + (numCeilings > 1 ? " ceilings" : " ceiling"));
			if(numThings > 0) results.Add(numThings + (numThings > 1 ? " things" : " thing"));
			if(numVerts > 0) results.Add(numVerts + (numVerts > 1 ? " vertices" : " vertex"));
			// Display results
			string result = string.Empty;
			if(results.Count > 0) 
				result = string.Join(", ", results.ToArray());
				int pos = result.LastIndexOf(",", StringComparison.Ordinal);
				if(pos != -1) result = result.Remove(pos, 1).Insert(pos, " and");

			General.Interface.DisplayStatus(StatusType.Selection, result);
		internal void StartRealtimeInterfaceUpdate(SelectionType selectiontype)
		{
			{
				case SelectionType.All:
				case SelectionType.Linedefs:
				case SelectionType.Sectors:
					General.Interface.OnEditFormValuesChanged += Interface_OnSectorEditFormValuesChanged;
					break;
				case SelectionType.Things:
					General.Interface.OnEditFormValuesChanged += Interface_OnThingEditFormValuesChanged;
					break;
				default:
					General.Interface.OnEditFormValuesChanged += Interface_OnEditFormValuesChanged;
					break;
		internal void StopRealtimeInterfaceUpdate(SelectionType selectiontype)
		{
			{
				case SelectionType.All:
				case SelectionType.Linedefs:
				case SelectionType.Sectors:
					General.Interface.OnEditFormValuesChanged -= Interface_OnSectorEditFormValuesChanged;
					break;
				case SelectionType.Things:
					General.Interface.OnEditFormValuesChanged -= Interface_OnThingEditFormValuesChanged;
					break;
				default:
					General.Interface.OnEditFormValuesChanged -= Interface_OnEditFormValuesChanged;
					break;
biwa's avatar
biwa committed

		private List<VisualSidedefSlope> GetSlopeHandlePair()
		{
			List<VisualSidedefSlope> handles = GetSelectedSlopeHandles();

			// No handles selected, try to slope between highlighted handle and it smart pivot
			if (handles.Count == 0 && HighlightedTarget is VisualSidedefSlope)
			{
				//VisualSidedefSlope handle = VisualSidedefSlope.GetSmartPivotHandle((VisualSidedefSlope)HighlightedTarget, this);
				VisualSidedefSlope handle = (VisualSidedefSlope)((VisualSidedefSlope)HighlightedTarget).GetSmartPivotHandle();
biwa's avatar
biwa committed
				if (handle == null)
				{
					General.Interface.DisplayStatus(StatusType.Warning, "Couldn't find a smart pivot handle.");
					return handles;
				}

				handles.Add((VisualSidedefSlope)HighlightedTarget);
				handles.Add(handle);
			}
			// One handle selected, try to slope between it and the highlighted handle or the selected one's smart pivot
			else if (handles.Count == 1)
			{
				if (HighlightedTarget == handles[0] || !(HighlightedTarget is VisualSidedefSlope))
				{
					VisualSidedefSlope handle;

					if (HighlightedTarget is VisualSidedefSlope)
						handle = (VisualSidedefSlope)((VisualSidedefSlope)HighlightedTarget).GetSmartPivotHandle();
biwa's avatar
biwa committed
					else
						handle = (VisualSidedefSlope)(handles[0].GetSmartPivotHandle());
biwa's avatar
biwa committed

					if (handle == null)
					{
						General.Interface.DisplayStatus(StatusType.Warning, "Couldn't find a smart pivot handle.");
						return handles;
					}

					handles.Add(handle);
				}
				else
				{
					handles.Add((VisualSidedefSlope)HighlightedTarget);
				}
			}
			// Return if more than two handles are selected
			else if (handles.Count > 2)
			{
				General.Interface.DisplayStatus(StatusType.Warning, "Too many slope handles selected.");
				return handles;
			}
			// Everything else
			else if (handles.Count != 2)
			{
				General.Interface.DisplayStatus(StatusType.Warning, "No slope handles selected or highlighted.");
				return handles;
			}

			return handles;
		}

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

		// This requests a sector's extra data
		internal SectorData GetSectorData(Sector s)
		{
			// Make fresh sector data when it doesn't exist yet
			if(!sectordata.ContainsKey(s))
				sectordata[s] = new SectorData(this, s);
			
			return sectordata[s];
		}

		//mxd. This requests a sector's extra data or null if given sector doesn't have it
		internal SectorData GetSectorDataEx(Sector s)
		{
			return (sectordata.ContainsKey(s) ? sectordata[s] : null);
		}

		// This requests a things's extra data
		internal ThingData GetThingData(Thing t)
		{
			// Make fresh sector data when it doesn't exist yet
			if(!thingdata.ContainsKey(t))
				thingdata[t] = new ThingData(this, t);
			
			return thingdata[t];
		}
MaxED's avatar
MaxED committed

		//mxd
		internal VertexData GetVertexData(Vertex v) 
		{
MaxED's avatar
MaxED committed
			if(!vertexdata.ContainsKey(v))
				vertexdata[v] = new VertexData(this, v);
			return vertexdata[v];
		}

		internal BaseVisualVertex GetVisualVertex(Vertex v, bool floor) 
		{
			if(!vertices.ContainsKey(v))
				vertices.Add(v, new VisualVertexPair(new BaseVisualVertex(this, v, false), new BaseVisualVertex(this, v, true)));

			return (floor ? (BaseVisualVertex)vertices[v].FloorVertex : (BaseVisualVertex)vertices[v].CeilingVertex);
MaxED's avatar
MaxED committed
		//mxd
		internal void UpdateVertexHandle(Vertex v) 
		{
MaxED's avatar
MaxED committed
			if(!vertices.ContainsKey(v))
				vertices.Add(v, new VisualVertexPair(new BaseVisualVertex(this, v, false), new BaseVisualVertex(this, v, true)));
MaxED's avatar
MaxED committed
			else
MaxED's avatar
MaxED committed
		}
		// This rebuilds the sector data
		// This requires that the blockmap is up-to-date!
		internal void RebuildElementData()
		{
			HashSet<Sector> effectsectors = null; //mxd
			List<Linedef>[] slopelinedefpass = new List<Linedef>[] { new List<Linedef>(), new List<Linedef>() };
			List<Thing>[] slopethingpass = new List<Thing>[] { new List<Thing>(), new List<Thing>() };
			if (!General.Settings.EnhancedRenderingEffects) //mxd
				// Store all sectors with effects
				if(sectordata != null && sectordata.Count > 0) 
					effectsectors = new HashSet<Sector>(sectordata.Keys);
MaxED's avatar
MaxED committed

				// Remove all vertex handles from selection
				if(vertices != null && vertices.Count > 0) 
				{
                    for (int i = 0; i < selectedobjects.Count; i++)
                    {
                        if (selectedobjects[i] is BaseVisualVertex)
                        {
                            RemoveSelectedObject(selectedobjects[i]);
                            i--;
                        }
                    }
MaxED's avatar
MaxED committed
				}
			Dictionary<int, List<Sector>> sectortags = new Dictionary<int, List<Sector>>();
			Dictionary<int, List<Thing>> thingtags = new Dictionary<int, List<Thing>>();
			Dictionary<int, List<Linedef>> linetags = new Dictionary<int, List<Linedef>>();
			sectordata = new Dictionary<Sector, SectorData>(General.Map.Map.Sectors.Count);
			thingdata = new Dictionary<Thing, ThingData>(General.Map.Map.Things.Count);
			//mxd. Rebuild all sectors with effects
			if(effectsectors != null) 
				foreach(Sector s in effectsectors)
					if(!VisualSectorExists(s)) continue;

					// The visual sector associated is now outdated
					BaseVisualSector vs = (BaseVisualSector)GetVisualSector(s);
					vs.UpdateSectorGeometry(true);
MaxED's avatar
MaxED committed
				vertexdata = new Dictionary<Vertex, VertexData>(General.Map.Map.Vertices.Count); //mxd
				vertices.Clear();
			}

			if(!General.Settings.EnhancedRenderingEffects) return; //mxd
			// Find all sector who's tag is not 0 and hash them so that we can find them quickly
			foreach(Sector s in General.Map.Map.Sectors)
			{
				foreach(int tag in s.Tags)
					if(tag == 0) continue;
					if(!sectortags.ContainsKey(tag)) sectortags[tag] = new List<Sector>();
					sectortags[tag].Add(s);
				// ========== Thing vertex slope, vertices with UDMF vertex offsets ==========
				if (s.Sidedefs.Count == 3)
				{
					if (General.Map.UDMF) GetSectorData(s).AddEffectVertexOffset(); //mxd
					List<Thing> slopeceilingthings = new List<Thing>(3);
					List<Thing> slopefloorthings = new List<Thing>(3);

					foreach (Sidedef sd in s.Sidedefs)
					{
						Vertex v = sd.IsFront ? sd.Line.End : sd.Line.Start;

						// Check if a thing is at this vertex
						foreach (VisualBlockEntry block in blockmap.GetBlocks(v.Position))
						{
							foreach (Thing t in block.Things)
							{
								if ((Vector2D)t.Position == v.Position)
								{
									switch (t.Type)
									{
										case 1504: slopefloorthings.Add(t); break;
										case 1505: slopeceilingthings.Add(t); break;
									}
								}
							}
						}
					}

					// Slope any floor vertices?
					if (slopefloorthings.Count > 0)
					{
						SectorData sd = GetSectorData(s);
						sd.AddEffectThingVertexSlope(slopefloorthings, true);
					}

					// Slope any ceiling vertices?
					if (slopeceilingthings.Count > 0)
					{
						SectorData sd = GetSectorData(s);
						sd.AddEffectThingVertexSlope(slopeceilingthings, false);
					}
				}
			// Find interesting things (such as sector slopes)
			// Pass one of slope things, and determine which one are for pass two
			//TODO: unfuck this because UDB decided to overhaul this...
			foreach (Thing t in General.Map.Map.Things)
			{
				// SRB2
				if (t.Type == 750)
				{
					if (!thingtags.ContainsKey(t.Tag)) thingtags[t.Tag] = new List<Thing>();
					thingtags[t.Tag].Add(t);
				}
				continue;

				switch (t.Type)
				{
					// ========== Copy slope ==========
					case 9511:
					case 9510:
						slopethingpass[1].Add(t);
						break;

					// ========== Thing line slope ==========
					case 9501:
					case 9500:
						if (linetags.ContainsKey(t.Args[0]))
						{
							// Only slope each sector once, even when multiple lines of the same sector are tagged. See https://github.com/jewalky/UltimateDoomBuilder/issues/491
							List<Sector> slopedsectors = new List<Sector>();

							foreach (Linedef ld in linetags[t.Args[0]])
							{
								if (ld.Line.GetSideOfLine(t.Position) < 0.0f)
								{
									if (ld.Front != null && !slopedsectors.Contains(ld.Front.Sector))
									{
										GetSectorData(ld.Front.Sector).AddEffectThingLineSlope(t, ld.Front);
										slopedsectors.Add(ld.Front.Sector);
									}
								}
								else if (ld.Back != null && !slopedsectors.Contains(ld.Back.Sector))
								{
									GetSectorData(ld.Back.Sector).AddEffectThingLineSlope(t, ld.Back);
									slopedsectors.Add(ld.Back.Sector);
								}
							}
						}
						break;

					// ========== Thing slope ==========
					case 9503:
					case 9502:
						t.DetermineSector(blockmap);
						if (t.Sector != null)
						{
							SectorData sd = GetSectorData(t.Sector);
							sd.AddEffectThingSlope(t);
						}
						break;
				}
			}

			// Find interesting linedefs (such as line slopes)
			// This also determines which slope lines belong to pass one and pass two. See https://zdoom.org/wiki/Slope
			foreach (Linedef l in General.Map.Map.Linedefs)
				// Builds a cache of linedef ids/tags. Used for slope things. Use linedef tags in UDMF
				if(General.Map.UDMF)
				{
					foreach(int tag in l.Tags)
					{
						if (!linetags.ContainsKey(tag)) linetags[tag] = new List<Linedef>();
						linetags[tag].Add(l);
					}
				}

				//mxd. Rewritten to use action ID instead of number
				if (l.Action == 0 || !General.Map.Config.LinedefActions.ContainsKey(l.Action)) continue;

				switch(General.Map.Config.LinedefActions[l.Action].Id.ToLowerInvariant())
					// ========== Line Set Identification (121) (see https://zdoom.org/wiki/Line_SetIdentification) ==========
					// Builds a cache of linedef ids/tags. Used for slope things. Only used for Hexen format
					case "line_setidentification":
						int tag = l.Args[0] + l.Args[4] * 256;
						if (!linetags.ContainsKey(tag)) linetags[tag] = new List<Linedef>();
						linetags[tag].Add(l);
						break;

					// ========== Plane Align (181) (see http://zdoom.org/wiki/Plane_Align) ==========
					case "plane_align":
					// ========== Plane Copy (118) (mxd) (see http://zdoom.org/wiki/Plane_Copy) ==========
					case "plane_copy":
						slopelinedefpass[1].Add(l);
					case "srb2_vertexslope":
					{
						List<Thing> sourcethings = new List<Thing>();
						if (!thingtags.ContainsKey(l.Args[1]) || thingtags[l.Args[1]].Count == 0)
							break;
						foreach (Thing thing in thingtags[l.Args[1]])
						{
							if (sourcethings.Contains(thing))
								continue;
							sourcethings.Add(thing);
							break;
						}
						if (!thingtags.ContainsKey(l.Args[2]) || thingtags[l.Args[2]].Count == 0)
							break;
						foreach (Thing thing in thingtags[l.Args[2]])
						{
							if (sourcethings.Contains(thing))
								continue;
							sourcethings.Add(thing);
							break;
						}
						if (!thingtags.ContainsKey(l.Args[3]) || thingtags[l.Args[3]].Count == 0)
							break;
						foreach (Thing thing in thingtags[l.Args[3]])
						{
							if (sourcethings.Contains(thing))
								continue;
							sourcethings.Add(thing);
							break;
						}
						SectorData sd = GetSectorData((l.Args[0] < 2) ? l.Front.Sector : l.Back.Sector);
						sd.AddEffectSRB2ThingVertexSlope(sourcethings, (l.Args[0] & 1) != 1);
						break;
					}

					// ========== Sector 3D floor (160) (see http://zdoom.org/wiki/Sector_Set3dFloor) ==========
					case "sector_set3dfloor":
							int sectortag = (General.Map.UDMF || (l.Args[1] & (int)Effect3DFloor.FloorTypes.HiTagIsLineID) != 0) ? l.Args[0] : l.Args[0] + (l.Args[4] << 8);
							if(sectortags.ContainsKey(sectortag)) 
							{
								List<Sector> sectors = sectortags[sectortag];
								foreach(Sector s in sectors) 
								{
									SectorData sd = GetSectorData(s);
									sd.AddEffect3DFloor(l);
								}
							}
						break;

					case "srb2_fofsolid":
					case "srb2_fofwater":
					case "srb2_fofsolidopaque":
					case "srb2_fofcrumbling":
					case "srb2_fofintangible":
					case "srb2_fofbustable":
					case "srb2_foflaser":
					case "srb2_fofcustom":
						if (l.Front != null && sectortags.ContainsKey(l.Args[0]))
							List<Sector> sectors = sectortags[l.Args[0]];
							foreach (Sector s in sectors)
								SectorData sd = GetSectorData(s);
								sd.AddEffect3DFloor(l);
							}
						}
						break;

					case "srb2_foflight":
					case "srb2_foffog":
					case "srb2_fofintangibleinvisible":
						if (l.Front != null && sectortags.ContainsKey(l.Args[0]))
						{
							List<Sector> sectors = sectortags[l.Args[0]];
							foreach (Sector s in sectors)
							{
								SectorData sd = GetSectorData(s);
								//sd.AddEffectBrightnessLevel(l);
								sd.AddEffect3DFloor(l);
					// ========== Transfer Brightness (50) (see http://zdoom.org/wiki/ExtraFloor_LightOnly) =========
					case "extrafloor_lightonly":
						if(l.Front != null && sectortags.ContainsKey(l.Args[0]))
							List<Sector> sectors = sectortags[l.Args[0]];
							foreach(Sector s in sectors) 
							{
								SectorData sd = GetSectorData(s);
								sd.AddEffectBrightnessLevel(l);
							}
					// ========== mxd. Transfer Floor Brightness (210) (see http://www.zdoom.org/w/index.php?title=Transfer_FloorLight) =========
					case "transfer_floorlight":
						if(l.Front != null && sectortags.ContainsKey(l.Args[0])) 
						{
							List<Sector> sectors = sectortags[l.Args[0]];
							foreach(Sector s in sectors) 
							{
								SectorData sd = GetSectorData(s);
								sd.AddEffectTransferFloorBrightness(l);
							}
						}
						break;

					// ========== mxd. Transfer Ceiling Brightness (211) (see http://www.zdoom.org/w/index.php?title=Transfer_CeilingLight) =========
					case "transfer_ceilinglight":
						if(l.Front != null && sectortags.ContainsKey(l.Args[0])) 
						{
							List<Sector> sectors = sectortags[l.Args[0]];
							foreach(Sector s in sectors) 
							{
								SectorData sd = GetSectorData(s);
								sd.AddEffectTransferCeilingBrightness(l);
							}
						}
						break;

					// ========== mxd. BOOM: Set Tagged Floor Lighting to Lighting on 1st Sidedef's Sector (213) =========
					case "boom_transfer_floorlight":
						if(l.Front != null && sectortags.ContainsKey(l.Tag))
						{
							List<Sector> sectors = sectortags[l.Tag];
							foreach(Sector s in sectors)
							{
								SectorData sd = GetSectorData(s);
								sd.AddEffectTransferFloorBrightness(l);
							}
						}
						break;

					// ========== mxd. BOOM: Set Tagged Ceiling Lighting to Lighting on 1st Sidedef's Sector (261) =========
					case "boom_transfer_ceilinglight":
						if(l.Front != null && sectortags.ContainsKey(l.Tag))
						{
							List<Sector> sectors = sectortags[l.Tag];
							foreach(Sector s in sectors)
							{
								SectorData sd = GetSectorData(s);
								sd.AddEffectTransferCeilingBrightness(l);
							}
						}
						break;
			// Pass one for linedefs
			foreach (Linedef l in slopelinedefpass[0])
			{
				//mxd. Rewritten to use action ID instead of number
				if (l.Action == 0 || !General.Map.Config.LinedefActions.ContainsKey(l.Action)) continue;

				switch (General.Map.Config.LinedefActions[l.Action].Id.ToLowerInvariant())
				{
					// ========== Plane Align (181) (see http://zdoom.org/wiki/Plane_Align) ==========
					case "plane_align":
						if (((l.Args[0] == 1) || (l.Args[1] == 1)) && (l.Front != null))
						{
							SectorData sd = GetSectorData(l.Front.Sector);
							sd.AddEffectLineSlope(l);
						}
						if (((l.Args[0] == 2) || (l.Args[1] == 2)) && (l.Back != null))
						{
							SectorData sd = GetSectorData(l.Back.Sector);
							sd.AddEffectLineSlope(l);
						}
						break;
				}
			}

			// Pass two of slope things
			//TODO: unfuck this because UDB decided to overhaul this...
			foreach (Thing t in slopethingpass[1])
			{
				switch (t.Type)
				{
					// ========== Copy slope ==========
					case 9511:
					case 9510:
						t.DetermineSector(blockmap);
						if (t.Sector != null)
							SectorData sd = GetSectorData(t.Sector);
							sd.AddEffectCopySlope(t);

			// Pass two for linedefs
			foreach (Linedef l in slopelinedefpass[1])
			{
				if (l.Action == 0 || !General.Map.Config.LinedefActions.ContainsKey(l.Action)) continue;

				switch (General.Map.Config.LinedefActions[l.Action].Id.ToLowerInvariant())
				{
					// ========== Plane Copy (118) (mxd) (see http://zdoom.org/wiki/Plane_Copy) ==========
					case "plane_copy":
						{
							//check the flags...
							bool floorCopyToBack = false;
							bool floorCopyToFront = false;
							bool ceilingCopyToBack = false;
							bool ceilingCopyToFront = false;

							if (l.Args[4] > 0 && l.Args[4] != 3 && l.Args[4] != 12)
							{
								floorCopyToBack = (l.Args[4] & 1) == 1;
								floorCopyToFront = (l.Args[4] & 2) == 2;
								ceilingCopyToBack = (l.Args[4] & 4) == 4;
								ceilingCopyToFront = (l.Args[4] & 8) == 8;
							}

							// Copy slope to front sector
							if (l.Front != null)
							{
								if ((l.Args[0] > 0 || l.Args[1] > 0) || (l.Back != null && (floorCopyToFront || ceilingCopyToFront)))
								{
									SectorData sd = GetSectorData(l.Front.Sector);
									sd.AddEffectPlaneClopySlope(l, true);
								}
							}

							// Copy slope to back sector
							if (l.Back != null)
							{
								if ((l.Args[2] > 0 || l.Args[3] > 0) || (l.Front != null && (floorCopyToBack || ceilingCopyToBack)))
								{
									SectorData sd = GetSectorData(l.Back.Sector);
									sd.AddEffectPlaneClopySlope(l, false);
								}
							}
						}
						break;
				}
			}
biwa's avatar
biwa committed

			// Visual slope handles
			foreach (KeyValuePair<Sector, List<VisualSlope>> kvp in allslopehandles)
			{
				foreach (VisualSlope handle in kvp.Value)
biwa's avatar
biwa committed
						if (handle is BaseVisualSlope)
							RemoveSelectedObject((BaseVisualSlope)handle);
biwa's avatar
biwa committed

				kvp.Value.Clear();
			}
			usedslopehandles.Clear();
biwa's avatar
biwa committed
			allslopehandles.Clear();
			sidedefslopehandles.Clear();
			vertexslopehandles.Clear();
			BuildSlopeHandles(General.Map.Map.Sectors.ToList());
		}

		private void BuildSlopeHandles(List<Sector> sectors)
		{
			if (!General.Map.UDMF)
				return;

			foreach (Sector s in sectors)
biwa's avatar
biwa committed
			{
				if (s.IsDisposed)
					continue;

				SectorData sectordata = GetSectorData(s);
				sectordata.Update();

				// Clear old data
				if (allslopehandles.ContainsKey(s)) allslopehandles.Remove(s);
				if (sidedefslopehandles.ContainsKey(s))	sidedefslopehandles.Remove(s);
				if (vertexslopehandles.ContainsKey(s)) vertexslopehandles.Remove(s);


				// Create visual sidedef slope handles
				foreach (Sidedef sidedef in s.Sidedefs)
biwa's avatar
biwa committed
				{
					// Create handles for the regular floor and ceiling
					CreateVisualSlopeHandle(sectordata.Floor, sidedef, true);
					CreateVisualSlopeHandle(sectordata.Ceiling, sidedef, false);

					// Create handles for 3D floors
					if (sectordata.ExtraFloors.Count > 0)
						foreach (Effect3DFloor floor in sectordata.ExtraFloors)
						{
							CreateVisualSlopeHandle(floor.Floor, sidedef, false);
							CreateVisualSlopeHandle(floor.Ceiling, sidedef, true);
						}
			// Create visual vertex slope handles
			foreach(Vertex v in General.Map.Map.Vertices)
			{
				if (v.IsDisposed || v.Linedefs.Count == 0)
					continue;

				HashSet<Sector> vertexsectors = new HashSet<Sector>();

				// Find all sectors that have lines connected to this vertex
				foreach(Linedef ld in v.Linedefs)
				{
					if (ld.IsDisposed)
						continue;

					if (ld.Front != null && ld.Front.Sector != null && !ld.Front.Sector.IsDisposed) vertexsectors.Add(ld.Front.Sector);
					if (ld.Back != null && ld.Back.Sector != null && !ld.Back.Sector.IsDisposed) vertexsectors.Add(ld.Back.Sector);
				foreach(Sector s in vertexsectors)
				{
					SectorData sectordata = GetSectorData(s);
biwa's avatar
biwa committed
					sectordata.Update();

					// Create handles for the regular floor and ceiling
					CreateVisualSlopeHandle(sectordata.Floor, v, s, true);
					CreateVisualSlopeHandle(sectordata.Ceiling, v, s, false);
					// Create handles for 3D floors
					if (sectordata.ExtraFloors.Count > 0)
biwa's avatar
biwa committed
					{
						foreach (Effect3DFloor floor in sectordata.ExtraFloors)
biwa's avatar
biwa committed
						{
							CreateVisualSlopeHandle(floor.Floor, v, s, false);
							CreateVisualSlopeHandle(floor.Ceiling, v, s, true);
		// Help!
		public override void OnHelp()
		{
			General.ShowHelp("e_visual.html");
		}
		// When entering this mode
		public override void OnEngage()
		{
MaxED's avatar
MaxED committed
			//mxd
			useSelectionFromClassicMode = BuilderPlug.Me.SyncSelection ? !General.Interface.ShiftState : General.Interface.ShiftState;
			if(useSelectionFromClassicMode)	UpdateSelectionInfo();
			// Read settings
			cameraflooroffset = General.Map.Config.ReadSetting("cameraflooroffset", cameraflooroffset);
			cameraceilingoffset = General.Map.Config.ReadSetting("cameraceilingoffset", cameraceilingoffset);
            //mxd. Update fog color (otherwise FogBoundaries won't be setup correctly)
            foreach (Sector s in General.Map.Map.Sectors)
                s.UpdateFogColor();
			// biwa. We need a blockmap for the slope things. Can't wait until it's built in base.OnEngage
			// This was the root cause for issue #160
			FillBlockMap();

            // (Re)create special effects
            RebuildElementData();
			// Objects are only selected when they are created, so for objects that are selected we have to make sure
			// that they are created immediately. Otherwise the selection order will not be correct, or the objects
			// will not be selected at all if they are out of the user's camera range when entering visual mode
			// See https://github.com/jewalky/UltimateDoomBuilder/issues/938
			if (useSelectionFromClassicMode)
			{
				foreach (Sector s in General.Map.Map.GetSelectedSectors(true))
				{
					BaseVisualSector bvs = CreateBaseVisualSector(s);
					bvs.Ceiling.PerformAutoSelection();
					bvs.Floor.PerformAutoSelection();
				}

				// Things are automatically selected on creation
				foreach (Thing t in General.Map.Map.GetSelectedThings(true))
					allthings[t] = CreateVisualThing(t);

				// For linedefs it's a bit more complicated...
				foreach (Linedef ld in General.Map.Map.GetSelectedLinedefs(true))
				{
					foreach (Sidedef sd in new Sidedef[] { ld.Front, ld.Back })
					{
						if (sd != null)
						{
							if (!allsectors.ContainsKey(sd.Sector))
								CreateBaseVisualSector(sd.Sector).Rebuild(); // We have to rebuild the sector so that potential 3D floors get created

							VisualSidedefParts vsp = ((BaseVisualSector)allsectors[sd.Sector]).Sides[sd];
							vsp.upper?.PerformAutoSelection();
							vsp.middlesingle?.PerformAutoSelection();
							vsp.middledouble?.PerformAutoSelection();
							vsp.lower?.PerformAutoSelection();

							if (vsp.middle3d != null)
								foreach (VisualMiddle3D vm in vsp.middle3d)
									vm.PerformAutoSelection();

							if (vsp.middleback != null)
								foreach (VisualMiddleBack vm in vsp.middleback)
									vm.PerformAutoSelection();
						}
					}
				}
			}

			//mxd. Update event lines
			renderer.SetEventLines(LinksCollector.GetHelperShapes(General.Map.ThingsFilter.VisibleThings, blockmap));

            // [ZZ] this enables calling of this object from the outside world. Only after properly initialized pls.
            base.OnEngage();
        }
		// When returning to another mode
		public override void OnDisengage()
		{
			base.OnDisengage();
MaxED's avatar
MaxED committed

			//mxd
			if(BuilderPlug.Me.SyncSelection ? !General.Interface.ShiftState : General.Interface.ShiftState)
			{
MaxED's avatar
MaxED committed
				//clear previously selected stuff
				General.Map.Map.ClearAllSelected();
				
				//refill selection
				List<int> selectedsectorindices = new List<int>();
				List<int> selectedlineindices = new List<int>();
				List<int> selectedvertexindices = new List<int>();
MaxED's avatar
MaxED committed

				foreach(IVisualEventReceiver obj in selectedobjects)
				{
					if(obj is BaseVisualThing) 
					{
MaxED's avatar
MaxED committed
						((BaseVisualThing)obj).Thing.Selected = true;
					}
					else if(obj is VisualFloor || obj is VisualCeiling) 
					{
						VisualGeometry vg = (VisualGeometry)obj;
						if(vg.Sector != null && vg.Sector.Sector != null && !selectedsectorindices.Contains(vg.Sector.Sector.Index))
						{
							selectedsectorindices.Add(vg.Sector.Sector.Index);
							vg.Sector.Sector.Selected = true;
						}
					}
					else if(obj is VisualLower || obj is VisualUpper || obj is VisualMiddleDouble 
						|| obj is VisualMiddleSingle || obj is VisualMiddle3D) 
					{
						VisualGeometry vg = (VisualGeometry)obj;
						if(vg.Sidedef != null && !selectedlineindices.Contains(vg.Sidedef.Line.Index))
						{
							selectedlineindices.Add(vg.Sidedef.Line.Index);
							vg.Sidedef.Line.Selected = true;
						}
MaxED's avatar
MaxED committed
					}
						VisualVertex v = (VisualVertex)obj;
						if(!selectedvertexindices.Contains(v.Vertex.Index))
						{
							selectedvertexindices.Add(v.Vertex.Index);
							v.Vertex.Selected = true;
						}
		public override void OnProcess(long deltatime)
			long pickinterval = PICK_INTERVAL; // biwa
			// Process things?
			base.ProcessThings = (BuilderPlug.Me.ShowVisualThings != 0);
			
			// Setup the move multiplier depending on gravity
			Vector3D movemultiplier = new Vector3D(1.0, 1.0, 1.0);
			if(BuilderPlug.Me.UseGravity) movemultiplier.z = 0.0;
			General.Map.VisualCamera.MoveMultiplier = movemultiplier;
			
			// Apply gravity?
			if(BuilderPlug.Me.UseGravity && (General.Map.VisualCamera.Sector != null))
			{
				SectorData sd = GetSectorData(General.Map.VisualCamera.Sector);
				if(!sd.Updated) sd.Update();

				// Camera below floor level?
				Vector3D feetposition = General.Map.VisualCamera.Position;
				SectorLevel floorlevel = sd.GetFloorBelow(feetposition) ?? sd.Floor;
biwa's avatar
biwa committed
				double floorheight = floorlevel.plane.GetZ(General.Map.VisualCamera.Position);
				if(General.Map.VisualCamera.Position.z < (floorheight + cameraflooroffset + 0.1))
					General.Map.VisualCamera.Position = new Vector3D(General.Map.VisualCamera.Position.x,
																	 General.Map.VisualCamera.Position.y,
																	 floorheight + cameraflooroffset);
					gravity.z += GRAVITY * General.Map.VisualCamera.Gravity * deltatime;

					// Test if we don't go through a floor
					if((General.Map.VisualCamera.Position.z + gravity.z) < (floorheight + cameraflooroffset + 0.1))
						General.Map.VisualCamera.Position = new Vector3D(General.Map.VisualCamera.Position.x,
																		 General.Map.VisualCamera.Position.y,
																		 floorheight + cameraflooroffset);
					}
					else
					{
						// Apply gravity vector
						General.Map.VisualCamera.Position += gravity;
					}
				feetposition = General.Map.VisualCamera.Position - new Vector3D(0, 0, cameraflooroffset - 7.0);
				SectorLevel ceillevel = sd.GetCeilingAbove(feetposition) ?? sd.Ceiling;
biwa's avatar
biwa committed
				double ceilheight = ceillevel.plane.GetZ(General.Map.VisualCamera.Position);
				if(General.Map.VisualCamera.Position.z > (ceilheight - cameraceilingoffset - 0.01))
				{
					// Stay below ceiling
					General.Map.VisualCamera.Position = new Vector3D(General.Map.VisualCamera.Position.x,
																	 General.Map.VisualCamera.Position.y,
																	 ceilheight - cameraceilingoffset);
			}
			
			// Do processing
			base.OnProcess(deltatime);

			// Process visible geometry
			foreach(IVisualEventReceiver g in visiblegeometry)
			{
				g.OnProcess(deltatime);
			}

			// biwa. Use a lower pick interval for paint selection, to make it more reliable
			if (paintselectpressed)
				pickinterval = PICK_INTERVAL_PAINT_SELECT;
			
			// Time to pick a new target?
			if(Clock.CurrentTime > (lastpicktime + pickinterval))
			{
				PickTargetUnlocked();
				lastpicktime = Clock.CurrentTime;
			}
			
			// The mouse is always in motion
			MouseEventArgs args = new MouseEventArgs(General.Interface.MouseButtons, 0, 0, 0, 0);
			OnMouseMove(args);
		}

		//mxd
		public override void OnClockReset()
		{
			base.OnClockReset();
			lastpicktime = 0;
		}

		// This draws a frame
		public override void OnRedrawDisplay()
		{
			renderer.SetClassicLightingColorMap(General.Map.Data.MainColorMap);

			// Start drawing
			if(renderer.Start())
			{
				// Use fog!
				renderer.SetFogMode(true);

				// Set target for highlighting
				renderer.ShowSelection = General.Settings.GZOldHighlightMode || General.Settings.UseHighlight; //mxd
					renderer.SetHighlightedObject(target.picked);
				
				// Begin with geometry
				renderer.StartGeometry();

				// Render all visible sectors
				foreach(VisualGeometry g in visiblegeometry)
					renderer.AddSectorGeometry(g);

				if(BuilderPlug.Me.ShowVisualThings != 0)
				{
					// Render things in cages?
					renderer.DrawThingCages = ((BuilderPlug.Me.ShowVisualThings & 2) != 0);
					
					// Render all visible things
					foreach(VisualThing t in visiblethings)
						renderer.AddThingGeometry(t);
				}
MaxED's avatar
MaxED committed

				//mxd
				if(General.Map.UDMF && General.Map.Config.VertexHeightSupport && General.Settings.GZShowVisualVertices && vertices.Count > 0) 
MaxED's avatar
MaxED committed
					List<VisualVertex> verts = new List<VisualVertex>();

					foreach(KeyValuePair<Vertex, VisualVertexPair> pair in vertices)
						verts.AddRange(pair.Value.Vertices);

					renderer.SetVisualVertices(verts);
MaxED's avatar
MaxED committed
				}
				renderer.SetVisualSlopeHandles(usedslopehandles);
				// Done rendering geometry
				renderer.FinishGeometry();
				
				// Render crosshair
				renderer.RenderCrosshair();
				
				// Present!
				renderer.Finish();
			}
		}
		
		// After resources were reloaded
		protected override void ResourcesReloaded()
		{
			base.ResourcesReloaded();
		// This usually happens when geometry is changed by undo, redo, cut or paste actions
		// and uses the marks to check what needs to be reloaded.
		protected override void ResourcesReloadedPartial()
		{
			// Let the core do this (it will just dispose the sectors that were changed)
			base.ResourcesReloadedPartial();
			if (General.Map.UndoRedo.GeometryChanged)
			{
				// The base doesn't know anything about slobe handles, so we have to clear them up ourself
				if (General.Map.UDMF)
				{
					List<Sector> removedsectors = new List<Sector>();

					// Get the sectors that were disposed...
					foreach(Sector s in allslopehandles.Keys)
					{
						if (s.IsDisposed)
							removedsectors.Add(s);
					}

					// ... so that we can remove their slope handles
					foreach(Sector s in removedsectors)
					{
						allslopehandles[s].Clear();
						allslopehandles.Remove(s);

						sidedefslopehandles[s].Clear();
						sidedefslopehandles.Remove(s);

						vertexslopehandles[s].Clear();
						vertexslopehandles.Remove(s);
					}

					// Rebuild slope handles for the changed sectors
					BuildSlopeHandles(General.Map.Map.GetMarkedSectors(true));
				}
				// Neighbour sectors must be updated as well
				foreach (Sector s in General.Map.Map.Sectors)
				{
					if(s.Marked)
					{
						sectorsmarked = true;
						foreach(Sidedef sd in s.Sidedefs)
							if(sd.Other != null) sd.Other.Marked = true;
					}
				}
				
				// Go for all sidedefs to update
				foreach(Sidedef sd in General.Map.Map.Sidedefs)
				{
					if(sd.Marked && VisualSectorExists(sd.Sector))
						BaseVisualSector vs = (BaseVisualSector)GetVisualSector(sd.Sector);
						VisualSidedefParts parts = vs.GetSidedefParts(sd);
						parts.SetupAllParts();
					}
				}
				
				// Go for all sectors to update
				foreach(Sector s in General.Map.Map.Sectors)
				{
						SectorData sd = GetSectorDataEx(s);
						if(sd != null)
							sd.Reset(false); //mxd (changed Reset implementation)

							// UpdateSectorGeometry for associated sectors (sd.UpdateAlso) as well!
							foreach(KeyValuePair<Sector, bool> us in sd.UpdateAlso)
								if(VisualSectorExists(us.Key))
								{
									BaseVisualSector vs = (BaseVisualSector)GetVisualSector(us.Key);
									vs.UpdateSectorGeometry(us.Value);
								}
							}
						}
						
						// And update for this sector ofcourse
						if(VisualSectorExists(s))
						{
							BaseVisualSector vs = (BaseVisualSector)GetVisualSector(s);
							vs.UpdateSectorGeometry(false);
						}
					}
				}
				
				if(!sectorsmarked)
				{
					// No sectors or geometry changed. So we only have
					// to update things when they have changed.
					HashSet<Thing> toremove = new HashSet<Thing>(); //mxd
					foreach(KeyValuePair<Thing, VisualThing> vt in allthings)
						if((vt.Value != null) && vt.Key.Marked)
						{
							if(vt.Key.IsDisposed) toremove.Add(vt.Key); //mxd. Disposed things will cause problems
							else ((BaseVisualThing)vt.Value).Rebuild();
						}
					}

					//mxd. Remove disposed things
					foreach(Thing t in toremove)
					{
						if(allthings[t] != null) allthings[t].Dispose();
						allthings.Remove(t);
				}
				else
				{
					// Things depend on the sector they are in and because we can't
					// easily determine which ones changed, we dispose all things
					foreach(KeyValuePair<Thing, VisualThing> vt in allthings)
						if(vt.Value != null) vt.Value.Dispose();
					
					// Apply new lists
					allthings = new Dictionary<Thing, VisualThing>(allthings.Count);
				}
				
				// Clear visibility collections
				visiblesectors.Clear();
				visibleblocks.Clear();
				visiblegeometry.Clear();
				visiblethings.Clear();
				
				// Make new blockmap
				if(sectorsmarked || General.Map.UndoRedo.PopulationChanged || General.Map.IsChanged)
				RebuildElementData();
				UpdateChangedObjects();
				
				// Visibility culling (this re-creates the needed resources)
				DoCulling();
			}
			
			// Determine what we're aiming at now
		// Mouse moves
		public override void OnMouseMove(MouseEventArgs e)
		{
			base.OnMouseMove(e);
			IVisualEventReceiver o = GetTargetEventReceiver(true);
			o.OnMouseMove(e);

			//mxd. Show hints!
			if(o.GetType() != lasthighlighttype) 
			{
				if(General.Interface.ActiveDockerTabName == "Help") 
				{
					{
						General.Hints.ShowHints(this.GetType(), "sidedefs");
					}
					else if(o is BaseVisualGeometrySector) 
					{
						General.Hints.ShowHints(this.GetType(), "sectors");
					}
					{
						General.Hints.ShowHints(this.GetType(), "things");
					}
					{
						General.Hints.ShowHints(this.GetType(), "vertices");
					}
					else 
					{
						General.Hints.ShowHints(this.GetType(), HintsManager.GENERAL);
					}
				}

				lasthighlighttype = o.GetType();
			}

			// biwa
			if (o is NullVisualEventReceiver)
				highlighted = null;
			else if (o is VisualGeometry)
				highlighted = (VisualGeometry)o;
			else if (o is VisualThing)
				highlighted = (VisualThing)o;
		// Undo performed
		public override void OnUndoEnd()
		{
			//mxd. Effects may've become invalid
			if(sectordata != null && sectordata.Count > 0) RebuildElementData();
MaxED's avatar
MaxED committed

			//mxd. As well as geometry...
			foreach(VisualSector sector in visiblesectors)
				BaseVisualSector vs = (BaseVisualSector)sector;
			// We can't group with this undo level anymore
			lastundogroup = UndoGroup.None;
		}
		
		// Redo performed
		public override void OnRedoEnd()
		{
			base.OnRedoEnd();
MaxED's avatar
MaxED committed
			//mxd. Effects may've become invalid
			if(sectordata != null && sectordata.Count > 0) RebuildElementData();
MaxED's avatar
MaxED committed

			//mxd. As well as geometry...
			foreach(VisualSector sector in visiblesectors) 
				BaseVisualSector vs = (BaseVisualSector)sector;
		public override void OnScriptRunEnd()
		{
			base.OnScriptRunEnd();

			FillBlockMap();

			// Effects may've become invalid
			if (sectordata != null && sectordata.Count > 0) RebuildElementData();

			// As well as geometry...
			foreach (VisualSector sector in visiblesectors)
			{
				BaseVisualSector vs = (BaseVisualSector)sector;
				if (vs != null) vs.Rebuild();
			}

			RebuildSelectedObjectsList();
		}

		private void Interface_OnSectorEditFormValuesChanged(object sender, EventArgs e) 
		{
			foreach(KeyValuePair<Sector, VisualSector> vs in allsectors) 
				BaseVisualSector bvs = (BaseVisualSector)vs.Value;
MaxED's avatar
MaxED committed
				foreach(VisualFloor vf in bvs.ExtraFloors) vf.Changed = false;
				foreach(VisualCeiling vc in bvs.ExtraCeilings) vc.Changed = false;
				foreach(VisualFloor vf in bvs.ExtraBackFloors) vf.Changed = false;
				foreach(VisualCeiling vc in bvs.ExtraBackCeilings) vc.Changed = false;
				bvs.Floor.Changed = false;
				bvs.Ceiling.Changed = false;
			}

			UpdateChangedObjects();
			ShowTargetInfo();
		}

		private void Interface_OnThingEditFormValuesChanged(object sender, EventArgs e) 
		{
			//update visual sectors, which are affected by certain things
			List<Thing> things = GetSelectedThings();
			foreach(Thing t in things) 
			{
				if(thingdata.ContainsKey(t)) 
				{
					// Update what must be updated
					ThingData td = GetThingData(t);
					foreach(KeyValuePair<Sector, bool> s in td.UpdateAlso) 
					{
						if(VisualSectorExists(s.Key)) 
						{
							BaseVisualSector vs = (BaseVisualSector)GetVisualSector(s.Key);
							vs.UpdateSectorGeometry(s.Value);
						}
					}
				}
			}
			
			UpdateChangedObjects();
			ShowTargetInfo();
		}

		private void Interface_OnEditFormValuesChanged(object sender, EventArgs e) 
		{
biwa's avatar
biwa committed
		private void Interface_OnUpdateChangedObjects(object sender, EventArgs e)
		{
			UpdateChangedObjects();
		}

		//mxd
		private void SelectioninfoupdatetimerOnTick(object sender, EventArgs eventArgs) 
		{
			selectioninfoupdatetimer.Stop();
			UpdateSelectionInfo();
		}
		#region ================== Action Assist

		// Because some actions can only be called on a single (the targeted) object because
		// they show a dialog window or something, these functions help applying the result
		// to all compatible selected objects.
		
		// Apply texture offsets
		public void ApplyTextureOffsetChange(int dx, int dy)
		{
biwa's avatar
biwa committed
			List<IVisualEventReceiver> objs = GetSelectedObjects(false, true, false, false, false);
			
			//mxd. Because Upper/Middle/Lower textures offsets should be threated separately in UDMF
			//MaxW. But they're not for Eternity, so this needs its own config setting
			if(General.Map.UDMF && General.Map.Config.UseLocalSidedefTextureOffsets)
				HashSet<BaseVisualGeometrySidedef> donesides = new HashSet<BaseVisualGeometrySidedef>();
				foreach(IVisualEventReceiver i in objs) 
				{
					BaseVisualGeometrySidedef vs = (BaseVisualGeometrySidedef)i; //mxd
					if(!donesides.Contains(vs)) 
					{
						//mxd. added scaling by texture scale
						if(vs.Texture.UsedInMap) //mxd. Otherwise it's MissingTexture3D and we probably don't want to drag that
							vs.OnChangeTextureOffset((int)(dx / vs.Texture.Scale.x), (int)(dy / vs.Texture.Scale.y), false);
				HashSet<Sidedef> donesides = new HashSet<Sidedef>();
				foreach(IVisualEventReceiver i in objs) 
					BaseVisualGeometrySidedef vs = (BaseVisualGeometrySidedef)i; //mxd
					if(!donesides.Contains(vs.Sidedef)) 
						//mxd. added scaling by texture scale
						if(vs.Texture.UsedInMap) //mxd. Otherwise it's MissingTexture3D and we probably don't want to drag that
							vs.OnChangeTextureOffset((int)(dx / vs.Texture.Scale.x), (int)(dy / vs.Texture.Scale.y), false);
		// Apply flat offsets
		public void ApplyFlatOffsetChange(int dx, int dy)
		{
			HashSet<int> donesectors = new HashSet<int>();
biwa's avatar
biwa committed
			List<IVisualEventReceiver> objs = GetSelectedObjects(true, false, false, false, false);
			foreach(IVisualEventReceiver i in objs)
			{
				BaseVisualGeometrySector bvs = (BaseVisualGeometrySector)i;
				if(bvs != null && !donesectors.Contains(bvs.Sector.Sector.Index))
					//mxd. Sector surface belongs to 3d-floor?
					if(bvs.Level.sector.Index != bvs.Sector.Sector.Index)
					{
						// Don't update control sector several times
						if(!donesectors.Contains(bvs.Level.sector.Index))
						{
							// Update the offsets
							bvs.OnChangeTextureOffset(dx, dy, false);

							// Update control sector
							SectorData sd = GetSectorData(bvs.Level.sector);
							sd.Update();
							BaseVisualSector vs = (BaseVisualSector)GetVisualSector(bvs.Level.sector);
							vs.Rebuild();

							// Add to collection
							donesectors.Add(bvs.Level.sector.Index);

							// Update 3d-floors
							List<Sector> updatealso = new List<Sector>(sd.UpdateAlso.Keys);
							foreach(Sector other in updatealso)
Loading
Loading full blame...