/*
* Copyright (c) CovertJaguar, 2014 http://railcraft.info
*
* This code is the property of CovertJaguar
* and may only be used with explicit written
* permission unless otherwise specified on the
* license page at http://railcraft.info/wiki/info:license.
*/
package mods.railcraft.common.blocks.tracks;
import mods.railcraft.api.tracks.ISwitchDevice;
import mods.railcraft.api.tracks.ISwitchDevice.ArrowDirection;
import mods.railcraft.api.tracks.ITrackSwitch;
import mods.railcraft.common.blocks.RailcraftTileEntity;
import mods.railcraft.common.carts.CartUtils;
import mods.railcraft.common.carts.LinkageManager;
import mods.railcraft.common.carts.Train;
import mods.railcraft.common.util.misc.Game;
import mods.railcraft.common.util.misc.MiscTools;
import net.minecraft.block.Block;
import net.minecraft.entity.item.EntityMinecart;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraftforge.common.util.ForgeDirection;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.*;
/**
* @author CovertJaguar <http://www.railcraft.info>
*/
public abstract class TrackSwitchBase extends TrackBaseRailcraft implements ITrackSwitch {
private static final int SPRING_DURATION = 30;
protected boolean mirrored;
protected boolean shouldSwitch;
protected Set<UUID> lockingCarts = new HashSet<UUID>();
protected Set<UUID> springingCarts = new HashSet<UUID>();
protected Set<UUID> decidingCarts = new HashSet<UUID>();
private byte sprung;
private byte locked;
private UUID currentCart;
private ISwitchDevice switchDevice;
private boolean clientSwitched;
@Override
public boolean canMakeSlopes() {
return false;
}
@Override
public boolean canUpdate() {
return true;
}
@Override
public boolean isFlexibleRail() {
return false;
}
@Override
public boolean isMirrored() {
return mirrored;
}
@Override
public boolean isVisuallySwitched() {
if (Game.isHost(getWorld()))
return !isLocked() && (shouldSwitch || isSprung());
return clientSwitched;
}
/**
* This is a method provided to the subclasses to determine more accurately for
* the passed in cart whether the switch is sprung or not. It caches the server
* responses for the clients to use.
* Note: This method should not modify any variables except the cache, we leave
* that to updateEntity().
*/
protected boolean shouldSwitchForCart(EntityMinecart cart) {
if (cart == null || Game.isNotHost(getWorld()))
return isVisuallySwitched();
if (springingCarts.contains(cart.getPersistentID()))
return true; // Carts at the spring entrance always are on switched tracks
if (lockingCarts.contains(cart.getPersistentID()))
return false; // Carts at the locking entrance always are on locked tracks
boolean sameTrain = Train.areInSameTrain(LinkageManager.instance().getCartFromUUID(currentCart), cart);
boolean shouldSwitch = (switchDevice != null) ? switchDevice.shouldSwitch(this, cart) : false;
if (isSprung()) {
if (shouldSwitch || sameTrain) {
// we're either same train or switched so return true
return true;
}
// detected new train, we can safely treat this as not switched
return false;
}
if (isLocked()) {
if (shouldSwitch && !sameTrain) {
// detected new train, we can safely treat this as switched
return true;
}
// other cases we obey locked
return false;
}
// we're not sprung or locked so we should return shouldSwitch
return shouldSwitch;
}
private void springTrack(UUID cartOnTrack) {
sprung = SPRING_DURATION;
locked = 0;
currentCart = cartOnTrack;
}
private void lockTrack(UUID cartOnTrack) {
locked = SPRING_DURATION;
sprung = 0;
currentCart = cartOnTrack;
}
public boolean isLocked() {
return locked > 0;
}
public boolean isSprung() {
return sprung > 0;
}
@Override
public void onBlockPlaced() {
determineTrackMeta();
determineMirror();
// Notify any neighboring switches that we exist so they know to register themselves with us
((RailcraftTileEntity) tileEntity).notifyBlocksOfNeighborChange();
}
@Override
public void onBlockRemoved() {
super.onBlockRemoved();
// Notify any neighboring switches that we exist so they know to register themselves with us
((RailcraftTileEntity) tileEntity).notifyBlocksOfNeighborChange();
}
protected void determineTrackMeta() {
int x = tileEntity.xCoord;
int y = tileEntity.yCoord;
int z = tileEntity.zCoord;
int meta = tileEntity.getBlockMetadata();
if (TrackTools.isRailBlockAt(getWorld(), x + 1, y, z) && TrackTools.isRailBlockAt(getWorld(), x - 1, y, z)) {
if (meta != EnumTrackMeta.EAST_WEST.ordinal())
getWorld().setBlockMetadataWithNotify(x, y, z, EnumTrackMeta.EAST_WEST.ordinal(), 3);
} else if (TrackTools.isRailBlockAt(getWorld(), x, y, z + 1) && TrackTools.isRailBlockAt(getWorld(), x, y, z - 1)) {
if (meta != EnumTrackMeta.NORTH_SOUTH.ordinal())
getWorld().setBlockMetadataWithNotify(x, y, z, EnumTrackMeta.NORTH_SOUTH.ordinal(), 3);
} else if (meta != EnumTrackMeta.NORTH_SOUTH.ordinal())
getWorld().setBlockMetadataWithNotify(x, y, z, EnumTrackMeta.NORTH_SOUTH.ordinal(), 3);
}
protected void determineMirror() {
int x = tileEntity.xCoord;
int y = tileEntity.yCoord;
int z = tileEntity.zCoord;
int meta = tileEntity.getBlockMetadata();
boolean prevValue = isMirrored();
if (meta == EnumTrackMeta.NORTH_SOUTH.ordinal()) {
int ii = x;
if (TrackTools.isRailBlockAt(getWorld(), x - 1, y, z)) {
ii--;
mirrored = true; // West
} else {
ii++;
mirrored = false; // East
}
if (TrackTools.isRailBlockAt(getWorld(), ii, y, z)) {
int otherMeta = getWorld().getBlockMetadata(ii, y, z);
if (otherMeta == EnumTrackMeta.NORTH_SOUTH.ordinal())
getWorld().setBlockMetadataWithNotify(ii, y, z, EnumTrackMeta.EAST_WEST.ordinal(), 3);
}
} else if (meta == EnumTrackMeta.EAST_WEST.ordinal())
mirrored = TrackTools.isRailBlockAt(getWorld(), x, y, z - 1);
if (prevValue != isMirrored())
sendUpdateToClient();
}
@Override
public void onNeighborBlockChange(Block block) {
if (Game.isHost(getWorld())) {
determineTrackMeta();
determineMirror();
}
super.onNeighborBlockChange(block);
}
private void writeCartsToNBT(String key, Set<UUID> carts, NBTTagCompound data) {
data.setByte(key + "Size", (byte) carts.size());
int i = 0;
for (UUID uuid : carts)
MiscTools.writeUUID(data, key + i++, uuid);
}
private Set<UUID> readCartsFromNBT(String key, NBTTagCompound data) {
Set<UUID> cartUUIDs = new HashSet<UUID>();
String sizeKey = key + "Size";
if (data.hasKey(sizeKey)) {
byte size = data.getByte(sizeKey);
for (int i = 0; i < size; i++) {
UUID id = MiscTools.readUUID(data, key + i);
if (id != null)
cartUUIDs.add(id);
}
}
return cartUUIDs;
}
@Override
public void writeToNBT(NBTTagCompound data) {
super.writeToNBT(data);
data.setBoolean("Direction", mirrored);
data.setBoolean("Switched", shouldSwitch);
data.setByte("sprung", sprung);
data.setByte("locked", locked);
writeCartsToNBT("springingCarts", springingCarts, data);
writeCartsToNBT("lockingCarts", lockingCarts, data);
writeCartsToNBT("decidingCarts", lockingCarts, data);
MiscTools.writeUUID(data, "currentCart", currentCart);
}
@Override
public void readFromNBT(NBTTagCompound data) {
super.readFromNBT(data);
mirrored = data.getBoolean("Direction");
shouldSwitch = data.getBoolean("Switched");
sprung = data.getByte("sprung");
locked = data.getByte("locked");
springingCarts = readCartsFromNBT("springingCarts", data);
lockingCarts = readCartsFromNBT("lockingCarts", data);
decidingCarts = readCartsFromNBT("decidingCarts", data);
currentCart = MiscTools.readUUID(data, "currentCart");
}
@Override
public void writePacketData(DataOutputStream data) throws IOException {
super.writePacketData(data);
data.writeBoolean(mirrored);
data.writeBoolean(isVisuallySwitched());
}
@Override
public void readPacketData(DataInputStream data) throws IOException {
super.readPacketData(data);
boolean changed = false;
boolean m = data.readBoolean();
if (m != mirrored) {
mirrored = m;
changed = true;
}
boolean cs = data.readBoolean();
if (cs != clientSwitched) {
clientSwitched = cs;
changed = true;
}
if (changed) {
switchDevice = getSwitchDevice();
if (switchDevice != null)
switchDevice.updateArrows();
markBlockNeedsUpdate();
}
}
private void updateSet(Set<UUID> setToUpdate, List<UUID> potentialUpdates, Set<UUID> reject1, Set<UUID> reject2) {
for (UUID cartUUID : potentialUpdates) {
reject1.remove(cartUUID);
reject2.remove(cartUUID);
setToUpdate.add(cartUUID);
}
}
@Override
public void updateEntity() {
super.updateEntity();
boolean wasSwitched = isVisuallySwitched();
if (locked > 0)
locked--;
if (sprung > 0)
sprung--;
if (locked == 0 && sprung == 0) {
lockingCarts.clear(); // Clear out our sets so we don't keep
springingCarts.clear(); // these carts forever
decidingCarts.clear();
currentCart = null;
}
// updating carts we just found in appropriate sets
// this keeps exiting carts from getting mixed up with entering carts
updateSet(lockingCarts, getCartsAtLockEntrance(), springingCarts, decidingCarts);
updateSet(springingCarts, getCartsAtSpringEntrance(), lockingCarts, decidingCarts);
updateSet(decidingCarts, getCartsAtDecisionEntrance(), lockingCarts, springingCarts);
// We only set sprung/locked when a cart enters our track, this is
// mainly for visual purposes as the subclass's getBasicRailMetadata()
// determines which direction the carts actually take.
List<UUID> cartsOnTrack = CartUtils.getMinecartUUIDsAt(
getWorld(), tileEntity.xCoord, tileEntity.yCoord,
tileEntity.zCoord, 0.3f);
EntityMinecart bestCart = getBestCartForVisualState(cartsOnTrack);
// We must ask the switch every tick so we can update shouldSwitch properly
switchDevice = getSwitchDevice();
if (switchDevice == null) {
shouldSwitch = false;
} else {
shouldSwitch = switchDevice.shouldSwitch(this, bestCart);
}
// Only allow cartsOnTrack to actually spring or lock the track
if (bestCart != null && cartsOnTrack.contains(bestCart.getPersistentID())) {
if (shouldSwitchForCart(bestCart)) {
springTrack(bestCart.getPersistentID());
} else {
lockTrack(bestCart.getPersistentID());
}
}
if (isVisuallySwitched() != wasSwitched) {
if (switchDevice != null) {
switchDevice.onSwitch(isVisuallySwitched());
}
sendUpdateToClient();
}
}
private double crudeDistance(EntityMinecart cart) {
double cx = getX() + .5; // Why not calc this outside and cache it?
double cz = getZ() + .5; // b/c this is a rare occurance that we need to calc this
return Math.abs(cart.posX - cx) + Math.abs(cart.posZ - cz); // not the real distance function but enough for us
}
// To render the state of the track most accurately, we choose the "best" cart from our set of
// carts based on distance.
private EntityMinecart getBestCartForVisualState(List<UUID> cartsOnTrack) {
UUID cartUUID = null;
if (!cartsOnTrack.isEmpty()) {
cartUUID = cartsOnTrack.get(0);
return LinkageManager.instance().getCartFromUUID(cartUUID);
} else {
EntityMinecart closestCart = null;
ArrayList<UUID> allCarts = new ArrayList<UUID>();
allCarts.addAll(lockingCarts);
allCarts.addAll(springingCarts);
allCarts.addAll(decidingCarts);
for (UUID testCartUUID : allCarts) {
if (closestCart == null) {
closestCart = LinkageManager.instance().getCartFromUUID(testCartUUID);
} else {
double closestDist = crudeDistance(closestCart);
EntityMinecart testCart = LinkageManager.instance().getCartFromUUID(testCartUUID);
if (testCart != null) {
double testDist = crudeDistance(testCart);
if (testDist < closestDist)
closestCart = testCart;
}
}
}
return closestCart;
}
}
protected abstract List<UUID> getCartsAtLockEntrance();
protected abstract List<UUID> getCartsAtSpringEntrance();
protected abstract List<UUID> getCartsAtDecisionEntrance();
public abstract ForgeDirection getActuatorLocation();
public abstract ArrowDirection getRedSignDirection();
public abstract ArrowDirection getWhiteSignDirection();
public ISwitchDevice getSwitchDevice() {
TileEntity entity = ((RailcraftTileEntity) this.tileEntity).getTileCache().getTileOnSide(getActuatorLocation());
if (entity instanceof ISwitchDevice) {
return (ISwitchDevice) entity;
}
return null;
}
}