#region ================== Copyright (c) 2026 Boris Iwanski /* * Copyright (c) 2026 Boris Iwanski * * This file is part of Ultimate Doom Builder. * * Ultimate Doom Builder is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * Ultimate Doom Builder 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. * * You should have received a copy of the GNU General Public License along with * Ultimate Doom Builder. If not, see . * */ #endregion #region ================== Namespaces using CodeImp.DoomBuilder.BlockmapExplorer.Controls; using CodeImp.DoomBuilder.Controls; using CodeImp.DoomBuilder.Editing; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.Rendering; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows.Forms; #endregion namespace CodeImp.DoomBuilder.BlockmapExplorer { [EditMode(DisplayName = "Blockmap Explorer Mode", SwitchAction = "blockmapexplorermode", // Action name used to switch to this mode ButtonImage = "blockmap.png", // Image resource name for the button ButtonOrder = int.MinValue + 504, // Position of the button (lower is more to the bottom) ButtonGroup = "000_editing", SupportedMapFormats = new[] { "DoomMapSetIO", "HexenMapSetIO" }, UseByDefault = true, Volatile = true)] public class BlockmapExplorerMode : ClassicMode { #region ================== Constants private const float LINE_LENGTH_SCALER = 0.001f; #endregion #region ================== Variables // Highlighted items private int highlightedBlockRow; private int highlightedBlockCol; private FlatVertex[] overlayGeometry; private FlatVertex[] questionableBlocksOverlayGeometry; private BlockmapData blockmapData; private bool showSharedBlocks; private BlockmapExplorerDocker panel; private Docker docker; #endregion #region ================== Constructor / Disposer //mxd public BlockmapExplorerMode() { // Do something } #endregion #region ================== Methods [MethodImpl(MethodImplOptions.AggressiveInlining)] private short GetShort(byte[] data, int pos) => (short)(data[pos] | (data[pos + 1] << 8)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private ushort GetUShort(byte[] data, int pos) => (ushort)(data[pos] | (data[pos + 1] << 8)); private bool LoadBlockmapData() { if (!General.Map.LumpExists("BLOCKMAP")) { General.ToastManager.ShowToast(ToastMessages.BLOCKMAPEXPLORER, ToastType.ERROR, "Failed to engage Blockmap Explorer Mode", "Map has no BLOCKMAP lump."); General.Editing.CancelMode(); return false; } MemoryStream ms = General.Map.GetLumpData("BLOCKMAP"); if (ms.Length == 0) { General.ToastManager.ShowToast(ToastMessages.BLOCKMAPEXPLORER, ToastType.ERROR, "Failed to engage Blockmap Explorer Mode", "BLOCKMAP lump is empty."); General.Editing.CancelMode(); return false; } byte[] data = ms.ToArray(); ms.Close(); // Read the header short xOffset = GetShort(data, 0); short yOffset = GetShort(data, 2); short numCols = GetShort(data, 4); short numRows = GetShort(data, 6); blockmapData = new BlockmapData { Offset = new Vector2D(xOffset, yOffset), NumCols = numCols, NumRows = numRows, Blocks = new ConcurrentDictionary(), BlockPointers = new int[numCols, numRows], LumpSize = data.Length }; int[] blockListOffsets = new int[numCols * numRows]; // Read the block list offsets for (int i = 0; i < numCols * numRows; i++) { // The offset is given in number of shorts, so multiply by 2 to get the actual byte offset blockListOffsets[i] = GetUShort(data, 8 + i * 2) * 2; } bool success = true; // Read the block lists. Done in parallel since loading large, overflown blocklists can take a while otherwise Parallel.For(0, numCols, col => { try { for (int row = numRows - 1; row >= 0; row--) { int offset = blockListOffsets[row * numCols + col]; if (offset > data.Length) { DebugConsole.WriteLine($"BLOCKMAP lump is malformed, block list offset for block ({col}, {row}) points beyond the end of the lump. Offset is {offset}, but lump is only {data.Length} bytes long."); blockmapData.BlockPointers[col, row] = -1; continue; } // It's not a sublist if either // - the offset is right at the end of the header + blocklist offsets // - the short before the offset is 0xFFFF // Also check that the offset is greater than 2 to avoid reading before the start of the lump in case of a malformed lump with an offset of 0 or 1 bool isSublist = (offset > 2) && !((offset == 8 + 2 * numCols * numRows) || GetUShort(data, offset-2) == 0xFFFF); // Arleady add a dummy entry to the dictionary to prevent multiple threads from processing the same blocklist if (blockmapData.Blocks.TryAdd(offset, null)) { int pos = offset; HashSet uniqueIndices = new HashSet(); while (true) { ushort index = GetUShort(data, pos); // (ushort)(data[pos] | (data[pos + 1] << 8)); pos += 2; // 0xFFFF (-1 in signed short) indicates the end of the block list if (index == 0xFFFF) break; uniqueIndices.Add(index); } blockmapData.Blocks[offset] = new BlockmapBlockList(uniqueIndices, isSublist); } blockmapData.BlockPointers[col, row] = offset; } } catch (IndexOutOfRangeException) { success = false; } }); if (!success) { General.ToastManager.ShowToast(ToastMessages.BLOCKMAPEXPLORER, ToastType.ERROR, "Failed to engage Blockmap Explorer Mode", "BLOCKMAP lump is malformed, tried to read beyond the end of the lump. Some block lists may not have been loaded."); General.Editing.CancelMode(); return false; } return true; } private void UpdateOverlayGeometry() { if(highlightedBlockCol == -1 || highlightedBlockRow == -1) { overlayGeometry = new FlatVertex[0]; return; } // Create darkened highlight color to use for the highlighted block and the blocks sharing the same block list (if enabled) PixelColor highlightColor = General.Colors.Highlight; highlightColor.r /= 2; highlightColor.g /= 2; highlightColor.b /= 2; int color = highlightColor.ToInt(); // Get the column and row of the highlighted block, and if enabled, all blocks sharing the same block list List<(int, int)> blocks = showSharedBlocks ? blockmapData.GetSharedBlocks(highlightedBlockCol, highlightedBlockRow) : new List<(int, int)> { (highlightedBlockCol, highlightedBlockRow) }; overlayGeometry = new FlatVertex[blocks.Count * 6]; for (int i=0; i < blocks.Count; i++) { (int col, int row) = blocks[i]; overlayGeometry[i*6].x = (float)(blockmapData.Offset.x + col * 128); overlayGeometry[i*6].y = (float)(blockmapData.Offset.y + row * 128); overlayGeometry[i*6].c = color; overlayGeometry[i*6 + 1].x = (float)(blockmapData.Offset.x + (col + 1) * 128); overlayGeometry[i*6 + 1].y = (float)(blockmapData.Offset.y + row * 128); overlayGeometry[i*6 + 1].c = color; overlayGeometry[i*6 + 2].x = (float)(blockmapData.Offset.x + (col + 1) * 128); overlayGeometry[i*6 + 2].y = (float)(blockmapData.Offset.y + (row + 1) * 128); overlayGeometry[i*6 + 2].c = color; overlayGeometry[i*6 + 3].x = (float)(blockmapData.Offset.x + col * 128); overlayGeometry[i*6 + 3].y = (float)(blockmapData.Offset.y + row * 128); overlayGeometry[i*6 + 3].c = color; overlayGeometry[i*6 + 4].x = (float)(blockmapData.Offset.x + (col + 1) * 128); overlayGeometry[i*6 + 4].y = (float)(blockmapData.Offset.y + (row + 1) * 128); overlayGeometry[i*6 + 4].c = color; overlayGeometry[i*6 + 5].x = (float)(blockmapData.Offset.x + col * 128); overlayGeometry[i*6 + 5].y = (float)(blockmapData.Offset.y + (row + 1) * 128); overlayGeometry[i*6 + 5].c = color; } } private void CreateQuestionableBlockOverlay() { PixelColor highlightColor = General.Colors.Selection; highlightColor.r /= 4; highlightColor.g /= 4; highlightColor.b /= 4; int color = highlightColor.ToInt(); // If enabled, get all blocks that have questionable offsets List<(int, int)> blocks = panel.ShowQuestionableBlocks.Checked ? blockmapData.GetQuestionableBlocks() : new List<(int, int)>(); questionableBlocksOverlayGeometry = new FlatVertex[blocks.Count * 6]; for (int i = 0; i < blocks.Count; i++) { (int col, int row) = blocks[i]; questionableBlocksOverlayGeometry[i * 6].x = (float)(blockmapData.Offset.x + col * 128); questionableBlocksOverlayGeometry[i * 6].y = (float)(blockmapData.Offset.y + row * 128); questionableBlocksOverlayGeometry[i * 6].c = color; questionableBlocksOverlayGeometry[i * 6 + 1].x = (float)(blockmapData.Offset.x + (col + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 1].y = (float)(blockmapData.Offset.y + row * 128); questionableBlocksOverlayGeometry[i * 6 + 1].c = color; questionableBlocksOverlayGeometry[i * 6 + 2].x = (float)(blockmapData.Offset.x + (col + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 2].y = (float)(blockmapData.Offset.y + (row + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 2].c = color; questionableBlocksOverlayGeometry[i * 6 + 3].x = (float)(blockmapData.Offset.x + col * 128); questionableBlocksOverlayGeometry[i * 6 + 3].y = (float)(blockmapData.Offset.y + row * 128); questionableBlocksOverlayGeometry[i * 6 + 3].c = color; questionableBlocksOverlayGeometry[i * 6 + 4].x = (float)(blockmapData.Offset.x + (col + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 4].y = (float)(blockmapData.Offset.y + (row + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 4].c = color; questionableBlocksOverlayGeometry[i * 6 + 5].x = (float)(blockmapData.Offset.x + col * 128); questionableBlocksOverlayGeometry[i * 6 + 5].y = (float)(blockmapData.Offset.y + (row + 1) * 128); questionableBlocksOverlayGeometry[i * 6 + 5].c = color; } } private void UpdateOptions() { showSharedBlocks = General.Interface.ShiftState; UpdateOverlayGeometry(); General.Interface.RedrawDisplay(); } #endregion #region ================== Events public override void OnHelp() { General.ShowHelp("/gzdb/features/classic_modes/mode_blockmapexplorer.html"); } // Cancel mode public override void OnCancel() { base.OnCancel(); // Return to this mode General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name); } // Mode engages public override void OnEngage() { base.OnEngage(); if (General.Map.IsChanged) { General.ToastManager.ShowToast(ToastMessages.BLOCKMAPEXPLORER, ToastType.INFO, "Blockmap Explorer", "Map was changed, rebuilding nodes with testing settings."); // We need to build the nodes! if (!General.Map.RebuildNodes(General.Map.ConfigSettings.NodebuilderTest, true)) { General.ToastManager.ShowToast(ToastMessages.BLOCKMAPEXPLORER, ToastType.ERROR, "Failed to engage Blockmap Explorer Mode", "Failed to rebuild the nodes.", "Failed to engage Blockmap Explorer Mode: failed to rebuild the nodes"); General.Editing.CancelMode(); return; } } panel = new BlockmapExplorerDocker(); docker = new Docker("blockmapexplorer", "Blockmap Explorer", panel); General.Interface.AddDocker(docker); General.Interface.SelectDocker(docker); panel.ShowQuestionableBlocks.CheckedChanged += (s, e) => { CreateQuestionableBlockOverlay(); General.Interface.RedrawDisplay(); }; CustomPresentation presentation = new CustomPresentation(); presentation.AddLayer(new PresentLayer(RendererLayer.Overlay, BlendingMode.Alpha, 1.0f, false)); presentation.AddLayer(new PresentLayer(RendererLayer.Geometry, BlendingMode.Alpha, 1.0f, true)); renderer.SetPresentation(presentation); if (LoadBlockmapData()) { CreateQuestionableBlockOverlay(); panel.SetInfo( blockmapData.NumRows * blockmapData.NumCols, blockmapData.Blocks.Count, blockmapData.GetQuestionableOffsetCount(), blockmapData.NumCols, blockmapData.NumRows, blockmapData.GetLinesNotInBlocksCount(General.Map.Map.Linedefs), blockmapData.LumpSize ); } } // Mode disengages public override void OnDisengage() { base.OnDisengage(); General.Interface.RemoveDocker(docker); // Hide highlight info General.Interface.HideInfo(); } public override void OnUndoEnd() { base.OnUndoEnd(); General.Editing.CancelVolatileMode(); } public override void OnRedoEnd() { base.OnRedoEnd(); General.Editing.CancelVolatileMode(); } // This redraws the display public override void OnRedrawDisplay() { renderer.RedrawSurface(); // Render lines if (renderer.StartPlotter(true)) { int counter = 0; List lines = blockmapData.GetLinesInBlock(highlightedBlockCol, highlightedBlockRow); foreach (Linedef ld in General.Map.Map.Linedefs) { // Draw the line in the normal color when it's the next line in the list of lines in the highlighted block, otherwise draw it darkened. // This requires that the lines in the list are sorted (which is done in the BlockmapData constructor) if (counter < lines.Count && ld.Index == lines[counter]) { renderer.PlotLine(ld.Start.Position, ld.End.Position, renderer.DetermineLinedefColor(ld), LINE_LENGTH_SCALER); counter++; } else { PixelColor ldc = renderer.DetermineLinedefColor(ld); PixelColor pc = new PixelColor(ldc.a, (byte)(ldc.r / 3), (byte)(ldc.g / 3), (byte)(ldc.b / 3)); renderer.PlotLine(ld.Start.Position, ld.End.Position, pc, LINE_LENGTH_SCALER); } } renderer.Finish(); } if (renderer.StartOverlay(true)) { // Render questionable blocks overlay if (panel.ShowQuestionableBlocks.Checked) renderer.RenderGeometry(questionableBlocksOverlayGeometry, null, true); // Render the highlighted block and all blocks sharing the same block list (if enabled) if (overlayGeometry != null && highlightedBlockCol != -1 && highlightedBlockRow != -1) renderer.RenderGeometry(overlayGeometry, null, true); // Draw blockmap grid. This has to be done last to ensure it's on top of everything and not partially covered by the highlighted block overlay for (int row = 0; row <= blockmapData.NumRows; row++) renderer.RenderLine(blockmapData.Offset + new Vector2D(0, row * 128), blockmapData.Offset + new Vector2D(blockmapData.NumCols * 128, row * 128), 0.5f, new PixelColor(255, 32, 32, 32), true); for (int col = 0; col <= blockmapData.NumCols; col++) renderer.RenderLine(blockmapData.Offset + new Vector2D(col * 128, 0), blockmapData.Offset + new Vector2D(col * 128, blockmapData.NumRows * 128), 0.5f, new PixelColor(255, 32, 32, 32), true); renderer.Finish(); } renderer.Present(); } // Mouse moves public override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); int oldRow = highlightedBlockRow; int oldCol = highlightedBlockCol; (highlightedBlockRow, highlightedBlockCol) = blockmapData.GetColumnAndRowByPosition(mousemappos); if(oldCol != highlightedBlockCol || oldRow != highlightedBlockRow) { UpdateOverlayGeometry(); if (highlightedBlockCol != -1 && highlightedBlockRow != -1) panel.SetBlockInfo(highlightedBlockCol, highlightedBlockRow, blockmapData); else panel.ClearBlockInfo(); General.Interface.RedrawDisplay(); } } // Mouse leaves public override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); highlightedBlockRow = highlightedBlockCol = -1; panel.ClearBlockInfo(); General.Interface.RedrawDisplay(); } public override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); UpdateOptions(); } public override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); UpdateOptions(); } #endregion } }