/*
* 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.carts;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import mods.railcraft.api.carts.ILinkableCart;
import mods.railcraft.api.core.items.IToolCrowbar;
import mods.railcraft.api.tracks.RailTools;
import mods.railcraft.common.modules.ModuleManager;
import mods.railcraft.common.modules.ModuleManager.Module;
import mods.railcraft.common.util.misc.Vec2D;
import net.minecraft.entity.item.EntityMinecart;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.event.entity.EntityEvent;
import net.minecraftforge.event.entity.minecart.MinecartInteractEvent;
import net.minecraftforge.event.entity.minecart.MinecartUpdateEvent;
public class LinkageHandler {
public static final String LINK_A_TIMER = "linkA_timer";
public static final String LINK_B_TIMER = "linkB_timer";
public static final double LINK_DRAG = 0.95;
public static final float MAX_DISTANCE = 8f;
private static final float STIFFNESS = 0.7f;
private static final float HS_STIFFNESS = 0.7f;
// private static final float TRANSFER = 0.15f;
private static final float DAMPING = 0.4f;
private static final float HS_DAMPING = 0.3f;
private static final float FORCE_LIMITER = 6f;
private static final int TICK_HISTORY = 200;
private static LinkageHandler instance;
// private static Map<EntityMinecart, CircularVec3Queue> history = new MapMaker().weakKeys().makeMap();
private LinkageHandler() {
}
public static LinkageHandler getInstance() {
if (instance == null)
instance = new LinkageHandler();
return instance;
}
/**
* Returns the optimal distance between two linked carts that the
* LinkageHandler will attempt to maintain at all times.
*
* @param cart1 EntityMinecart
* @param cart2 EntityMinecart
* @return The optimal distance
*/
private float getOptimalDistance(EntityMinecart cart1, EntityMinecart cart2) {
float dist = 0;
if (cart1 instanceof ILinkableCart)
dist += ((ILinkableCart) cart1).getOptimalDistance(cart2);
else
dist += LinkageManager.OPTIMAL_DISTANCE;
if (cart2 instanceof ILinkableCart)
dist += ((ILinkableCart) cart2).getOptimalDistance(cart1);
else
dist += LinkageManager.OPTIMAL_DISTANCE;
return dist;
}
private boolean canCartBeAdjustedBy(EntityMinecart cart1, EntityMinecart cart2) {
if (cart1 == cart2)
return false;
if (cart1 instanceof ILinkableCart && !((ILinkableCart) cart1).canBeAdjusted(cart2))
return false;
return !RailTools.isCartLockedDown(cart1);
}
/**
* This is where the physics magic actually gets performed. It uses Spring
* Forces and Damping Forces to maintain a fixed distance between carts.
*
* @param cart1 EntityMinecart
* @param cart2 EntityMinecart
* @param link char
*/
protected void adjustVelocity(EntityMinecart cart1, EntityMinecart cart2, char link) {
String timer = LINK_A_TIMER;
if (link == 'B')
timer = LINK_B_TIMER;
if (cart1.worldObj.provider.dimensionId != cart2.worldObj.provider.dimensionId) {
short count = cart1.getEntityData().getShort(timer);
count++;
if (count > 200) {
LinkageManager.instance().breakLink(cart1, cart2);
LinkageManager.printDebug("Reason For Broken Link: Carts in different dimensions.");
}
cart1.getEntityData().setShort(timer, count);
return;
}
cart1.getEntityData().setShort(timer, (short) 0);
double dist = cart1.getDistanceToEntity(cart2);
if (dist > MAX_DISTANCE) {
LinkageManager.instance().breakLink(cart1, cart2);
LinkageManager.printDebug("Reason For Broken Link: Max distance exceeded.");
return;
}
boolean adj1 = canCartBeAdjustedBy(cart1, cart2);
boolean adj2 = canCartBeAdjustedBy(cart2, cart1);
Vec2D cart1Pos = new Vec2D(cart1.posX, cart1.posZ);
Vec2D cart2Pos = new Vec2D(cart2.posX, cart2.posZ);
Vec2D unit = Vec2D.subtract(cart2Pos, cart1Pos);
unit.normalize();
// Energy transfer
// double transX = TRANSFER * (cart2.motionX - cart1.motionX);
// double transZ = TRANSFER * (cart2.motionZ - cart1.motionZ);
//
// transX = limitForce(transX);
// transZ = limitForce(transZ);
//
// if(adj1) {
// cart1.motionX += transX;
// cart1.motionZ += transZ;
// }
//
// if(adj2) {
// cart2.motionX -= transX;
// cart2.motionZ -= transZ;
// }
// Spring force
float optDist = getOptimalDistance(cart1, cart2);
double stretch = dist - optDist;
// if(Math.abs(stretch) > 0.5) {
// stretch *= 2;
// }
boolean highSpeed = cart1.getEntityData().getBoolean("HighSpeed");
double stiffness = highSpeed ? HS_STIFFNESS : STIFFNESS;
double springX = stiffness * stretch * unit.getX();
double springZ = stiffness * stretch * unit.getY();
springX = limitForce(springX);
springZ = limitForce(springZ);
if (adj1) {
cart1.motionX += springX;
cart1.motionZ += springZ;
}
if (adj2) {
cart2.motionX -= springX;
cart2.motionZ -= springZ;
}
// Damping
Vec2D cart1Vel = new Vec2D(cart1.motionX, cart1.motionZ);
Vec2D cart2Vel = new Vec2D(cart2.motionX, cart2.motionZ);
double dot = Vec2D.subtract(cart2Vel, cart1Vel).dotProduct(unit);
double damping = highSpeed ? HS_DAMPING : DAMPING;
double dampX = damping * dot * unit.getX();
double dampZ = damping * dot * unit.getY();
dampX = limitForce(dampX);
dampZ = limitForce(dampZ);
if (adj1) {
cart1.motionX += dampX;
cart1.motionZ += dampZ;
}
if (adj2) {
cart2.motionX -= dampX;
cart2.motionZ -= dampZ;
}
}
private double limitForce(double force) {
return Math.copySign(Math.min(Math.abs(force), FORCE_LIMITER), force);
}
/**
* This function inspects the links and determines if any physics
* adjustments need to be made.
*
* @param cart EntityMinecart
* @param lm LinkageManager
*/
private void adjustCart(EntityMinecart cart, LinkageManager lm) {
int launched = cart.getEntityData().getInteger("Launched");
if (launched > 0)
return;
if (isOnElevator(cart))
return;
boolean linked = false;
EntityMinecart link_A = lm.getLinkedCartA(cart);
if (link_A != null) {
// sanity check to ensure links are consistent
if (!Train.areInSameTrain(cart, link_A)) {
lm.breakLink(cart, link_A);
lm.createLink(cart, link_A);
return;
}
launched = link_A.getEntityData().getInteger("Launched");
if (launched <= 0 && !isOnElevator(link_A)) {
linked = true;
adjustVelocity(cart, link_A, 'A');
// adjustCartFromHistory(cart, link_A);
}
}
EntityMinecart link_B = lm.getLinkedCartB(cart);
if (link_B != null) {
// sanity check to ensure links are consistent
if (!Train.areInSameTrain(cart, link_B)) {
lm.breakLink(cart, link_B);
lm.createLink(cart, link_B);
return;
}
launched = link_B.getEntityData().getInteger("Launched");
if (launched <= 0 && !isOnElevator(link_B)) {
linked = true;
adjustVelocity(cart, link_B, 'B');
// adjustCartFromHistory(cart, link_B);
}
}
if (linked && ModuleManager.isModuleLoaded(Module.LOCOMOTIVES)) {
cart.motionX *= LINK_DRAG;
cart.motionZ *= LINK_DRAG;
}
if ((link_A == null && link_B != null) || (link_A != null && link_B == null)) {
Train.getTrain(cart).refreshMaxSpeed();
} else if (link_A == null)
Train.getTrain(cart).setMaxSpeed(1.2f);
}
// /**
// * Determines whether a cart is leading another.
// *
// * @param leader EntityMinecart
// * @param follower EntityMinecart
// * @return true if leader is leading follower
// */
// private boolean isCartLeading(EntityMinecart leader, EntityMinecart follower) {
// return true; // TODO: magic
// }
// /**
// * Adjust the current cart's position based on the linked cart its following
// * so that it follows the same path at a set distance.
// *
// * @param current EntityMinecart
// * @param linked EntityMinecart
// */
// private void adjustCartFromHistory(EntityMinecart current, EntityMinecart linked) {
// // If we are leading, we don't want to adjust anything
// if (isCartLeading(current, linked))
// return;
//
// CircularVec3Queue leaderHistory = history.get(linked);
//
// // Optimal distance is how far apart the carts should be
// double optimalDist = getOptimalDistance(current, linked);
// optimalDist *= optimalDist;
//
// double currentDistance = linked.getDistanceSqToEntity(current);
//
// // Search the history for the point closest to the optimal distance.
// // There may be some issues with it chosing the wrong side of the cart.
// // Probably needs some kind of logic to compare the distance from the
// // new position to the current position and determine if its a valid position.
// Vec3 closestPoint = null;
// Vec3 linkedVec = Vec3.createVectorHelper(linked.posX, linked.posY, linked.posZ);
// double distance = Math.abs(optimalDist - currentDistance);
// for (Vec3 pos : leaderHistory) {
// double historyDistance = linkedVec.squareDistanceTo(pos);
// double diff = Math.abs(optimalDist - historyDistance);
// if (diff < distance) {
// closestPoint = pos;
// distance = diff;
// }
// }
//
// // If we found a point closer to our desired distance, move us there
// if (closestPoint != null)
// current.setPosition(closestPoint.xCoord, closestPoint.yCoord, closestPoint.zCoord);
// }
// /**
// * Saved the position history of the cart every tick in a Circular Buffer.
// *
// * @param cart EntityMinecart
// */
// private void savePosition(EntityMinecart cart) {
// CircularVec3Queue myHistory = history.get(cart);
// if (myHistory == null) {
// myHistory = new CircularVec3Queue(TICK_HISTORY);
// history.put(cart, myHistory);
// }
// myHistory.add(cart.posX, cart.posY, cart.posZ);
// }
/**
* This is our entry point, its triggered once per tick per cart.
*
* @param event MinecartUpdateEvent
*/
@SubscribeEvent
public void onMinecartUpdate(MinecartUpdateEvent event) {
EntityMinecart cart = event.minecart;
LinkageManager lm = LinkageManager.instance();
if (cart.isDead) {
lm.removeLinkageId(cart);
return;
}
// Causes a link id cache store
lm.getLinkageId(cart);
// Physics done here
adjustCart(cart, lm);
// savePosition(cart);
}
@SubscribeEvent
public void onMinecartInteract(MinecartInteractEvent event) {
EntityPlayer player = event.player;
if (player.getCurrentEquippedItem() != null && player.getCurrentEquippedItem().getItem() instanceof IToolCrowbar)
event.setCanceled(true);
}
private boolean isOnElevator(EntityMinecart cart) {
int elevator = cart.getEntityData().getByte("elevator");
return elevator > 0;
}
@SubscribeEvent
public void canMinecartTick(EntityEvent.CanUpdate event) {
if (event.entity instanceof EntityMinecart) {
EntityMinecart cart = (EntityMinecart) event.entity;
Train train = Train.getTrain(cart);
for (EntityCartAnchor anchor : train.getCarts(EntityCartAnchor.class)) {
if (anchor.hasActiveTicket()) {
event.canUpdate = true;
return;
}
}
}
}
}