#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. */ #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 Nodes { get; } public HashSet Sectors { get; } public bool Finished { get; internal set; } private ConcurrentDictionary linedefs2nodes; private int numblockingnodes; public LeakFinder(Sector source, Vector2D sourceposition, Sector destination, Vector2D destinationposition, HashSet 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() { Start, End }; linedefs2nodes = new ConcurrentDictionary(); GenerateNodes(sectors); PopulateStartEndNeighbors(source, Start); PopulateStartEndNeighbors(destination, End); } /// /// Checks if the linedef is valid for passing sound. /// /// The linedef to check /// true if sound can travel through the linedef, false if not 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); } /// /// Generates all nodes for the A* search algorithm. /// /// sectors to generate the nodes from private void GenerateNodes(HashSet sectors) { // Create sound nodes for all valid linedefs in all given sectors foreach(Sector s in sectors) { IEnumerable 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 } /// /// Populates a sound node's neightbors to the linedefs of a sector. This is required for the start and end sound nodes. /// /// The sector which linedef's sound nodes are used /// The sound node to add the neighbors to 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); } } } /// /// Finds a sound leak between the start and end sound nodes. /// /// true if a leak was found, false if no leak was found 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 openset = new List() { 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; } } } }