diff --git a/pom.xml b/pom.xml index e3fc888..162477a 100644 --- a/pom.xml +++ b/pom.xml @@ -42,13 +42,9 @@ - codemc-snapshots - https://repo.codemc.org/repository/maven-snapshots + bentoboxworld + https://repo.codemc.org/repository/bentoboxworld/ - - codemc-releases - https://repo.codemc.org/repository/maven-releases - @@ -58,14 +54,14 @@ 2.0.9 - 1.20.4-R0.1-SNAPSHOT - 2.5.0-SNAPSHOT + 1.21.3-R0.1-SNAPSHOT + 2.7.1-SNAPSHOT ${build.version}-SNAPSHOT -LOCAL - 1.18.1 + 1.19.0 BentoBoxWorld_BSkyBlock bentobox-world @@ -121,7 +117,7 @@ codemc - https://repo.codemc.org/repository/maven-snapshots/ + https://repo.codemc.org/repository/bentoboxworld/ codemc-repo diff --git a/src/main/java/world/bentobox/bskyblock/BSkyBlock.java b/src/main/java/world/bentobox/bskyblock/BSkyBlock.java index 1a485ff..6c55c44 100644 --- a/src/main/java/world/bentobox/bskyblock/BSkyBlock.java +++ b/src/main/java/world/bentobox/bskyblock/BSkyBlock.java @@ -2,9 +2,9 @@ package world.bentobox.bskyblock; import org.bukkit.World; import org.bukkit.World.Environment; -import org.bukkit.entity.SpawnCategory; import org.bukkit.WorldCreator; import org.bukkit.WorldType; +import org.bukkit.entity.SpawnCategory; import org.bukkit.event.Listener; import org.bukkit.generator.ChunkGenerator; import org.eclipse.jdt.annotation.Nullable; diff --git a/src/main/java/world/bentobox/bskyblock/Settings.java b/src/main/java/world/bentobox/bskyblock/Settings.java index 4fc2f2b..4eb7b1b 100644 --- a/src/main/java/world/bentobox/bskyblock/Settings.java +++ b/src/main/java/world/bentobox/bskyblock/Settings.java @@ -1,6 +1,12 @@ package world.bentobox.bskyblock; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.bukkit.Difficulty; import org.bukkit.GameMode; @@ -8,8 +14,6 @@ import org.bukkit.block.Biome; import org.bukkit.entity.EntityType; import org.eclipse.jdt.annotation.NonNull; -import com.google.common.base.Enums; - import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.configuration.ConfigComment; import world.bentobox.bentobox.api.configuration.ConfigEntry; @@ -155,7 +159,7 @@ public class Settings implements WorldSettings { private Biome defaultBiome = Biome.PLAINS; @ConfigComment("The default biome for the nether world (this may affect what mobs can spawn)") @ConfigEntry(path = "world.default-nether-biome") - private Biome defaultNetherBiome = Enums.getIfPresent(Biome.class, "NETHER").or(Enums.getIfPresent(Biome.class, "NETHER_WASTES").or(Biome.BADLANDS)); + private Biome defaultNetherBiome = Biome.NETHER_WASTES; @ConfigComment("The default biome for the end world (this may affect what mobs can spawn)") @ConfigEntry(path = "world.default-end-biome") private Biome defaultEndBiome = Biome.THE_END; diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 8c9511a..957b5d3 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -1,7 +1,7 @@ name: BSkyBlock main: world.bentobox.bskyblock.BSkyBlock version: ${version}${build.number} -api-version: 2.3.0 +api-version: 2.7.1 metrics: true icon: "OAK_SAPLING" repository: "BentoBoxWorld/BSkyBlock" diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7705ccd..ae156f1 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -84,6 +84,11 @@ world: # Maximum number of islands in the world. Set to -1 or 0 for unlimited. # If the number of islands is greater than this number, it will stop players from creating islands. max-islands: 0 + # The number of concurrent islands a player can have in the world + # A value of 0 will use the BentoBox config.yml default + concurrent-islands: 1 + # Disallow players to have other islands if they are in a team. + disallow-team-member-islands: true # The default game mode for this world. Players will be set to this mode when they create # a new island for example. Options are SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR default-game-mode: SURVIVAL @@ -147,9 +152,12 @@ world: OBSIDIAN_SCOOPING: true ISLAND_RESPAWN: true CREEPER_GRIEFING: false + WORLD_BLOCK_EXPLODE_DAMAGE: false + VISITOR_KEEP_INVENTORY: false PETS_STAY_AT_HOME: true NATURAL_SPAWNING_OUTSIDE_RANGE: true LIQUIDS_FLOWING_OUT: false + VISITOR_TRIGGER_RAID: true REMOVE_MOBS: false ENDER_CHEST: false TREES_GROWING_OUTSIDE_RANGE: false @@ -164,11 +172,13 @@ world: PREVENT_TELEPORT_WHEN_FALLING: false WORLD_TNT_DAMAGE: false ENTER_EXIT_MESSAGES: true + ALLOW_MOVE_BOX: true ENDERMAN_DEATH_DROP: true OFFLINE_REDSTONE: true REMOVE_END_EXIT_ISLAND: true OFFLINE_GROWTH: true ITEM_FRAME_DAMAGE: false + ENTITY_PORTAL_TELEPORT: false SPAWNER_SPAWN_EGGS: true # These are the default protection settings for new islands. # The value is the minimum island rank required allowed to do the action @@ -181,20 +191,26 @@ world: # OWNER = 1000 default-island-flags: HURT_ANIMALS: 500 + LOOM: 500 DRAGON_EGG: 500 REDSTONE: 500 BUCKET: 500 LOCK: 0 ENDER_PEARL: 500 + BELL_RINGING: 500 DOOR: 500 BREAK_HOPPERS: 500 FURNACE: 500 + HURT_TAMED_ANIMALS: 500 MINECART: 500 ANVIL: 500 FISH_SCOOPING: 500 + TRAPPED_CHEST: 500 END_PORTAL: 500 BREEDING: 500 HURT_VILLAGERS: 500 + BOOKSHELF: 500 + HARVEST: 500 TURTLE_EGGS: 500 FROST_WALKER: 500 COLLECT_LAVA: 500 @@ -206,23 +222,35 @@ world: CAKE: 500 NAME_TAG: 500 ARMOR_STAND: 500 + CHANGE_SETTINGS: 1000 + SIGN_EDITING: 500 TRADING: 0 EGGS: 500 ITEM_DROP: 0 + CHEST: 500 NOTE_BLOCK: 0 FLINT_AND_STEEL: 500 NETHER_PORTAL: 500 + SCULK_SENSOR: 500 LECTERN: 500 + GRINDSTONE: 500 + SHULKER_BOX: 500 ITEM_PICKUP: 0 CROP_TRAMPLE: 500 DROPPER: 500 BREWING: 500 + MOVE_BOX: 1000 TNT_PRIMING: 500 + PARKOUR_CREATIVE: 500 COLLECT_WATER: 500 + AXOLOTL_SCOOPING: 500 BUTTON: 500 + COMPOSTER: 500 + STONECUTTING: 500 FIRE_EXTINGUISH: 500 COMMAND_RANKS: 500 BEACON: 500 + ALLAY: 500 TRAPDOOR: 500 EXPERIENCE_BOTTLE_THROWING: 500 PRESSURE_PLATE: 0 @@ -230,17 +258,22 @@ world: DYE: 500 PLACE_BLOCKS: 500 ITEM_FRAME: 500 + CROP_PLANTING: 500 CRAFTING: 0 ENCHANTING: 0 SHEARING: 500 + FLOWER_POT: 500 BOAT: 500 BED: 500 SPAWN_EGGS: 500 MILKING: 0 DISPENSER: 500 + SCULK_SHRIEKER: 500 GATE: 0 + SMITHING: 500 EXPERIENCE_PICKUP: 500 HOPPER: 500 + CANDLES: 500 LEASH: 500 MOUNT_INVENTORY: 500 BREAK_BLOCKS: 500 @@ -248,48 +281,30 @@ world: CONTAINER: 500 POTION_THROWING: 500 JUKEBOX: 500 + BARREL: 500 + CARTOGRAPHY: 500 + COLLECT_POWDERED_SNOW: 500 # These are the default settings for new islands default-island-settings: PVP_END: false PVP_NETHER: false LEAF_DECAY: true - TNT_DAMAGE: true - MONSTER_SPAWNERS_SPAWN: true + ENDERMAN_TELEPORT: true ANIMAL_NATURAL_SPAWN: true MONSTER_NATURAL_SPAWN: true - FIRE_IGNITE: true + SHULKER_TELEPORT: true FIRE_SPREAD: true - ANIMAL_SPAWNERS_SPAWN: true FIRE_BURNING: true PVP_OVERWORLD: false + TNT_DAMAGE: true + MONSTER_SPAWNERS_SPAWN: true + FIRE_IGNITE: true + ANIMAL_SPAWNERS_SPAWN: true + BLOCK_EXPLODE_DAMAGE: true # These settings/flags are hidden from users # Ops can toggle hiding in-game using SHIFT-LEFT-CLICK on flags in settings # Added since 1.4.1. - hidden-flags: - - CHEST_DAMAGE - - HURT_MONSTERS - - BREAK_SPAWNERS - - DISPENSER - - DRAGON_EGG - - DROPPER - - EXPERIENCE_BOTTLE_THROWING - - HURT_VILLAGERS - - MOUNT_INVENTORY - - NOTE_BLOCK - - TURTLE_EGGS - - PVP_END - - FIRE_BURNING - - FIRE_IGNITE - - FIRE_SPREAD - - PVP_NETHER - - PVP_OVERWORLD - - TNT_DAMAGE - - MONSTER_NATURAL_SPAWN - - MONSTER_SPAWNERS_SPAWN - - ANIMAL_SPAWNERS_SPAWN - - ANIMAL_NATURAL_SPAWN - - LEAF_DECAY - - BREAK_HOPPERS + hidden-flags: [] # Visitor banned commands - Visitors to islands cannot use these commands in this world visitor-banned-commands: - spawner diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d84f487..5b78ade 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: BentoBox-BSkyBlock main: world.bentobox.bskyblock.BSkyBlockPladdon version: ${project.version}${build.number} -api-version: "1.19" +api-version: "1.21" authors: [tastybento] contributors: ["The BentoBoxWorld Community"] diff --git a/src/test/java/world/bentobox/bskyblock/BSkyBlockTest.java b/src/test/java/world/bentobox/bskyblock/BSkyBlockTest.java index 9254a84..6264a11 100644 --- a/src/test/java/world/bentobox/bskyblock/BSkyBlockTest.java +++ b/src/test/java/world/bentobox/bskyblock/BSkyBlockTest.java @@ -30,6 +30,7 @@ import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.Server; +import org.bukkit.UnsafeValues; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; import org.junit.After; @@ -59,6 +60,7 @@ import world.bentobox.bentobox.managers.FlagsManager; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bskyblock.generators.ChunkGeneratorWorld; +import world.bentobox.bskyblock.mocks.ServerMocks; /** * @author tastybento @@ -101,6 +103,7 @@ public class BSkyBlockTest { @After public void tearDown() throws IOException { + ServerMocks.unsetBukkitServer(); User.clearUsers(); Mockito.framework().clearInlineMocks(); deleteAll(new File("database")); @@ -123,6 +126,7 @@ public class BSkyBlockTest { */ @Before public void setUp() throws Exception { + ServerMocks.newServer(); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); @@ -160,6 +164,9 @@ public class BSkyBlockTest { when(Bukkit.getServer()).thenReturn(server); when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); + @SuppressWarnings("deprecation") + UnsafeValues unsafe = mock(UnsafeValues.class); + when(Bukkit.getUnsafe()).thenReturn(unsafe); // Addon addon = new BSkyBlock(); diff --git a/src/test/java/world/bentobox/bskyblock/SettingsTest.java b/src/test/java/world/bentobox/bskyblock/SettingsTest.java index 30a11e9..0acdccb 100644 --- a/src/test/java/world/bentobox/bskyblock/SettingsTest.java +++ b/src/test/java/world/bentobox/bskyblock/SettingsTest.java @@ -13,9 +13,12 @@ import org.bukkit.Difficulty; import org.bukkit.GameMode; import org.bukkit.block.Biome; import org.bukkit.entity.EntityType; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import world.bentobox.bskyblock.mocks.ServerMocks; + /** * @author tastybento * @@ -29,9 +32,15 @@ public class SettingsTest { */ @Before public void setUp() throws Exception { + ServerMocks.newServer(); s = new Settings(); } + @After + public void tearDown() { + ServerMocks.unsetBukkitServer(); + } + /** * Test method for {@link world.bentobox.bskyblock.Settings#setFriendlyName(java.lang.String)}. */ diff --git a/src/test/java/world/bentobox/bskyblock/generators/ChunkGeneratorWorldTest.java b/src/test/java/world/bentobox/bskyblock/generators/ChunkGeneratorWorldTest.java index 9dfd720..0e2e3d6 100644 --- a/src/test/java/world/bentobox/bskyblock/generators/ChunkGeneratorWorldTest.java +++ b/src/test/java/world/bentobox/bskyblock/generators/ChunkGeneratorWorldTest.java @@ -17,6 +17,7 @@ import java.util.Random; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Server; +import org.bukkit.UnsafeValues; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.generator.ChunkGenerator.BiomeGrid; @@ -32,6 +33,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import world.bentobox.bskyblock.BSkyBlock; import world.bentobox.bskyblock.Settings; +import world.bentobox.bskyblock.mocks.ServerMocks; /** * @author tastybento @@ -47,9 +49,9 @@ public class ChunkGeneratorWorldTest { @Mock private World world; private final Random random = new Random(); + @SuppressWarnings("deprecation") @Mock private BiomeGrid biomeGrid; - @Mock private Settings settings; @Mock private ChunkData data; @@ -57,13 +59,18 @@ public class ChunkGeneratorWorldTest { /** * @throws java.lang.Exception */ + @SuppressWarnings("deprecation") @Before public void setUp() throws Exception { + ServerMocks.newServer(); // Bukkit + PowerMockito.mockStatic(Bukkit.class); Server server = mock(Server.class); when(server.createChunkData(any())).thenReturn(data); when(Bukkit.getServer()).thenReturn(server); + UnsafeValues unsafe = mock(UnsafeValues.class); + when(Bukkit.getUnsafe()).thenReturn(unsafe); // Instance cg = new ChunkGeneratorWorld(addon); @@ -71,34 +78,28 @@ public class ChunkGeneratorWorldTest { when(world.getEnvironment()).thenReturn(World.Environment.NORMAL); when(world.getMaxHeight()).thenReturn(16); // Settings + settings = new Settings(); when(addon.getSettings()).thenReturn(settings); - when(settings.getSeaHeight()).thenReturn(0); - when(settings.isNetherRoof()).thenReturn(true); - when(settings.getDefaultBiome()).thenReturn(Biome.TAIGA); - when(settings.getDefaultNetherBiome()).thenReturn(Biome.CRIMSON_FOREST); - when(settings.getDefaultEndBiome()).thenReturn(Biome.END_MIDLANDS); } /** * @throws java.lang.Exception */ @After - public void tearDown() throws Exception { + public void tearDown() { + ServerMocks.unsetBukkitServer(); } /** * Test method for {@link world.bentobox.bskyblock.generators.ChunkGeneratorWorld#generateChunkData(org.bukkit.World, java.util.Random, int, int, org.bukkit.generator.ChunkGenerator.BiomeGrid)}. */ + @SuppressWarnings("deprecation") @Test public void testGenerateChunkDataWorldRandomIntIntBiomeGridOverworldVoid() { ChunkData cd = cg.generateChunkData(world, random, 0 , 0 , biomeGrid); assertEquals(data, cd); // Verifications - // Default biome - verify(settings).getDefaultBiome(); verify(biomeGrid, times(64)).setBiome(anyInt(), anyInt(), anyInt(), any()); - // Sea height - verify(settings).getSeaHeight(); // Void verify(cd, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(Material.class)); } @@ -106,18 +107,15 @@ public class ChunkGeneratorWorldTest { /** * Test method for {@link world.bentobox.bskyblock.generators.ChunkGeneratorWorld#generateChunkData(org.bukkit.World, java.util.Random, int, int, org.bukkit.generator.ChunkGenerator.BiomeGrid)}. */ + @SuppressWarnings("deprecation") @Test public void testGenerateChunkDataWorldRandomIntIntBiomeGridOverworldSea() { // Set sea height - when(settings.getSeaHeight()).thenReturn(10); - ChunkData cd = cg.generateChunkData(world, random, 0 , 0 , biomeGrid); + settings.setSeaHeight(10); + ChunkData cd = cg.generateChunkData(world, random, 0, 0, biomeGrid); assertEquals(data, cd); // Verifications - // Default biome - verify(settings).getDefaultBiome(); verify(biomeGrid, times(64)).setBiome(anyInt(), anyInt(), anyInt(), eq(Biome.TAIGA)); - // Sea height - verify(settings, times(2)).getSeaHeight(); // Water. Blocks = 16 x 16 x 11 because block 0 verify(cd).setRegion(0, 0, 0, 16, 11, 16, Material.WATER); } @@ -125,18 +123,15 @@ public class ChunkGeneratorWorldTest { /** * Test method for {@link world.bentobox.bskyblock.generators.ChunkGeneratorWorld#generateChunkData(org.bukkit.World, java.util.Random, int, int, org.bukkit.generator.ChunkGenerator.BiomeGrid)}. */ + @SuppressWarnings("deprecation") @Test public void testGenerateChunkDataWorldRandomIntIntBiomeGridEnd() { when(world.getEnvironment()).thenReturn(World.Environment.THE_END); ChunkData cd = cg.generateChunkData(world, random, 0 , 0 , biomeGrid); assertEquals(data, cd); // Verifications - // Default biome - verify(settings).getDefaultEndBiome(); // Set biome in end verify(biomeGrid, times(64)).setBiome(anyInt(), anyInt(), anyInt(), eq(Biome.END_MIDLANDS)); - // Sea height - verify(settings, never()).getSeaHeight(); // Void verify(cd, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(Material.class)); } @@ -144,14 +139,13 @@ public class ChunkGeneratorWorldTest { /** * Test method for {@link world.bentobox.bskyblock.generators.ChunkGeneratorWorld#generateChunkData(org.bukkit.World, java.util.Random, int, int, org.bukkit.generator.ChunkGenerator.BiomeGrid)}. */ + @SuppressWarnings("deprecation") @Test public void testGenerateChunkDataWorldRandomIntIntBiomeGridNetherWithRoof() { when(world.getEnvironment()).thenReturn(World.Environment.NETHER); ChunkData cd = cg.generateChunkData(world, random, 0 , 0 , biomeGrid); assertEquals(data, cd); // Verifications - // Nether roof check - verify(settings).isNetherRoof(); // Set biome in nether verify(biomeGrid, times(64)).setBiome(anyInt(), anyInt(), anyInt(), eq(Biome.CRIMSON_FOREST)); // Nether roof - at least bedrock layer @@ -161,16 +155,14 @@ public class ChunkGeneratorWorldTest { /** * Test method for {@link world.bentobox.bskyblock.generators.ChunkGeneratorWorld#generateChunkData(org.bukkit.World, java.util.Random, int, int, org.bukkit.generator.ChunkGenerator.BiomeGrid)}. */ + @SuppressWarnings("deprecation") @Test public void testGenerateChunkDataWorldRandomIntIntBiomeGridNetherNoRoof() { - when(settings.isNetherRoof()).thenReturn(false); + settings.setNetherRoof(false); when(world.getEnvironment()).thenReturn(World.Environment.NETHER); ChunkData cd = cg.generateChunkData(world, random, 0 , 0 , biomeGrid); assertEquals(data, cd); // Verifications - verify(settings).getDefaultNetherBiome(); - // Nether roof check - verify(settings).isNetherRoof(); // Set biome in nether verify(biomeGrid, times(64)).setBiome(anyInt(), anyInt(), anyInt(), eq(Biome.CRIMSON_FOREST)); // Nether roof - at least bedrock layer diff --git a/src/test/java/world/bentobox/bskyblock/mocks/ServerMocks.java b/src/test/java/world/bentobox/bskyblock/mocks/ServerMocks.java new file mode 100644 index 0000000..4f0d4d1 --- /dev/null +++ b/src/test/java/world/bentobox/bskyblock/mocks/ServerMocks.java @@ -0,0 +1,118 @@ +package world.bentobox.bskyblock.mocks; + +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Server; +import org.bukkit.Tag; +import org.bukkit.UnsafeValues; +import org.eclipse.jdt.annotation.NonNull; + +public final class ServerMocks { + + public static @NonNull Server newServer() { + Server mock = mock(Server.class); + + Logger noOp = mock(Logger.class); + when(mock.getLogger()).thenReturn(noOp); + when(mock.isPrimaryThread()).thenReturn(true); + + // Unsafe + UnsafeValues unsafe = mock(UnsafeValues.class); + when(mock.getUnsafe()).thenReturn(unsafe); + + // Server must be available before tags can be mocked. + Bukkit.setServer(mock); + + // Bukkit has a lot of static constants referencing registry values. To initialize those, the + // registries must be able to be fetched before the classes are touched. + Map, Object> registers = new HashMap<>(); + + doAnswer(invocationGetRegistry -> registers.computeIfAbsent(invocationGetRegistry.getArgument(0), clazz -> { + Registry registry = mock(Registry.class); + Map cache = new HashMap<>(); + doAnswer(invocationGetEntry -> { + NamespacedKey key = invocationGetEntry.getArgument(0); + // Some classes (like BlockType and ItemType) have extra generics that will be + // erased during runtime calls. To ensure accurate typing, grab the constant's field. + // This approach also allows us to return null for unsupported keys. + Class constantClazz; + try { + //noinspection unchecked + constantClazz = (Class) clazz + .getField(key.getKey().toUpperCase(Locale.ROOT).replace('.', '_')).getType(); + } catch (ClassCastException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + return null; + } + + return cache.computeIfAbsent(key, key1 -> { + Keyed keyed = mock(constantClazz); + doReturn(key).when(keyed).getKey(); + return keyed; + }); + }).when(registry).get(notNull()); + return registry; + })).when(mock).getRegistry(notNull()); + + // Tags are dependent on registries, but use a different method. + // This will set up blank tags for each constant; all that needs to be done to render them + // functional is to re-mock Tag#getValues. + doAnswer(invocationGetTag -> { + Tag tag = mock(Tag.class); + doReturn(invocationGetTag.getArgument(1)).when(tag).getKey(); + doReturn(Set.of()).when(tag).getValues(); + doAnswer(invocationIsTagged -> { + Keyed keyed = invocationIsTagged.getArgument(0); + Class type = invocationGetTag.getArgument(2); + if (!type.isAssignableFrom(keyed.getClass())) { + return null; + } + // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. + return tag.getValues().contains(keyed) + || tag.getValues().stream().anyMatch(value -> value.getKey().equals(keyed.getKey())); + }).when(tag).isTagged(notNull()); + return tag; + }).when(mock).getTag(notNull(), notNull(), notNull()); + + // Once the server is all set up, touch BlockType and ItemType to initialize. + // This prevents issues when trying to access dependent methods from a Material constant. + try { + Class.forName("org.bukkit.inventory.ItemType"); + Class.forName("org.bukkit.block.BlockType"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + return mock; + } + + public static void unsetBukkitServer() { + try { + Field server = Bukkit.class.getDeclaredField("server"); + server.setAccessible(true); + server.set(null, null); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private ServerMocks() { + } + +} \ No newline at end of file