mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-12-02 08:32:55 +00:00
4389870162
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.
221 lines
7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|