diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java index aad0c461b..72eb2b249 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java @@ -28,6 +28,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinWorkerThread; +import java.util.function.Function; import java.util.function.Predicate; public final class HydrologyPlanner { @@ -66,6 +67,7 @@ public final class HydrologyPlanner { final HydrologyFeatureSitePlanner featureSites; final HydrologyRegionalPlanner regional; private volatile Predicate earlyOwnerScope; + private volatile Function plannedTiles; public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) { this( @@ -255,6 +257,14 @@ public final class HydrologyPlanner { earlyOwnerScope = scope; } + /** + * Where the plans of tiles already planned are found. An owner draft that left the owner cache is + * restored from its tile's plan when a neighbour demands it again, instead of being drafted twice. + */ + void usePlannedTiles(Function source) { + plannedTiles = source; + } + void clearOwnerDrafts() { resolvedOwners.invalidateAll(); regional.clear(); @@ -300,6 +310,9 @@ public final class HydrologyPlanner { return local; } CrossTileResolvedOwner cached = resolvedOwners.getIfPresent(key); + if (cached == null) { + cached = plannedOwner(key); + } if (cached != null) { context.remember(key, cached); return cached; @@ -346,6 +359,17 @@ public final class HydrologyPlanner { } } + private CrossTileResolvedOwner plannedOwner(HydrologyTileKey key) { + Function source = plannedTiles; + HydrologyTile tile = source == null ? null : source.apply(key); + if (tile == null || tile.resolvedOwner() == null) { + return null; + } + reuseResolvedTile(tile); + CrossTileResolvedOwner restored = resolvedOwners.getIfPresent(key); + return restored == null ? tile.resolvedOwner() : restored; + } + List resolveLowerRankOwners( List candidateKeys, CrossTileResolutionContext context, diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java index 865614566..ffccce3ab 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java @@ -161,6 +161,17 @@ public final class HydrologyTileCache implements AutoCloseable { this.closed = new AtomicBoolean(); this.localChunks = ThreadLocal.withInitial(LocalChunks::new); this.neighbourPrefetchEnabled = true; + planner.usePlannedTiles(this::plannedTile); + } + + /** A tile already planned, resident or in the prepared-plan store, loaded without planning it. */ + private HydrologyTile plannedTile(HydrologyTileKey key) { + HydrologyTile resident = tiles.getIfPresent(key); + if (resident != null) { + return resident; + } + PreparedHydrologyTileStore store = persistentStore; + return store == null ? null : store.load(key).orElse(null); } /** diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java index a9fafc889..17cd9bb99 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java @@ -133,6 +133,33 @@ public class HydrologyPlannerTest { assertTileContentsEqual(tile, planner.plan(EARLY_OWNER_TILE)); } + @Test + public void evictedOwnerIsRestoredFromItsPlannedTileWithoutDrafting() { + AtomicInteger samples = new AtomicInteger(); + HydrologyTerrainSampler counted = (int x, int z) -> { + samples.incrementAndGet(); + return EARLY_OWNER_TERRAIN.sample(x, z); + }; + HydrologyPlanner planner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, counted); + HydrologyTile tile = planner.plan(EARLY_OWNER_TILE); + CrossTileResolvedOwner original = tile.resolvedOwner(); + planner.clearOwnerDrafts(); + AtomicInteger lookups = new AtomicInteger(); + planner.usePlannedTiles(key -> { + lookups.incrementAndGet(); + return key.equals(EARLY_OWNER_TILE) ? tile : null; + }); + samples.set(0); + + CrossTileResolvedOwner restored = planner.resolveIndependentOwner(EARLY_OWNER_TILE); + + assertEquals(original, restored); + assertEquals(0, samples.get()); + assertEquals(1, lookups.get()); + assertSame(restored, planner.resolveIndependentOwner(EARLY_OWNER_TILE)); + assertEquals(1, lookups.get()); + } + @Test public void restoringPreparedTileKeepsExistingCanonicalOwner() { HydrologyPlanner planner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java index 9c72114fc..f9b670632 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java @@ -38,6 +38,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Predicate; import org.mockito.ArgumentCaptor; @@ -45,6 +46,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -615,6 +617,25 @@ public class HydrologyTileCacheTest { verify(planner).limitEarlyOwners(null); } + @Test + @SuppressWarnings("unchecked") + public void plannerFindsTilesThisCachePlanned() { + HydrologyPlanner planner = mock(HydrologyPlanner.class); + HydrologyTile tile = mock(HydrologyTile.class); + HydrologyPlannerSettings settings = stalePrefetchSettings(); + when(planner.settings()).thenReturn(settings); + when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile); + HydrologyTileCache cache = new HydrologyTileCache(planner, 128); + ArgumentCaptor> planned = ArgumentCaptor.forClass(Function.class); + verify(planner).usePlannedTiles(planned.capture()); + HydrologyTileKey key = new HydrologyTileKey(3, -2); + + assertNull(planned.getValue().apply(key)); + assertSame(tile, cache.get(key)); + assertSame(tile, planned.getValue().apply(key)); + verify(planner, times(1)).plan(key); + } + @Test public void invalidPregenBoundsLeaveQueuedPrefetchIntact() { HydrologyPlanner planner = mock(HydrologyPlanner.class);