UltimateZoneBuilder/Source/Plugins/SoundPropagationMode/LeakFinder.cs
biwa 4389870162
Sound Propagation Mode: added sound leak finder (#944)
In Sound Propagation Mode you can set the start (default: Shift+S) and end (default: Shift+E) sectors between which a path the sound can travel will be found and displayed.
2023-08-13 21:19:25 +02:00

221 lines
7 KiB
C#

#region ================== Copyright (c) 2023 Boris Iwanski
/*
* This program 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.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program.If not, see<http://www.gnu.org/licenses/>.
*/
#endregion
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Map;
namespace CodeImp.DoomBuilder.SoundPropagationMode
{
internal class LeakFinder
{
public SoundNode Start { get; }
public SoundNode End { get; }
public List<SoundNode> Nodes { get; }
public HashSet<Sector> Sectors { get; }
public bool Finished { get; internal set; }
private ConcurrentDictionary<Linedef, SoundNode> linedefs2nodes;
private int numblockingnodes;
public LeakFinder(Sector source, Vector2D sourceposition, Sector destination, Vector2D destinationposition, HashSet<Sector> sectors)
{
if (!sectors.Contains(source) || !sectors.Contains(destination))
throw new ArgumentException("Sound propagation domain does not contain both the start and end sectors");
End = new SoundNode(destinationposition);
Start = new SoundNode(sourceposition, End) { G = 0 };
Sectors = sectors;
Finished = false;
Nodes = new List<SoundNode>() { Start, End };
linedefs2nodes = new ConcurrentDictionary<Linedef, SoundNode>();
GenerateNodes(sectors);
PopulateStartEndNeighbors(source, Start);
PopulateStartEndNeighbors(destination, End);
}
/// <summary>
/// Checks if the linedef is valid for passing sound.
/// </summary>
/// <param name="linedef">The linedef to check</param>
/// <returns>true if sound can travel through the linedef, false if not</returns>
private bool CheckLinedefValidity(Linedef linedef)
{
if (linedef.Back == null)
return false;
if (linedef.Front.Sector == linedef.Back.Sector)
return false;
if (SoundPropagationDomain.IsSoundBlockedByHeight(linedef))
return false;
return Sectors.Contains(linedef.Front.Sector) && Sectors.Contains(linedef.Back.Sector);
}
/// <summary>
/// Generates all nodes for the A* search algorithm.
/// </summary>
/// <param name="sectors">sectors to generate the nodes from</param>
private void GenerateNodes(HashSet<Sector> sectors)
{
// Create sound nodes for all valid linedefs in all given sectors
foreach(Sector s in sectors)
{
IEnumerable<Sidedef> sidedefs = s.Sidedefs.Where(sd => CheckLinedefValidity(sd.Line));
foreach(Sidedef sd in sidedefs)
{
if(!linedefs2nodes.ContainsKey(sd.Line))
{
linedefs2nodes[sd.Line] = new SoundNode(sd.Line, End);
Nodes.Add(linedefs2nodes[sd.Line]);
}
}
}
// We need the number of blocking nodes for safety checking
numblockingnodes = linedefs2nodes.Values.Count(n => n.IsBlocking);
// Set the neighbors for each node. The amount of interconnections can be very high in complex maps
// (for example there are nearly 3.9 million in Sunder map 20), so do it in parallel for speed
Parallel.ForEach(linedefs2nodes.Keys, ld =>
{
foreach (Sidedef sd in ld.Front.Sector.Sidedefs)
{
if (sd.Line != ld && CheckLinedefValidity(sd.Line))
linedefs2nodes[ld].Neighbors.Add(linedefs2nodes[sd.Line]);
}
foreach (Sidedef sd in ld.Back.Sector.Sidedefs)
{
if (sd.Line != ld && CheckLinedefValidity(sd.Line))
linedefs2nodes[ld].Neighbors.Add(linedefs2nodes[sd.Line]);
}
});
#if DEBUG
int bla = linedefs2nodes.Values.Sum(n => n.Neighbors.Count);
Console.WriteLine($"There are {linedefs2nodes.Keys.Count} nodes with {bla} interconnections.");
#endif
}
/// <summary>
/// Populates a sound node's neightbors to the linedefs of a sector. This is required for the start and end sound nodes.
/// </summary>
/// <param name="sector">The sector which linedef's sound nodes are used</param>
/// <param name="node">The sound node to add the neighbors to</param>
private void PopulateStartEndNeighbors(Sector sector, SoundNode node)
{
foreach(Sidedef sd in sector.Sidedefs)
{
if(CheckLinedefValidity(sd.Line) && linedefs2nodes.ContainsKey(sd.Line))
{
node.Neighbors.Add(linedefs2nodes[sd.Line]);
linedefs2nodes[sd.Line].Neighbors.Add(node);
}
}
}
/// <summary>
/// Finds a sound leak between the start and end sound nodes.
/// </summary>
/// <returns>true if a leak was found, false if no leak was found</returns>
public bool FindLeak()
{
Finished = false;
// Basic A* search. The twist is that sound blocking lines: we can only pass through one of them,
// and A* doesn't backtrack, so it can fail to find a path even if there is a possible one. If that
// happens we set the sound blocking node we traveled through to be ignored, and start again. We repeat
// that until a path was found, or all blocking nodes are set to be ignored (which shouldn't happen)
while (true)
{
List<SoundNode> openset = new List<SoundNode>() { Start };
while (openset.Count > 0)
{
// Find the node with the lowest F score. Doing it that way seems to be fastest
SoundNode current = openset[0];
for (int i = 1; i < openset.Count; i++)
{
if (openset[i].F < current.F)
current = openset[i];
}
// We're done if the node with the lowest F score is the end node
if (current == End)
{
Finished = true;
return true;
}
// Remove the current node from the open set
openset.Remove(current);
// Compute new values for the current node's neighbors
current.ProcessNeighbors(openset, Start);
}
// If we got here we didn't find a path. So we have to start over
int currentnumblockingnodes = 0;
// Reset all nodes
foreach(SoundNode sn in Nodes)
{
// Set the sound nodes that block sound and were visited (the G value was set to something) to be skipped.
if(sn.IsBlocking && sn.G != double.MaxValue)
{
sn.IsSkip = true;
currentnumblockingnodes++;
}
// We need to reset the sound node's G and F values
sn.Reset();
}
// All blocking sound nodes are being skipped, so no path is possible
if (currentnumblockingnodes == numblockingnodes)
{
Finished = true;
return false;
}
// Don't forget the reset the start node to its special values
Start.G = 0.0;
Start.F = Start.H;
}
}
}
}