Skip to content

Commit f254f04

Browse files
tastybentoclaude
andcommitted
fix: refresh initialCount in tidyUp to close zero-scan/regular-scan race
When a player runs /level shortly after island creation and the zero scan is parked on awaitPendingZeros for newly-generated chunks (typical when /po create teleports the player and that fires isNewChunk events inside the island bounds), the regular scan's constructor reads DB initialCount before the zero scan's setInitialIslandCount has fired, snapshotting initialCount=0 into results. By the time the regular scan's tidyUp runs, the zero scan has finished, persisted is populated, and reconcile returns delta=0 — so results.initialCount stays at 0 and the level math treats every block as player-placed (level = totalPoints / level_cost). Refresh results.initialCount from the DB after reconcile so the live computation picks up: (a) a zero scan's setInitialIslandCount that landed between our constructor and now, (b) any reconcile delta this scan just folded in. This produces level=0 on a fresh island regardless of whether the regular scan or the zero scan wins the race to tidyUp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 22f20bb commit f254f04

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -865,13 +865,24 @@ public void tidyUp() {
865865
// doesn't already know about is added to both the map and the
866866
// initialCount — that's what catches chunks pregenerated before the
867867
// island existed, async-load misses at zero-scan time, and late
868-
// chunk decoration. The returned delta is folded into the in-memory
869-
// results so the level math below sees the up-to-date baseline.
868+
// chunk decoration.
870869
if (addon.getSettings().isZeroNewIslandLevels() && !addon.getSettings().isDonationsOnly()
871870
&& !scannedChunkValues.isEmpty()) {
872-
long delta = addon.getManager().reconcileHandicapChunks(island, scannedChunkValues, zeroIsland);
873-
if (delta != 0L && !zeroIsland) {
874-
results.initialCount.addAndGet(delta);
871+
addon.getManager().reconcileHandicapChunks(island, scannedChunkValues, zeroIsland);
872+
if (!zeroIsland) {
873+
// Race-safe refresh of the in-memory baseline. Between the
874+
// constructor's snapshot and now, two updates can land on the
875+
// database that this calculator instance won't otherwise see:
876+
// (a) a still-in-flight zero scan's setInitialIslandCount —
877+
// happens when the player runs /level seconds after island
878+
// creation while the zero scan is parked on
879+
// awaitPendingZeros for newly-generated chunks;
880+
// (b) the reconcile call above, which folds chunks the scan
881+
// just discovered into the initialCount.
882+
// The level math below uses results.initialCount, so reading
883+
// the DB value back here keeps the live computation aligned
884+
// with the actual persisted baseline.
885+
results.initialCount.set(addon.getInitialIslandCount(island));
875886
}
876887
}
877888

src/test/java/world/bentobox/level/calculators/IslandLevelCalculatorTidyUpTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,31 @@ void donatedBlocksExceedLimit() {
232232
"donated points should be capped at 500 * 3 = 1500, not 1000 * 3 = 3000");
233233
}
234234

235+
@Test
236+
@DisplayName("Race: zero scan setInitialIslandCount lands after constructor — refresh keeps level math honest")
237+
void initialCountRefreshAfterReconcile() {
238+
// Constructor reads 0 (zero scan still in flight, hasn't called
239+
// setInitialIslandCount yet). By the time tidyUp runs, the zero scan
240+
// has finished and the DB has 130. Without the refresh, results
241+
// .initialCount stays at 0 from the constructor and the level math
242+
// computes 200/100=2 instead of the correct (200-130)/100=0. The
243+
// refresh inside tidyUp picks up the updated DB value, producing the
244+
// expected level.
245+
when(addon.getInitialIslandCount(any(Island.class))).thenReturn(0L, INITIAL_COUNT);
246+
when(manager.getInitialCount(any(Island.class))).thenReturn(INITIAL_COUNT);
247+
248+
IslandLevelCalculator calc = newCalculator();
249+
Results r = calc.getResults();
250+
// Force a non-empty scanned-chunk map so the reconcile/refresh branch fires.
251+
calc.getScannedChunkValues().put("world:1", 200L);
252+
r.rawBlockCount.set(200L);
253+
calc.tidyUp();
254+
255+
assertEquals(0L, r.getLevel(),
256+
"level should be 0 — refresh pulls the up-to-date initialCount=130 from the DB, "
257+
+ "so modifiedPoints = 200 - 130 = 70 → 0 levels");
258+
}
259+
235260
@Test
236261
@DisplayName("Donated blocks with no limit: full count is used")
237262
void donatedBlocksNoLimit() {

0 commit comments

Comments
 (0)