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 000000000..9d41e5622
--- /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 super Entity> 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 super Block> 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 000000000..19820201c
--- /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 e99fa923d..8784842d1 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 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
@Nullable RayTraceResult rayTrace(io.papermc.paper.math.@NotNull Position start, @NotNull Vector direction, double maxDistance, @NotNull FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks, double raySize, @Nullable Predicate super Entity> filter, @Nullable Predicate super Block> 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 000000000..3d078858d
--- /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 super Entity> entityFilter;
+ public @Nullable Predicate super Block> 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 super Entity> entityFilter) {
+ Preconditions.checkArgument(entityFilter != null, "entityFilter must not be null");
+ this.entityFilter = entityFilter;
+ return this;
+ }
+
+ @Override
+ public PositionedRayTraceConfigurationBuilder blockFilter(final Predicate super Block> 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 ba32db69c..20f709a4e 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.ImmutableList;
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 class CraftWorld extends CraftRegionAccessor implements World {
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());