Implement region manager
This commit is contained in:
@@ -50,8 +50,7 @@ public class RegionLifecycleController {
|
|||||||
/**
|
/**
|
||||||
* Private constructor to prevent instantiation.
|
* Private constructor to prevent instantiation.
|
||||||
*/
|
*/
|
||||||
private RegionLifecycleController() {
|
public RegionLifecycleController() {
|
||||||
// utility class
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.livingworld.regions;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.livingworld.config.SimulationConfig;
|
||||||
|
import com.livingworld.regions.cache.RegionCache;
|
||||||
|
import com.livingworld.regions.query.RegionQueryEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates region creation, loading, caching, dirty tracking, and unloading.
|
||||||
|
*/
|
||||||
|
public final class RegionManager implements com.livingworld.core.simulation.RegionManager {
|
||||||
|
|
||||||
|
private final RegionFactory regionFactory;
|
||||||
|
private final RegionStorage regionStorage;
|
||||||
|
private final RegionCache regionCache;
|
||||||
|
private final RegionQueryEngine queryEngine;
|
||||||
|
private final RegionLifecycleController lifecycleController;
|
||||||
|
private final SimulationConfig simulationConfig;
|
||||||
|
|
||||||
|
public RegionManager(
|
||||||
|
RegionFactory regionFactory,
|
||||||
|
RegionStorage regionStorage,
|
||||||
|
RegionCache regionCache,
|
||||||
|
RegionQueryEngine queryEngine,
|
||||||
|
RegionLifecycleController lifecycleController,
|
||||||
|
SimulationConfig simulationConfig) {
|
||||||
|
this.regionFactory = requireNonNull(regionFactory, "regionFactory");
|
||||||
|
this.regionStorage = requireNonNull(regionStorage, "regionStorage");
|
||||||
|
this.regionCache = requireNonNull(regionCache, "regionCache");
|
||||||
|
this.queryEngine = requireNonNull(queryEngine, "queryEngine");
|
||||||
|
this.lifecycleController = requireNonNull(lifecycleController, "lifecycleController");
|
||||||
|
this.simulationConfig = requireNonNull(simulationConfig, "simulationConfig");
|
||||||
|
this.simulationConfig.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Region getOrCreateRegion(RegionCoordinate coordinate) {
|
||||||
|
requireNonNull(coordinate, "coordinate");
|
||||||
|
|
||||||
|
Optional<Region> cached = regionCache.get(coordinate);
|
||||||
|
if (cached.isPresent()) {
|
||||||
|
return cached.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Region region = regionStorage.load(coordinate)
|
||||||
|
.map(this::prepareLoadedRegion)
|
||||||
|
.orElseGet(() -> regionFactory.createNewRegion(coordinate, 0L));
|
||||||
|
|
||||||
|
if (!region.getCoordinate().equals(coordinate)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Loaded region coordinate " + region.getCoordinate()
|
||||||
|
+ " does not match requested coordinate " + coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
regionCache.put(region);
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Region> findRegion(RegionCoordinate coordinate) {
|
||||||
|
requireNonNull(coordinate, "coordinate");
|
||||||
|
return queryEngine.getRegion(coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Region getOrCreateRegionAtBlock(
|
||||||
|
String dimensionId,
|
||||||
|
int blockX,
|
||||||
|
int blockZ) {
|
||||||
|
RegionCoordinate coordinate = RegionCoordinate.fromBlock(
|
||||||
|
dimensionId,
|
||||||
|
blockX,
|
||||||
|
blockZ,
|
||||||
|
simulationConfig.getRegionSizeChunks());
|
||||||
|
return getOrCreateRegion(coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Region> getActiveRegions() {
|
||||||
|
return regionCache.allActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markDirty(Region region) {
|
||||||
|
requireNonNull(region, "region");
|
||||||
|
Region cached = regionCache.get(region.getCoordinate())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
"region is not managed by this RegionManager: " + region.getCoordinate()));
|
||||||
|
if (cached != region) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"a different region instance is already cached for " + region.getCoordinate());
|
||||||
|
}
|
||||||
|
region.markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unloadRegion(RegionCoordinate coordinate) {
|
||||||
|
requireNonNull(coordinate, "coordinate");
|
||||||
|
Optional<Region> existing = regionCache.get(coordinate);
|
||||||
|
if (existing.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Region region = existing.get();
|
||||||
|
if (region.isDirty()) {
|
||||||
|
regionStorage.save(region);
|
||||||
|
region.clearDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionForUnload(region);
|
||||||
|
regionCache.remove(coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Region> resolve(RegionCoordinate coordinate) {
|
||||||
|
return findRegion(coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Region> resolveAll(List<RegionCoordinate> coordinates) {
|
||||||
|
requireNonNull(coordinates, "coordinates");
|
||||||
|
List<Region> resolved = new ArrayList<>();
|
||||||
|
for (RegionCoordinate coordinate : coordinates) {
|
||||||
|
findRegion(requireNonNull(coordinate, "coordinate"))
|
||||||
|
.ifPresent(resolved::add);
|
||||||
|
}
|
||||||
|
return List.copyOf(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Region prepareLoadedRegion(Region region) {
|
||||||
|
requireNonNull(region, "loaded region");
|
||||||
|
region.validate();
|
||||||
|
|
||||||
|
switch (region.getLifecycleState()) {
|
||||||
|
case UNLOADED, FAILED -> {
|
||||||
|
lifecycleController.transition(region, RegionLifecycleState.LOADING);
|
||||||
|
lifecycleController.transition(region, RegionLifecycleState.ACTIVE);
|
||||||
|
}
|
||||||
|
case LOADING -> lifecycleController.transition(region, RegionLifecycleState.ACTIVE);
|
||||||
|
case ACTIVE, DIRTY -> {
|
||||||
|
// Already available for simulation.
|
||||||
|
}
|
||||||
|
case SAVING, UNLOADING -> throw new IllegalStateException(
|
||||||
|
"Cannot activate a region in state " + region.getLifecycleState());
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transitionForUnload(Region region) {
|
||||||
|
switch (region.getLifecycleState()) {
|
||||||
|
case ACTIVE -> {
|
||||||
|
lifecycleController.transition(region, RegionLifecycleState.UNLOADING);
|
||||||
|
lifecycleController.transition(region, RegionLifecycleState.UNLOADED);
|
||||||
|
region.clearDirty();
|
||||||
|
}
|
||||||
|
case UNLOADED -> {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException(
|
||||||
|
"Cannot unload a region in state " + region.getLifecycleState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T requireNonNull(T value, String name) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException(name + " must not be null");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.livingworld.regions;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.livingworld.config.SimulationConfig;
|
||||||
|
import com.livingworld.core.services.PersistenceService;
|
||||||
|
import com.livingworld.regions.cache.RegionCache;
|
||||||
|
import com.livingworld.regions.query.RegionQueryEngine;
|
||||||
|
|
||||||
|
class RegionManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checksCacheBeforeStorageAndPreventsDuplicateInstances() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 0, 0);
|
||||||
|
|
||||||
|
Region first = fixture.manager.getOrCreateRegion(coordinate);
|
||||||
|
Region second = fixture.manager.getOrCreateRegion(coordinate);
|
||||||
|
|
||||||
|
assertSame(first, second);
|
||||||
|
assertEquals(1, fixture.persistence.loadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsFromStorageBeforeCreatingRegion() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 2, 3);
|
||||||
|
Region stored = fixture.factory.createNewRegion(coordinate, 12);
|
||||||
|
stored.clearDirty();
|
||||||
|
fixture.persistence.stored.put(coordinate, stored);
|
||||||
|
|
||||||
|
Region loaded = fixture.manager.getOrCreateRegion(coordinate);
|
||||||
|
|
||||||
|
assertSame(stored, loaded);
|
||||||
|
assertEquals(1, fixture.cache.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsMissingRegionAndConvertsBlockCoordinates() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
|
||||||
|
Region created = fixture.manager.getOrCreateRegionAtBlock(
|
||||||
|
"minecraft:overworld", -129, 128);
|
||||||
|
|
||||||
|
assertEquals(new RegionCoordinate("minecraft:overworld", -2, 1), created.getCoordinate());
|
||||||
|
assertTrue(created.isDirty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unloadSavesDirtyRegionAndRemovesItFromCache() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
Region region = fixture.manager.getOrCreateRegion(
|
||||||
|
new RegionCoordinate("minecraft:overworld", 0, 0));
|
||||||
|
|
||||||
|
fixture.manager.unloadRegion(region.getCoordinate());
|
||||||
|
|
||||||
|
assertEquals(1, fixture.persistence.saveCount);
|
||||||
|
assertTrue(fixture.manager.findRegion(region.getCoordinate()).isEmpty());
|
||||||
|
assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsDifferentInstanceForCachedCoordinate() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
RegionCoordinate coordinate = new RegionCoordinate("minecraft:overworld", 0, 0);
|
||||||
|
Region managed = fixture.manager.getOrCreateRegion(coordinate);
|
||||||
|
Region duplicate = fixture.factory.createNewRegion(coordinate, 0);
|
||||||
|
|
||||||
|
assertNotSame(managed, duplicate);
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> fixture.manager.markDirty(duplicate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final RegionFactory factory = new RegionFactory();
|
||||||
|
private final TestPersistenceService persistence = new TestPersistenceService();
|
||||||
|
private final RegionCache cache = new RegionCache();
|
||||||
|
private final RegionManager manager = new RegionManager(
|
||||||
|
factory,
|
||||||
|
new RegionStorage(persistence),
|
||||||
|
cache,
|
||||||
|
new RegionQueryEngine(cache),
|
||||||
|
new RegionLifecycleController(),
|
||||||
|
new SimulationConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestPersistenceService implements PersistenceService {
|
||||||
|
private final Map<RegionCoordinate, Region> stored = new LinkedHashMap<>();
|
||||||
|
private int loadCount;
|
||||||
|
private int saveCount;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markRegionDirty(Region region) {
|
||||||
|
region.markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveDirtyRegions() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Region> loadRegion(RegionCoordinate coordinate) {
|
||||||
|
loadCount++;
|
||||||
|
return Optional.ofNullable(stored.get(coordinate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveRegion(Region region) {
|
||||||
|
saveCount++;
|
||||||
|
stored.put(region.getCoordinate(), region);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flushAll() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user