mirror of
https://github.com/PolyhedralDev/Terra.git
synced 2026-04-02 22:06:05 +00:00
Terra Search Command
This commit is contained in:
5
common/addons/command-locate/build.gradle.kts
Normal file
5
common/addons/command-locate/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
version = version("1.0.0")
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.dfsek.terra.addons.commands.locate;
|
||||||
|
|
||||||
|
import com.dfsek.seismic.type.vector.Vector2Int;
|
||||||
|
import com.dfsek.seismic.type.vector.Vector3Int;
|
||||||
|
import com.dfsek.terra.api.util.generic.either.Either;
|
||||||
|
import com.dfsek.terra.api.world.biome.Biome;
|
||||||
|
import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
|
||||||
|
import com.dfsek.terra.api.world.info.WorldProperties;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class BiomeLocator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locates the nearest biome matching the given predicate using a parallelized square spiral search.
|
||||||
|
*
|
||||||
|
* @param provider The BiomeProvider to search in.
|
||||||
|
* @param properties The world properties (needed for seed and height bounds).
|
||||||
|
* @param originX Starting X coordinate.
|
||||||
|
* @param originZ Starting Z coordinate.
|
||||||
|
* @param radius The maximum radius (in blocks) to search.
|
||||||
|
* @param step The search step/increment. Higher values are faster but less accurate.
|
||||||
|
* @param filter The condition to match the biome.
|
||||||
|
* @param search3D If true, searches the entire vertical column at each step. If false, only checks originY.
|
||||||
|
* @return An Optional containing the location of the found biome, or empty if not found.
|
||||||
|
*/
|
||||||
|
public static Optional<Either<Vector3Int, Vector2Int>> search(
|
||||||
|
@NotNull BiomeProvider provider,
|
||||||
|
@NotNull WorldProperties properties,
|
||||||
|
int originX,
|
||||||
|
int originZ,
|
||||||
|
int radius,
|
||||||
|
int step,
|
||||||
|
@NotNull Predicate<Biome> filter,
|
||||||
|
boolean search3D
|
||||||
|
) {
|
||||||
|
long seed = properties.getSeed();
|
||||||
|
int minHeight = properties.getMinHeight();
|
||||||
|
int maxHeight = properties.getMaxHeight();
|
||||||
|
|
||||||
|
// 1. Check the exact center first
|
||||||
|
Optional<Either<Vector3Int, Vector2Int>> centerResult = check(provider, seed, originX, originZ, step, filter, search3D, minHeight, maxHeight);
|
||||||
|
if (centerResult.isPresent()) {
|
||||||
|
return centerResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Begin Parallel Square Spiral Search
|
||||||
|
// We iterate rings sequentially to guarantee finding the *nearest* result.
|
||||||
|
// However, we process all points within a specific ring in parallel.
|
||||||
|
for (int r = step; r <= radius; r += step) {
|
||||||
|
final int currentRadius = r;
|
||||||
|
final int minX = -currentRadius;
|
||||||
|
final int maxX = currentRadius;
|
||||||
|
final int minZ = -currentRadius;
|
||||||
|
final int maxZ = currentRadius;
|
||||||
|
|
||||||
|
Stream<int[]> northSide = IntStream.iterate(minX, n -> n < maxX, n -> n + step)
|
||||||
|
.mapToObj(x -> new int[]{x, minZ}); // Fixed Z (min), varying X
|
||||||
|
|
||||||
|
Stream<int[]> eastSide = IntStream.iterate(minZ, n -> n < maxZ, n -> n + step)
|
||||||
|
.mapToObj(z -> new int[]{maxX, z}); // Fixed X (max), varying Z
|
||||||
|
|
||||||
|
Stream<int[]> southSide = IntStream.iterate(maxX, n -> n > minX, n -> n - step)
|
||||||
|
.mapToObj(x -> new int[]{x, maxZ}); // Fixed Z (max), varying X
|
||||||
|
|
||||||
|
Stream<int[]> westSide = IntStream.iterate(maxZ, n -> n > minZ, n -> n - step)
|
||||||
|
.mapToObj(z -> new int[]{minX, z}); // Fixed X (min), varying Z
|
||||||
|
|
||||||
|
Optional<Either<Vector3Int, Vector2Int>> ringResult = Stream.of(northSide, eastSide, southSide, westSide)
|
||||||
|
.flatMap(Function.identity())
|
||||||
|
.parallel()
|
||||||
|
.map(coords -> check(
|
||||||
|
provider,
|
||||||
|
seed,
|
||||||
|
originX + coords[0],
|
||||||
|
originZ + coords[1],
|
||||||
|
step,
|
||||||
|
filter,
|
||||||
|
search3D,
|
||||||
|
minHeight,
|
||||||
|
maxHeight
|
||||||
|
))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.findFirst(); // findFirst() respects encounter order (North -> East -> South -> West)
|
||||||
|
|
||||||
|
if (ringResult.isPresent()) {
|
||||||
|
return ringResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check a specific coordinate column or point.
|
||||||
|
* This logic is executed inside the worker threads.
|
||||||
|
*/
|
||||||
|
private static Optional<Either<Vector3Int, Vector2Int>> check(
|
||||||
|
BiomeProvider provider,
|
||||||
|
long seed,
|
||||||
|
int x,
|
||||||
|
int z,
|
||||||
|
int step,
|
||||||
|
Predicate<Biome> filter,
|
||||||
|
boolean search3D,
|
||||||
|
int minHeight,
|
||||||
|
int maxHeight
|
||||||
|
) {
|
||||||
|
if (search3D) {
|
||||||
|
// Iterate from bottom to top of the world using the step
|
||||||
|
for (int y = minHeight; y < maxHeight; y += step) {
|
||||||
|
if (filter.test(provider.getBiome(x, y, z, seed))) {
|
||||||
|
return Optional.of(Either.left(Vector3Int.of(x, y, z)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
// 2D Mode: Check only the base biome
|
||||||
|
// We use a flatMap approach here to be safe with Optionals inside the stream
|
||||||
|
return provider.getBaseBiome(x, z, seed)
|
||||||
|
.filter(filter)
|
||||||
|
.map(b -> Either.right(Vector2Int.of(x, z)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.dfsek.terra.addons.commands.locate;
|
||||||
|
|
||||||
|
|
||||||
|
import com.dfsek.seismic.type.vector.Vector2Int;
|
||||||
|
import com.dfsek.seismic.type.vector.Vector3Int;
|
||||||
|
import org.incendo.cloud.CommandManager;
|
||||||
|
import org.incendo.cloud.component.DefaultValue;
|
||||||
|
import org.incendo.cloud.context.CommandContext;
|
||||||
|
import org.incendo.cloud.description.Description;
|
||||||
|
import org.incendo.cloud.parser.standard.IntegerParser;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
|
||||||
|
import com.dfsek.terra.api.Platform;
|
||||||
|
import com.dfsek.terra.api.addon.BaseAddon;
|
||||||
|
import com.dfsek.terra.api.command.CommandSender;
|
||||||
|
import com.dfsek.terra.api.command.arguments.RegistryArgument;
|
||||||
|
import com.dfsek.terra.api.entity.Entity;
|
||||||
|
import com.dfsek.terra.api.event.events.platform.CommandRegistrationEvent;
|
||||||
|
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
|
||||||
|
import com.dfsek.terra.api.inject.annotations.Inject;
|
||||||
|
import com.dfsek.terra.api.registry.Registry;
|
||||||
|
import com.dfsek.terra.api.structure.Structure;
|
||||||
|
import com.dfsek.terra.api.util.generic.either.Either;
|
||||||
|
import com.dfsek.terra.api.util.reflection.TypeKey;
|
||||||
|
import com.dfsek.terra.api.world.World;
|
||||||
|
import com.dfsek.terra.api.world.biome.Biome;
|
||||||
|
import com.dfsek.terra.api.world.info.WorldProperties;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
||||||
|
public class LocateCommandAddon implements AddonInitializer {
|
||||||
|
@Inject
|
||||||
|
private Platform platform;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BaseAddon addon;
|
||||||
|
|
||||||
|
private static Registry<Biome> getBiomeRegistry(CommandContext<CommandSender> sender) {
|
||||||
|
return sender.sender().getEntity().orElseThrow().world().getPack().getRegistry(Biome.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
platform.getEventManager()
|
||||||
|
.getHandler(FunctionalEventHandler.class)
|
||||||
|
.register(addon, CommandRegistrationEvent.class)
|
||||||
|
.then(event -> {
|
||||||
|
CommandManager<CommandSender> manager = event.getCommandManager();
|
||||||
|
manager.command(
|
||||||
|
manager.commandBuilder("search", Description.of("Locate things in the world"))
|
||||||
|
.literal("biome")
|
||||||
|
// Argument 1: The Biome to search for
|
||||||
|
.argument(RegistryArgument.builder("biome",
|
||||||
|
LocateCommandAddon::getBiomeRegistry,
|
||||||
|
TypeKey.of(Biome.class)))
|
||||||
|
// Argument 2: Radius (Optional, default 5000)
|
||||||
|
.optional("radius", IntegerParser.integerParser(100), DefaultValue.constant(5000))
|
||||||
|
// Argument 3: Step/Resolution (Optional, default 16)
|
||||||
|
.optional("step", IntegerParser.integerParser(1), DefaultValue.constant(16))
|
||||||
|
// Flag: Toggle 3D search (e.g., --3d or -3)
|
||||||
|
.flag(manager.flagBuilder("3d").withAliases("3").build())
|
||||||
|
.handler(context -> {
|
||||||
|
//Gather Context & Arguments
|
||||||
|
Biome targetBiome = context.get("biome");
|
||||||
|
Entity sender = context.sender().getEntity().orElseThrow(
|
||||||
|
() -> new Error("Only entities can run this command."));
|
||||||
|
World world = sender.world();
|
||||||
|
|
||||||
|
int radius = context.get("radius");
|
||||||
|
int step = context.get("step");
|
||||||
|
boolean search3D = context.flags().hasFlag("3d");
|
||||||
|
|
||||||
|
//Notify player that search has started (as it might take a moment)
|
||||||
|
context.sender().sendMessage("Searching for " + targetBiome.getID() + " within " + radius + " blocks...");
|
||||||
|
|
||||||
|
//Execute Search
|
||||||
|
Optional<Either<Vector3Int, Vector2Int>> result = BiomeLocator.search(
|
||||||
|
world.getBiomeProvider(),
|
||||||
|
world,
|
||||||
|
sender.position().getFloorX(),
|
||||||
|
sender.position().getFloorZ(),
|
||||||
|
radius,
|
||||||
|
step,
|
||||||
|
found -> found.equals(targetBiome), // Predicate: Match exact biome instance
|
||||||
|
search3D
|
||||||
|
);
|
||||||
|
|
||||||
|
//Handle Result
|
||||||
|
if(result.isPresent()) {
|
||||||
|
Either<Vector3Int, Vector2Int> location = result.get();
|
||||||
|
String coords;
|
||||||
|
|
||||||
|
if(location.hasLeft()) {
|
||||||
|
// 3D Result
|
||||||
|
Optional<Vector3Int> vec = location.getLeft();
|
||||||
|
if (vec.isPresent()) {
|
||||||
|
Vector3Int vecUnwrapped = vec.get();
|
||||||
|
coords = String.format("%d, %d, %d", vecUnwrapped.getX(), vecUnwrapped.getY(), vecUnwrapped.getZ());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
context.sender().sendMessage("Could not find " + targetBiome.getID() + " within " + radius + " blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2D Result
|
||||||
|
Optional<Vector2Int> vec = location.getRight();
|
||||||
|
if (vec.isPresent()) {
|
||||||
|
Vector2Int vecUnwrapped = vec.get();
|
||||||
|
coords = String.format("%d, %d", vecUnwrapped.getX(), vecUnwrapped.getZ());
|
||||||
|
} else {
|
||||||
|
context.sender().sendMessage("Could not find " + targetBiome.getID() + " within " + radius + " blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sender().sendMessage("Found " + targetBiome.getID() + " at [" + coords + "]");
|
||||||
|
} else {
|
||||||
|
context.sender().sendMessage("Could not find " + targetBiome.getID() + " within " + radius + " blocks.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.permission("terra.locate.biome")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
schema-version: 1
|
||||||
|
contributors:
|
||||||
|
- Terra contributors
|
||||||
|
id: command-locate
|
||||||
|
version: @VERSION@
|
||||||
|
entrypoints:
|
||||||
|
- "com.dfsek.terra.addons.commands.locate.LocateCommandAddon"
|
||||||
|
website:
|
||||||
|
issues: https://github.com/PolyhedralDev/Terra/issues
|
||||||
|
source: https://github.com/PolyhedralDev/Terra
|
||||||
|
docs: https://terra.polydev.org
|
||||||
|
license: MIT License
|
||||||
@@ -18,7 +18,7 @@ public final class LifecycleEntryPoint {
|
|||||||
logger.info("Initializing Terra {} mod...", modName);
|
logger.info("Initializing Terra {} mod...", modName);
|
||||||
|
|
||||||
FabricServerCommandManager<CommandSender> manager = new FabricServerCommandManager<>(
|
FabricServerCommandManager<CommandSender> manager = new FabricServerCommandManager<>(
|
||||||
ExecutionCoordinator.simpleCoordinator(),
|
ExecutionCoordinator.asyncCoordinator(),
|
||||||
SenderMapper.create(
|
SenderMapper.create(
|
||||||
serverCommandSource -> (CommandSender) serverCommandSource,
|
serverCommandSource -> (CommandSender) serverCommandSource,
|
||||||
commandSender -> (ServerCommandSource) commandSender)
|
commandSender -> (ServerCommandSource) commandSender)
|
||||||
|
|||||||
Reference in New Issue
Block a user