Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /region, fix bounds & improve union perf #1491

Merged
merged 3 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions core/src/main/java/tc/oc/pgm/command/MapDevCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@
import static net.kyori.adventure.text.Component.join;
import static net.kyori.adventure.text.Component.text;
import static tc.oc.pgm.command.util.ParserConstants.CURRENT;
import static tc.oc.pgm.util.bukkit.Effects.EFFECTS;
import static tc.oc.pgm.util.text.TextException.exception;

import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Default;
import org.incendo.cloud.annotations.Flag;
import org.incendo.cloud.annotations.Permission;
import tc.oc.pgm.api.PGM;
import tc.oc.pgm.api.Permissions;
import tc.oc.pgm.api.filter.Filter;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.api.region.Region;
import tc.oc.pgm.util.Audience;
import tc.oc.pgm.util.PrettyPaginatedComponentResults;
import tc.oc.pgm.util.block.BlockFaces;
import tc.oc.pgm.util.block.BlockVectors;
import tc.oc.pgm.util.material.BlockMaterialData;
import tc.oc.pgm.util.material.MaterialData;
import tc.oc.pgm.util.text.TextFormatter;
import tc.oc.pgm.variables.Variable;
import tc.oc.pgm.variables.VariablesMatchModule;
Expand Down Expand Up @@ -110,4 +125,114 @@ public void evaluateFilter(
.append(text(" to ", NamedTextColor.YELLOW))
.append(target.getName()));
}

@Command("region <region>")
@CommandDescription("Visualize a region")
@Permission(Permissions.DEBUG)
public void visualizeRegion(MatchPlayer viewer, @Argument("region") Region region) {
var pl = viewer.getBukkit();
var world = pl.getWorld();
var reg = region.getStatic(world);
if (!reg.getBounds().isBlockFinite()) {
throw exception("Region is not finite");
}
new DisplayRunner(pl, reg);
}

private static class DisplayRunner implements Runnable {
private static final BlockMaterialData GLASS = MaterialData.block(Material.GLASS);
private static final float SPREAD_MUL = 0.2f;
private static final float COUNT_MUL = 0.1f;

private final Player player;
private final Region.Static region;
private final Location loc;

private final float[][] directions;
private final double[][] edges, vertices;

private final Future<?> task;
private int ticks = 150;

private DisplayRunner(Player player, Region.Static region) {
this.player = player;
this.region = region;
this.loc = new Location(player.getWorld(), 0, 0, 0);

var bounds = region.getBounds();
Vector min = bounds.getMin(), max = bounds.getMax();
boolean hasBottom = min.getY() >= -64, hasTop = max.getY() <= 320;
if (!hasBottom) min.setY(-32);
if (!hasTop) max.setY(288);

var center = min.clone().add(max).multiply(0.5);
var size = max.clone().subtract(min);

this.directions = new float[][] {
{(float) size.getX() * SPREAD_MUL, 0, 0, (float) Math.max(10, size.getX() * COUNT_MUL)},
{0, (float) size.getY() * SPREAD_MUL, 0, (float) Math.max(10, size.getY() * COUNT_MUL)},
{0, 0, (float) size.getZ() * SPREAD_MUL, (float) Math.max(10, size.getZ() * COUNT_MUL)}
};
if (!hasBottom) min.setY(Double.NaN);
if (!hasTop) max.setY(Double.NaN);

this.edges = new double[][] {
{center.getX(), min.getY(), min.getZ()},
{center.getX(), min.getY(), max.getZ()},
{center.getX(), max.getY(), min.getZ()},
{center.getX(), max.getY(), max.getZ()},
{min.getX(), center.getY(), min.getZ()},
{min.getX(), center.getY(), max.getZ()},
{max.getX(), center.getY(), min.getZ()},
{max.getX(), center.getY(), max.getZ()},
{min.getX(), min.getY(), center.getZ()},
{min.getX(), max.getY(), center.getZ()},
{max.getX(), min.getY(), center.getZ()},
{max.getX(), max.getY(), center.getZ()},
};
this.vertices = new double[][] {
{min.getX(), min.getY(), min.getZ()},
{min.getX(), min.getY(), max.getZ()},
{min.getX(), max.getY(), min.getZ()},
{min.getX(), max.getY(), max.getZ()},
{max.getX(), min.getY(), min.getZ()},
{max.getX(), min.getY(), max.getZ()},
{max.getX(), max.getY(), min.getZ()},
{max.getX(), max.getY(), max.getZ()},
};

region.getBlockVectors().forEach(bv -> {
var b = BlockVectors.blockAt(loc.getWorld(), bv);
for (var dir : BlockFaces.NEIGHBORS) {
if (!region.contains(b.getRelative(dir))) {
GLASS.sendBlockChange(player, b.getLocation());
break;
}
}
});

task = PGM.get().getExecutor().scheduleWithFixedDelay(this, 0, 100, TimeUnit.MILLISECONDS);
}

@Override
public void run() {
if (ticks-- < 0) {
task.cancel(true);
region.getBlocks(player.getWorld()).forEach(b -> MaterialData.block(b)
.sendBlockChange(player, b.getLocation()));
return;
}
for (int i = 0; i < edges.length; i++) {
if (Double.isNaN(edges[i][1])) continue;
loc.set(edges[i][0], edges[i][1], edges[i][2]);
var dirs = directions[(i / 4)];
EFFECTS.spawnFlame(player, loc, dirs[0], dirs[1], dirs[2], (int) dirs[3]);
}
for (double[] vertex : vertices) {
if (Double.isNaN(vertex[1])) continue;
loc.set(vertex[0], vertex[1], vertex[2]);
EFFECTS.coloredDust(player, loc, Color.RED);
}
}
}
}
37 changes: 37 additions & 0 deletions core/src/main/java/tc/oc/pgm/command/parsers/RegionParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tc.oc.pgm.command.parsers;

