/**
* This class was created by <Vazkii>. It's distributed as
* part of the Botania Mod. Get the Source Code in github:
* https://github.com/Vazkii/Botania
*
* Botania is Open Source and distributed under the
* Botania License: http://botaniamod.net/license.php
*
* File Created @ [May 15, 2015, 6:55:34 PM (GMT)]
*/
package vazkii.botania.common.item.equipment.tool.terrasteel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Set;
import javax.annotation.Nonnull;
import gnu.trove.map.hash.TIntObjectHashMap;
import net.minecraft.block.Block;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Enchantments;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent.Phase;
import vazkii.botania.api.BotaniaAPI;
import vazkii.botania.api.item.ISequentialBreaker;
import vazkii.botania.common.item.ItemTemperanceStone;
import vazkii.botania.common.item.equipment.tool.ToolCommons;
import vazkii.botania.common.item.equipment.tool.manasteel.ItemManasteelAxe;
import vazkii.botania.common.item.relic.ItemLokiRing;
import vazkii.botania.common.lib.LibItemNames;
import vazkii.botania.common.lib.LibMisc;
public class ItemTerraAxe extends ItemManasteelAxe implements ISequentialBreaker {
/**
* The number of blocks per tick which the Terra Truncator will
* collect.
*/
private static final int BLOCK_SWAP_RATE = 10;
/**
* The maximum radius (in blocks) which the Terra Truncator will go
* in order to try and murder/cut down the tree.
*/
public static final int BLOCK_RANGE = 32;
/**
* The maximum number of leaf blocks which the Terra Truncator will chew/go
* through once a leaf block is encountered.
*/
private static final int LEAF_BLOCK_RANGE = 3;
/**
* The amount of mana required to restore 1 point of damage.
*/
private static final int MANA_PER_DAMAGE = 100;
/**
* Represents a map of dimension IDs to a set of all block swappers
* active in that dimension.
*/
private static final TIntObjectHashMap<Set<BlockSwapper>> blockSwappers = new TIntObjectHashMap<>();
public ItemTerraAxe() {
super(BotaniaAPI.terrasteelToolMaterial, LibItemNames.TERRA_AXE);
MinecraftForge.EVENT_BUS.register(this);
attackSpeed = -3f;
addPropertyOverride(new ResourceLocation(LibMisc.MOD_ID, "terraaxe_on"), (stack, world, entity) -> {
if(entity instanceof EntityPlayer && !shouldBreak((EntityPlayer) entity))
return 0;
return 1;
});
}
private boolean shouldBreak(EntityPlayer player) {
return !player.isSneaking() && !ItemTemperanceStone.hasTemperanceActive(player);
}
@Override
public boolean onBlockStartBreak(ItemStack stack, BlockPos pos, EntityPlayer player) {
RayTraceResult raycast = ToolCommons.raytraceFromEntity(player.world, player, true, 10);
if(raycast != null) {
breakOtherBlock(player, stack, pos, pos, raycast.sideHit);
ItemLokiRing.breakOnAllCursors(player, this, stack, pos, raycast.sideHit);
}
return false;
}
@Override
public int getManaPerDamage() {
return MANA_PER_DAMAGE;
}
@Override
public void breakOtherBlock(EntityPlayer player, ItemStack stack, BlockPos pos, BlockPos originPos, EnumFacing side) {
if(shouldBreak(player)) {
addBlockSwapper(player.world, player, stack, pos, 32, true);
}
}
@Override
public boolean disposeOfTrashBlocks(ItemStack stack) {
return false;
}
@SubscribeEvent
public void onTickEnd(TickEvent.WorldTickEvent event) {
// Block Swapping ticking should only occur on the server
if(event.world.isRemote)
return;
if(event.phase == Phase.END) {
int dim = event.world.provider.getDimension();
if(blockSwappers.containsKey(dim)) {
Set<BlockSwapper> swappers = blockSwappers.get(dim);
// Iterate through all of our swappers, removing any
// which no longer need to tick.
Iterator<BlockSwapper> swapper = swappers.iterator();
while(swapper.hasNext()) {
BlockSwapper next = swapper.next();
// If a null sneaks in or the swapper is done, remove it
if(next == null || !next.tick())
swapper.remove();
}
}
}
}
/**
* Adds a new block swapper to the provided world as the provided player.
* Block swappers are only added on the server, and a marker instance
* which is not actually ticked but contains the proper passed in
* information will be returned to the client.
*
* @param world The world to add the swapper to.
* @param player The player who is responsible for this swapper.
* @param stack The Terra Truncator which caused this block swapper.
* @param origCoords The original coordinates the swapper should start at.
* @param steps The range of the block swapper, in blocks.
* @param leaves If true, will treat leaves specially (see the BlockSwapper
* documentation).
*/
private static void addBlockSwapper(World world, EntityPlayer player, ItemStack stack, BlockPos origCoords, int steps, boolean leaves) {
BlockSwapper swapper = new BlockSwapper(world, player, stack, origCoords, steps, leaves);
// Block swapper registration should only occur on the server
if(world.isRemote)
return;
// If the mapping for this dimension doesn't exist, create it.
int dim = world.provider.getDimension();
if(!blockSwappers.containsKey(dim))
blockSwappers.put(dim, new HashSet<>());
// Add the swapper
blockSwappers.get(dim).add(swapper);
}
/**
* A block swapper for the Terra Truncator, which uses a standard
* Breadth First Search to try and murder/cut down trees.
*
* The Terra Truncator will look up to BLOCK_RANGE blocks to find wood
* to cut down (only cutting down adjacent pieces of wood, so it doesn't
* jump through the air). However, the truncator will only go through
* LEAF_BLOCK_RANGE leave blocks in order to prevent adjacent trees which
* are connected only by leaves from being devoured as well.
*
* The leaf restriction is implemented by reducing the number of remaining
* steps to the min of LEAF_BLOCK_RANGE and the current range. The restriction
* can be removed entirely by setting the "leaves" variable to true, in which
* case leaves will be treated normally.
*/
private static class BlockSwapper {
/**
* Represents the range which a single block will scan when looking
* for the next candidates for swapping. 1 is a good default.
*/
public static final int SINGLE_BLOCK_RADIUS = 1;
/**
* The world the block swapper is doing the swapping in.
*/
private final World world;
/**
* The player the swapper is swapping for.
*/
private final EntityPlayer player;
/**
* The Terra Truncator which created this swapper.
*/
private final ItemStack truncator;
/**
* The origin of the swapper (eg, where it started).
*/
private final BlockPos origin;
/**
* Denotes whether leaves should be treated specially.
*/
private final boolean treatLeavesSpecial;
/**
* The initial range which this block swapper starts with.
*/
private final int range;
/**
* The priority queue of all possible candidates for swapping.
*/
private final PriorityQueue<SwapCandidate> candidateQueue;
/**
* The set of already swaps coordinates which do not have
* to be revisited.
*/
private final Set<BlockPos> completedCoords;
/**
* Creates a new block swapper with the provided parameters.
* @param world The world the swapper is in.
* @param player The player responsible for creating this swapper.
* @param truncator The Terra Truncator responsible for creating this swapper.
* @param origCoords The original coordinates this swapper should start at.
* @param range The range this swapper should swap in.
* @param leaves If true, leaves will be treated specially and
* severely reduce the radius of further spreading when encountered.
*/
public BlockSwapper(World world, EntityPlayer player, ItemStack truncator, BlockPos origCoords, int range, boolean leaves) {
this.world = world;
this.player = player;
this.truncator = truncator;
origin = origCoords;
this.range = range;
treatLeavesSpecial = leaves;
candidateQueue = new PriorityQueue<>();
completedCoords = new HashSet<>();
// Add the origin to our candidate queue with the original range
candidateQueue.offer(new SwapCandidate(origin, this.range));
}
/**
* Ticks this Block Swapper, which allows it to swap BLOCK_SWAP_RATE
* further blocks and expands the breadth first search. The return
* value signifies whether or not the block swapper has more blocks
* to swap, or if it has finished swapping.
* @return True if the block swapper has more blocks to swap, false
* otherwise (implying it can be safely removed).
*/
public boolean tick() {
// If empty, this swapper is done.
if(candidateQueue.isEmpty())
return false;
int remainingSwaps = BLOCK_SWAP_RATE;
while(remainingSwaps > 0 && !candidateQueue.isEmpty()) {
SwapCandidate cand = candidateQueue.poll();
// If we've already completed this location, move along, as this
// is just a suboptimal one.
if(completedCoords.contains(cand.coordinates))
continue;
// If this candidate is out of range, discard it.
if(cand.range <= 0)
continue;
// Otherwise, perform the break and then look at the adjacent tiles.
// This is a ridiculous function call here.
ToolCommons.removeBlockWithDrops(player, truncator, world,
cand.coordinates,
origin,
null, ToolCommons.materialsAxe,
EnchantmentHelper.getEnchantmentLevel(Enchantments.SILK_TOUCH, truncator) > 0,
EnchantmentHelper.getEnchantmentLevel(Enchantments.FORTUNE, truncator),
false, treatLeavesSpecial);
remainingSwaps--;
completedCoords.add(cand.coordinates);
// Then, go through all of the adjacent blocks and look if
// any of them are any good.
for(BlockPos adj : adjacent(cand.coordinates)) {
Block block = world.getBlockState(adj).getBlock();
boolean isWood = block.isWood(world, adj);
boolean isLeaf = block.isLeaves(world.getBlockState(adj), world, adj);
// If it's not wood or a leaf, we aren't interested.
if(!isWood && !isLeaf)
continue;
// If we treat leaves specially and this is a leaf, it gets
// the minimum of the leaf range and the current range - 1.
// Otherwise, it gets the standard range - 1.
int newRange = treatLeavesSpecial && isLeaf ?
Math.min(LEAF_BLOCK_RANGE, cand.range - 1) :
cand.range - 1;
candidateQueue.offer(new SwapCandidate(adj, newRange));
}
}
// If we did any iteration, then hang around until next tick.
return true;
}
public List<BlockPos> adjacent(BlockPos original) {
List<BlockPos> coords = new ArrayList<>();
// Visit all the surrounding blocks in the provided radius.
// Gotta love these nested loops, right?
for(int dx = -SINGLE_BLOCK_RADIUS; dx <= SINGLE_BLOCK_RADIUS; dx++)
for(int dy = -SINGLE_BLOCK_RADIUS; dy <= SINGLE_BLOCK_RADIUS; dy++)
for(int dz = -SINGLE_BLOCK_RADIUS; dz <= SINGLE_BLOCK_RADIUS; dz++) {
// Skip the central tile.
if(dx == 0 && dy == 0 && dz == 0)
continue;
coords.add(original.add(dx, dy, dz));
}
return coords;
}
/**
* Represents a potential candidate for swapping/removal. Sorted by
* range (where a larger range is more preferable). As we're using
* a priority queue, which is a min-heap internally, larger ranges
* are considered "smaller" than smaller ranges (so they show up in the
* min-heap first).
*/
public static final class SwapCandidate implements Comparable<SwapCandidate> {
/**
* The location of this swap candidate.
*/
public final BlockPos coordinates;
/**
* The remaining range of this swap candidate.
*/
public final int range;
/**
* Constructs a new Swap Candidate with the provided
* coordinates and range.
* @param coordinates The coordinates of this candidate.
* @param range The remaining range of this candidate.
*/
public SwapCandidate(BlockPos coordinates, int range) {
this.coordinates = coordinates;
this.range = range;
}
@Override
public int compareTo(@Nonnull SwapCandidate other) {
// Aka, a bigger range implies a smaller value, meaning
// bigger ranges will be preferred in a min-heap
return other.range - range;
}
@Override
public boolean equals(Object other) {
if(!(other instanceof SwapCandidate)) return false;
SwapCandidate cand = (SwapCandidate) other;
return coordinates.equals(cand.coordinates) && range == cand.range;
}
@Override
public int hashCode() {
return Objects.hash(coordinates, range);
}
}
}
}