UltimateZoneBuilder/Source/Plugins/VisplaneExplorer/VisplaneExplorerMode.cs

568 lines
17 KiB
C#
Executable file

#region === Copyright (c) 2010 Pascal van der Heiden ===
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.IO;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.Windows;
#endregion
namespace CodeImp.DoomBuilder.Plugins.VisplaneExplorer
{
[EditMode(DisplayName = "Visplane Explorer",
SwitchAction = "visplaneexplorermode",
ButtonImage = "Gauge.png",
ButtonOrder = 300,
ButtonGroup = "002_tools",
Volatile = true,
UseByDefault = true,
SupportedMapFormats = new[] { "DoomMapSetIO", "HexenMapSetIO" }, //mxd
AllowCopyPaste = false)]
public class VisplaneExplorerMode : ClassicMode
{
#region ================== Variables
// The image is the ImageData resource for Doom Builder to work with
private DynamicBitmapImage image;
// This is the bitmap that we will be drawing on
private Bitmap canvas;
private ViewStats lastviewstats;
// Temporary WAD file written for the vpo.dll library
private string tempfile;
// Rectangle around the map
private Rectangle mapbounds;
// 64x64 tiles in map space. These are discarded when outside view.
private Dictionary<Point, Tile> tiles = new Dictionary<Point, Tile>();
// Time when to do another update
private long nextupdate;
// Are we processing?
private bool processingenabled;
#endregion
#region ================== Methods
// This cleans up anything we used for this mode
private void CleanUp()
{
BuilderPlug.VPO.Stop();
if(processingenabled)
{
General.Interface.DisableProcessing();
processingenabled = false;
}
if(!string.IsNullOrEmpty(tempfile))
{
File.Delete(tempfile);
tempfile = null;
}
if(image != null)
{
image.Dispose();
image = null;
}
if(canvas != null)
{
canvas.Dispose();
canvas = null;
}
tiles.Clear();
BuilderPlug.InterfaceForm.HideTooltip();
BuilderPlug.InterfaceForm.RemoveFromInterface();
}
// This returns the tile position for the given map coordinate
private static Point TileForPoint(float x, float y)
{
return new Point((int)Math.Floor(x / Tile.TILE_SIZE) * Tile.TILE_SIZE, (int)Math.Floor(y / Tile.TILE_SIZE) * Tile.TILE_SIZE);
}
// This draws all tiles on the image
// THIS MUST BE FAST! TOP PERFORMANCE REQUIRED!
private unsafe void RedrawAllTiles()
{
if(canvas == null) return;
// Determine viewport rectangle in map space
Vector2D mapleftbot = Renderer.DisplayToMap(new Vector2D(0f, 0f));
Vector2D maprighttop = Renderer.DisplayToMap(new Vector2D(General.Interface.Display.ClientSize.Width, General.Interface.Display.ClientSize.Height));
Rectangle mapviewrect = new Rectangle((int)mapleftbot.x - Tile.TILE_SIZE, (int)maprighttop.y - Tile.TILE_SIZE, (int)maprighttop.x - (int)mapleftbot.x + Tile.TILE_SIZE, (int)mapleftbot.y - (int)maprighttop.y + Tile.TILE_SIZE);
int viewstats = (int)BuilderPlug.InterfaceForm.ViewStats;
Palette pal = (BuilderPlug.InterfaceForm.ShowHeatmap ? BuilderPlug.Palettes[(int)ViewStats.Heatmap] : BuilderPlug.Palettes[viewstats]);
BitmapData bd = canvas.LockBits(new Rectangle(0, 0, canvas.Size.Width, canvas.Size.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
uint* p = (uint*)bd.Scan0.ToPointer();
int count = bd.Width * bd.Height;
for (int i = 0; i < count; i++)
{
p[i] = 0;
}
foreach(KeyValuePair<Point, Tile> t in tiles)
{
if(!mapviewrect.Contains(t.Key)) continue;
// Map this tile to screen space
Vector2D lb = Renderer.MapToDisplay(new Vector2D(t.Value.Position.X, t.Value.Position.Y));
Vector2D rt = Renderer.MapToDisplay(new Vector2D(t.Value.Position.X + Tile.TILE_SIZE, t.Value.Position.Y + Tile.TILE_SIZE));
// Make sure the coordinates are aligned with canvas pixels
float x1 = (float)Math.Round(lb.x);
float x2 = (float)Math.Round(rt.x);
float y1 = (float)Math.Round(rt.y);
float y2 = (float)Math.Round(lb.y);
// Determine width and height of the screen space area for this tile
float w = x2 - x1;
float h = y2 - y1;
float winv = 1f / w;
float hinv = 1f / h;
// Loop ranges. These are relative to the left-top of the tile.
float sx = 0f;
float sy = 0f;
float ex = w;
float ey = h;
int screenx = (int)x1;
int screenystart = (int)y1;
// Clipping the loop ranges against canvas boundary.
if(x1 < 0f) { sx = -x1; screenx = 0; }
if(y1 < 0f) { sy = -y1; screenystart = 0; }
if(x2 > bd.Width) ex = w - (x2 - bd.Width);
if(y2 > bd.Height) ey = h - (y2 - bd.Height);
// Draw all pixels within this tile
for(float x = sx; x < ex; x++, screenx++)
{
int screeny = screenystart;
for(float y = sy; y < ey; y++, screeny++)
{
// Calculate the relative offset in map coordinates for this pixel
float ux = x * winv * Tile.TILE_SIZE;
float uy = y * hinv * Tile.TILE_SIZE;
// Get the data and apply the color
byte value = t.Value.GetHeatmapByte((int)ux, Tile.TILE_SIZE - 1 - (int)uy, viewstats);
p[screeny * bd.Width + screenx] = (uint)pal.Colors[value];
}
}
}
canvas.UnlockBits(bd);
image.UpdateTexture(canvas);
}
// This queues points for all current tiles
private void QueuePoints(int pointsleft)
{
// Determine viewport rectangle in map space
Vector2D mapleftbot = Renderer.DisplayToMap(new Vector2D(0f, 0f));
Vector2D maprighttop = Renderer.DisplayToMap(new Vector2D(General.Interface.Display.ClientSize.Width, General.Interface.Display.ClientSize.Height));
Rectangle mapviewrect = new Rectangle((int)mapleftbot.x - Tile.TILE_SIZE, (int)maprighttop.y - Tile.TILE_SIZE, (int)maprighttop.x - (int)mapleftbot.x + Tile.TILE_SIZE, (int)mapleftbot.y - (int)maprighttop.y + Tile.TILE_SIZE);
while(pointsleft < (VPOManager.POINTS_PER_ITERATION * BuilderPlug.VPO.NumThreads * 5))
{
// Collect points from the tiles in the current view
List<TilePoint> newpoints = new List<TilePoint>(tiles.Count);
foreach(KeyValuePair<Point, Tile> t in tiles)
if((!t.Value.IsComplete) && (mapviewrect.Contains(t.Key))) newpoints.Add(t.Value.GetNextPoint());
// If the current view is complete, try getting points from all tiles
if(newpoints.Count == 0)
{
foreach(KeyValuePair<Point, Tile> t in tiles)
if(!t.Value.IsComplete) newpoints.Add(t.Value.GetNextPoint());
}
if(newpoints.Count == 0) break;
pointsleft = BuilderPlug.VPO.EnqueuePoints(newpoints);
}
}
// This updates the overlay
private void UpdateOverlay()
{
// We must redraw the tiles to the canvas when the stats to view has changed
if(lastviewstats != BuilderPlug.InterfaceForm.ViewStats)
{
RedrawAllTiles();
lastviewstats = BuilderPlug.InterfaceForm.ViewStats;
}
// Render the overlay
if(renderer.StartOverlay(true))
{
// Render the canvas to screen
RectangleF r = new RectangleF(0, 0, canvas.Width, canvas.Height);
renderer.RenderRectangleFilled(r, PixelColor.FromColor(Color.White), false, image);
// Render any selection
if(selecting) RenderMultiSelection();
// Finish our rendering to this layer.
renderer.Finish();
}
}
/// <summary>
/// Checks if the given map is valid for the Visplane Explorer Mode. Specifically it may not have
/// ZDBSP nodes. See https://github.com/jewalky/UltimateDoomBuilder/issues/736
/// </summary>
/// <param name="file">Full path and file name of the WAD to check. Only the first map is checked</param>
/// <returns>true if the check was successful, false if there was a problem. Also returns a message in case something is wrong</returns>
private (bool, string) CheckMapValidity(string file)
{
WAD wad = new WAD(file, true);
try
{
Lump nodes = wad.FindLump("NODES");
if (nodes == null)
throw new Exception("NODES lump not found");
Stream stream = nodes.GetSafeStream();
if (stream.Length == 0)
throw new Exception("NODES lump is empty");
using (BinaryReader reader = new BinaryReader(stream))
{
List<byte[]> formats = new List<byte[]> { Encoding.ASCII.GetBytes("ZNOD"), Encoding.ASCII.GetBytes("XNOD") };
byte[] format = reader.ReadBytes(4);
if(formats.Any(f => f.SequenceEqual(format)))
throw new Exception("ZDBSP nodes detected. This format is not supporeted by the Visplane Explorer Mode");
}
}
catch(Exception e)
{
return (false, e.Message);
}
finally
{
// Get rid of the WAD ASAP. If something failed and we don't do this the WAD can't be deleted by the
// cleanup method
wad.Dispose();
}
return (true, string.Empty);
}
#endregion
#region ================== Events
// Mode starts
public override void OnEngage()
{
Cursor.Current = Cursors.WaitCursor;
base.OnEngage();
General.Interface.DisplayStatus(StatusType.Busy, "Setting up test environment...");
BuilderPlug.InitVPO(); //mxd
CleanUp();
BuilderPlug.InterfaceForm.AddToInterface();
BuilderPlug.InterfaceForm.OnVisplaneSettingsChanged += OnVisplaneSettingsChanged; //mxd
lastviewstats = BuilderPlug.InterfaceForm.ViewStats;
// Export the current map to a temporary WAD file
tempfile = BuilderPlug.MakeTempFilename(".wad");
if(!General.Map.ExportToFile(tempfile))
{
//mxd. Abort on export fail
Cursor.Current = Cursors.Default;
General.Interface.DisplayStatus(StatusType.Warning, "Unable to set test environment...");
OnCancel();
return;
}
(bool mapvalid, string message) = CheckMapValidity(tempfile);
if(!mapvalid)
{
MessageBox.Show($"Error: {message}.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
General.Editing.CancelMode();
return;
}
// Load the map in VPO_DLL
BuilderPlug.VPO.Start(tempfile, General.Map.Options.LevelName);
// Determine map boundary
mapbounds = Rectangle.Round(MapSet.CreateArea(General.Map.Map.Vertices));
// Create tiles for all points inside the map
CreateTiles(); //mxd
QueuePoints(0);
// Make an image to draw on.
// The BitmapImage for Doom Builder's resources must be Format32bppArgb and NOT using color correction,
// otherwise DB will make a copy of the bitmap when LoadImage() is called! This is normally not a problem,
// but we want to keep drawing to the same bitmap.
int width = General.NextPowerOf2(General.Interface.Display.ClientSize.Width);
int height = General.NextPowerOf2(General.Interface.Display.ClientSize.Height);
canvas = new Bitmap(width, height, PixelFormat.Format32bppArgb);
image = new DynamicBitmapImage(canvas, "_CANVAS_");
image.UseColorCorrection = false;
image.MipMapLevels = 1;
image.LoadImageNow();
// Make custom presentation
CustomPresentation p = new CustomPresentation();
p.AddLayer(new PresentLayer(RendererLayer.Overlay, BlendingMode.Mask, 1f, false));
p.AddLayer(new PresentLayer(RendererLayer.Grid, BlendingMode.Mask));
p.AddLayer(new PresentLayer(RendererLayer.Geometry, BlendingMode.Alpha, 1f, true));
renderer.SetPresentation(p);
// Setup processing
nextupdate = Clock.CurrentTime + 100;
General.Interface.EnableProcessing();
processingenabled = true;
RedrawAllTiles();
Cursor.Current = Cursors.Default;
General.Interface.SetCursor(Cursors.Cross);
General.Interface.DisplayReady();
}
//mxd
private void CreateTiles()
{
Point lt = TileForPoint(mapbounds.Left - Tile.TILE_SIZE, mapbounds.Top - Tile.TILE_SIZE);
Point rb = TileForPoint(mapbounds.Right + Tile.TILE_SIZE, mapbounds.Bottom + Tile.TILE_SIZE);
Rectangle tilesrect = new Rectangle(lt.X, lt.Y, rb.X - lt.X, rb.Y - lt.Y);
NearestLineBlockmap blockmap = new NearestLineBlockmap(tilesrect);
for(int x = tilesrect.X; x <= tilesrect.Right; x += Tile.TILE_SIZE)
{
for(int y = tilesrect.Y; y <= tilesrect.Bottom; y += Tile.TILE_SIZE)
{
// If the tile is obviously outside the map, don't create it
Vector2D pc = new Vector2D(x + (Tile.TILE_SIZE >> 1), y + (Tile.TILE_SIZE >> 1));
Linedef ld = MapSet.NearestLinedef(blockmap.GetBlockAt(pc).Lines, pc);
double distancesq = ld.DistanceToSq(pc, true);
if(distancesq > (Tile.TILE_SIZE * Tile.TILE_SIZE))
{
double side = ld.SideOfLine(pc);
if((side > 0.0f) && (ld.Back == null))
continue;
}
Point tp = new Point(x, y);
tiles.Add(tp, new Tile(tp));
}
}
}
// Mode ends
public override void OnDisengage()
{
CleanUp();
BuilderPlug.InterfaceForm.OnVisplaneSettingsChanged -= OnVisplaneSettingsChanged;
base.OnDisengage();
}
// Cancelled
public override void OnCancel()
{
// Cancel base class
base.OnCancel();
// Return to previous mode
General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name);
}
// View position/scale changed!
protected override void OnViewChanged()
{
base.OnViewChanged();
RedrawAllTiles();
// Update the screen sooner
nextupdate = Clock.CurrentTime + 100;
}
// Draw the display
public override void OnRedrawDisplay()
{
base.OnRedrawDisplay();
// Render the overlay
UpdateOverlay();
// Render lines and vertices
if(renderer.StartPlotter(true))
{
renderer.PlotLinedefSet(General.Map.Map.Linedefs);
renderer.PlotVerticesSet(General.Map.Map.Vertices);
renderer.Finish();
}
renderer.Present();
}
// Processing
public override void OnProcess(long deltatime)
{
base.OnProcess(deltatime);
if(Clock.CurrentTime >= nextupdate)
{
// Get the processed points from the VPO manager
List<PointData> points = new List<PointData>();
int pointsleft = BuilderPlug.VPO.DequeueResults(points);
// Queue more points if needed
QueuePoints(pointsleft);
// Apply the points to the tiles
foreach(PointData pd in points)
{
Tile t;
Point tp = TileForPoint(pd.point.x, pd.point.y);
if(tiles.TryGetValue(tp, out t))
t.StorePointData(pd);
}
// Redraw
RedrawAllTiles();
General.Interface.RedrawDisplay();
nextupdate = Clock.CurrentTime + 500;
}
else
{
// Queue more points if needed
QueuePoints(BuilderPlug.VPO.GetRemainingPoints());
}
}
// LMB pressed
protected override void OnSelectBegin()
{
StartMultiSelection();
BuilderPlug.InterfaceForm.HideTooltip();
base.OnSelectBegin();
}
// Multiselecting
protected override void OnUpdateMultiSelection()
{
base.OnUpdateMultiSelection();
UpdateOverlay();
renderer.Present();
}
// Multiselect ends
protected override void OnEndMultiSelection()
{
base.OnEndMultiSelection();
if((selectionrect.Width < 64f) && (selectionrect.Height < 64f))
selectionrect.Inflate(64f, 64f);
base.CenterOnArea(selectionrect, 0.1f);
}
// Mouse moves
public override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if(!selecting)
{
int viewstats = (int)BuilderPlug.InterfaceForm.ViewStats;
// Get the tile data for the current position
Point tp = TileForPoint((float)mousemappos.x, (float)mousemappos.y);
Tile t;
if(tiles.TryGetValue(tp, out t))
{
int x = (int)Math.Floor(mousemappos.x) - t.Position.X;
int y = (int)Math.Floor(mousemappos.y) - t.Position.Y;
byte b = t.GetPointByte(x, y, viewstats);
if(b != Tile.POINT_VOID_B)
{
// Setup hoverlabel
int value = t.GetPointValue(x, y, viewstats);
Point p = new Point((int)mousepos.x + 5, (int)mousepos.y + 5);
string appendoverflow = (b == Tile.POINT_OVERFLOW_B) ? "+" : "";
BuilderPlug.InterfaceForm.ShowTooltip(value + appendoverflow + " / " + StaticLimit(BuilderPlug.InterfaceForm.ViewStats), p);
}
else
{
// Void
BuilderPlug.InterfaceForm.HideTooltip();
}
}
else
{
BuilderPlug.InterfaceForm.HideTooltip();
}
}
}
// Mouse leaves
public override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
BuilderPlug.InterfaceForm.HideTooltip();
}
//mxd
private void OnVisplaneSettingsChanged(object sender, EventArgs e)
{
// Restart processing
BuilderPlug.VPO.Stop();
tiles.Clear();
CreateTiles();
BuilderPlug.VPO.Start(tempfile, General.Map.Options.LevelName);
General.Interface.RedrawDisplay();
}
// Get the configured static limit for the given stat.
private uint StaticLimit(ViewStats stat)
{
switch (stat)
{
case ViewStats.Visplanes:
return General.Map.Config.StaticLimits.Visplanes;
case ViewStats.Drawsegs:
return General.Map.Config.StaticLimits.Drawsegs;
case ViewStats.Solidsegs:
return General.Map.Config.StaticLimits.Solidsegs;
case ViewStats.Openings:
return General.Map.Config.StaticLimits.Openings;
default:
return 0;
}
}
#endregion
}
}