Implement region query engine
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user