Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/main/java/world/bentobox/bentobox/database/objects/Island.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 /<gamemode> 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());
}
}
Loading