ZoneBuilder/Source/Plugins/SoundPropagationMode/BuilderPlug.cs
2023-04-26 17:17:28 +02:00

495 lines
17 KiB
C#

#region ================== Copyright (c) 2014 Boris Iwanski
/*
* Copyright (c) 2014 Boris Iwanski
* This program is released under GNU General Public License
*
* 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.
*
*/
#endregion
#region ================== Namespaces
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Plugins;
using CodeImp.DoomBuilder.Rendering;
#endregion
namespace CodeImp.DoomBuilder.SoundPropagationMode
{
//
// MANDATORY: The plug!
// This is an important class to the Doom Builder core. Every plugin must
// have exactly 1 class that inherits from Plug. When the plugin is loaded,
// this class is instantiated and used to receive events from the core.
// Make sure the class is public, because only public classes can be seen
// by the core.
//
public class BuilderPlug : Plug
{
#region ================== Constants
internal const int SOUND_ENVIROMNEMT_THING_TYPE = 9048; //mxd
internal const float LINE_LENGTH_SCALER = 0.001f; //mxd
#endregion
#region ================== Variables
// Interface
private MenusForm menusform;
// Colors
private PixelColor highlightcolor;
private PixelColor level1color;
private PixelColor level2color;
private PixelColor blocksoundcolor;
private PixelColor nosoundcolor;
private List<PixelColor> distinctcolors;
private List<Bitmap> distincticons; //mxd
private List<SoundEnvironment> soundenvironments;
private List<Linedef> blockinglinedefs;
private FlatVertex[] overlayGeometry;
private bool soundenvironmentisupdated;
private bool dataisdirty;
#endregion
#region ================== Properties
// Interface
public MenusForm MenusForm { get { return menusform; } }
internal List<Bitmap> DistinctIcons { get { return distincticons; } } //mxd
internal List<PixelColor> DistinctColors { get { return distinctcolors; } } //mxd
// Colors
public PixelColor HighlightColor { get { return highlightcolor; } set { highlightcolor = value; } }
public PixelColor Level1Color { get { return level1color; } set { level1color = value; } }
public PixelColor Level2Color { get { return level2color; } set { level2color = value; } }
public PixelColor BlockSoundColor { get { return blocksoundcolor; } set { blocksoundcolor = value; } }
public PixelColor NoSoundColor { get { return nosoundcolor; } set { nosoundcolor = value; } }
public List<SoundEnvironment> SoundEnvironments { get { return soundenvironments; } set { soundenvironments = value; } }
public List<Linedef> BlockingLinedefs { get { return blockinglinedefs; } set { blockinglinedefs = value; } }
public FlatVertex[] OverlayGeometry { get { return overlayGeometry; } set { overlayGeometry = value; } }
public bool SoundEnvironmentIsUpdated { get { return soundenvironmentisupdated; } }
public bool DataIsDirty { get { return dataisdirty; } set { dataisdirty = value; } }
#endregion
// Static instance. We can't use a real static class, because BuilderPlug must
// be instantiated by the core, so we keep a static reference. (this technique
// should be familiar to object-oriented programmers)
private static BuilderPlug me;
// Static property to access the BuilderPlug
public static BuilderPlug Me { get { return me; } }
// This plugin relies on some functionality that wasn't there in older versions
public override int MinimumRevision { get { return 2201; } }
// This event is called when the plugin is initialized
public override void OnInitialize()
{
base.OnInitialize();
highlightcolor = PixelColor.FromInt(General.Settings.ReadPluginSetting("highlightcolor", new PixelColor(255, 0, 192, 0).ToInt()));
level1color = PixelColor.FromInt(General.Settings.ReadPluginSetting("level1color", new PixelColor(255, 0, 255, 0).ToInt()));
level2color = PixelColor.FromInt(General.Settings.ReadPluginSetting("level2color", new PixelColor(255, 255, 255, 0).ToInt()));
nosoundcolor = PixelColor.FromInt(General.Settings.ReadPluginSetting("nosoundcolor", new PixelColor(255, 160, 160, 160).ToInt()));
blocksoundcolor = PixelColor.FromInt(General.Settings.ReadPluginSetting("blocksoundcolor", new PixelColor(255, 255, 0, 0).ToInt()));
distinctcolors = new List<PixelColor>
{
PixelColor.FromInt(0x84d5a4),
PixelColor.FromInt(0xc059cb),
PixelColor.FromInt(0xd0533d),
PixelColor.FromInt(0x415354),
PixelColor.FromInt(0xcea953),
PixelColor.FromInt(0x91d44b),
PixelColor.FromInt(0xcd5b89),
PixelColor.FromInt(0xa8b6c0),
PixelColor.FromInt(0x797ecb),
PixelColor.FromInt(0x567539),
PixelColor.FromInt(0x72422f),
PixelColor.FromInt(0x5d3762),
PixelColor.FromInt(0xffed6f),
PixelColor.FromInt(0xccebc5),
PixelColor.FromInt(0xbc80bd),
PixelColor.FromInt(0xd9d9d9),
PixelColor.FromInt(0xfccde5),
PixelColor.FromInt(0x80b1d3),
PixelColor.FromInt(0xfdb462),
PixelColor.FromInt(0xb3de69),
PixelColor.FromInt(0xfb8072),
PixelColor.FromInt(0xbebada),
PixelColor.FromInt(0xffffb3),
PixelColor.FromInt(0x8dd3c7),
};
//mxd. Create coloured icons
distincticons = new List<Bitmap>(distinctcolors.Count);
foreach(PixelColor color in distinctcolors)
{
distincticons.Add(MakeTintedImage(Properties.Resources.Status0, color));
}
soundenvironments = new List<SoundEnvironment>();
blockinglinedefs = new List<Linedef>();
soundenvironmentisupdated = false;
dataisdirty = true;
// This binds the methods in this class that have the BeginAction
// and EndAction attributes with their actions. Without this, the
// attributes are useless. Note that in classes derived from EditMode
// this is not needed, because they are bound automatically when the
// editing mode is engaged.
General.Actions.BindMethods(this);
menusform = new MenusForm();
// TODO: Add DB2 version check so that old DB2 versions won't crash
// General.ErrorLogger.Add(ErrorType.Error, "zomg!");
// Keep a static reference
me = this;
}
public override void OnMapOpenBegin()
{
base.OnMapOpenBegin();
ResetData();
}
public override void OnMapNewBegin()
{
base.OnMapNewBegin();
ResetData();
}
public override void OnMapSaveBegin(SavePurpose purpose)
{
base.OnMapSaveBegin(purpose);
soundenvironmentisupdated = false;
}
public override void OnEditEngage(EditMode oldmode, EditMode newmode)
{
base.OnEditEngage(oldmode, newmode);
ResetData();
}
// This is called when the plugin is terminated
public override void Dispose()
{
base.Dispose();
//mxd
foreach(Bitmap b in distincticons) b.Dispose();
// This must be called to remove bound methods for actions.
General.Actions.UnbindMethods(this);
}
// Resets all data. This will trigger both rediscovering sound environments and recalculating
// sound propagation domains
private void ResetData()
{
dataisdirty = true;
soundenvironmentisupdated = false;
soundenvironments = new List<SoundEnvironment>();
blockinglinedefs = new List<Linedef>();
}
public void UpdateSoundEnvironments(object sender, DoWorkEventArgs e)
{
List<Sector> sectorstocheck = new List<Sector>();
List<Sector> checkedsectors = new List<Sector>();
List<Sector> allsectors = new List<Sector>();
BackgroundWorker worker = sender as BackgroundWorker;
List<FlatVertex> vertsList = new List<FlatVertex>();
Dictionary<Thing, PixelColor> secolor = new Dictionary<Thing, PixelColor>();
Dictionary<Thing, int> senumber = new Dictionary<Thing, int>();
soundenvironments.Clear();
blockinglinedefs.Clear();
SoundEnvironmentMode.SoundEnvironmentPanel.BeginUpdate(); //mxd
// Keep track of all the sectors in the map. Sectors that belong to a sound environment
// will be removed from the list, so in the end only sectors that don't belong to any
// sound environment will be in this list
foreach(Sector s in General.Map.Map.Sectors) allsectors.Add(s);
List<Thing> soundenvironmenthings = GetSoundEnvironmentThings(General.Map.Map.Sectors.ToList());
int numthings = soundenvironmenthings.Count;
// Assign each thing a color and a number, so each sound environment will always have the same color
// and id, no matter in what order they are discovered
for(int i = 0; i < soundenvironmenthings.Count; i++)
{
//mxd. Make sure same environments use the same color
int seid = (soundenvironmenthings[i].Args[0] << 8) + soundenvironmenthings[i].Args[1];
secolor[soundenvironmenthings[i]] = distinctcolors[seid % distinctcolors.Count];
senumber.Add(soundenvironmenthings[i], i + 1);
}
while(soundenvironmenthings.Count > 0 && !worker.CancellationPending)
{
// Sort things by distance to center of the screen, so that sound environments the user want to look at will (hopefully) be discovered first
Vector2D center = General.Map.Renderer2D.DisplayToMap(new Vector2D(General.Interface.Display.Width / 2f, General.Interface.Display.Height / 2f));
soundenvironmenthings = soundenvironmenthings.OrderBy(o => Math.Abs(Vector2D.Distance(center, o.Position))).ToList();
Thing thing = soundenvironmenthings[0];
if(thing.Sector == null) thing.DetermineSector();
// Ignore things that are outside the map
if(thing.Sector == null)
{
soundenvironmenthings.Remove(thing);
continue;
}
SoundEnvironment environment = new SoundEnvironment();
// Add initial sector. Additional adjacant sectors will be added later
// as they are discovered
sectorstocheck.Add(thing.Sector);
while(sectorstocheck.Count > 0)
{
Sector sector = sectorstocheck[0];
if(!environment.Sectors.Contains(sector)) environment.Sectors.Add(sector);
if(!checkedsectors.Contains(sector)) checkedsectors.Add(sector);
sectorstocheck.Remove(sector);
allsectors.Remove(sector);
// Find adjacant sectors and add them to the list of sectors to check if necessary
foreach(Sidedef sd in sector.Sidedefs)
{
if(LinedefBlocksSoundEnvironment(sd.Line))
{
if(!environment.Linedefs.Contains(sd.Line)) environment.Linedefs.Add(sd.Line);
continue;
}
if(sd.Line.Back == null) continue;
Sector oppositesector = (sd.Line.Front.Sector == sector ? sd.Line.Back.Sector : sd.Line.Front.Sector);
if(!sectorstocheck.Contains(oppositesector) && !checkedsectors.Contains(oppositesector))
sectorstocheck.Add(oppositesector);
}
}
// Get all things that are in the current sound environment...
environment.Things = GetSoundEnvironmentThings(environment.Sectors);
// ... and remove them from the list of sound environment things to check, because we know that
// they already belong to a sound environment
foreach(Thing t in environment.Things)
{
if(soundenvironmenthings.Contains(t)) soundenvironmenthings.Remove(t);
}
// Set color and id of the sound environment
environment.Color = secolor[environment.Things[0]];
environment.ID = senumber[environment.Things[0]];
// Create the data for the overlay geometry
List<FlatVertex> severtslist = new List<FlatVertex>(environment.Sectors.Count * 3); //mxd
foreach(Sector s in environment.Sectors)
{
FlatVertex[] fv = new FlatVertex[s.FlatVertices.Length];
s.FlatVertices.CopyTo(fv, 0);
for(int j = 0; j < fv.Length; j++) fv[j].c = environment.Color.WithAlpha(128).ToInt();
vertsList.AddRange(fv);
severtslist.AddRange(fv); //mxd
// Get all Linedefs that will block sound environments
foreach(Sidedef sd in s.Sidedefs)
{
if(!LinedefBlocksSoundEnvironment(sd.Line)) continue;
lock(blockinglinedefs)
{
blockinglinedefs.Add(sd.Line);
}
}
}
// Update the overlay geometry with the newly added sectors
if(overlayGeometry == null)
{
overlayGeometry = vertsList.ToArray();
}
else
{
lock(overlayGeometry)
{
overlayGeometry = vertsList.ToArray();
}
}
environment.Things = environment.Things.OrderBy(o => o.Index).ToList();
environment.Linedefs = environment.Linedefs.OrderBy(o => o.Index).ToList();
//mxd. Find the first non-dormant thing
Thing activeenv = null;
foreach(Thing t in environment.Things)
{
if(!ThingDormant(t))
{
activeenv = t;
break;
}
}
//mxd. Update environment name
if(activeenv != null)
{
foreach(KeyValuePair<string, KeyValuePair<int, int>> group in General.Map.Data.Reverbs)
{
if(group.Value.Key == activeenv.Args[0] && group.Value.Value == activeenv.Args[1])
{
environment.Name = group.Key + " (" + activeenv.Args[0] + " " + activeenv.Args[1] + ")";
break;
}
}
//mxd. No suitable name found?..
if(environment.Name == SoundEnvironment.DEFAULT_NAME)
{
environment.Name += " (" + activeenv.Args[0] + " " + activeenv.Args[1] + ")";
}
}
//mxd. Still no suitable name?..
if(environment.Name == SoundEnvironment.DEFAULT_NAME) environment.Name += " " + environment.ID;
lock(soundenvironments)
{
soundenvironments.Add(environment);
}
// Tell the worker that discovering a sound environment is finished. This will update the tree view, and also
// redraw the interface, so the sectors of this sound environment are colored
worker.ReportProgress((int)((1.0f - (soundenvironmenthings.Count / (float)numthings)) * 100), environment);
}
// Create overlay geometry for sectors that don't belong to a sound environment
foreach(Sector s in allsectors)
{
FlatVertex[] fv = new FlatVertex[s.FlatVertices.Length];
s.FlatVertices.CopyTo(fv, 0);
for(int j = 0; j < fv.Length; j++) fv[j].c = NoSoundColor.WithAlpha(128).ToInt();
vertsList.AddRange(fv);
}
if(overlayGeometry == null)
{
overlayGeometry = vertsList.ToArray();
}
else
{
lock(overlayGeometry)
{
overlayGeometry = vertsList.ToArray();
}
}
soundenvironmentisupdated = true;
SoundEnvironmentMode.SoundEnvironmentPanel.EndUpdate(); //mxd
}
private static List<Thing> GetSoundEnvironmentThings(List<Sector> sectors)
{
List<Thing> things = new List<Thing>();
foreach(Thing thing in General.Map.Map.Things)
{
// SoundEnvironment thing, see http://zdoom.org/wiki/Classes:SoundEnvironment
if(thing.Type != SOUND_ENVIROMNEMT_THING_TYPE) continue;
if(thing.Sector == null) thing.DetermineSector();
if(thing.Sector != null && sectors.Contains(thing.Sector)) things.Add(thing);
}
return things;
}
private static bool LinedefBlocksSoundEnvironment(Linedef linedef)
{
if(General.Map.UDMF) return linedef.IsFlagSet(SoundEnvironmentMode.ZoneBoundaryFlag); //mxd. Fancier this way :)
// In Hexen format the line must have action 121 (Line_SetIdentification) and bit 1 of
// the second argument set (see http://zdoom.org/wiki/Line_SetIdentification)
return (linedef.Action == 121 && (linedef.Args[1] & 1) == 1); //mxd. Fancier this way :)
}
//mxd
internal static bool ThingDormant(Thing thing)
{
return thing.IsFlagSet(General.Map.UDMF ? "dormant" : "14");
}
//mxd
internal static void SetThingDormant(Thing thing, bool dormant)
{
thing.SetFlag(General.Map.UDMF ? "dormant" : "14", dormant);
}
//mxd. Based on http://www.getcodesamples.com/src/792A0BB0/6CD40E7B
private static Bitmap MakeTintedImage(Bitmap source, PixelColor tint)
{
BitmapData sourcedata = source.LockBits(new Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] pixelBuffer = new byte[sourcedata.Stride * sourcedata.Height];
Marshal.Copy(sourcedata.Scan0, pixelBuffer, 0, pixelBuffer.Length);
source.UnlockBits(sourcedata);
//Translate tint color to 0.0-1.0 range
float redtint = tint.r / 256.0f;
float greentint = tint.g / 256.0f;
float bluetint = tint.b / 256.0f;
for(int k = 0; k + 4 < pixelBuffer.Length; k += 4)
{
float blue = 60 + pixelBuffer[k] * bluetint;
float green = 60 + pixelBuffer[k + 1] * greentint;
float red = 60 + pixelBuffer[k + 2] * redtint;
pixelBuffer[k] = (byte)Math.Min(255, blue);
pixelBuffer[k + 1] = (byte)Math.Min(255, green);
pixelBuffer[k + 2] = (byte)Math.Min(255, red);
}
Bitmap result = new Bitmap(source.Width, source.Height);
BitmapData resultdata = result.LockBits(new Rectangle(0, 0, result.Width, result.Height),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(pixelBuffer, 0, resultdata.Scan0, pixelBuffer.Length);
result.UnlockBits(resultdata);
return result;
}
}
}