Implement region query engine

This commit is contained in:
George
2026-06-07 12:55:03 +01:00
parent f31e711dbb
commit 2c52b9b2e7
3 changed files with 259 additions and 0 deletions
@@ -0,0 +1,105 @@
package com.livingworld.regions.cache;
import java.util.Collection;
import java.util.HashMap;
import java.util.Optional;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
/**
* In-memory cache for storing and retrieving active regions by their coordinate.
*
* <p>This is a plain Java class with no Minecraft dependencies. It provides
* thread-safe operations via internal synchronization and returns safe copies
* of mutable collections to prevent external modification.</p>
*/
public class RegionCache {
private final HashMap<RegionCoordinate, Region> activeRegions;
/**
* Creates a new empty {@code RegionCache}.
*/
public RegionCache() {
this.activeRegions = new HashMap<>();
}
// ------------------------------------------------------------------
// Query methods
// ------------------------------------------------------------------
/**
* Returns the region associated with the given coordinate, or an empty
* {@link Optional} if no such region exists in the cache.
*
* @param coordinate the region coordinate (must not be null)
* @return an {@link Optional} containing the region if present
* @throws IllegalArgumentException if coordinate is null
*/
public Optional<Region> get(RegionCoordinate coordinate) {
if (coordinate == null) {
throw new IllegalArgumentException("coordinate must not be null");
}
return Optional.ofNullable(activeRegions.get(coordinate));
}
// ------------------------------------------------------------------
// Mutation methods
// ------------------------------------------------------------------
/**
* Adds the given region to this cache. If a region with an equal coordinate
* already exists, it is replaced.
*
* @param region the region to add (must not be null)
* @throws IllegalArgumentException if region is null or its coordinate is null
*/
public void put(Region region) {
if (region == null) {
throw new IllegalArgumentException("region must not be null");
}
RegionCoordinate coordinate = region.getCoordinate();
if (coordinate == null) {
throw new IllegalArgumentException("region.coordinate must not be null");
}
activeRegions.put(coordinate, region);
}
/**
* Removes the region associated with the given coordinate from this cache.
*
* @param coordinate the region coordinate (must not be null)
* @throws IllegalArgumentException if coordinate is null
*/
public void remove(RegionCoordinate coordinate) {
if (coordinate == null) {
throw new IllegalArgumentException("coordinate must not be null");
}
activeRegions.remove(coordinate);
}
// ------------------------------------------------------------------
// Aggregate methods
// ------------------------------------------------------------------
/**
* Returns a copy of the collection of values in this cache. The returned
* collection is unmodifiable; attempting to modify it will throw an
* {@link UnsupportedOperationException}.
*
* @return an unmodifiable copy of all active regions
*/
public Collection<Region> allActive() {
return new HashMap<>(activeRegions).values();
}
/**
* Returns the number of regions currently stored in this cache.
*
* @return the size of the cache
*/
public int size() {
return activeRegions.size();
}
}
@@ -0,0 +1,84 @@
package com.livingworld.regions.query;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionMetrics;
import com.livingworld.regions.cache.RegionCache;
/**
* Runs ecosystem-focused queries against active region data.
*/
public final class RegionQueryEngine {
private static final Comparator<Region> BY_COORDINATE =
Comparator.comparing((Region region) -> region.getCoordinate().dimensionId())
.thenComparingInt(region -> region.getCoordinate().x())
.thenComparingInt(region -> region.getCoordinate().z());
private final RegionCache cache;
public RegionQueryEngine(RegionCache cache) {
if (cache == null) {
throw new IllegalArgumentException("cache must not be null");
}
this.cache = cache;
}
public Optional<Region> getRegion(RegionCoordinate coordinate) {
if (coordinate == null) {
throw new IllegalArgumentException("coordinate must not be null");
}
return cache.get(coordinate);
}
public List<Region> getRegionsInRadius(RegionCoordinate center, int radius) {
if (center == null) {
throw new IllegalArgumentException("center must not be null");
}
if (radius < 0) {
throw new IllegalArgumentException("radius must be non-negative");
}
return cache.allActive().stream()
.filter(region -> region.getCoordinate().dimensionId().equals(center.dimensionId()))
.filter(region -> Math.abs((long) region.getCoordinate().x() - center.x()) <= radius)
.filter(region -> Math.abs((long) region.getCoordinate().z() - center.z()) <= radius)
.sorted(BY_COORDINATE)
.toList();
}
public List<Region> getRegionsWithActiveEcosystemEvent() {
return cache.allActive().stream()
.filter(region -> region.getFlags().isHasActiveEcosystemEvent())
.sorted(BY_COORDINATE)
.toList();
}
public List<Region> getRegionsAbovePollution(double threshold) {
validateMetricThreshold(threshold);
return cache.allActive().stream()
.filter(region -> region.getMetrics().getPollutionScore() > threshold)
.sorted(BY_COORDINATE)
.toList();
}
public List<Region> getRegionsBelowSoilQuality(double threshold) {
validateMetricThreshold(threshold);
return cache.allActive().stream()
.filter(region -> region.getMetrics().getSoilQuality() < threshold)
.sorted(BY_COORDINATE)
.toList();
}
private static void validateMetricThreshold(double threshold) {
if (!Double.isFinite(threshold)
|| threshold < RegionMetrics.MIN_VALUE
|| threshold > RegionMetrics.MAX_VALUE) {
throw new IllegalArgumentException("threshold must be finite and in range [0, 100]");
}
}
}
@@ -0,0 +1,70 @@
package com.livingworld.regions.query;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.cache.RegionCache;
class RegionQueryEngineTest {
private final RegionFactory factory = new RegionFactory();
private final RegionCache cache = new RegionCache();
private final RegionQueryEngine queries = new RegionQueryEngine(cache);
@Test
void radiusQueryUsesRegionCoordinatesAndCurrentDimension() {
Region center = add("minecraft:overworld", 0, 0);
add("minecraft:overworld", 1, 1);
add("minecraft:overworld", 2, 0);
add("minecraft:the_nether", 0, 0);
assertEquals(
List.of(center.getCoordinate(), new RegionCoordinate("minecraft:overworld", 1, 1)),
queries.getRegionsInRadius(center.getCoordinate(), 1).stream()
.map(Region::getCoordinate)
.toList());
}
@Test
void ecosystemQueriesUseFlagsAndMetrics() {
Region affected = add("minecraft:overworld", 0, 0);
affected.getFlags().setHasActiveEcosystemEvent(true);
affected.getMetrics().setPollutionScore(80);
affected.getMetrics().setSoilQuality(20);
Region healthy = add("minecraft:overworld", 1, 0);
healthy.getMetrics().setPollutionScore(10);
healthy.getMetrics().setSoilQuality(80);
assertEquals(List.of(affected), queries.getRegionsWithActiveEcosystemEvent());
assertEquals(List.of(affected), queries.getRegionsAbovePollution(50));
assertEquals(List.of(affected), queries.getRegionsBelowSoilQuality(50));
}
@Test
void validatesArguments() {
assertThrows(IllegalArgumentException.class,
() -> new RegionQueryEngine(null));
assertThrows(IllegalArgumentException.class,
() -> queries.getRegion(null));
assertThrows(IllegalArgumentException.class,
() -> queries.getRegionsInRadius(new RegionCoordinate("minecraft:overworld", 0, 0), -1));
assertThrows(IllegalArgumentException.class,
() -> queries.getRegionsAbovePollution(Double.NaN));
assertThrows(IllegalArgumentException.class,
() -> queries.getRegionsBelowSoilQuality(101));
}
private Region add(String dimensionId, int x, int z) {
Region region = factory.createNewRegion(new RegionCoordinate(dimensionId, x, z), 0);
cache.put(region);
return region;
}
}