Implement region manager
This commit is contained in:
@@ -50,8 +50,7 @@ public class RegionLifecycleController {
|
||||
/**
|
||||
* Private constructor to prevent instantiation.
|
||||
*/
|
||||
private RegionLifecycleController() {
|
||||
// utility class
|
||||
public RegionLifecycleController() {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -112,4 +111,4 @@ public class RegionLifecycleController {
|
||||
region.setLifecycleState(target);
|
||||
region.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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