diff --git a/paper-api/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilder.java b/paper-api/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilder.java new file mode 100644 index 000000000000..9d41e5622351 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilder.java @@ -0,0 +1,112 @@ +package io.papermc.paper.raytracing; + +import java.util.function.Predicate; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; +import org.checkerframework.checker.index.qual.NonNegative; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NullMarked; + +/** + * A builder for configuring a raytrace with a starting location + * and direction. + */ +@NullMarked +public interface PositionedRayTraceConfigurationBuilder { + + /** + * Sets the starting location. + * + * @param start the new starting location + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder start(Location start); + + /** + * Sets the direction. + * + * @param direction the new direction + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder direction(Vector direction); + + /** + * Sets the maximum distance. + * + * @param maxDistance the new maxDistance + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder maxDistance(@NonNegative double maxDistance); + + /** + * Sets the FluidCollisionMode when looking for block collisions. + *

+ * If collisions with passable blocks are ignored, fluid collisions are + * ignored as well regardless of the fluid collision mode. + * + * @param fluidCollisionMode the new FluidCollisionMode + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder fluidCollisionMode(FluidCollisionMode fluidCollisionMode); + + /** + * Sets whether the raytrace should ignore passable blocks when looking for + * block collisions. + *

+ * If collisions with passable blocks are ignored, fluid collisions are + * ignored as well regardless of the fluid collision mode. + *

+ * Portal blocks are only considered passable if the ray starts within them. + * Apart from that collisions with portal blocks will be considered even if + * collisions with passable blocks are otherwise ignored. + * + * @param ignorePassableBlocks if the raytrace should ignore passable blocks + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder ignorePassableBlocks(boolean ignorePassableBlocks); + + /** + * Sets the size of the raytrace when looking for entity collisions. + * + * @param raySize the new raytrace size + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder raySize(@NonNegative double raySize); + + /** + * Sets the current entity filter when looking for entity collisions. + * + * @param entityFilter predicate for entities the ray can potentially collide with + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder entityFilter(Predicate entityFilter); + + /** + * Sets the current block filter when looking for block collisions. + * + * @param blockFilter predicate for blocks the ray can potentially collide with + * @return a reference to this object + */ + @Contract(value = "_ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder blockFilter(Predicate blockFilter); + + /** + * Sets the targets for the rayTrace. + * + * @param first the first target + * @param others the other targets + * @return a reference to this object + */ + @Contract(value = "_, _ -> this", mutates = "this") + PositionedRayTraceConfigurationBuilder targets(RayTraceTarget first, RayTraceTarget... others); +} diff --git a/paper-api/src/main/java/io/papermc/paper/raytracing/RayTraceTarget.java b/paper-api/src/main/java/io/papermc/paper/raytracing/RayTraceTarget.java new file mode 100644 index 000000000000..19820201ce55 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/raytracing/RayTraceTarget.java @@ -0,0 +1,9 @@ +package io.papermc.paper.raytracing; + +/** + * List of Targets a builder can target. + */ +public enum RayTraceTarget { + ENTITY, + BLOCK +} diff --git a/paper-api/src/main/java/org/bukkit/World.java b/paper-api/src/main/java/org/bukkit/World.java index e99fa923d35b..8784842d14bb 100644 --- a/paper-api/src/main/java/org/bukkit/World.java +++ b/paper-api/src/main/java/org/bukkit/World.java @@ -1,6 +1,7 @@ package org.bukkit; import java.io.File; +import io.papermc.paper.raytracing.PositionedRayTraceConfigurationBuilder; import org.bukkit.generator.ChunkGenerator; import java.util.ArrayList; @@ -1938,6 +1939,20 @@ default Iterable audiences() { @Nullable RayTraceResult rayTrace(io.papermc.paper.math.@NotNull Position start, @NotNull Vector direction, double maxDistance, @NotNull FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks, double raySize, @Nullable Predicate filter, @Nullable Predicate canCollide); // Paper end + /** + * Performs a ray trace that checks for collisions with the specified + * targets. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param builderConsumer a consumer to configure the ray trace configuration. + * The received builder is not valid for use outside the consumer + * @return the closest ray trace hit result with either a block or an + * entity, or null if there is no hit + */ + @Nullable RayTraceResult rayTrace(@NotNull Consumer builderConsumer); + /** * Gets the default spawn {@link Location} of this world * diff --git a/paper-server/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilderImpl.java b/paper-server/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilderImpl.java new file mode 100644 index 000000000000..3d078858d990 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/raytracing/PositionedRayTraceConfigurationBuilderImpl.java @@ -0,0 +1,90 @@ +package io.papermc.paper.raytracing; + +import com.google.common.base.Preconditions; +import java.util.EnumSet; +import java.util.OptionalDouble; +import java.util.function.Predicate; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class PositionedRayTraceConfigurationBuilderImpl implements PositionedRayTraceConfigurationBuilder { + + public @Nullable Location start; + public @Nullable Vector direction; + public OptionalDouble maxDistance = OptionalDouble.empty(); + public FluidCollisionMode fluidCollisionMode = FluidCollisionMode.NEVER; + public boolean ignorePassableBlocks; + public double raySize = 0.0D; + public @Nullable Predicate entityFilter; + public @Nullable Predicate blockFilter; + public EnumSet targets = EnumSet.noneOf(RayTraceTarget.class); + + @Override + public PositionedRayTraceConfigurationBuilder start(final Location start) { + Preconditions.checkArgument(start != null, "start must not be null"); + this.start = start.clone(); + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder direction(final Vector direction) { + Preconditions.checkArgument(direction != null, "direction must not be null"); + this.direction = direction.clone(); + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder maxDistance(final double maxDistance) { + Preconditions.checkArgument(maxDistance >= 0, "maxDistance must be non-negative"); + this.maxDistance = OptionalDouble.of(maxDistance); + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder fluidCollisionMode(final FluidCollisionMode fluidCollisionMode) { + Preconditions.checkArgument(fluidCollisionMode != null, "fluidCollisionMode must not be null"); + this.fluidCollisionMode = fluidCollisionMode; + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder ignorePassableBlocks(final boolean ignorePassableBlocks) { + this.ignorePassableBlocks = ignorePassableBlocks; + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder raySize(final double raySize) { + Preconditions.checkArgument(raySize >= 0, "raySize must be non-negative"); + this.raySize = raySize; + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder entityFilter(final Predicate entityFilter) { + Preconditions.checkArgument(entityFilter != null, "entityFilter must not be null"); + this.entityFilter = entityFilter; + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder blockFilter(final Predicate blockFilter) { + Preconditions.checkArgument(blockFilter != null, "blockFilter must not be null"); + this.blockFilter = blockFilter; + return this; + } + + @Override + public PositionedRayTraceConfigurationBuilder targets(final RayTraceTarget first, final RayTraceTarget... others) { + Preconditions.checkArgument(first != null, "first must not be null"); + Preconditions.checkArgument(others != null, "others must not be null"); + this.targets = EnumSet.of(first, others); + return this; + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index ba32db69c95b..20f709a4e8d7 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -6,9 +6,12 @@ import com.google.common.collect.ImmutableMap; import com.mojang.datafixers.util.Pair; import io.papermc.paper.FeatureHooks; +import io.papermc.paper.raytracing.RayTraceTarget; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import io.papermc.paper.raytracing.PositionedRayTraceConfigurationBuilder; +import io.papermc.paper.raytracing.PositionedRayTraceConfigurationBuilderImpl; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.io.File; import java.util.ArrayList; @@ -1248,6 +1251,26 @@ public RayTraceResult rayTrace(io.papermc.paper.math.Position start, Vector dire return blockHit; } + @Override + public RayTraceResult rayTrace(Consumer builderConsumer) { + PositionedRayTraceConfigurationBuilderImpl builder = new PositionedRayTraceConfigurationBuilderImpl(); + + builderConsumer.accept(builder); + Preconditions.checkArgument(builder.start != null, "Start location cannot be null"); + Preconditions.checkArgument(builder.direction != null, "Direction vector cannot be null"); + Preconditions.checkArgument(builder.maxDistance.isPresent(), "Max distance must be set"); + Preconditions.checkArgument(!builder.targets.isEmpty(), "At least one target"); + + final double maxDistance = builder.maxDistance.getAsDouble(); + if (builder.targets.contains(RayTraceTarget.ENTITY)) { + if (builder.targets.contains(RayTraceTarget.BLOCK)) { + return this.rayTrace(builder.start, builder.direction, maxDistance, builder.fluidCollisionMode, builder.ignorePassableBlocks, builder.raySize, builder.entityFilter, builder.blockFilter); + } + return this.rayTraceEntities(builder.start, builder.direction, maxDistance, builder.raySize, builder.entityFilter); + } + return this.rayTraceBlocks(builder.start, builder.direction, maxDistance, builder.fluidCollisionMode, builder.ignorePassableBlocks, builder.blockFilter); + } + @Override public List getPlayers() { List list = new ArrayList(this.world.players().size());