import java.util.List;
import java.util.Map;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.parser.ParserParameters;
import tc.oc.pgm.api.feature.FeatureDefinition;
import tc.oc.pgm.api.region.Region;
import tc.oc.pgm.features.FeatureDefinitionContext;
import tc.oc.pgm.filters.FilterMatchModule;

public final class RegionParser
extends MatchObjectParser<Region, Map.Entry<String, FeatureDefinition>, FilterMatchModule> {

public RegionParser(CommandManager<CommandSender> manager, ParserParameters options) {
super(manager, options, Region.class, FilterMatchModule.class, "regions");
}

@Override
protected Iterable<Map.Entry<String, FeatureDefinition>> objects(FilterMatchModule module) {
if (!(module.getFilterContext() instanceof FeatureDefinitionContext fdc)) return List.of();
return () -> fdc.stream()
.filter(entry -> entry != null && entry.getValue() instanceof Region)
.iterator();
}

@Override
protected String getName(Map.Entry<String, FeatureDefinition> obj) {
return obj.getKey();
}

@Override
protected Region getValue(Map.Entry<String, FeatureDefinition> obj) {
return (Region) obj.getValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import tc.oc.pgm.api.party.Party;
import tc.oc.pgm.api.party.VictoryCondition;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.api.region.Region;
import tc.oc.pgm.api.setting.SettingKey;
import tc.oc.pgm.api.setting.SettingValue;
import tc.oc.pgm.classes.PlayerClass;
Expand Down Expand Up @@ -74,6 +75,7 @@
import tc.oc.pgm.command.parsers.PhasesParser;
import tc.oc.pgm.command.parsers.PlayerClassParser;
import tc.oc.pgm.command.parsers.PlayerParser;
import tc.oc.pgm.command.parsers.RegionParser;
import tc.oc.pgm.command.parsers.RotationParser;
import tc.oc.pgm.command.parsers.SettingValueParser;
import tc.oc.pgm.command.parsers.TeamParser;
Expand Down Expand Up @@ -192,6 +194,7 @@ protected void setupParsers() {
TypeFactory.parameterizedClass(Optional.class, VictoryCondition.class),
new VictoryConditionParser());
registerParser(Filter.class, FilterParser::new);
registerParser(Region.class, RegionParser::new);
registerParser(SettingKey.class, new EnumParser<>(SettingKey.class, CommandKeys.SETTING_KEY));
registerParser(SettingValue.class, new SettingValueParser());
registerParser(Phase.Phases.class, PhasesParser::new);
Expand Down
22 changes: 13 additions & 9 deletions core/src/main/java/tc/oc/pgm/regions/Bounds.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,29 @@ public Vector getRandomPoint(Random random) {
this.min.getZ() + size.getZ() * random.nextDouble());
}

private static double roundDown(double value) {
return Math.ceil(value - 0.5d);
}

public BlockVector getBlockMin() {
return new BlockVector(
(int) this.min.getX() + 0.5d,
(int) Math.max(0, Math.min(255, this.min.getY() + 0.5d)),
(int) this.min.getZ() + 0.5d);
roundDown(min.getX()) + 0.5d,
Math.clamp(roundDown(min.getY()), 0, 255) + 0.5d,
roundDown(min.getZ()) + 0.5d);
}

public BlockVector getBlockMaxInside() {
return new BlockVector(
(int) this.max.getX() - 0.5d,
(int) Math.max(0, Math.min(255, this.max.getY() - 0.5d)),
(int) this.max.getZ() - 0.5d);
Math.round(max.getX()) - 0.5d,
Math.clamp(Math.round(max.getY()), 1, 255) - 0.5,
Math.round(max.getZ()) - 0.5d);
}

