From 2779b8ab6ea99390c38a1ed163cfa9c68a83528b Mon Sep 17 00:00:00 2001 From: Alex 'mcmonkey' Goodwin Date: Fri, 22 Jan 2021 04:33:13 -0800 Subject: [PATCH] PolygonTag --- .../denizen/events/BukkitScriptEvent.java | 11 + .../denizen/objects/CuboidTag.java | 10 +- .../denizen/objects/InventoryTag.java | 6 +- .../denizen/objects/ItemTag.java | 6 +- .../denizen/objects/LocationTag.java | 31 +- .../denizen/objects/PolygonTag.java | 804 ++++++++++++++++++ .../objects/notable/NotableManager.java | 1 + .../bukkit/BukkitListProperties.java | 35 +- .../denizen/tags/core/PolygonTagBase.java | 25 + .../denizen/tags/core/ServerTagBase.java | 2 +- .../denizen/utilities/CommonRegistries.java | 3 + .../command/DenizenCommandHandler.java | 54 +- 12 files changed, 936 insertions(+), 52 deletions(-) create mode 100644 plugin/src/main/java/com/denizenscript/denizen/objects/PolygonTag.java create mode 100644 plugin/src/main/java/com/denizenscript/denizen/tags/core/PolygonTagBase.java diff --git a/plugin/src/main/java/com/denizenscript/denizen/events/BukkitScriptEvent.java b/plugin/src/main/java/com/denizenscript/denizen/events/BukkitScriptEvent.java index 15b169990f..22dce2d67e 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/events/BukkitScriptEvent.java +++ b/plugin/src/main/java/com/denizenscript/denizen/events/BukkitScriptEvent.java @@ -480,6 +480,9 @@ else if (subit.equals("ellipsoid")) { else if (lower.equals("ellipsoid")) { return EllipsoidTag.getNotableEllipsoidsContaining(location).size() > 0; } + else if (lower.equals("polygon")) { + return PolygonTag.getNotedPolygonsContaining(location).size() > 0; + } else if (WorldTag.matches(inputText)) { return CoreUtilities.equalsIgnoreCase(location.getWorld().getName(), lower); } @@ -499,6 +502,14 @@ else if (EllipsoidTag.matches(inputText)) { } return ellipsoid.contains(location); } + else if (PolygonTag.matches(inputText)) { + PolygonTag polygon = PolygonTag.valueOf(inputText, getTagContext(path)); + if (polygon == null || !polygon.isUnique()) { + Debug.echoError("Invalid event 'in:' switch [" + getName() + "] (invalid polygon): '" + path.event + "' for " + path.container.getName()); + return false; + } + return polygon.doesContainLocation(location); + } else if (isAdvancedMatchable(lower)) { MatchHelper matcher = createMatcher(lower); for (CuboidTag cuboid : NotableManager.getAllType(CuboidTag.class)) { diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/CuboidTag.java b/plugin/src/main/java/com/denizenscript/denizen/objects/CuboidTag.java index b0b80f8219..3ba925f721 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/CuboidTag.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/CuboidTag.java @@ -1,7 +1,6 @@ package com.denizenscript.denizen.objects; import com.denizenscript.denizen.objects.notable.NotableManager; -import com.denizenscript.denizen.utilities.Utilities; import com.denizenscript.denizen.utilities.debugging.Debug; import com.denizenscript.denizen.utilities.depends.Depends; import com.denizenscript.denizencore.objects.*; @@ -78,7 +77,6 @@ public static List getNotableCuboidsContaining(Location location) { cuboids.add(cuboid); } } - return cuboids; } @@ -510,7 +508,7 @@ public ListTag getBlocks(Attribute attribute) { return getBlocks(null, attribute); } - private boolean matchesMaterialList(Location loc, List materials, Attribute attribute) { + public static boolean matchesMaterialList(Location loc, List materials, Attribute attribute) { if (materials == null) { return true; } @@ -1540,11 +1538,11 @@ public static void registerTags() { // Gets the name of a noted CuboidTag. If the cuboid isn't noted, this is null. // --> registerTag("note_name", (attribute, cuboid) -> { - String notname = NotableManager.getSavedId(cuboid); - if (notname == null) { + String noteName = NotableManager.getSavedId(cuboid); + if (noteName == null) { return null; } - return new ElementTag(notname); + return new ElementTag(noteName); }, "notable_name"); registerTag("full", (attribute, cuboid) -> { diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/InventoryTag.java b/plugin/src/main/java/com/denizenscript/denizen/objects/InventoryTag.java index f86372ccd0..1d2d8cf58e 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/InventoryTag.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/InventoryTag.java @@ -1845,11 +1845,11 @@ else if (meta.hasDisplayName() && CoreUtilities.toLowerCase(meta.getDisplayName( // Gets the name of a noted InventoryTag. If the inventory isn't noted, this is null. // --> registerTag("note_name", (attribute, object) -> { - String notname = NotableManager.getSavedId(object); - if (notname == null) { + String noteName = NotableManager.getSavedId(object); + if (noteName == null) { return null; } - return new ElementTag(notname); + return new ElementTag(noteName); }, "notable_name"); // <--[tag] diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/ItemTag.java b/plugin/src/main/java/com/denizenscript/denizen/objects/ItemTag.java index c43f0bf68a..92c098578c 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/ItemTag.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/ItemTag.java @@ -731,11 +731,11 @@ else if (container != null) { registerTag("notable_name", (attribute, object) -> { Deprecations.notableItems.warn(); - String notname = NotableManager.getSavedId(object); - if (notname == null) { + String noteName = NotableManager.getSavedId(object); + if (noteName == null) { return null; } - return new ElementTag(notname); + return new ElementTag(noteName); }); // <--[tag] diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/LocationTag.java b/plugin/src/main/java/com/denizenscript/denizen/objects/LocationTag.java index 346e0070f6..7ce72e8aa5 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/LocationTag.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/LocationTag.java @@ -2656,11 +2656,11 @@ else if (attribute.startsWith("unexplored_structure", 2) && attribute.hasContext // Gets the name of a noted LocationTag. If the location isn't noted, this is null. // --> registerTag("note_name", (attribute, object) -> { - String notname = NotableManager.getSavedId((object)); - if (notname == null) { + String noteName = NotableManager.getSavedId((object)); + if (noteName == null) { return null; } - return new ElementTag(notname); + return new ElementTag(noteName); }, "notable_name"); ///////////////////// @@ -2897,10 +2897,10 @@ else if (attribute.startsWith("vertical", 2)) { }); // <--[tag] - // @attribute /]> + // @attribute ]> // @returns ElementTag(Boolean) // @description - // Returns whether the location is within the cuboid or ellipsoid. + // Returns whether the location is within the specified area (cuboid, ellipsoid, polygon, ...). // --> registerTag("is_within", (attribute, object) -> { if (!attribute.hasContext(1)) { @@ -2912,6 +2912,12 @@ else if (attribute.startsWith("vertical", 2)) { return new ElementTag(ellipsoid.contains(object)); } } + else if (PolygonTag.matches(attribute.getContext(1))) { + PolygonTag polygon = attribute.contextAsType(1, PolygonTag.class); + if (polygon != null) { + return new ElementTag(polygon.doesContainLocation(object)); + } + } else { CuboidTag cuboid = attribute.contextAsType(1, CuboidTag.class); if (cuboid != null) { @@ -2999,6 +3005,21 @@ else if (attribute.startsWith("vertical", 2)) { return ellipsoid_list; }); + // <--[tag] + // @attribute + // @returns ListTag(PolygonTag) + // @description + // Returns a ListTag of all noted PolygonTags that include this location. + // --> + registerTag("polygons", (attribute, object) -> { + List polygons = PolygonTag.getNotedPolygonsContaining(object); + ListTag polygon_list = new ListTag(); + for (PolygonTag polygon : polygons) { + polygon_list.addObject(polygon); + } + return polygon_list; + }); + // <--[tag] // @attribute // @returns ElementTag(Boolean) diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/PolygonTag.java b/plugin/src/main/java/com/denizenscript/denizen/objects/PolygonTag.java new file mode 100644 index 0000000000..0caa143943 --- /dev/null +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/PolygonTag.java @@ -0,0 +1,804 @@ +package com.denizenscript.denizen.objects; + +import com.denizenscript.denizen.objects.notable.NotableManager; +import com.denizenscript.denizen.utilities.Settings; +import com.denizenscript.denizen.utilities.debugging.Debug; +import com.denizenscript.denizen.utilities.depends.Depends; +import com.denizenscript.denizencore.objects.*; +import com.denizenscript.denizencore.objects.core.ElementTag; +import com.denizenscript.denizencore.objects.core.ListTag; +import com.denizenscript.denizencore.objects.notable.Notable; +import com.denizenscript.denizencore.objects.notable.Note; +import com.denizenscript.denizencore.tags.Attribute; +import com.denizenscript.denizencore.tags.ObjectTagProcessor; +import com.denizenscript.denizencore.tags.TagContext; +import com.denizenscript.denizencore.tags.TagRunnable; +import com.denizenscript.denizencore.utilities.CoreUtilities; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public class PolygonTag implements ObjectTag, Cloneable, Notable, Adjustable, AreaContainmentObject { + + // <--[language] + // @name PolygonTag Objects + // @group Object System + // @description + // A PolygonTag represents a polygonal region in the world. + // + // The word 'polygon' means an arbitrary 2D shape. + // PolygonTags, in addition to a 2D polygon, contain a minimum and maximum Y coordinate, to allow them to function in 3D. + // + // PolygonTags are NOT polyhedra. + // + // These use the object notation "polygon@". + // The identity format for cuboids is ,,,,,... (the x,z pair repeats for as many points as the polygon has). + // + // A PolygonTag with 4 points at right angles would cover an area also possible to be defined by a CuboidTag, however all other shapes a PolygonTag can form are unique. + // + // Compared to CuboidTags, PolygonTags are generally slower to process and more complex to work with, but offer the benefit of supporting more intricate shapes. + // + // Note that forming invalid polygons (duplicate corners, impossible shapes, etc) will not necessarily give any error message, and may cause weird results. + // + // --> + + public WorldTag world; + + public double yMin = 0, yMax = 0; + + public List corners = new ArrayList<>(); + + public Corner boxMin = new Corner(), boxMax = new Corner(); + + public String noteName = null; + + public static class Corner { + public double x, z; + + public Corner() { + } + + public Corner(double x, double z) { + this.x = x; + this.z = z; + } + } + + public PolygonTag(WorldTag world) { + this.world = world; + } + + public PolygonTag(WorldTag world, double yMin, double yMax, List corners) { + this.world = world; + this.yMin = yMin; + this.yMax = yMax; + this.corners.addAll(corners); + recalculateBox(); + } + + public void recalculateBox() { + if (corners.size() == 0) { + return; + } + Corner firstCorner = corners.get(0); + boxMin.x = firstCorner.x; + boxMin.z = firstCorner.z; + boxMax.x = firstCorner.x; + boxMax.z = firstCorner.z; + for (Corner corner : corners) { + recalculateToFit(corner); + } + } + + public void recalculateToFit(Corner corner) { + boxMin.x = Math.min(boxMin.x, corner.x); + boxMin.z = Math.min(boxMin.z, corner.z); + boxMax.x = Math.max(boxMax.x, corner.x); + boxMax.z = Math.max(boxMax.z, corner.z); + } + + @Override + public PolygonTag clone() { + return new PolygonTag(world, yMin, yMax, corners); + } + + @Fetchable("polygon") + public static PolygonTag valueOf(String string, TagContext context) { + if (string.startsWith("polygon@")) { + string = string.substring("polygon@".length()); + } + Notable saved = NotableManager.getSavedObject(string); + if (saved instanceof PolygonTag) { + return (PolygonTag) saved; + } + List parts = CoreUtilities.split(string, ','); + if (parts.size() < 3) { + return null; + } + WorldTag world = new WorldTag(parts.get(0)); + for (int i = 1; i < parts.size(); i++) { + if (!ArgumentHelper.matchesDouble(parts.get(i))) { + return null; + } + } + PolygonTag toReturn = new PolygonTag(world); + toReturn.yMin = Double.parseDouble(parts.get(1)); + toReturn.yMax = Double.parseDouble(parts.get(2)); + for (int i = 3; i < parts.size(); i += 2) { + Corner corner = new Corner(Double.parseDouble(parts.get(i)), Double.parseDouble(parts.get(i + 1))); + toReturn.corners.add(corner); + } + toReturn.recalculateBox(); + return toReturn; + } + + public static boolean matches(String string) { + if (valueOf(string, CoreUtilities.noDebugContext) != null) { + return true; + } + return false; + } + + @Override + public ObjectTag duplicate() { + if (noteName != null) { + return this; + } + return clone(); + } + + @Override + public int hashCode() { + if (noteName != null) { + return noteName.hashCode(); + } + return (int) (boxMin.x + boxMin.z * 7 + boxMax.x * 29 + boxMax.z * 61); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PolygonTag)) { + return false; + } + PolygonTag poly2 = (PolygonTag) other; + if (poly2.corners.size() != corners.size()) { + return false; + } + if ((noteName == null) != (poly2.noteName == null)) { + return false; + } + if (noteName != null && !noteName.equals(poly2.noteName)) { + return false; + } + if (!world.getName().equals(poly2.world.getName())) { + return false; + } + if (yMin != poly2.yMin || yMax != poly2.yMax) { + return false; + } + for (int i = 0; i < corners.size(); i++) { + Corner corner1 = corners.get(i); + Corner corner2 = poly2.corners.get(i); + if (corner1.x != corner2.x || corner1.z != corner2.z) { + return false; + } + } + return true; + } + + @Override + public String getNoteName() { + return noteName; + } + + public static List getNotedPolygonsContaining(Location location) { + List polygons = new ArrayList<>(); + for (PolygonTag polygon : NotableManager.getAllType(PolygonTag.class)) { + if (polygon.doesContainLocation(location)) { + polygons.add(polygon); + } + } + return polygons; + } + + @Override + public boolean doesContainLocation(Location loc) { + if (loc.getWorld() == null) { + return false; + } + if (!loc.getWorld().getName().equals(world.getName())) { + return false; + } + double x = loc.getX(); + double z = loc.getZ(); + if (x < boxMin.x || x > boxMax.x || z < boxMin.z || z > boxMax.z) { + return false; + } + double y = loc.getY(); + if (y < yMin || y > yMax) { + return false; + } + boolean isInside = false; + for (int i = 0; i < corners.size(); i++) { + Corner start = corners.get(i); + Corner end = (i + 1 == corners.size() ? corners.get(0) : corners.get(i + 1)); + if (((start.z > z) != (end.z > z)) && (x < (end.x - start.x) * (z - start.z) / (end.z - start.z) + start.x)) { + isInside = !isInside; + } + } + return isInside; + } + + public List generateFlatBlockShell(double y) { + int max = Settings.blockTagsMaxBlocks(); + ArrayList toOutput = new ArrayList<>(); + for (int x = (int) Math.floor(boxMin.x); x < boxMax.x; x++) { + for (int z = (int) Math.floor(boxMin.z); z < boxMax.z; z++) { + LocationTag possible = new LocationTag(x + 0.5, y, z + 0.5, world.getName()); + if (doesContainLocation(possible)) { + toOutput.add(possible); + } + max--; + if (max <= 0) { + return toOutput; + } + } + } + return toOutput; + } + + public ListTag getShell() { + int max = Settings.blockTagsMaxBlocks(); + ListTag addTo = new ListTag(); + List flatShell = generateFlatBlockShell(yMin); + for (LocationTag loc : flatShell) { + addTo.addObject(loc.clone()); + } + max -= flatShell.size(); + if (max <= 0) { + return addTo; + } + for (LocationTag loc : flatShell) { + LocationTag newLoc = loc.clone(); + newLoc.setY(yMax); + addTo.addObject(newLoc); + } + max -= flatShell.size(); + if (max <= 0) { + return addTo; + } + int per = (int) (yMax - yMin); + if (per == 0) { + return addTo; + } + for (int i = 0; i < corners.size(); i++) { + Corner start = corners.get(i); + Corner end = (i + 1 == corners.size() ? corners.get(0) : corners.get(i + 1)); + double xMove = (end.x - start.x); + double zMove = (end.z - start.z); + double len = Math.sqrt(xMove * xMove + zMove * zMove); + if (len < 0.1) { + continue; + } + xMove /= len; + zMove /= len; + double xSpot = start.x; + double zSpot = start.z; + max -= (int) (len + 1); + if (max <= 0) { + return addTo; + } + for (int j = 0; j < len; j++) { + for (double y = yMin + 1; y < yMax; y++) { + addTo.addObject(new LocationTag(xSpot, y, zSpot, world.getName())); + } + max -= per; + if (max <= 0) { + return addTo; + } + xSpot += xMove; + zSpot += zMove; + } + } + return addTo; + } + + public ListTag getBlocks(List materials, Attribute attribute) { + int max = Settings.blockTagsMaxBlocks(); + ListTag addTo = new ListTag(); + List flatShell = generateFlatBlockShell(yMin); + for (double y = yMin; y < yMax; y++) { + for (LocationTag loc : flatShell) { + LocationTag newLoc = loc.clone(); + newLoc.setY(y); + if (CuboidTag.matchesMaterialList(newLoc, materials, attribute)) { + addTo.addObject(newLoc); + } + } + max -= flatShell.size(); + if (max <= 0) { + return addTo; + } + } + return addTo; + } + + public void addOutline2D(double y, ListTag addTo) { + int max = Settings.blockTagsMaxBlocks(); + for (int i = 0; i < corners.size(); i++) { + Corner start = corners.get(i); + Corner end = (i + 1 == corners.size() ? corners.get(0) : corners.get(i + 1)); + double xMove = (end.x - start.x); + double zMove = (end.z - start.z); + double len = Math.sqrt(xMove * xMove + zMove * zMove); + if (len < 0.1) { + continue; + } + xMove /= len; + zMove /= len; + double xSpot = start.x; + double zSpot = start.z; + max -= (int) (len + 1); + if (max <= 0) { + return; + } + for (int j = 0; j < len; j++) { + addTo.addObject(new LocationTag(xSpot, y, zSpot, world.getName())); + xSpot += xMove; + zSpot += zMove; + } + } + } + + public ListTag getOutline() { + int max = Settings.blockTagsMaxBlocks(); + ListTag output = new ListTag(); + addOutline2D(yMin, output); + if (max <= output.size()) { + return output; + } + if (yMin != yMax) { + addOutline2D(yMax, output); + } + max -= output.size(); + if (max <= 0) { + return output; + } + int per = (int) (yMax - yMin); + if (per == 0) { + return output; + } + for (Corner corner : corners) { + for (double y = yMin + 1; y < yMax; y++) { + output.addObject(new LocationTag(corner.x, y, corner.z, world.getName())); + } + max -= per; + if (max <= 0) { + return output; + } + } + return output; + } + + @Override + public boolean isUnique() { + return NotableManager.isSaved(this); + } + + @Override + @Note("Polygons") + public String getSaveObject() { + return identifyFull().substring("polygon@".length()); + } + + @Override + public void makeUnique(String id) { + PolygonTag toNote = clone(); + toNote.noteName = id; + NotableManager.saveAs(toNote, id); + } + + @Override + public void forget() { + noteName = null; + NotableManager.remove(this); + } + + String prefix = "Polygon"; + + @Override + public String getObjectType() { + return "polygon"; + } + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public PolygonTag setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + @Override + public String debuggable() { + if (isUnique()) { + return "polygon@" + NotableManager.getSavedId(this) + " (" + identifyFull() + ")"; + } + else { + return identifyFull(); + } + } + + @Override + public String identify() { + if (isUnique()) { + return "polygon@" + NotableManager.getSavedId(this); + } + else { + return identifyFull(); + } + } + + @Override + public String identifySimple() { + return identify(); + } + + public String identifyFull() { + StringBuilder sb = new StringBuilder(); + sb.append("polygon@").append(world.getName()).append(",").append(yMin).append(",").append(yMax).append(","); + for (Corner corner : corners) { + sb.append(corner.x).append(",").append(corner.z).append(","); + } + if (corners.size() > 0) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public String toString() { + return identify(); + } + + public static void registerTags() { + + // <--[tag] + // @attribute ]> + // @returns ElementTag(Boolean) + // @description + // Returns whether the specified location is within the boundaries of the polygon. + // --> + registerTag("contains", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.contains[...] tag must have an input."); + return null; + } + LocationTag toCheck = attribute.contextAsType(1, LocationTag.class); + return new ElementTag(polygon.doesContainLocation(toCheck)); + }); + + // <--[tag] + // @attribute + // @returns CuboidTag + // @description + // Returns a cuboid approximately representing the maximal bounding box of the polygon (anything this cuboid does not contain, is also not contained by the polygon, but not vice versa). + // --> + registerTag("bounding_box", (attribute, polygon) -> { + LocationTag min = new LocationTag(Math.floor(polygon.boxMin.x), Math.floor(polygon.yMin), Math.floor(polygon.boxMin.z), polygon.world.getName()); + LocationTag max = new LocationTag(Math.ceil(polygon.boxMax.x), Math.ceil(polygon.yMax), Math.ceil(polygon.boxMax.z), polygon.world.getName()); + return new CuboidTag(min, max); + }); + + // <--[tag] + // @attribute + // @returns ElementTag(Decimal) + // @description + // Returns the maximum Y level for this polygon. + // --> + registerTag("max_y", (attribute, polygon) -> { + return new ElementTag(polygon.yMax); + }); + + // <--[tag] + // @attribute + // @returns ElementTag(Decimal) + // @description + // Returns the minimum Y level for this polygon. + // --> + registerTag("min_y", (attribute, polygon) -> { + return new ElementTag(polygon.yMin); + }); + + // <--[tag] + // @attribute + // @returns WorldTag + // @description + // Returns the polygon's world. + // --> + registerTag("world", (attribute, polygon) -> { + return polygon.world; + }); + + // <--[tag] + // @attribute + // @returns ListTag(PlayerTag) + // @description + // Gets a list of all players currently within the PolygonTag. + // --> + registerTag("players", (attribute, polygon) -> { + ArrayList players = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (polygon.doesContainLocation(player.getLocation())) { + players.add(PlayerTag.mirrorBukkitPlayer(player)); + } + } + return new ListTag(players); + }); + + // <--[tag] + // @attribute + // @returns ListTag(NPCTag) + // @description + // Gets a list of all NPCs currently within the PolygonTag. + // --> + if (Depends.citizens != null) { + registerTag("npcs", (attribute, polygon) -> { + ArrayList npcs = new ArrayList<>(); + for (NPC npc : CitizensAPI.getNPCRegistry()) { + NPCTag dnpc = new NPCTag(npc); + if (polygon.doesContainLocation(dnpc.getLocation())) { + npcs.add(dnpc); + } + } + return new ListTag(npcs); + }); + } + + // <--[tag] + // @attribute |...)]> + // @returns ListTag(EntityTag) + // @description + // Gets a list of all entities currently within the PolygonTag, with + // an optional search parameter for the entity type. + // --> + registerTag("entities", (attribute, polygon) -> { + ArrayList entities = new ArrayList<>(); + ListTag types = new ListTag(); + if (attribute.hasContext(1)) { + types = attribute.contextAsType(1, ListTag.class); + } + for (Entity ent : polygon.world.getEntitiesForTag()) { + EntityTag current = new EntityTag(ent); + if (polygon.doesContainLocation(ent.getLocation())) { + if (!types.isEmpty()) { + for (String type : types) { + if (current.identifySimpleType().equalsIgnoreCase(type)) { + entities.add(current); + break; + } + } + } + else { + entities.add(new EntityTag(ent)); + } + } + } + return new ListTag(entities); + }); + + // <--[tag] + // @attribute + // @returns ListTag(EntityTag) + // @description + // Gets a list of all living entities currently within the PolygonTag. + // This includes Players, mobs, NPCs, etc., but excludes dropped items, experience orbs, etc. + // --> + registerTag("living_entities", (attribute, polygon) -> { + ArrayList entities = new ArrayList<>(); + for (Entity ent : polygon.world.getWorld().getLivingEntities()) { + if (polygon.doesContainLocation(ent.getLocation()) && !EntityTag.isCitizensNPC(ent)) { + entities.add(new EntityTag(ent)); + } + } + return new ListTag(entities); + }); + + // <--[tag] + // @attribute + // @returns ElementTag + // @description + // Gets the name of a noted PolygonTag. If the polygon isn't noted, this is null. + // --> + registerTag("note_name", (attribute, polygon) -> { + String noteName = NotableManager.getSavedId(polygon); + if (noteName == null) { + return null; + } + return new ElementTag(noteName); + }); + + // <--[tag] + // @attribute + // @returns ListTag(LocationTag) + // @description + // Returns a list of the polygon's corners, as locations with Y coordinate set to the y-min value. + // --> + registerTag("corners", (attribute, polygon) -> { + ListTag list = new ListTag(); + for (Corner corner : polygon.corners) { + list.addObject(new LocationTag(corner.x, polygon.yMin, corner.z, polygon.world.getName())); + } + return list; + }); + + // <--[tag] + // @attribute ]> + // @returns PolygonTag + // @mechanism PolygonTag.add_corner + // @description + // Returns a copy of the polygon, with the specified corner added to the end of the corner list. + // --> + registerTag("with_corner", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.with_corner[...] tag must have an input."); + return null; + } + LocationTag corner = attribute.contextAsType(1, LocationTag.class); + PolygonTag toReturn = polygon.clone(); + toReturn.corners.add(new Corner(corner.getX(), corner.getZ())); + toReturn.recalculateBox(); + return toReturn; + }); + + // <--[tag] + // @attribute ]> + // @returns PolygonTag + // @description + // Returns a copy of the polygon, with the specified minimum-Y value. + // --> + registerTag("with_y_min", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.with_y_min[...] tag must have an input."); + return null; + } + PolygonTag toReturn = polygon.clone(); + toReturn.yMin = attribute.getDoubleContext(1); + return toReturn; + }); + + // <--[tag] + // @attribute ]> + // @returns PolygonTag + // @description + // Returns a copy of the polygon, with the specified maximum-Y value. + // --> + registerTag("with_y_max", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.with_y_max[...] tag must have an input."); + return null; + } + PolygonTag toReturn = polygon.clone(); + toReturn.yMax = attribute.getDoubleContext(1); + return toReturn; + }); + + // <--[tag] + // @attribute ]> + // @returns PolygonTag + // @description + // Returns a copy of the polygon, with the specified Y value included as part of the Y range (expanding the Y-min or Y-max as needed). + // --> + registerTag("include_y", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.include_y[...] tag must have an input."); + return null; + } + PolygonTag toReturn = polygon.clone(); + double y = attribute.getDoubleContext(1); + toReturn.yMin = Math.min(y, toReturn.yMin); + toReturn.yMax = Math.max(y, toReturn.yMax); + return toReturn; + }); + + // <--[tag] + // @attribute ]> + // @returns ListTag(LocationTag) + // @description + // Returns a list of locations along the 2D outline of this polygon, at the specified Y level (roughly a block-width of separation between each). + // --> + registerTag("outline_2d", (attribute, polygon) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("PolygonTag.outline_2d[...] tag must have an input."); + return null; + } + double y = attribute.getDoubleContext(1); + ListTag output = new ListTag(); + polygon.addOutline2D(y, output); + return output; + }); + + // <--[tag] + // @attribute + // @returns ListTag(LocationTag) + // @description + // Returns a list of locations along the 3D outline of this polygon (roughly a block-width of separation between each). + // --> + registerTag("outline", (attribute, polygon) -> { + return polygon.getOutline(); + }); + + // <--[tag] + // @attribute + // @returns ListTag(LocationTag) + // @description + // Returns a list of locations along the 3D shell of this polygon (roughly a block-width of separation between each). + // --> + registerTag("shell", (attribute, polygon) -> { + return polygon.getShell(); + }); + + // <--[tag] + // @attribute |...)]> + // @returns ListTag(LocationTag) + // @description + // Returns a list of block locations within the polygon. + // Optionally, specify a list of materials to only return locations with that block type. + // --> + registerTag("blocks", (attribute, polygon) -> { + List materials = null; + if (attribute.hasContext(1)) { + materials = attribute.contextAsType(1, ListTag.class).filter(MaterialTag.class, attribute.context); + } + return polygon.getBlocks(materials, attribute); + }); + } + + public static ObjectTagProcessor tagProcessor = new ObjectTagProcessor<>(); + + public static void registerTag(String name, TagRunnable.ObjectInterface runnable, String... variants) { + tagProcessor.registerTag(name, runnable, variants); + } + + @Override + public ObjectTag getObjectAttribute(Attribute attribute) { + return tagProcessor.getObjectAttribute(this, attribute); + } + + public void applyProperty(Mechanism mechanism) { + if (NotableManager.isExactSavedObject(this)) { + Debug.echoError("Cannot apply properties to noted objects."); + return; + } + adjust(mechanism); + } + + @Override + public void adjust(Mechanism mechanism) { + + // <--[mechanism] + // @object PolygonTag + // @name add_corner + // @input LocationTag + // @description + // Adds a corner to the end of the polygon's corner list. + // @tags + // ]> + // --> + if (mechanism.matches("add_corner") && mechanism.requireObject(LocationTag.class)) { + LocationTag loc = mechanism.valueAsType(LocationTag.class); + Corner newCorner = new Corner(loc.getX(), loc.getZ()); + corners.add(newCorner); + recalculateToFit(newCorner); + } + } +} diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/notable/NotableManager.java b/plugin/src/main/java/com/denizenscript/denizen/objects/notable/NotableManager.java index 0c4f90c259..da1feb07d8 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/notable/NotableManager.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/notable/NotableManager.java @@ -32,6 +32,7 @@ public NotableManager() { registerWithNotableManager(InventoryTag.class); registerWithNotableManager(ItemTag.class); registerWithNotableManager(LocationTag.class); + registerWithNotableManager(PolygonTag.class); } public static HashMap notableObjects = new HashMap<>(); diff --git a/plugin/src/main/java/com/denizenscript/denizen/objects/properties/bukkit/BukkitListProperties.java b/plugin/src/main/java/com/denizenscript/denizen/objects/properties/bukkit/BukkitListProperties.java index 5c9267b804..0701868101 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/objects/properties/bukkit/BukkitListProperties.java +++ b/plugin/src/main/java/com/denizenscript/denizen/objects/properties/bukkit/BukkitListProperties.java @@ -1,9 +1,7 @@ package com.denizenscript.denizen.objects.properties.bukkit; -import com.denizenscript.denizen.objects.EntityTag; -import com.denizenscript.denizen.objects.ItemTag; -import com.denizenscript.denizen.objects.MaterialTag; -import com.denizenscript.denizen.objects.PlayerTag; +import com.denizenscript.denizen.objects.*; +import com.denizenscript.denizen.utilities.Settings; import com.denizenscript.denizen.utilities.debugging.Debug; import com.denizenscript.denizencore.objects.core.ElementTag; import com.denizenscript.denizencore.objects.Mechanism; @@ -13,6 +11,8 @@ import com.denizenscript.denizencore.objects.properties.PropertyParser; import org.bukkit.ChatColor; +import java.util.List; + public class BukkitListProperties implements Property { public static boolean describes(ObjectTag list) { return list instanceof ListTag; @@ -100,6 +100,33 @@ public static void registerTags() { } return new ElementTag(output.toString().substring(0, output.length() - 2)); }); + + // <--[tag] + // @attribute + // @returns PolygonTag + // @description + // Converts a list of locations to a PolygonTag. + // The Y-Min and Y-Max values will be assigned based the range of Y values in the locations given. + // --> + PropertyParser.registerTag("to_polygon", (attribute, listObj) -> { + List locations = listObj.list.filter(LocationTag.class, attribute.context); + if (locations == null || locations.isEmpty()) { + return null; + } + if (locations.size() > Settings.blockTagsMaxBlocks()) { + return null; + } + PolygonTag polygon = new PolygonTag(new WorldTag(locations.get(0).getWorldName())); + polygon.yMin = locations.get(0).getY(); + polygon.yMax = polygon.yMin; + for (LocationTag location : locations) { + polygon.yMin = Math.min(polygon.yMin, location.getY()); + polygon.yMax = Math.max(polygon.yMax, location.getY()); + polygon.corners.add(new PolygonTag.Corner(location.getX(), location.getZ())); + } + polygon.recalculateBox(); + return polygon; + }); } @Override diff --git a/plugin/src/main/java/com/denizenscript/denizen/tags/core/PolygonTagBase.java b/plugin/src/main/java/com/denizenscript/denizen/tags/core/PolygonTagBase.java new file mode 100644 index 0000000000..2e20bd8b63 --- /dev/null +++ b/plugin/src/main/java/com/denizenscript/denizen/tags/core/PolygonTagBase.java @@ -0,0 +1,25 @@ +package com.denizenscript.denizen.tags.core; + +import com.denizenscript.denizen.objects.PolygonTag; +import com.denizenscript.denizencore.tags.TagManager; + +public class PolygonTagBase { + + public PolygonTagBase() { + + // <--[tag] + // @attribute ]> + // @returns PolygonTag + // @description + // Returns a polygon object constructed from the input value. + // Refer to <@link language PolygonTag objects>. + // --> + TagManager.registerTagHandler("polygon", (attribute) -> { + if (!attribute.hasContext(1)) { + attribute.echoError("Polygon tag base must have input."); + return null; + } + return PolygonTag.valueOf(attribute.getContext(1), attribute.context); + }); + } +} diff --git a/plugin/src/main/java/com/denizenscript/denizen/tags/core/ServerTagBase.java b/plugin/src/main/java/com/denizenscript/denizen/tags/core/ServerTagBase.java index a29c9d415b..f90001082a 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/tags/core/ServerTagBase.java +++ b/plugin/src/main/java/com/denizenscript/denizen/tags/core/ServerTagBase.java @@ -922,7 +922,7 @@ else if (recipe instanceof CookingRecipe) { // @returns ListTag // @description // Lists all saved notable objects of a specific type currently on the server. - // Valid types: locations, cuboids, ellipsoids, inventories + // Valid types: locations, cuboids, ellipsoids, inventories, polygons // This is primarily intended for debugging purposes, and it's best to avoid using this in a live script if possible. // --> if (attribute.startsWith("notables") || attribute.startsWith("list_notables")) { diff --git a/plugin/src/main/java/com/denizenscript/denizen/utilities/CommonRegistries.java b/plugin/src/main/java/com/denizenscript/denizen/utilities/CommonRegistries.java index f5737e4d77..5ae706b9fa 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/utilities/CommonRegistries.java +++ b/plugin/src/main/java/com/denizenscript/denizen/utilities/CommonRegistries.java @@ -74,6 +74,7 @@ public static void registerMainTagHandlers() { } new PlayerTagBase(); new PluginTagBase(); + new PolygonTagBase(); new TradeTagBase(); new WorldTagBase(); // Other bases @@ -98,6 +99,7 @@ public static void registerMainObjects() { } ObjectFetcher.registerWithObjectFetcher(PlayerTag.class, PlayerTag.tagProcessor); // p@ ObjectFetcher.registerWithObjectFetcher(PluginTag.class, PluginTag.tagProcessor); // pl@ + ObjectFetcher.registerWithObjectFetcher(PolygonTag.class, PolygonTag.tagProcessor); // polygon@ ObjectFetcher.registerWithObjectFetcher(TradeTag.class, TradeTag.tagProcessor); // trade@ ObjectFetcher.registerWithObjectFetcher(WorldTag.class, WorldTag.tagProcessor); // w@ StringBuilder debug = new StringBuilder(256); @@ -142,6 +144,7 @@ public boolean canBecome(ObjectTag inp) { CoreUtilities.registerTypeAsNoOtherTypeCode(NPCTag.class, "n"); CoreUtilities.registerTypeAsNoOtherTypeCode(PlayerTag.class, "p"); CoreUtilities.registerTypeAsNoOtherTypeCode(PluginTag.class, "pl"); + CoreUtilities.registerTypeAsNoOtherTypeCode(PolygonTag.class, "polygon"); CoreUtilities.registerTypeAsNoOtherTypeCode(TradeTag.class, "trade"); CoreUtilities.registerTypeAsNoOtherTypeCode(WorldTag.class, "w"); Debug.echoApproval("Loaded core object types: [" + debug.substring(0, debug.length() - 2) + "]"); diff --git a/plugin/src/main/java/com/denizenscript/denizen/utilities/command/DenizenCommandHandler.java b/plugin/src/main/java/com/denizenscript/denizen/utilities/command/DenizenCommandHandler.java index 552ccdf262..039e1d893f 100644 --- a/plugin/src/main/java/com/denizenscript/denizen/utilities/command/DenizenCommandHandler.java +++ b/plugin/src/main/java/com/denizenscript/denizen/utilities/command/DenizenCommandHandler.java @@ -43,8 +43,7 @@ public DenizenCommandHandler(Denizen denizen) { // denizen.npc.sit, denizen.npc.nameplate, denizen.npc.nickname, denizen.npc.trigger, // denizen.npc.assign, denizen.npc.constants, denizen.npc.pushable // - // However, we recommend just giving op to whoever needs to access Denizen - they can - // op themselves through Denizen anyway, why not save the trouble? + // However, we recommend just giving op to whoever needs to access Denizen - they can op themselves through Denizen anyway, why not save the trouble? // ( EG, /ex execute as_server "op " ) // // --> @@ -53,20 +52,17 @@ public DenizenCommandHandler(Denizen denizen) { // @name /denizen submit command // @group Console Commands // @description - // Use the '/denizen submit' command with '/denizen debug -r' to record debug output and post - // it online for assisting developers to see. + // Use the '/denizen submit' command with '/denizen debug -r' to record debug output and post it online for assisting developers to see. // - // To begin recording, simply use '/denizen debug -r'. After that, any debug output sent to the - // console and any player chat will be added to an internal record. Once enabled, you should then - // fire off scripts and events that aren't working fully. Finally, you use the '/denizen submit' - // command to take all the recording information and paste it to an online pastebin hosted by - // the Denizen team. It will give you back a direct link to the full debug output, which you - // can view yourself and send to other helpers without trouble. + // To begin recording, simply use '/denizen debug -r'. + // After that, any debug output sent to the console and any player chat will be added to an internal record. + // Once enabled, you should then fire off scripts and events that aren't working fully. + // Finally, you use the '/denizen submit' command to take all the recording information and paste it to an online pastebin hosted by the Denizen team. + // It will give you back a direct link to the full debug output, which you can view yourself and send to other helpers without trouble. // - // There is no limit to the recording size, to prevent any important information from being trimmed - // away. Be careful not to leave debug recording enabled by accident, as it may eventually begin - // using up large amounts of memory. (The submit command will automatically disable recording, - // or you can instead just use '/denizen debug -r' again.) + // There is no limit to the recording size, to prevent any important information from being trimmed away. + // Be careful not to leave debug recording enabled by accident, as it may eventually begin using up large amounts of memory. + // (The submit command will automatically disable recording, or you can instead just use '/denizen debug -r' again.) // // --> @Command( @@ -93,29 +89,27 @@ public void submit(CommandContext args, final CommandSender sender) throws Comma // @name /denizen debug command // @group Console Commands // @description - // Using the /denizen debug command interfaces with Denizen's debugger to allow control - // over debug messages. + // Using the /denizen debug command interfaces with Denizen's debugger to allow control over debug messages. // - // To enable debugging mode, simply type '/denizen debug'. While debug is enabled, all debuggable - // scripts, and any invoked actions, will output information to the console as they are executed. - // By default, all scripts are debuggable while the debugger is enabled. To disable a script - // specifically from debugging, simply add the 'debug:' node with a value of 'false' to your script - // container. This is typically used to silence particularly spammy scripts. Any kind of script - // container can be silenced using this method. + // To enable debugging mode, simply type '/denizen debug'. + // While debug is enabled, all debuggable scripts, and any invoked actions, will output information to the console as they are executed. + // By default, all scripts are debuggable while the debugger is enabled. + // To disable a script specifically from debugging, simply add the 'debug:' node with a value of 'false' to your script container. + // This is typically used to silence particularly spammy scripts. Any kind of script container can be silenced using this method. // - // To stop debugging, simply type the '/denizen debug' command again. This must be used without - // any additional options. A message will be sent to show the current status of the debugger. - // Note: While your server is in 'live production mode', the debugger should be disabled as your - // server will run slower while outputting debug information. + // To stop debugging, simply type the '/denizen debug' command again. + // This must be used without any additional options. A message will be sent to show the current status of the debugger. // - // There are also several options to further help debugging. To use an option, simply attach them - // to the /denizen debug command. One option, or multiple options can be used. For example: /denizen debug -sbi + // Note: you should almost NEVER disable debug entirely. Instead, always disable it on a per-script basis. + // If debug is globally disabled, that will hide important error messages, not just normal debug output. + // + // There are also several options to further help debugging. To use an option, simply attach them to the /denizen debug command. + // One option, or multiple options can be used. For example: /denizen debug -sbi // // '-c' enables/disables color. This is sometimes useful when debugging with a non-color console. // '-r' enables recording mode. See also: /denizen submit command // '-s' enables/disables stacktraces generated by Denizen. We might ask you to enable this when problems arise. - // '-b' enables/disables the ScriptBuilder debug. When enabled, Denizen will show info on script and argument creation. - // Warning: Can be spammy. + // '-b' enables/disables the ScriptBuilder debug. When enabled, Denizen will show info on script and argument creation. Warning: Can be spammy. // '-n' enables/disables debug trimming. When enabled, messages longer than 1024 characters will be 'snipped'. // '-i' enables/disables source information. When enabled, debug will show where it came from (when possible). // '-p' enables/disables packet debug logging. When enabled, all packets sent to players (from anywhere) will be logged to console.