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