public BlockVector getBlockMaxOutside() {
return new BlockVector(
(int) this.max.getX() + 0.5d,
(int) Math.max(0, Math.min(256, this.max.getY() + 0.5d)),
(int) this.max.getZ() + 0.5d);
Math.round(max.getX()) + 0.5d,
Math.clamp(Math.round(max.getY()), 0, 255) + 0.5d,
Math.round(max.getZ()) + 0.5d);
}

public boolean containsBlock(BlockVector v) {
Expand Down
59 changes: 59 additions & 0 deletions core/src/main/java/tc/oc/pgm/regions/Union.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package tc.oc.pgm.regions;

import static com.google.common.collect.Iterators.*;

import java.util.Iterator;
import java.util.function.Supplier;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.region.Region;
import tc.oc.pgm.api.region.RegionDefinition;
import tc.oc.pgm.util.block.BlockVectorSet;

public class Union implements RegionDefinition.Static {
private final Region[] regions;
private Supplier<Iterator<BlockVector>> iteratorFactory;

public Union(Region... regions) {
this.regions = regions;
Expand Down Expand Up @@ -82,6 +89,58 @@ public Bounds getBounds() {
return bounds;
}

@Override
public Iterator<BlockVector> getBlockVectorIterator() {
if (iteratorFactory == null) iteratorFactory = createIteratorFactory();
return iteratorFactory.get();
}

/**
* Decides on the best strategy for iterating all blocks in the union. There's 3 possible
* strategies: - Full-scan: iterate the whole region bounds, and for each block check if
* this::contains. - Pros: Always correct, no memory overhead. - Cons: is slow! for very disperse
* cuboids this is very bad. - Child-scan: concatenate iterating each of the children. - Pros:
* Fastest, no memory overhead. - Cons: if children overlap, you can pass through a block twice.
* Not acceptable. - Filtered-child-scan: concatenate iterating of each child, but filter by
* visited. - Pros: Always correct, fast even for disperse cuboids - Cons: requires extra memory
* to keep track of already visited positions. Ideally whenever possible, child scan is preferred
* (non-overlapping children). When children overlap, pgm will pick full-scan or filtered child
* scan based on how much emptiness exists.
*
* @return A supplier for iterators over the region
*/
private Supplier<Iterator<BlockVector>> createIteratorFactory() {
if (!isStatic()) return this::fullScan;

// Checking all children are disjoint is O(n²), assume overlap and skip the check if too many.
boolean disjoint = regions.length < 100;
var childrenVolume = 0;
for (int i = 0; i < regions.length; i++) {
var bounds = regions[i].getBounds();
childrenVolume += bounds.getBlockVolume();
for (int j = i + 1; disjoint && j < regions.length; j++) {
if (!Bounds.disjoint(bounds, regions[j].getBounds())) disjoint = false;
}
}

if (disjoint) return this::childScan;
if (getBounds().getBlockVolume() < childrenVolume * 5) return this::fullScan;

final int sumVolume = childrenVolume;
return () -> {
var visited = new BlockVectorSet(sumVolume);
return filter(childScan(), visited::add);
};
}

private Iterator<BlockVector> fullScan() {
return filter(getBounds().getBlockIterator(), this::contains);
}

private Iterator<BlockVector> childScan() {
return concat(transform(forArray(regions), r -> r.getStatic().getBlockVectorIterator()));
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@ public void blockBreak(Location location, BlockMaterialData material) {
.getWorld()
.playEffect(location, Effect.STEP_SOUND, ((ModernBlockMaterialData) material).getBlock());
}

@Override
public void spawnFlame(Player player, Location loc, float x, float y, float z, int amt) {
player.spawnParticle(Particle.FLAME, loc, amt, x, y, z, 0, null, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public void blockBreak(Location location, BlockMaterialData material) {
location.getWorld().playEffect(location, Effect.STEP_SOUND, material.encoded());
}

@Override
public void spawnFlame(Player player, Location loc, float x, float y, float z, int amt) {
player.spigot().playEffect(loc, Effect.FLAME, 0, 0, x, y, z, 0, amt, 256);
}

private float rgbToParticle(int rgb) {
return Math.max(0.001f, (rgb / 255.0f));
}
Expand Down
2 changes: 2 additions & 0 deletions util/src/main/java/tc/oc/pgm/util/bukkit/Effects.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ public interface Effects {
void explosion(Player player, Location location);

void blockBreak(Location location, BlockMaterialData material);

void spawnFlame(Player player, Location loc, float x, float y, float z, int amt);
}