Skip to content

Commit 874bfa8

Browse files
authored
Merge pull request #275 from BentoBoxWorld/develop
Release 1.28.4
2 parents 4273872 + 05e134e commit 874bfa8

6 files changed

Lines changed: 127 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ Two cancellable events are fired so other plugins can intercept limit applicatio
112112

113113
### Data Persistence
114114

115-
`IslandBlockCount` is persisted via BentoBox's `Database<IslandBlockCount>` (JSON flat-file by default, configurable in BentoBox). Saves are batched: every 10 block changes triggers an async save (`CHANGE_LIMIT = 9`). A full save of all changed islands occurs on addon disable.
115+
`IslandBlockCount` is persisted via BentoBox's `Database<IslandBlockCount>` (JSON flat-file by default, configurable in BentoBox). Saves are batched: every 10 changes to an island (block or entity) triggers an async save (`CHANGE_LIMIT = 9`). A full save of all changed islands occurs on addon disable.
116+
117+
Entity count mutations must go through `BlockLimitsListener.incrementEntity(Island, Environment, EntityType)` / `decrementEntity(String, Environment, EntityType)` — these mutate the count *and* enroll the change in the batch-save cycle. Never call `IslandBlockCount.incrementEntity`/`decrementEntity` directly from a listener, or the change is only persisted on addon disable and is lost on a crash. (`RecountCalculator` is the exception: it rebuilds a fresh `IslandBlockCount` and saves it wholesale via `setIsland`.)
116118

117119
### Testing
118120

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<!-- Do not change unless you want different name for local builds. -->
6363
<build.number>-LOCAL</build.number>
6464
<!-- This allows to change between versions. -->
65-
<build.version>1.28.3</build.version>
65+
<build.version>1.28.4</build.version>
6666
<sonar.projectKey>BentoBoxWorld_Limits</sonar.projectKey>
6767
<sonar.organization>bentobox-world</sonar.organization>
6868
<sonar.host.url>https://sonarcloud.io</sonar.host.url>

src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.bukkit.event.block.BlockSpreadEvent;
3838
import org.bukkit.event.block.EntityBlockFormEvent;
3939
import org.bukkit.event.block.LeavesDecayEvent;
40+
import org.bukkit.entity.EntityType;
4041
import org.bukkit.event.entity.EntityChangeBlockEvent;
4142
import org.bukkit.event.entity.EntityExplodeEvent;
4243
import org.bukkit.event.player.PlayerInteractEvent;
@@ -472,7 +473,6 @@ public void removeBlock(Block b) {
472473
}
473474

474475
private void updateSaveMap(String id) {
475-
saveMap.putIfAbsent(id, 0);
476476
if (saveMap.merge(id, 1, Integer::sum) > CHANGE_LIMIT) {
477477
handler.saveObjectAsync(islandCountMap.get(id));
478478
saveMap.remove(id);
@@ -566,4 +566,28 @@ public IslandBlockCount getIsland(Island island) {
566566
return islandCountMap.computeIfAbsent(island.getUniqueId(),
567567
k -> new IslandBlockCount(k, island.getGameMode()));
568568
}
569+
570+
/**
571+
* Increment the island's entity count for this environment and batch the change
572+
* for a save. Creates the island's count record if it does not exist yet.
573+
* Entity mutations must go through this or {@link #decrementEntity} so they join
574+
* the batch-save cycle — block changes go through {@link #process} which handles
575+
* this internally.
576+
*/
577+
public void incrementEntity(Island island, Environment env, EntityType type) {
578+
getIsland(island).incrementEntity(env, type);
579+
updateSaveMap(island.getUniqueId());
580+
}
581+
582+
/**
583+
* Decrement the island's entity count for this environment and batch the change
584+
* for a save. No-op if the island has no count record.
585+
*/
586+
public void decrementEntity(String islandId, Environment env, EntityType type) {
587+
IslandBlockCount ibc = getIsland(islandId);
588+
if (ibc != null) {
589+
ibc.decrementEntity(env, type);
590+
updateSaveMap(islandId);
591+
}
592+
}
569593
}

src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,7 @@ private void trackSpawn(Entity entity) {
270270
addon.getIslands().getIslandAt(entity.getLocation())
271271
.filter(island -> !island.isSpawn())
272272
.ifPresent(island -> {
273-
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island);
274-
ibc.incrementEntity(envOf(w), entity.getType());
273+
addon.getBlockLimitListener().incrementEntity(island, envOf(w), entity.getType());
275274
entityIslandMap.put(entity.getUniqueId(), island.getUniqueId());
276275
});
277276
}
@@ -323,21 +322,14 @@ public void onEntityRemove(EntityRemoveEvent e) {
323322

324323
String islandId = entityIslandMap.remove(entity.getUniqueId());
325324
if (islandId != null) {
326-
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(islandId);
327-
if (ibc != null) {
328-
ibc.decrementEntity(envOf(w), entity.getType());
329-
}
325+
addon.getBlockLimitListener().decrementEntity(islandId, envOf(w), entity.getType());
330326
return;
331327
}
332328

333329
addon.getIslands().getIslandAt(entity.getLocation())
334330
.filter(island -> !island.isSpawn())
335-
.ifPresent(island -> {
336-
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId());
337-
if (ibc != null) {
338-
ibc.decrementEntity(envOf(w), entity.getType());
339-
}
340-
});
331+
.ifPresent(island -> addon.getBlockLimitListener().decrementEntity(island.getUniqueId(), envOf(w),
332+
entity.getType()));
341333
}
342334

