#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 distinctcolors; private List distincticons; //mxd private List soundenvironments; private List blockinglinedefs; private FlatVertex[] overlayGeometry; private bool soundenvironmentisupdated; private bool dataisdirty; #endregion #region ================== Properties // Interface public MenusForm MenusForm { get { return menusform; } } internal List DistinctIcons { get { return distincticons; } } //mxd internal List 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 SoundEnvironments { get { return soundenvironments; } set { soundenvironments = value; } } public List 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.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(distinctcolors.Count); foreach(PixelColor color in distinctcolors) { distincticons.Add(MakeTintedImage(Properties.Resources.Status0, color)); } soundenvironments = new List(); blockinglinedefs = new List(); 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(); blockinglinedefs = new List(); } public void UpdateSoundEnvironments(object sender, DoWorkEventArgs e) { List sectorstocheck = new List(); List checkedsectors = new List(); List allsectors = new List(); BackgroundWorker worker = sender as BackgroundWorker; List vertsList = new List(); Dictionary secolor = new Dictionary(); Dictionary senumber = new Dictionary(); 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 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.Other == 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 severtslist = new List(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); } } } //mxd. Store sector environment verts environment.SectorsGeometry = severtslist.ToArray(); // 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> 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 GetSoundEnvironmentThings(List sectors) { List things = new List(); 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; } } }