Implement region manager

This commit is contained in:
George
2026-06-07 12:56:23 +01:00
parent 2c52b9b2e7
commit 524ccb2e60
3 changed files with 297 additions and 3 deletions
@@ -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() {
}
}
}