/* * 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.carts.CartTools; import mods.railcraft.api.core.items.IToolCrowbar; import mods.railcraft.api.events.CartLockdownEvent; import mods.railcraft.api.tracks.ITrackLockdown; import mods.railcraft.api.tracks.ITrackPowered; import mods.railcraft.common.blocks.tracks.locking.BoardingLockingProfile; import mods.railcraft.common.blocks.tracks.locking.HoldingLockingProfile; import mods.railcraft.common.blocks.tracks.locking.LockdownLockingProfile; import mods.railcraft.common.blocks.tracks.locking.LockingProfile; import mods.railcraft.common.carts.LinkageManager; import mods.railcraft.common.carts.Train; import mods.railcraft.common.plugins.forge.ChatPlugin; import mods.railcraft.common.plugins.forge.LocalizationPlugin; import mods.railcraft.common.util.misc.Game; import net.minecraft.entity.item.EntityMinecart; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.IIcon; import net.minecraftforge.common.MinecraftForge; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.List; import java.util.UUID; /** * @author CovertJaguar <http://www.railcraft.info/> */ public class TrackNextGenLocking extends TrackBaseRailcraft implements ITrackLockdown, ITrackPowered { public static double START_BOOST = 0.04; public static double BOOST_FACTOR = 0.06; private LockingProfileType profile = LockingProfileType.LOCKDOWN; private LockingProfile profileInstance = profile.create(this); private EntityMinecart currentCart, prevCart; private Train currentTrain; private UUID uuid; private boolean trainLeaving = false; private boolean redstone = false; private boolean locked = false; private int trainDelay = 0; // Temporary variables to hold loaded data while we restore from NBT private UUID prevCartUUID; private UUID currentCartUUID; private boolean justLoaded = true; @Override public EnumTrack getTrackType() { return EnumTrack.LOCKING; } @Override public IIcon getIcon() { if (!locked) return getIcon(profile.ordinal() * 2); // glowing return getIcon(profile.ordinal() * 2 + 1); // not glowing } public LockingProfileType getProfileType() { return profile; } public void setProfile(LockingProfileType type) { profile = type; profileInstance = profile.create(this); if (tileEntity != null && Game.isHost(getWorld())) sendUpdateToClient(); } @Override public boolean canUpdate() { return true; } /** * We try to calculate all the logic here so we can isolate it on the server side. * Its a bit tricky to determine whether a cart or train is on top of us, but we can do it * if we maintain a record of the last cart that was on us and the next cart that triggers * an onMinecartPass() event. */ @Override public void updateEntity() { if (Game.isHost(getWorld())) { boolean updateClient = false; // flag determines whether we send an update to the client, only update when visible changes occur // At the time we read from NBT, LinkageManager has not been initialized so we cannot // lookup the carts by UUID in readFromNBT(). We must wait until updateEntity(), which // occurs after LinkageManager gets initialized. The justLoaded flag lets us lookup // the carts only after restoring from NBT. if (justLoaded) { prevCart = LinkageManager.instance().getCartFromUUID(prevCartUUID); currentCart = LinkageManager.instance().getCartFromUUID(currentCartUUID); justLoaded = false; updateClient = true; } if (currentCart != null && currentCart.isDead) { releaseCurrentCart(); currentCart = null; updateClient = true; } boolean oldLocked = locked; // simple check to determine if "locked" has changed calculateLocked(); if (oldLocked != locked) updateClient = true; if (locked) { lockCurrentCart(); } else { releaseCurrentCart(); } // Store our last found cart in prevCart if (currentCart != null) prevCart = currentCart; currentCart = null; // reset currentCart so we know if onMinecartPass() actually found one if (updateClient) sendUpdateToClient(); } } @Override public boolean blockActivated(EntityPlayer player) { ItemStack current = player.getCurrentEquippedItem(); if (current != null && current.getItem() instanceof IToolCrowbar) { IToolCrowbar crowbar = (IToolCrowbar) current.getItem(); if (crowbar.canWhack(player, current, getX(), getY(), getZ())) { LockingProfileType p; if (player.isSneaking()) p = profile.previous(); else p = profile.next(); crowbar.onWhack(player, current, getX(), getY(), getZ()); if (Game.isHost(getWorld())) setProfile(p); else ChatPlugin.sendLocalizedChat(player, "railcraft.gui.track.mode.change", "\u00A75" + LocalizationPlugin.translate("railcraft.gui.track.locking.mode." + p.tag)); return true; } } return false; } @Override public void onBlockRemoved() { super.onBlockRemoved(); releaseCart(); // Release any carts still holding on } private UUID getUUID() { if (uuid == null) uuid = UUID.randomUUID(); return uuid; } private void lockCurrentCart() { if (currentCart != null) { Train train = Train.getTrain(currentCart); if (currentTrain != train && currentTrain != null) currentTrain.removeLockingTrack(getUUID()); currentTrain = train; currentTrain.addLockingTrack(getUUID()); MinecraftForge.EVENT_BUS.post(new CartLockdownEvent.Lock(currentCart, getX(), getY(), getZ())); profileInstance.onLock(currentCart); currentCart.motionX = 0.0D; currentCart.motionZ = 0.0D; int meta = tileEntity.getBlockMetadata(); if (meta == 0 || meta == 4 || meta == 5) currentCart.posZ = tileEntity.zCoord + 0.5D; else currentCart.posX = tileEntity.xCoord + 0.5D; } } @Override public void onMinecartPass(EntityMinecart cart) { currentCart = cart; } private void releaseCurrentCart() { if (currentTrain != null) currentTrain.removeLockingTrack(getUUID()); if (currentCart != null) { MinecraftForge.EVENT_BUS.post(new CartLockdownEvent.Release(currentCart, getX(), getY(), getZ())); profileInstance.onRelease(currentCart); } } @Override public void releaseCart() { trainLeaving = true; } @Override public boolean isCartLockedDown(EntityMinecart cart) { return locked && prevCart == cart; } /** * Determines if the current train is the same train or cart (depending on track type) * as the train or cart in previous ticks. The <code>trainDelay</code> is needed because there are * gaps between carts in a train where onMinecartPass() doesn't get called even though * the train is still passing over us. * * @return whether the current cart or train (depending on LockType) is the same as previous cart or trains */ private boolean isSameTrainOrCart() { if (profile.lockType == LockType.TRAIN) { if (currentCart != null) { if (Train.areInSameTrain(currentCart, prevCart)) trainDelay = TrackTools.TRAIN_LOCKDOWN_DELAY; // reset trainDelay else trainDelay = 0; // We've encountered a new train, force the delay to 0 so we return false } else if (trainLeaving) { List<EntityMinecart> carts = CartTools.getMinecartsAt(getWorld(), getX(), getY(), getZ(), 0.0f); for (EntityMinecart cart : carts) { if (Train.areInSameTrain(cart, prevCart)) { trainDelay = TrackTools.TRAIN_LOCKDOWN_DELAY; break; } } } if (trainDelay > 0) trainDelay--; else prevCart = null; return trainDelay > 0; } else { return currentCart != null && (profile.lockType == LockType.CART && currentCart == prevCart); } } /** * The heart of the logic for this class is done here. If you understand what's going * on here, the rest will make much more sense to you. Basically, we're trying to determine * whether this track should be trying to lock the current or next cart that passes over it. * First of all we must realize that we only have 2 inputs: 1) whether a train/cart * is passing over us and 2) whether our track is receiving a redstone signal. If we try to * create a truth table with 2 boolean inputs to calculate "locked", we find that we can't quite * express the correct value for "locked". When we analyze the situation, we notice that when * a train is passing over the track, we need both the redstone to be off and the last cart to be * off the track in order to lock the track. However after the train has already left the track, * then we want the track to be "locked" when the redstone is off, regardless of whether a * new or old cart starts moving onto the track. In the end, what we're really after is * having 2 truth tables and a way to decide which of the 2 tables to use. To do this, we * use the boolean <code>trainLeaving</code> to indicate which table to use. As the name * implies, <code>trainLeaving</code> indicates whether the train or cart is in the process * of leaving the track. */ private void calculateLocked() { boolean isSameCart = isSameTrainOrCart(); if (trainLeaving) { if (!isSameCart && !redstone) { // When the train is in the process of leaving, we know that the "trainLeaving" state ends // when both the carts and redstone signal are false trainLeaving = false; } locked = !(isSameCart || redstone); } else { if (isSameCart && redstone) { // When we get both signals we know a train is leaving, so we set the state as so trainLeaving = true; } locked = !redstone; } } @Override public boolean isPowered() { return redstone; // Why call this "redstone" instead of "powered"? Powered gives the impression that // the track will accelerate or unlock a cart and for this track we cannot assume that // having a redstone signal applied is equivalent to being powered. Based on the usage // of the isPowered()/setPowered() calls, it seems that they more accurately describe // whether a redstone signal is being applied. I have refrained from using "powered" in the code // in the hopes that the logic is easier to understand. } @Override public void setPowered(boolean powered) { this.redstone = powered; } /** * A utility method for writing out UUID's to NBT */ private void setUUID(UUID id, String key, NBTTagCompound data) { if (id == null) { data.setLong(key + "High", 0); data.setLong(key + "Low", 0); } else { data.setLong(key + "High", id.getMostSignificantBits()); data.setLong(key + "Low", id.getLeastSignificantBits()); } } /** * A utility method for reading in UUID's from NBT */ private UUID readUUID(String key, NBTTagCompound data) { if (data.hasKey(key + "High")) return new UUID(data.getLong(key + "High"), data.getLong(key + "Low")); return null; } @Override public void writeToNBT(NBTTagCompound data) { super.writeToNBT(data); data.setByte("profile", (byte) profile.ordinal()); profileInstance.writeToNBT(data); data.setBoolean("powered", redstone); data.setBoolean("locked", locked); data.setBoolean("trainLeaving", trainLeaving); data.setInteger("trainDelay", trainDelay); if (prevCart != null) setUUID(prevCart.getPersistentID(), "prevCart", data); if (currentCart != null) setUUID(currentCart.getPersistentID(), "currentCart", data); setUUID(getUUID(), "uuid", data); } @Override public void readFromNBT(NBTTagCompound data) { super.readFromNBT(data); if (data.hasKey("profile")) profile = LockingProfileType.fromOrdinal(data.getByte("profile")); profileInstance = profile.create(this); profileInstance.readFromNBT(data); redstone = data.getBoolean("powered"); if (data.hasKey("locked")) locked = data.getBoolean("locked"); if (data.hasKey("trainLeaving")) trainLeaving = data.getBoolean("trainLeaving"); if (data.hasKey("trainDelay")) trainDelay = data.getInteger("trainDelay"); prevCartUUID = readUUID("prevCart", data); currentCartUUID = readUUID("currentCart", data); uuid = readUUID("uuid", data); justLoaded = true; // This signals updateEntity() to dereference the cart UUID's we read in here } @Override public void writePacketData(DataOutputStream data) throws IOException { super.writePacketData(data); data.writeByte(profile.ordinal()); data.writeBoolean(redstone); data.writeBoolean(locked); profileInstance.writePacketData(data); } @Override public void readPacketData(DataInputStream data) throws IOException { super.readPacketData(data); LockingProfileType p = LockingProfileType.fromOrdinal(data.readByte()); if (profile != p) { profile = p; profileInstance = p.create(this); } redstone = data.readBoolean(); locked = data.readBoolean(); profileInstance.readPacketData(data); markBlockNeedsUpdate(); } public enum LockType { CART, TRAIN } public enum LockingProfileType { LOCKDOWN(LockdownLockingProfile.class, LockType.CART, "lockdown"), LOCKDOWN_TRAIN(LockdownLockingProfile.class, LockType.TRAIN, "lockdown.train"), HOLDING(HoldingLockingProfile.class, LockType.CART, "holding"), HOLDING_TRAIN(HoldingLockingProfile.class, LockType.TRAIN, "holding.train"), BOARDING_A(BoardingLockingProfile.class, LockType.CART, "boarding"), BOARDING_B(BoardingLockingProfile.class, LockType.CART, "boarding"), BOARDING_A_TRAIN(BoardingLockingProfile.class, LockType.TRAIN, "boarding.train"), BOARDING_B_TRAIN(BoardingLockingProfile.class, LockType.TRAIN, "boarding.train"); public static final LockingProfileType[] VALUES = values(); public final LockType lockType; public final String tag; private final Class<? extends LockingProfile> profileClass; private LockingProfileType(Class<? extends LockingProfile> profileClass, LockType lockType, String tag) { this.profileClass = profileClass; this.lockType = lockType; this.tag = tag; } public static LockingProfileType fromOrdinal(int ordinal) { if (ordinal < 0 || ordinal >= VALUES.length) return LOCKDOWN; return VALUES[ordinal]; } public LockingProfileType next() { LockingProfileType next = VALUES[(ordinal() + 1) % VALUES.length]; return next; } public LockingProfileType previous() { LockingProfileType next = VALUES[(ordinal() + VALUES.length - 1) % VALUES.length]; return next; } public LockingProfile create(TrackNextGenLocking track) { try { Constructor<? extends LockingProfile> con = profileClass.getConstructor(TrackNextGenLocking.class); return con.newInstance(track); } catch (Throwable ex) { Game.logThrowable("Failed to create Locking Profile!", 10, ex); throw new RuntimeException(ex); } } } }