343335
/* =========================================================================
@@ -363,20 +355,15 @@ public void onEntityPortal(EntityPortalEvent e) {
363355
entityIslandMap.remove(entity.getUniqueId());
364356
addon.getIslands().getIslandAt(entity.getLocation())
365357
.filter(island -> !island.isSpawn())
366-
.ifPresent(island -> {
367-
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId());
368-
if (ibc != null) {
369-
ibc.decrementEntity(fromEnv, entity.getType());
370-
}
371-
});
358+
.ifPresent(island -> addon.getBlockLimitListener().decrementEntity(island.getUniqueId(), fromEnv,
359+
entity.getType()));
372360
}
373361
// Increment at destination if on a tracked island
374362
if (addon.inGameModeWorld(toWorld)) {
375363
addon.getIslands().getIslandAt(e.getTo())
376364
.filter(island -> !island.isSpawn())
377365
.ifPresent(island -> {
378-
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island);
379-
ibc.incrementEntity(toEnv, entity.getType());
366+
addon.getBlockLimitListener().incrementEntity(island, toEnv, entity.getType());
380367
entityIslandMap.put(entity.getUniqueId(), island.getUniqueId());
381368
});
382369
}

src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.bukkit.block.data.type.TechnicalPiston;
3636
import org.bukkit.configuration.file.FileConfiguration;
3737
import org.bukkit.configuration.file.YamlConfiguration;
38+
import org.bukkit.entity.EntityType;
3839
import org.bukkit.entity.Player;
3940
import org.bukkit.ExplosionResult;
4041
import org.bukkit.entity.Entity;
@@ -1308,4 +1309,79 @@ void testSaveSavesChangedIslands() {
13081309
// saveObjectAsync is called once by setIsland and once by save
13091310
verify(dbMock, atLeastOnce()).saveObjectAsync(any());
13101311
}
1312+
1313+
// --- Entity count persistence tests ---
1314+
1315+
@Test
1316+
void testIncrementEntityCreatesRecordAndCounts() {
1317+
when(island.getGameMode()).thenReturn("BSkyBlock");
1318+
1319+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1320+
1321+
IslandBlockCount ibc = listener.getIsland("test-island-id");
1322+
assertNotNull(ibc);
1323+
assertEquals(1, ibc.getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
1324+
}
1325+
1326+
@Test
1327+
void testDecrementEntityLowersCount() {
1328+
when(island.getGameMode()).thenReturn("BSkyBlock");
1329+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1330+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1331+
1332+
listener.decrementEntity("test-island-id", Environment.NORMAL, EntityType.CHICKEN);
1333+
1334+
assertEquals(1, listener.getIsland("test-island-id").getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
1335+
}
1336+
1337+
@Test
1338+
void testDecrementEntityWithoutRecordIsNoOp() {
1339+
// No record exists and repeated no-op decrements must not count toward the batch save
1340+
for (int i = 0; i < 20; i++) {
1341+
listener.decrementEntity("unknown-island", Environment.NORMAL, EntityType.CHICKEN);
1342+
}
1343+
1344+
assertNull(listener.getIsland("unknown-island"));
1345+
Database<?> dbMock = mockedDb.constructed().get(0);
1346+
verify(dbMock, never()).saveObjectAsync(any());
1347+
}
1348+
1349+
@Test
1350+
void testEntityChangesBatchSaveAfterThreshold() {
1351+
when(island.getGameMode()).thenReturn("BSkyBlock");
1352+
// CHANGE_LIMIT = 9, so the 10th change triggers a save — entity changes must
1353+
// join the same batch-save cycle as block changes (they used to be persisted
1354+
// only on addon disable, losing counts on a crash)
1355+
for (int i = 0; i < 10; i++) {
1356+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1357+
}
1358+
1359+
Database<?> dbMock = mockedDb.constructed().get(0);
1360+
verify(dbMock, atLeastOnce()).saveObjectAsync(any());
1361+
}
1362+
1363+
@Test
1364+
void testEntityChangesNoSaveBeforeThreshold() {
1365+
when(island.getGameMode()).thenReturn("BSkyBlock");
1366+
for (int i = 0; i < 9; i++) {
1367+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1368+
}
1369+
1370+
Database<?> dbMock = mockedDb.constructed().get(0);
1371+
verify(dbMock, never()).saveObjectAsync(any());
1372+
}
1373+
1374+
@Test
1375+
void testMixedEntityIncrementAndDecrementCountTowardBatchSave() {
1376+
when(island.getGameMode()).thenReturn("BSkyBlock");
1377+
for (int i = 0; i < 5; i++) {
1378+
listener.incrementEntity(island, Environment.NORMAL, EntityType.CHICKEN);
1379+
}
1380+
for (int i = 0; i < 5; i++) {
1381+
listener.decrementEntity("test-island-id", Environment.NORMAL, EntityType.CHICKEN);
1382+
}
1383+
1384+
Database<?> dbMock = mockedDb.constructed().get(0);
1385+
verify(dbMock, atLeastOnce()).saveObjectAsync(any());
1386+
}
13111387
}

src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.jupiter.api.Assertions.assertTrue;
66
import static org.mockito.ArgumentMatchers.any;
77
import static org.mockito.ArgumentMatchers.anyString;
8+
import static org.mockito.Mockito.doAnswer;
89
import static org.mockito.Mockito.mock;
910
import static org.mockito.Mockito.never;
1011
import static org.mockito.Mockito.verify;
@@ -116,6 +117,20 @@ void setUp() throws Exception {
116117
ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN);
117118
when(bll.getIsland(anyString())).thenReturn(ibc);
118119
when(addon.getBlockLimitListener()).thenReturn(bll);
120+
// Delegate the count-mutation wrappers to the real IslandBlockCount so tests
121+
// can assert on counts. Mirrors BlockLimitsListener's contract: increment
122+
// creates the record, decrement is a no-op when there is none.
123+
doAnswer(inv -> {
124+
ibc.incrementEntity(inv.getArgument(1), inv.getArgument(2));
125+
return null;
126+
}).when(bll).incrementEntity(any(Island.class), any(Environment.class), any(EntityType.class));
127+
doAnswer(inv -> {
128+
IslandBlockCount target = bll.getIsland((String) inv.getArgument(0));
129+
if (target != null) {
130+
target.decrementEntity(inv.getArgument(1), inv.getArgument(2));
131+
}
132+
return null;
133+
}).when(bll).decrementEntity(anyString(), any(Environment.class), any(EntityType.class));
119134

120135
FileConfiguration config = new YamlConfiguration();
121136
config.load("src/main/resources/config.yml");

0 commit comments

Comments
 (0)