update UserBlockTracker API

This commit is contained in:
nossr50 2024-05-19 12:44:34 -07:00
parent c2054a5d45
commit 8b82163e3d
26 changed files with 708 additions and 468 deletions

View File

@ -1,3 +1,11 @@
Version 2.2.013
(API) Added new methods to com.gmail.nossr50.util.blockmeta.UserBlockTracker for easier readability
(API) Deprecated the old poorly named methods in UserBlockTracker
(Codebase) Cleaned up and organized unit tests relating to UserBlockTracker
NOTES:
Not planning to delete the deprecated methods in UserBlockTracker anytime soon, as nothing has really changed other than the names
Version 2.2.012
Fixed a bug where Daze would cause an exception in older game versions (1.20.4 and older)

View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.nossr50.mcMMO</groupId>
<artifactId>mcMMO</artifactId>
<version>2.2.012</version>
<version>2.2.013-SNAPSHOT</version>
<name>mcMMO</name>
<url>https://github.com/mcMMO-Dev/mcMMO</url>
<scm>

View File

@ -4,7 +4,6 @@ import com.gmail.nossr50.config.LegacyConfigLoader;
import com.gmail.nossr50.datatypes.skills.alchemy.AlchemyPotion;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.ItemUtils;
import com.gmail.nossr50.util.PotionUtil;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.Material;

View File

@ -1,7 +1,6 @@
package com.gmail.nossr50.datatypes.skills.alchemy;
import com.gmail.nossr50.util.PotionUtil;
import org.bukkit.Bukkit;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionEffect;

View File

