diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index 6b747ab66..743c9e1ee 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -32,6 +32,7 @@ import com.google.gson.annotations.Expose; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.flags.Flag; @@ -1186,10 +1187,35 @@ public void setPurgeProtected(boolean purgeProtected) { * @see #setProtectionRange(int) */ public void setRange(int range) { - if (this.range != range) { - this.range = range; - setChanged(); + if (this.range == range) { + return; + } + // Refuse to mutate range to a value that disagrees with the game mode's + // configured distance-between-islands. Storing a mismatched range corrupts + // the database — on the next restart IslandsManager.load() will reject the + // island with "Island distance mismatch" and panic-disable BentoBox. + // + // Game modes that opt out of the equality check (isEnforceEqualRanges == false, + // e.g. claim-based addons that resize on team changes) are allowed any value. + // A configured distance of 0 means the world isn't currently registered with + // IWM (e.g. unit tests, deserialization), so we also pass through in that case. + BentoBox plugin = BentoBox.getInstance(); + if (plugin != null && world != null && plugin.getIWM() != null) { + int configured = plugin.getIWM().getIslandDistance(world); + boolean enforce = plugin.getIWM().getAddon(world) + .map(GameModeAddon::isEnforceEqualRanges).orElse(true); + if (enforce && configured > 0 && configured != range) { + StackTraceElement[] trace = new Throwable().getStackTrace(); + String caller = trace.length > 1 ? trace[1].toString() : "unknown"; + plugin.logWarning("Refusing Island.setRange(" + range + ") on island " + + uniqueId + " in world '" + world.getName() + + "': value does not match configured distance-between-islands (" + + configured + "). Caller: " + caller); + return; + } } + this.range = range; + setChanged(); } /** diff --git a/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java b/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java index 1482c53c0..196ae410a 100644 --- a/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java +++ b/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import world.bentobox.bentobox.CommonTestSetup; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.metadata.MetaDataValue; @@ -45,6 +46,7 @@ class IslandTest extends CommonTestSetup { private Island island; // real Island under test (shadows the mock in CommonTestSetup) private Location center; private UUID ownerUUID; + private GameModeAddon gameModeAddon; @Override @BeforeEach @@ -71,6 +73,9 @@ public void setUp() throws Exception { // Create the real Island under test island = new Island(center, ownerUUID, 50); + + // GameModeAddon mock for setRange validation tests (used per-test as needed) + gameModeAddon = mock(GameModeAddon.class); } @Override @@ -1189,4 +1194,64 @@ void testGetCenterReturnsClone() { assertNotSame(c1, c2); assertEquals(c1.getBlockX(), c2.getBlockX()); } + + // ======================== setRange guarding ======================== + // + // Background: Island.setRange used to accept any value, which let a third-party + // addon (StrangerRealms TeamListener) overwrite the range of islands belonging + // to other game modes whenever a / team kick or leave fired. On the + // next server restart, IslandsManager.load() refused to load those islands + // because their stored range no longer matched the configured + // distance-between-islands, and BentoBox panic-disabled with + // "Island distance mismatch". setRange now refuses to mutate range to a value + // inconsistent with the gamemode's configured distance unless the addon opts + // out via isEnforceEqualRanges() == false. + + @Test + void testSetRangeRefusedWhenMismatchesConfiguredDistance() { + // iwm.getIslandDistance(world) -> 100 (set in @BeforeEach) + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(true); + + int originalRange = island.getRange(); + island.setRange(64); // would corrupt the database — must be rejected + + assertEquals(originalRange, island.getRange(), + "setRange must refuse a value that disagrees with the configured distance"); + } + + @Test + void testSetRangeAcceptedWhenAddonOptsOutOfEqualRanges() { + // Game mode that legitimately resizes claims (e.g. StrangerRealms). + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(false); + + island.setRange(64); + + assertEquals(64, island.getRange()); + } + + @Test + void testSetRangeAcceptedWhenValueMatchesConfiguredDistance() { + // Configured distance is 100 (set in @BeforeEach); island starts at 100. + // Setting to the same value is a no-op but still legal. + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(true); + + island.setRange(100); + + assertEquals(100, island.getRange()); + } + + @Test + void testSetRangeAcceptedWhenWorldNotRegistered() { + // iwm.getIslandDistance returns 0 when the world isn't keyed in gameModes + // (e.g. unit tests, deserialization). In that case we have no authoritative + // value to validate against, so the call should pass through. + when(iwm.getIslandDistance(any())).thenReturn(0); + + island.setRange(64); + + assertEquals(64, island.getRange()); + } }