#region ================== Copyright (c) 2021 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
#region ================== Namespaces
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using CodeImp.DoomBuilder.BuilderModes;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.VisualModes;
#endregion
namespace CodeImp.DoomBuilder.UDBScript.Wrapper
{
class SectorWrapper : MapElementWrapper, IMoreTags, IEquatable
{
#region ================== Variables
private Sector sector;
#endregion
#region IEquatable members
public bool Equals(SectorWrapper other)
{
return sector == other.sector;
}
public override bool Equals(object obj)
{
return Equals((SectorWrapper)obj);
}
public override int GetHashCode()
{
return sector.GetHashCode();
}
#endregion
#region ================== Properties
internal Sector Sector { get { return sector; } }
///
/// The `Sector`'s index. Read-only.
///
public int index
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the index property can not be accessed.");
return sector.Index;
}
}
///
/// Floor height of the `Sector`.
///
public int floorHeight
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorHeight property can not be accessed.");
return sector.FloorHeight;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorHeight property can not be accessed.");
sector.FloorHeight = value;
}
}
///
/// Ceiling height of the `Sector`.
///
public int ceilingHeight
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingHeight property can not be accessed.");
return sector.CeilHeight;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingHeight property can not be accessed.");
sector.CeilHeight = value;
}
}
///
/// Floor texture of the `Sector`.
///
public string floorTexture
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorTexture property can not be accessed.");
return sector.FloorTexture;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorTexture property can not be accessed.");
sector.SetFloorTexture(value);
// Make sure to update the used textures, so that they are shown immediately
General.Map.Data.UpdateUsedTextures();
}
}
///
/// Ceiling texture of the `Sector`.
///
public string ceilingTexture
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingTexture property can not be accessed.");
return sector.CeilTexture;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingTexture property can not be accessed.");
sector.SetCeilTexture(value);
// Make sure to update the used textures, so that they are shown immediately
General.Map.Data.UpdateUsedTextures();
}
}
///
/// If the `Sector` is selected or not.
///
public bool selected
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the selected property can not be accessed.");
return sector.Selected;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the selected property can not be accessed.");
sector.Selected = value;
// Make update lines selection
foreach (Sidedef sd in sector.Sidedefs)
{
bool front, back;
if (sd.Line.Front != null) front = sd.Line.Front.Sector.Selected; else front = false;
if (sd.Line.Back != null) back = sd.Line.Back.Sector.Selected; else back = false;
sd.Line.Selected = front | back;
}
}
}
///
/// If the `Sector`'s floor is selected or not. Will always return `true` in classic modes if the `Sector` is selected. Read-only.
///
/// 3
public bool floorSelected
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorSelected property can not be accessed.");
if (General.Editing.Mode is BaseVisualMode)
{
bool f, c;
((BaseVisualMode)General.Editing.Mode).GetSelectedSurfaceTypesBySector(sector, out f, out c);
return f;
}
else
{
return sector.Selected;
}
}
}
///
/// If the `Sector`'s floor is highlighted or not. Will always return `true` in classic modes if the `Sector` is highlighted. Read-only.
///
/// 3
public bool floorHighlighted
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorHighlighted property can not be accessed.");
if (General.Editing.Mode is BaseVisualMode)
{
VisualGeometry vs = (VisualGeometry)((BaseVisualMode)General.Editing.Mode).Highlighted;
if (vs == null)
return false;
return (vs.Sector.Sector == sector && vs.GeometryType == VisualGeometryType.FLOOR);
}
else
{
Sector s = ((ClassicMode)General.Editing.Mode).HighlightedObject as Sector;
if(s == null)
return false;
return s == sector;
}
}
}
///
/// If the `Sector`'s ceiling is selected or not. Will always return `true` in classic modes if the `Sector` is selected. Read-only.
///
/// 3
public bool ceilingSelected
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingSelected property can not be accessed.");
if (General.Editing.Mode is BaseVisualMode)
{
bool f, c;
((BaseVisualMode)General.Editing.Mode).GetSelectedSurfaceTypesBySector(sector, out f, out c);
return c;
}
else
{
return sector.Selected;
}
}
}
///
/// If the `Sector`'s ceiling is highlighted or not. Will always return `true` in classic modes if the `Sector` is highlighted. Read-only.
///
/// 3
public bool ceilingHighlighted
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingHighlighted property can not be accessed.");
if (General.Editing.Mode is BaseVisualMode)
{
VisualGeometry vs = (VisualGeometry)((BaseVisualMode)General.Editing.Mode).Highlighted;
if (vs == null)
return false;
return (vs.Sector.Sector == sector && vs.GeometryType == VisualGeometryType.CEILING);
}
else
{
Sector s = ((ClassicMode)General.Editing.Mode).HighlightedObject as Sector;
if (s == null)
return false;
return s == sector;
}
}
}
///
/// If the `Sector` is marked or not. It is used to mark map elements that were created or changed (for example after drawing new geometry).
///
public bool marked
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the marked property can not be accessed.");
return sector.Marked;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the marked property can not be accessed.");
sector.Marked = value;
}
}
///
/// `Sector` flags. It's an object with the flags as properties. Only available in UDMF.
///
/// ```
/// s.flags['noattack'] = true; // Monsters in this sector don't attack
/// s.flags.noattack = true; // Also works
/// ```
///
public ExpandoObject flags
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the flags property can not be accessed.");
dynamic eo = new ExpandoObject();
IDictionary o = eo as IDictionary;
foreach (KeyValuePair kvp in sector.GetFlags())
{
o.Add(kvp.Key, kvp.Value);
}
// Create event that gets called when a property is changed. This sets the flag
((INotifyPropertyChanged)eo).PropertyChanged += new PropertyChangedEventHandler((sender, ea) => {
PropertyChangedEventArgs pcea = ea as PropertyChangedEventArgs;
IDictionary so = sender as IDictionary;
// Only allow known flags to be set
if (!General.Map.Config.SectorFlags.Keys.Contains(pcea.PropertyName))
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Flag name '" + pcea.PropertyName + "' is not valid.");
// New value must be bool
if (!(so[pcea.PropertyName] is bool))
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Flag values must be bool.");
sector.SetFlag(pcea.PropertyName, (bool)so[pcea.PropertyName]);
});
return eo;
}
}
///
/// The `Sector`'s special type.
///
public int special
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the special property can not be accessed.");
return sector.Effect;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the special property can not be accessed.");
sector.Effect = value;
}
}
///
/// The `Sector`'s tag.
///
public int tag
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the tag property can not be accessed.");
return sector.Tag;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the tag property can not be accessed.");
sector.Tag = value;
}
}
///
/// The `Sector`'s brightness.
///
public int brightness
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the brightness property can not be accessed.");
return sector.Brightness;
}
set
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the brightness property can not be accessed.");
sector.Brightness = value;
}
}
///
/// The floor's slope offset.
///
public double floorSlopeOffset
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the floorSlopeOffset property can not be accessed.");
return sector.FloorSlopeOffset;
}
set
{
sector.FloorSlopeOffset = value;
}
}
///
/// The ceiling's slope offset.
///
public double ceilingSlopeOffset
{
get
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the ceilingSlopeOffset property can not be accessed.");
return sector.CeilSlopeOffset;
}
set
{
sector.CeilSlopeOffset = value;
}
}
#endregion
#region ================== Constructors
internal SectorWrapper(Sector sector) : base(sector)
{
this.sector = sector;
}
#endregion
#region ================== Update
internal override void AfterFieldsUpdate()
{
sector.UpdateNeeded = true;
}
#endregion
#region ================== Methods
public override string ToString()
{
return sector.ToString();
}
///
/// Returns an `Array` of all `Sidedef`s of the `Sector`.
///
/// `Array` of the `Sector`'s `Sidedef`s
public SidedefWrapper[] getSidedefs()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getSidedefs method can not be accessed.");
List sidedefs = new List(sector.Sidedefs.Count);
foreach (Sidedef sd in sector.Sidedefs)
if (!sd.IsDisposed)
sidedefs.Add(new SidedefWrapper(sd));
return sidedefs.ToArray();
}
///
/// Clears all flags.
///
public void clearFlags()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the cleaFlags method can not be accessed.");
sector.ClearFlags();
}
///
/// Copies the properties from this `Sector` to another.
///
/// the `Sector` to copy the properties to
public void copyPropertiesTo(SectorWrapper s)
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the cleaFlags method can not be accessed.");
sector.CopyPropertiesTo(s.sector);
}
///
/// Checks if the given point is in this `Sector` or not. The given point can be a `Vector2D` or an `Array` of two numbers.
/// ```
/// if(s.intersect(new Vector2D(32, 64)))
/// UDB.showMessage('Point is in the sector!');
///
/// if(s.intersect([ 32, 64 ]))
/// UDB.showMessage('Point is in the sector!');
/// ```
///
/// Point to test
/// `true` if the point is in the `Sector`, `false` if it isn't
public bool intersect(object p)
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the intersect method can not be accessed.");
try
{
Vector2D v = BuilderPlug.Me.GetVector3DFromObject(p);
return sector.Intersect(v);
}
catch (CantConvertToVectorException e)
{
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException(e.Message);
}
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Position values must be a Vector2D, or an array of numbers.");
}
///
/// Joins this `Sector` with another `Sector`. Lines shared between the sectors will not be removed.
///
/// Sector to join with
public void join(SectorWrapper other)
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the join method can not be accessed.");
if(other.sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector to join with is disposed, the join method can not be used.");
sector.Join(other.sector);
}
///
/// Deletes the `Sector` and its `Sidedef`s.
///
public void delete()
{
if (sector.IsDisposed)
return;
// Taken right from SectorsMode.DeleteItem()
List lines = new List(sector.Sidedefs.Count);
foreach (Sidedef side in sector.Sidedefs) lines.Add(side.Line);
General.Map.Map.BeginAddRemove();
// Dispose the sector
sector.Dispose();
// Check all the lines
for (int i = lines.Count - 1; i >= 0; i--)
{
// If the line has become orphaned, remove it
if ((lines[i].Front == null) && (lines[i].Back == null))
{
// Remove line
lines[i].Dispose();
}
else
{
// If the line only has a back side left, flip the line and sides
if ((lines[i].Front == null) && (lines[i].Back != null))
{
lines[i].FlipVertices();
lines[i].FlipSidedefs();
}
//mxd. Check textures.
if (lines[i].Front.MiddleRequired() && lines[i].Front.LongMiddleTexture == MapSet.EmptyLongName)
{
if (lines[i].Front.LongHighTexture != MapSet.EmptyLongName)
{
lines[i].Front.SetTextureMid(lines[i].Front.HighTexture);
}
else if (lines[i].Front.LongLowTexture != MapSet.EmptyLongName)
{
lines[i].Front.SetTextureMid(lines[i].Front.LowTexture);
}
}
//mxd. Do we still need high/low textures?
lines[i].Front.RemoveUnneededTextures(false);
// Update sided flags
lines[i].ApplySidedFlags();
}
}
General.Map.Map.EndAddRemove();
}
///
/// Gets an array of `Vector2D` arrays, representing the vertices of the triangulated sector. Note that for sectors with islands some triangles may not always have their points on existing vertices.
///
/// Array of `Vector2D` arrays
public Vector2DWrapper[][] getTriangles()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getTriangles method can not be accessed.");
// Updating the cache will also triangulate the sector (if necessary)
sector.UpdateCache();
// The triangles are stored in a flat list of Vector2D, so there should always be a multiple of 3 Vector2D
if(sector.Triangles.Vertices.Count % 3 != 0)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector triangle vertices is not a multiple of 3.");
int numtriangles = sector.Triangles.Vertices.Count / 3;
Vector2DWrapper[][] triangles = new Vector2DWrapper[numtriangles][];
for(int i=0; i < numtriangles; i++)
{
triangles[i] = new Vector2DWrapper[3] {
new Vector2DWrapper(sector.Triangles.Vertices[i * 3]),
new Vector2DWrapper(sector.Triangles.Vertices[i * 3 + 1]),
new Vector2DWrapper(sector.Triangles.Vertices[i * 3 + 2]),
};
}
return triangles;
}
///
/// Gets the floor's slope vector.
///
/// The floor's slope normal as a `Vector3D`
public Vector3DWrapper getFloorSlope()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getFloorSlope method can not be accessed.");
return new Vector3DWrapper(sector.FloorSlope.GetNormal());
}
///
/// Gets the floor's plane, accounting for flat planes, UDMF slopes and line slopes.
///
/// The floor's plane as a `Plane`
public PlaneWrapper getFloorPlane()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getFloorPlane method can not be accessed.");
Plane plane = Sector.GetFloorPlane(sector);
return new PlaneWrapper(new Vector3D(plane.a, plane.b, plane.c), plane.d);
}
///
/// Sets the floor's slope vector. The vector has to be normalized.
///
/// The new slope vector as `Vector3D`
public void setFloorSlope(object normal)
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the setFloorSlope method can not be accessed.");
try
{
sector.FloorSlope = BuilderPlug.Me.GetVector3DFromObject(normal);
}
catch (CantConvertToVectorException e)
{
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException(e.Message);
}
}
///
/// Gets the ceiling's slope vector.
///
/// The ceiling's slope normal as a `Vector3D`
public Vector3DWrapper getCeilingSlope()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getCeilingSlope method can not be accessed.");
return new Vector3DWrapper(sector.CeilSlope.GetNormal());
}
///
/// Gets the ceiling's plane, accounting for flat planes, UDMF slopes and line slopes.
///
/// The ceiling's plane as a `Plane`
public PlaneWrapper getCeilingPlane()
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the getCeilingPlane method can not be accessed.");
Plane plane = Sector.GetCeilingPlane(sector);
return new PlaneWrapper(new Vector3D(plane.a, plane.b, plane.c), plane.d);
}
///
/// Sets the ceiling's slope vector. The vector has to be normalized.
///
/// The new slope vector as `Vector3D`
public void setCeilingSlope(object normal)
{
if (sector.IsDisposed)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException("Sector is disposed, the setCeilingSlope method can not be accessed.");
try
{
sector.CeilSlope = BuilderPlug.Me.GetVector3DFromObject(normal);
}
catch (CantConvertToVectorException e)
{
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException(e.Message);
}
}
///
/// Returns an `Array` of `Vector2D` of label positions for the `Sector`. This are the positions where for example selection number or tags are shown.
///
/// This example adds an imp to the label position of each sector in the map:
/// ```
/// UDB.Map.getSectors().forEach(s => {
/// const positions = s.getLabelPositions();
/// if(positions.length > 0)
/// UDB.Map.createThing(positions[0], 3001);
/// });
/// ```
///
/// `Array` of `Vector2D` of all label positions
/// 5
[UDBScriptSettings(MinVersion = 5)]
public Vector2DWrapper[] getLabelPositions()
{
return Tools.FindLabelPositions(sector).Select(lpi => new Vector2DWrapper(lpi.position)).ToArray();
}
#endregion
#region ================== Interface implementations
///
/// Returns an `Array` of the `Sector`'s tags. UDMF only. Supported game configurations only.
///
/// `Array` of tags
public int[] getTags()
{
return sector.Tags.ToArray();
}
///
/// Adds a tag to the `Sector`. UDMF only. Supported game configurations only.
///
/// Tag to add
/// `true` when the tag was added, `false` when the tag already exists
public bool addTag(int tag)
{
if (sector.Tags.Contains(tag))
return false;
// We have to take the detour by creating a new list and assigning that because otherwise the
// BeforePropsChange will not be triggered
List tags = new List(sector.Tags);
tags.Add(tag);
tags.Remove(0);
sector.Tags = tags;
return true;
}
///
/// Removes a tag from the `Sector`. UDMF only. Supported game configurations only.
///
/// Tag to remove
/// `true` when the tag was removed successfully, `false` when the tag did not exist
public bool removeTag(int tag)
{
if(sector.Tags.Contains(tag))
{
// If it's the only tag just set it to 0, otherwise remove the tag completely
if (sector.Tags.Count == 1)
sector.Tag = 0;
else
{
// We have to take the detour by creating a new list and assigning that because otherwise the
// BeforePropsChange will not be triggered
List tags = new List(sector.Tags);
tags.Remove(tag);
sector.Tags = tags;
}
return true;
}
return false;
}
#endregion
}
}