@ -310,7 +310,7 @@ public class BlockListener implements Listener {
// Minecraft is dumb, the events still throw when a plant "grows" higher than the max block height. Even though no new block is created
if (BlockUtils.isWithinWorldBounds(block)) {
mcMMO.getPlaceStore().setFalse(block);
mcMMO.getPlaceStore().setEligible(block);
}
}
@ -400,14 +400,14 @@ public class BlockListener implements Listener {
else if (BlockUtils.affectedBySuperBreaker(blockState)
&& (ItemUtils.isPickaxe(heldItem) || ItemUtils.isHoe(heldItem))
&& mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.MINING)
&& !mcMMO.getPlaceStore().isTrue(blockState)) {
&& !mcMMO.getPlaceStore().isIneligible(blockState)) {
MiningManager miningManager = mcMMOPlayer.getMiningManager();
miningManager.miningBlockCheck(blockState);
}
/* WOOD CUTTING */
else if (BlockUtils.hasWoodcuttingXP(blockState) && ItemUtils.isAxe(heldItem)
&& mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.WOODCUTTING) && !mcMMO.getPlaceStore().isTrue(blockState)) {
&& mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.WOODCUTTING) && !mcMMO.getPlaceStore().isIneligible(blockState)) {
WoodcuttingManager woodcuttingManager = mcMMOPlayer.getWoodcuttingManager();
if (woodcuttingManager.canUseTreeFeller(heldItem)) {
woodcuttingManager.processTreeFeller(blockState);
@ -422,7 +422,7 @@ public class BlockListener implements Listener {
}
/* EXCAVATION */
else if (BlockUtils.affectedByGigaDrillBreaker(blockState) && ItemUtils.isShovel(heldItem) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.EXCAVATION) && !mcMMO.getPlaceStore().isTrue(blockState)) {
else if (BlockUtils.affectedByGigaDrillBreaker(blockState) && ItemUtils.isShovel(heldItem) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.EXCAVATION) && !mcMMO.getPlaceStore().isIneligible(blockState)) {
ExcavationManager excavationManager = mcMMOPlayer.getExcavationManager();
excavationManager.excavationBlockCheck(blockState);
@ -687,7 +687,7 @@ public class BlockListener implements Listener {
if (UserManager.getPlayer(player).isDebugMode())
{
if (mcMMO.getPlaceStore().isTrue(blockState))
if (mcMMO.getPlaceStore().isIneligible(blockState))
player.sendMessage("[mcMMO DEBUG] This block is not natural and does not reward treasures/XP");
else
{

View File

@ -44,7 +44,6 @@ import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.projectiles.ProjectileSource;
import org.jetbrains.annotations.NotNull;
import static com.gmail.nossr50.util.MobMetadataUtils.*;
@ -207,8 +206,8 @@ public class EntityListener implements Listener {
if (entity instanceof FallingBlock || entity instanceof Enderman) {
boolean isTracked = entity.hasMetadata(MetadataConstants.METADATA_KEY_TRAVELING_BLOCK);
if (mcMMO.getPlaceStore().isTrue(block) && !isTracked) {
mcMMO.getPlaceStore().setFalse(block);
if (mcMMO.getPlaceStore().isIneligible(block) && !isTracked) {
mcMMO.getPlaceStore().setEligible(block);
entity.setMetadata(MetadataConstants.METADATA_KEY_TRAVELING_BLOCK, MetadataConstants.MCMMO_METADATA_VALUE);
TravelingBlockMetaCleanup metaCleanupTask = new TravelingBlockMetaCleanup(entity, pluginRef);
@ -222,8 +221,8 @@ public class EntityListener implements Listener {
//Redstone ore fire this event and should be ignored
}
else {
if (mcMMO.getPlaceStore().isTrue(block)) {
mcMMO.getPlaceStore().setFalse(block);
if (mcMMO.getPlaceStore().isIneligible(block)) {
mcMMO.getPlaceStore().setEligible(block);
}
}
}

View File

@ -852,7 +852,7 @@ public class PlayerListener implements Listener {
case "NETHER_WART_BLOCK":
case "POTATO":
case "MANGROVE_PROPAGULE":
mcMMO.getPlaceStore().setFalse(blockState);
mcMMO.getPlaceStore().setEligible(blockState);
break;
}
}

View File

@ -32,7 +32,7 @@ public class WorldListener implements Listener {
// Using 50 ms later as I do not know of a way to run one tick later (safely)
plugin.getFoliaLib().getImpl().runLater(() -> {
for (BlockState blockState : event.getBlocks()) {
mcMMO.getPlaceStore().setFalse(blockState);
mcMMO.getPlaceStore().setEligible(blockState);
}
}, 1);
}

View File

@ -1,47 +0,0 @@
package com.gmail.nossr50.runnables;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.gmail.nossr50.util.CancellableRunnable;
import com.gmail.nossr50.util.MetadataConstants;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import java.util.List;
public class PistonTrackerTask extends CancellableRunnable {
private final List<Block> blocks;
private final BlockFace direction;
private final Block futureEmptyBlock;
public PistonTrackerTask(List<Block> blocks, BlockFace direction, Block futureEmptyBlock) {
this.blocks = blocks;
this.direction = direction;
this.futureEmptyBlock = futureEmptyBlock;
}
@Override
public void run() {
// Check to see if futureEmptyBlock is empty - if it isn't; the blocks didn't move
if (!BlockUtils.isPistonPiece(futureEmptyBlock.getState())) {
return;
}
if (mcMMO.getPlaceStore().isTrue(futureEmptyBlock)) {
mcMMO.getPlaceStore().setFalse(futureEmptyBlock);
}
for (Block b : blocks) {
Block nextBlock = b.getRelative(direction);
if (nextBlock.hasMetadata(MetadataConstants.METADATA_KEY_PISTON_TRACKING)) {
BlockUtils.setUnnaturalBlock(nextBlock);
nextBlock.removeMetadata(MetadataConstants.METADATA_KEY_PISTON_TRACKING, mcMMO.p);
}
else if (mcMMO.getPlaceStore().isTrue(nextBlock)) {
// Block doesn't have metadatakey but isTrue - set it to false
mcMMO.getPlaceStore().setFalse(nextBlock);
}
}
}
}

View File

@ -19,7 +19,7 @@ public class StickyPistonTrackerTask extends CancellableRunnable {
@Override
public void run() {
if (!mcMMO.getPlaceStore().isTrue(movedBlock.getRelative(direction))) {
if (!mcMMO.getPlaceStore().isIneligible(movedBlock.getRelative(direction))) {
return;
}
@ -29,7 +29,7 @@ public class StickyPistonTrackerTask extends CancellableRunnable {
}
// The sticky piston actually pulled the block so move the PlaceStore data
mcMMO.getPlaceStore().setFalse(movedBlock.getRelative(direction));
mcMMO.getPlaceStore().setEligible(movedBlock.getRelative(direction));
BlockUtils.setUnnaturalBlock(movedBlock);
}
}

View File

@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables.skills;
import com.gmail.nossr50.datatypes.player.McMMOPlayer;
import com.gmail.nossr50.skills.alchemy.Alchemy;
import com.gmail.nossr50.skills.alchemy.AlchemyPotionBrewer;
import com.gmail.nossr50.util.CancellableRunnable;
import com.gmail.nossr50.util.ContainerMetadataUtils;
import com.gmail.nossr50.util.player.UserManager;
@ -17,7 +16,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import static com.gmail.nossr50.skills.alchemy.AlchemyPotionBrewer.isValidBrew;
import static com.gmail.nossr50.util.EventUtils.getMcMMOPlayer;
public class AlchemyBrewCheckTask extends CancellableRunnable {
private final BrewingStand brewingStand;

View File

@ -282,7 +282,7 @@ public class HerbalismManager extends SkillManager {
if (brokenPlant.getLocation().equals(originalBreak.getBlock().getLocation())) {
//If its the same block as the original, we are going to directly check it for being a valid XP gain and add it to the nonChorusBlocks list even if its a chorus block
//This stops a delay from happening when bringing up the XP bar for chorus trees
if (!mcMMO.getPlaceStore().isTrue(originalBreak)) {
if (!mcMMO.getPlaceStore().isIneligible(originalBreak)) {
//Even if its a chorus block, the original break will be moved to nonChorusBlocks for immediate XP rewards
noDelayPlantBlocks.add(brokenPlant);
} else {
@ -335,7 +335,7 @@ public class HerbalismManager extends SkillManager {
BlockData plantData = brokenPlantState.getBlockData();
//Check for double drops
if (!mcMMO.getPlaceStore().isTrue(brokenPlant)) {
if (!mcMMO.getPlaceStore().isIneligible(brokenPlant)) {
/*
*
@ -413,7 +413,7 @@ public class HerbalismManager extends SkillManager {
BlockState brokenBlockNewState = brokenPlantBlock.getState();
BlockData plantData = brokenBlockNewState.getBlockData();
if (mcMMO.getPlaceStore().isTrue(brokenBlockNewState)) {
if (mcMMO.getPlaceStore().isIneligible(brokenBlockNewState)) {
/*
*
* Unnatural Blocks
@ -427,7 +427,7 @@ public class HerbalismManager extends SkillManager {
}
//Mark it as natural again as it is being broken
mcMMO.getPlaceStore().setFalse(brokenBlockNewState);
mcMMO.getPlaceStore().setEligible(brokenBlockNewState);
} else {
/*
*
@ -489,9 +489,9 @@ public class HerbalismManager extends SkillManager {
continue;
}
if (mcMMO.getPlaceStore().isTrue(brokenBlockNewState)) {
if (mcMMO.getPlaceStore().isIneligible(brokenBlockNewState)) {
//Mark it as natural again as it is being broken
mcMMO.getPlaceStore().setFalse(brokenBlockNewState);
mcMMO.getPlaceStore().setEligible(brokenBlockNewState);
} else {
//TODO: Do we care about chorus flower age?
//Calculate XP for the old type

View File

@ -181,7 +181,7 @@ public class MiningManager extends SkillManager {
//Containers usually have 0 XP unless someone edited their config in a very strange way
if (ExperienceConfig.getInstance().getXp(PrimarySkillType.MINING, targetBlock) != 0
&& !(targetBlock instanceof Container)
&& !mcMMO.getPlaceStore().isTrue(targetBlock)) {
&& !mcMMO.getPlaceStore().isIneligible(targetBlock)) {
if (BlockUtils.isOre(blockState)) {
ores.add(blockState);
} else {
@ -216,7 +216,7 @@ public class MiningManager extends SkillManager {
Misc.spawnItem(getPlayer(), Misc.getBlockCenter(blockState), new ItemStack(blockState.getType()), ItemSpawnReason.BLAST_MINING_ORES); // Initial block that would have been dropped
if (mcMMO.p.getAdvancedConfig().isBlastMiningBonusDropsEnabled() && !mcMMO.getPlaceStore().isTrue(blockState)) {
if (mcMMO.p.getAdvancedConfig().isBlastMiningBonusDropsEnabled() && !mcMMO.getPlaceStore().isIneligible(blockState)) {
for (int i = 1; i < dropMultiplier; i++) {
// Bukkit.broadcastMessage("Bonus Drop on Ore: "+blockState.getType().toString());
Misc.spawnItem(getPlayer(), Misc.getBlockCenter(blockState), new ItemStack(blockState.getType()), ItemSpawnReason.BLAST_MINING_ORES_BONUS_DROP); // Initial block that would have been dropped

View File

@ -113,7 +113,7 @@ public class WoodcuttingManager extends SkillManager {
}
public void processWoodcuttingBlockXP(@NotNull BlockState blockState) {
if (mcMMO.getPlaceStore().isTrue(blockState))
if (mcMMO.getPlaceStore().isIneligible(blockState))
return;
int xp = getExperienceFromLog(blockState);
@ -269,7 +269,7 @@ public class WoodcuttingManager extends SkillManager {
* in treeFellerBlocks.
*/
private boolean processTreeFellerTargetBlock(@NotNull BlockState blockState, @NotNull List<BlockState> futureCenterBlocks, @NotNull Set<BlockState> treeFellerBlocks) {
if (treeFellerBlocks.contains(blockState) || mcMMO.getPlaceStore().isTrue(blockState)) {
if (treeFellerBlocks.contains(blockState) || mcMMO.getPlaceStore().isIneligible(blockState)) {
return false;
}
@ -373,7 +373,7 @@ public class WoodcuttingManager extends SkillManager {
* @return Amount of experience
*/
private static int processTreeFellerXPGains(BlockState blockState, int woodCount) {
if (mcMMO.getPlaceStore().isTrue(blockState))
if (mcMMO.getPlaceStore().isIneligible(blockState))
return 0;
int rawXP = ExperienceConfig.getInstance().getXp(PrimarySkillType.WOODCUTTING, blockState.getType());

View File

@ -65,7 +65,7 @@ public final class BlockUtils {
* @param block target block
*/
public static void setUnnaturalBlock(@NotNull Block block) {
mcMMO.getPlaceStore().setTrue(block);
mcMMO.getPlaceStore().setIneligible(block);
// Failsafe against lingering metadata
if (block.hasMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS))
@ -82,7 +82,7 @@ public final class BlockUtils {
block.removeMetadata(MetadataConstants.METADATA_KEY_REPLANT, mcMMO.p);
}
mcMMO.getPlaceStore().setFalse(block);
mcMMO.getPlaceStore().setEligible(block);
}
/**

View File

@ -1,7 +1,6 @@
package com.gmail.nossr50.util;
import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.skills.taming.TrackedTamingEntity;
import com.gmail.nossr50.util.player.NotificationManager;
import com.gmail.nossr50.util.skills.ParticleEffectUtils;

View File

@ -152,7 +152,7 @@ public class HashChunkManager implements ChunkManager {
}
}
private synchronized boolean isTrue(int x, int y, int z, @NotNull World world) {
private synchronized boolean isIneligible(int x, int y, int z, @NotNull World world) {
CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
// Get chunk, load from file if necessary
@ -178,32 +178,42 @@ public class HashChunkManager implements ChunkManager {
}
@Override
public synchronized boolean isTrue(@NotNull Block block) {
return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
public synchronized boolean isIneligible(@NotNull Block block) {
return isIneligible(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized boolean isTrue(@NotNull BlockState blockState) {
return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
public synchronized boolean isIneligible(@NotNull BlockState blockState) {
return isIneligible(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void setTrue(@NotNull Block block) {
public synchronized boolean isEligible(@NotNull Block block) {
return !isIneligible(block);
}
@Override
public synchronized boolean isEligible(@NotNull BlockState blockState) {
return !isIneligible(blockState);
}
@Override
public synchronized void setIneligible(@NotNull Block block) {
set(block.getX(), block.getY(), block.getZ(), block.getWorld(), true);
}
@Override
public synchronized void setTrue(@NotNull BlockState blockState) {
public synchronized void setIneligible(@NotNull BlockState blockState) {
set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), true);
}
@Override
public synchronized void setFalse(@NotNull Block block) {
public synchronized void setEligible(@NotNull Block block) {
set(block.getX(), block.getY(), block.getZ(), block.getWorld(), false);
}
@Override
public synchronized void setFalse(@NotNull BlockState blockState) {
public synchronized void setEligible(@NotNull BlockState blockState) {
set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), false);
}

View File

@ -17,24 +17,34 @@ public class NullChunkManager implements ChunkManager {
public void unloadWorld(@NotNull World world) {}
@Override
public boolean isTrue(@NotNull Block block) {
public boolean isIneligible(@NotNull Block block) {
return false;
}
@Override
public boolean isTrue(@NotNull BlockState blockState) {
public boolean isIneligible(@NotNull BlockState blockState) {
return false;
}
@Override
public void setTrue(@NotNull Block block) {}
public boolean isEligible(@NotNull Block block) {
return false;
}
@Override
public void setTrue(@NotNull BlockState blockState) {}
public boolean isEligible(@NotNull BlockState blockState) {
return false;
}
@Override
public void setFalse(@NotNull Block block) {}
public void setIneligible(@NotNull Block block) {}
@Override
public void setFalse(@NotNull BlockState blockState) {}
public void setIneligible(@NotNull BlockState blockState) {}
@Override
public void setEligible(@NotNull Block block) {}
@Override
public void setEligible(@NotNull BlockState blockState) {}
}

View File

@ -11,46 +11,136 @@ import org.jetbrains.annotations.NotNull;
*/
public interface UserBlockTracker {
/**
* Check to see if a given block location is set to true
* Check to see if a given {@link Block} is ineligible for rewards.
* This is a location-based lookup, and the other properties of the {@link Block} do not matter.
*
* @param block Block location to check
* @return true if the given block location is set to true, false if otherwise
* @param block Block to check
* @return true if the given block should not give rewards, false if otherwise
*/
boolean isTrue(@NotNull Block block);
boolean isIneligible(@NotNull Block block);
/**
* Check to see if a given BlockState location is set to true
* Check to see if a given {@link Block} is eligible for rewards.
* This is a location-based lookup, and the other properties of the {@link Block} do not matter.
*
* @param block Block to check
* @return true if the given block should give rewards, false if otherwise
*/
boolean isEligible(@NotNull Block block);
/**
* Check to see if a given {@link BlockState} is eligible for rewards.
* This is a location-based lookup, and the other properties of the {@link BlockState} do not matter.
*
* @param blockState BlockState to check
* @return true if the given BlockState location is set to true, false if otherwise
*/
boolean isTrue(@NotNull BlockState blockState);
boolean isEligible(@NotNull BlockState blockState);
/**
* Set a given block location to true
* Check to see if a given {@link BlockState} is ineligible for rewards.
* This is a location-based lookup, and the other properties of the {@link BlockState} do not matter.
*
* @param block Block location to set
* @param blockState BlockState to check
* @return true if the given BlockState location is set to true, false if otherwise
*/
void setTrue(@NotNull Block block);
boolean isIneligible(@NotNull BlockState blockState);
/**
* Set a given {@link Block} as ineligible for rewards.
* This is a location-based lookup, and the other properties of the {@link Block} do not matter.
*
* @param block block whose location to set as ineligible
*/
void setIneligible(@NotNull Block block);
/**
* Set a given BlockState location to true
*
* @param blockState BlockState location to set
*/
void setTrue(@NotNull BlockState blockState);
void setIneligible(@NotNull BlockState blockState);
/**
* Set a given block location to false
* Set a given {@link Block} as eligible for rewards.
* This is a location-based lookup, and the other properties of the {@link Block} do not matter.
*
* @param block Block location to set
* @param block block whose location to set as eligible
*/
void setFalse(@NotNull Block block);
void setEligible(@NotNull Block block);
/**
* Set a given BlockState location to false
*
* @param blockState BlockState location to set
*/
void setFalse(@NotNull BlockState blockState);
void setEligible(@NotNull BlockState blockState);
/**
* Check to see if a given block location is set to true
*
* @param block Block location to check
* @return true if the given block location is set to true, false if otherwise
* @deprecated Use {@link #isIneligible(Block)} instead
*/
@Deprecated(since = "2.2.013")
default boolean isTrue(@NotNull Block block) {
return isIneligible(block);
}
/**
* Check to see if a given BlockState location is set to true
*
* @param blockState BlockState to check
* @return true if the given BlockState location is set to true, false if otherwise
* @deprecated Use {@link #isIneligible(BlockState)} instead
*/
@Deprecated(since = "2.2.013")
default boolean isTrue(@NotNull BlockState blockState) {
return isIneligible(blockState);
}
/**
* Set a given block location to true
*
* @param block Block location to set
* @deprecated Use {@link #setIneligible(Block)} instead
*/
@Deprecated(since = "2.2.013")
default void setTrue(@NotNull Block block) {
setIneligible(block);
}
/**
* Set a given BlockState location to true
*
* @param blockState BlockState location to set
* @deprecated Use {@link #setIneligible(BlockState)} instead
*/
@Deprecated(since = "2.2.013")
default void setTrue(@NotNull BlockState blockState) {
setIneligible(blockState);
}
/**
* Set a given block location to false
*
* @param block Block location to set
* @deprecated Use {@link #setEligible(Block)} instead
*/
@Deprecated(since = "2.2.013")
default void setFalse(@NotNull Block block) {
setEligible(block);
}
/**
* Set a given BlockState location to false
*
* @param blockState BlockState location to set
* @deprecated Use {@link #setEligible(BlockState)} instead
*/
@Deprecated(since = "2.2.013")
default void setFalse(@NotNull BlockState blockState) {
setEligible(blockState);
}
}

View File

@ -13,9 +13,8 @@ import org.mockito.MockedStatic;
import static com.gmail.nossr50.util.PotionEffectUtil.getNauseaPotionEffectType;
import static java.util.logging.Logger.getLogger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;
class PotionEffectUtilTest {
private MockedStatic<mcMMO> mockedStaticMcMMO;

View File

@ -0,0 +1,121 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
import static org.bukkit.Bukkit.getWorld;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
class BitSetChunkStoreTest {
private static File tempDir;
@BeforeAll
public static void setUpClass() {
tempDir = Files.createTempDir();
}
@AfterAll
public static void tearDownClass() {
recursiveDelete(tempDir);
}
private World mockWorld;
private MockedStatic<Bukkit> bukkitMock;
private MockedStatic<mcMMO> mcMMOMock;
@BeforeEach
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = mockStatic(mcMMO.class);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void testSetValue() {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
original.setTrue(0, 0, 0);
assertTrue(original.isTrue(0, 0, 0));
original.setFalse(0, 0, 0);
assertFalse(original.isTrue(0, 0, 0));
}
@Test
void testIsEmpty() {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
assertTrue(original.isEmpty());
original.setTrue(0, 0, 0);
original.setFalse(0, 0, 0);
assertTrue(original.isEmpty());
}
@Test
void testRoundTrip() throws IOException {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkStore(original);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertChunkStoreEquals(original, deserialized);
}
@Test
void testNegativeWorldMin() throws IOException {
when(mockWorld.getMinHeight()).thenReturn(-64);
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, -32, 12);
original.setTrue(14, -64, 12);
original.setTrue(13, -63, 12);
byte[] serializedBytes = serializeChunkStore(original);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertChunkStoreEquals(original, deserialized);
}
@Test
void testNegativeWorldMinUpgrade() throws IOException {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 1, 12);
original.setTrue(14, 2, 12);
original.setTrue(13, 3, 12);
byte[] serializedBytes = serializeChunkStore(original);
when(mockWorld.getMinHeight()).thenReturn(-64);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqualIgnoreMinMax(original, deserialized);
}
}

View File

@ -0,0 +1,49 @@
package com.gmail.nossr50.util.blockmeta;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BlockStoreTestUtils {
public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
/**
* Asserts that the two ChunkStores are equal.
* @param expected The expected ChunkStore
* @param actual The actual ChunkStore
*/
static void assertChunkStoreEquals(ChunkStore expected, ChunkStore actual) {
assertEquals(expected.getChunkMin(), actual.getChunkMin());
assertEquals(expected.getChunkMax(), actual.getChunkMax());
assertEqualIgnoreMinMax(expected, actual);
}
static byte[] serializeChunkStore(@NotNull ChunkStore chunkStore) throws IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (chunkStore instanceof BitSetChunkStore)
BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
else
new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
// it were the old
// PrimitiveChunkStore
return byteArrayOutputStream.toByteArray();
}
static void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
assertEquals(expected.getChunkX(), actual.getChunkX());
assertEquals(expected.getChunkZ(), actual.getChunkZ());
assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
continue; // Ignore
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
}
}
}

View File

@ -1,13 +1,9 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@ -15,14 +11,13 @@ import org.mockito.Mockito;
import java.io.*;
import java.util.UUID;
/**
* Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
* that the serialization isn't completely broken.
*/
class ChunkStoreTest {
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
import static org.bukkit.Bukkit.getWorld;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
class ChunkStoreTest {
private static File tempDir;
@BeforeAll
@ -44,151 +39,39 @@ class ChunkStoreTest {
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = Mockito.mockStatic(Bukkit.class);
bukkitMock.when(() -> Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = Mockito.mockStatic(mcMMO.class);
mcMMOMock = mockStatic(mcMMO.class);
Mockito.when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
Mockito.when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void testIndexOutOfBounds() {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
HashChunkManager hashChunkManager = new HashChunkManager();
// Top Block
Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(illegalHeightBlock));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setTrue(illegalHeightBlock));
}
@Test
void testSetTrue() {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
HashChunkManager hashChunkManager = new HashChunkManager();
int radius = 2; // Could be anything but drastically changes test time
for (int x = -radius; x <= radius; x++) {
for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
for (int z = -radius; z <= radius; z++) {
Block testBlock = initMockBlock(x, y, z);
hashChunkManager.setTrue(testBlock);
Assertions.assertTrue(hashChunkManager.isTrue(testBlock));
hashChunkManager.setFalse(testBlock);
Assertions.assertFalse(hashChunkManager.isTrue(testBlock));
}
}
}
// Bot Block
Block bottomBlock = initMockBlock(1337, 0, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(bottomBlock));
Assertions.assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
hashChunkManager.setTrue(bottomBlock);
Assertions.assertTrue(hashChunkManager.isTrue(bottomBlock));
// Top Block
Block topBlock = initMockBlock(1337, 255, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(topBlock));
Assertions.assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
hashChunkManager.setTrue(topBlock);
Assertions.assertTrue(hashChunkManager.isTrue(topBlock));
}
@Test
void testSetValue() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
original.setTrue(0, 0, 0);
Assertions.assertTrue(original.isTrue(0, 0, 0));
original.setFalse(0, 0, 0);
Assertions.assertFalse(original.isTrue(0, 0, 0));
}
@Test
void testIsEmpty() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertTrue(original.isEmpty());
original.setTrue(0, 0, 0);
original.setFalse(0, 0, 0);
Assertions.assertTrue(original.isEmpty());
}
@Test
void testRoundTrip() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
void testNegativeWorldMin() throws IOException {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, -32, 12);
original.setTrue(14, -64, 12);
original.setTrue(13, -63, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
void testNegativeWorldMinUpgrade() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 1, 12);
original.setTrue(14, 2, 12);
original.setTrue(13, 3, 12);
byte[] serializedBytes = serializeChunkstore(original);
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqualIgnoreMinMax(original, deserialized);
}
@Test
void testChunkCoords() throws IOException {
for (int x = -96; x < 0; x++) {
int cx = x >> 4;
int ix = Math.abs(x) % 16;
//System.out.print(cx + ":" + ix + " ");
}
}
@Test
void testUpgrade() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 32);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
byte[] serializedBytes = serializeChunkStore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqual(original, deserialized);
assertChunkStoreEquals(original, deserialized);
}
@Test
void testSimpleRegionRoundtrip() throws IOException {
void testSimpleRegionRoundTrip() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
@ -196,7 +79,7 @@ class ChunkStoreTest {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
try (DataOutputStream outputStream = region.getOutputStream(12, 12)) {
outputStream.write(serializeChunkstore(original));
outputStream.write(serializeChunkStore(original));
}
region.close();
region = new McMMOSimpleRegionFile(file, 0, 0);
@ -204,229 +87,10 @@ class ChunkStoreTest {
Assertions.assertNotNull(is);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
assert deserialized != null;
assertEqual(original, deserialized);
assertChunkStoreEquals(original, deserialized);
}
region.close();
file.delete();
}
@Test
void testSimpleRegionRejectsOutOfBounds() {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
region.close();
}
@Test
void testChunkStoreRejectsOutOfBounds() {
ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
}
@Test
void testRegressionChunkMirrorBug() {
ChunkManager chunkManager = new HashChunkManager();
Block mockBlockA = Mockito.mock(Block.class);
Mockito.when(mockBlockA.getX()).thenReturn(15);
Mockito.when(mockBlockA.getZ()).thenReturn(15);
Mockito.when(mockBlockA.getY()).thenReturn(0);
Mockito.when(mockBlockA.getWorld()).thenReturn(mockWorld);
Block mockBlockB = Mockito.mock(Block.class);
Mockito.when(mockBlockB.getX()).thenReturn(-15);
Mockito.when(mockBlockB.getZ()).thenReturn(-15);
Mockito.when(mockBlockB.getY()).thenReturn(0);
Mockito.when(mockBlockB.getWorld()).thenReturn(mockWorld);
chunkManager.setTrue(mockBlockA);
chunkManager.setFalse(mockBlockB);
Assertions.assertTrue(chunkManager.isTrue(mockBlockA));
}
private void assertEqual(ChunkStore expected, ChunkStore actual) {
Assertions.assertEquals(expected.getChunkMin(), actual.getChunkMin());
Assertions.assertEquals(expected.getChunkMax(), actual.getChunkMax());
assertEqualIgnoreMinMax(expected, actual);
}
private void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
Assertions.assertEquals(expected.getChunkX(), actual.getChunkX());
Assertions.assertEquals(expected.getChunkZ(), actual.getChunkZ());
Assertions.assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
continue; // Ignore
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
Assertions.assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
}
}
private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (chunkStore instanceof BitSetChunkStore)
BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
else
new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
// it were the old
// PrimitiveChunkStore
return byteArrayOutputStream.toByteArray();
}
public static class LegacyChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private final int cx;
private final int cz;
private final @NotNull UUID worldUid;
public LegacyChunkStore(@NotNull World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public int getChunkMin() {
return 0;
}
@Override
public int getChunkMax() {
return store[0][0].length;
}
@Override
public @NotNull UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public void set(int x, int y, int z, boolean value) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = value;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
}
private static class UnitTestObjectOutputStream extends ObjectOutputStream {
public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
super(outputStream);
}
@Override
public void writeUTF(@NotNull String str) throws IOException {
// Pretend to be the old class
if (str.equals(LegacyChunkStore.class.getName()))
str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
super.writeUTF(str);
}
}
@NotNull
private Block initMockBlock(int x, int y, int z) {
Block mockBlock = Mockito.mock(Block.class);
Mockito.when(mockBlock.getX()).thenReturn(x);
Mockito.when(mockBlock.getY()).thenReturn(y);
Mockito.when(mockBlock.getZ()).thenReturn(z);
Mockito.when(mockBlock.getWorld()).thenReturn(mockWorld);
return mockBlock;
}
public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
if (directoryToBeDeleted.isDirectory()) {
for (File file : directoryToBeDeleted.listFiles()) {
recursiveDelete(file);
}
}
directoryToBeDeleted.delete();
}
}
}

View File

@ -0,0 +1,127 @@
package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.UUID;
/**
* Used for unit testing upgrades from the old ChunkStore class.
*/
class LegacyChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private final int cx;
private final int cz;
private final @NotNull UUID worldUid;
public LegacyChunkStore(@NotNull World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public int getChunkMin() {
return 0;
}
@Override
public int getChunkMax() {
return store[0][0].length;
}
@Override
public @NotNull UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public void set(int x, int y, int z, boolean value) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = value;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,23 @@
package com.gmail.nossr50.util.blockmeta;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
class UnitTestObjectOutputStream extends ObjectOutputStream {
public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
super(outputStream);
}
@Override
public void writeUTF(@NotNull String str) throws IOException {
// Pretend to be the old class
if (str.equals(LegacyChunkStore.class.getName()))
str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
super.writeUTF(str);
}
}

View File

@ -0,0 +1,192 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MAX;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MIN;
import static org.bukkit.Bukkit.getWorld;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
* Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
* that the serialization isn't completely broken.
*/
class UserBlockTrackerTest {
private static File tempDir;
@BeforeAll
public static void setUpClass() {
tempDir = Files.createTempDir();
}
@AfterAll
public static void tearDownClass() {
recursiveDelete(tempDir);
}
private World mockWorld;
private MockedStatic<Bukkit> bukkitMock;
private MockedStatic<mcMMO> mcMMOMock;
@BeforeEach
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = mockStatic(mcMMO.class);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void setIneligibleShouldThrowIndexOutOfBoundsException() {
when(mockWorld.getMinHeight()).thenReturn(-64);
final HashChunkManager hashChunkManager = new HashChunkManager();
// Top Block
final Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
assertFalse(hashChunkManager.isIneligible(illegalHeightBlock));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setIneligible(illegalHeightBlock));
}
@Test
void testSetEligibility() {
when(mockWorld.getMinHeight()).thenReturn(-64);
final HashChunkManager hashChunkManager = new HashChunkManager();
int radius = 2; // Could be anything but drastically changes test time
for (int x = -radius; x <= radius; x++) {
for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
for (int z = -radius; z <= radius; z++) {
final Block testBlock = initMockBlock(x, y, z);
// mark ineligible
hashChunkManager.setIneligible(testBlock);
assertTrue(hashChunkManager.isIneligible(testBlock));
// mark eligible
hashChunkManager.setEligible(testBlock);
// Might as well test both isIneligible and isEligible while we are here
assertFalse(hashChunkManager.isIneligible(testBlock));
assertTrue(hashChunkManager.isEligible(testBlock));
}
}
}
// TODO: Whatever is going on down here should be in its own test
// Bot Block
final Block bottomBlock = initMockBlock(1337, 0, -1337);
assertFalse(hashChunkManager.isIneligible(bottomBlock));
assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
hashChunkManager.setIneligible(bottomBlock);
assertTrue(hashChunkManager.isIneligible(bottomBlock));
// Top Block
final Block topBlock = initMockBlock(1337, 255, -1337);
assertFalse(hashChunkManager.isIneligible(topBlock));
assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
hashChunkManager.setIneligible(topBlock);
assertTrue(hashChunkManager.isIneligible(topBlock));
}
@Test
void testChunkCoords() throws IOException {
// TODO: Unfinished test?
for (int x = -96; x < 0; x++) {
int cx = x >> 4;
int ix = Math.abs(x) % 16;
//System.out.print(cx + ":" + ix + " ");
}
}
@Test
void testSimpleRegionRejectsOutOfBounds() {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
region.close();
}
@Test
void testChunkStoreRejectsOutOfBounds() {
ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
}
@Test
void testRegressionChunkMirrorBug() {
final UserBlockTracker chunkManager = new HashChunkManager();
Block mockBlockA = Mockito.mock(Block.class);
when(mockBlockA.getX()).thenReturn(15);
when(mockBlockA.getZ()).thenReturn(15);
when(mockBlockA.getY()).thenReturn(0);
when(mockBlockA.getWorld()).thenReturn(mockWorld);
Block mockBlockB = Mockito.mock(Block.class);
when(mockBlockB.getX()).thenReturn(-15);
when(mockBlockB.getZ()).thenReturn(-15);
when(mockBlockB.getY()).thenReturn(0);
when(mockBlockB.getWorld()).thenReturn(mockWorld);
chunkManager.setIneligible(mockBlockA);
chunkManager.setEligible(mockBlockB);
assertTrue(chunkManager.isIneligible(mockBlockA));
}
@NotNull
private Block initMockBlock(int x, int y, int z) {
final Block mockBlock = Mockito.mock(Block.class);
when(mockBlock.getX()).thenReturn(x);
when(mockBlock.getY()).thenReturn(y);
when(mockBlock.getZ()).thenReturn(z);
when(mockBlock.getWorld()).thenReturn(mockWorld);
return mockBlock;
}
public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
if (directoryToBeDeleted.isDirectory()) {
for (File file : directoryToBeDeleted.listFiles()) {
recursiveDelete(file);
}
}
directoryToBeDeleted.delete();
}
}