package com.flansmod.common.guns.raytracing; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.flansmod.common.FlansMod; import com.flansmod.common.PlayerData; import com.flansmod.common.PlayerHandler; import com.flansmod.common.driveables.EntityDriveable; import com.flansmod.common.driveables.EnumDriveablePart; import com.flansmod.common.guns.AttachmentType; import com.flansmod.common.guns.EntityAAGun; import com.flansmod.common.guns.EntityGrenade; import com.flansmod.common.guns.GunType; import com.flansmod.common.guns.ItemGun; import com.flansmod.common.teams.Team; import com.flansmod.common.vector.Vector3f; import io.netty.buffer.ByteBuf; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.server.MinecraftServer; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumFacing; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.relauncher.Side; public class FlansModRaytracer { public static List<BulletHit> Raytrace(World world, Entity playerToIgnore, boolean canHitSelf, Entity entityToIgnore, Vector3f origin, Vector3f motion, int pingOfShooter) { //Create a list for all bullet hits ArrayList<BulletHit> hits = new ArrayList<BulletHit>(); float speed = motion.length(); //Iterate over all entities for(int i = 0; i < world.loadedEntityList.size(); i++) { Object obj = world.loadedEntityList.get(i); boolean shouldDoNormalHitDetect = true; //Get driveables if(obj instanceof EntityDriveable) { EntityDriveable driveable = (EntityDriveable)obj; shouldDoNormalHitDetect = false; if(driveable.isDead() || driveable.isPartOfThis(playerToIgnore)) continue; //If this bullet is within the driveable's detection range if(driveable.getDistanceSq(origin.x, origin.y, origin.z) <= (driveable.getDriveableType().bulletDetectionRadius + speed) * (driveable.getDriveableType().bulletDetectionRadius + speed)) { //Raytrace the bullet ArrayList<BulletHit> driveableHits = driveable.attackFromBullet(origin, motion); hits.addAll(driveableHits); } } //Get players else if(obj instanceof EntityPlayer) { EntityPlayer player = (EntityPlayer)obj; PlayerData data = PlayerHandler.getPlayerData(player); shouldDoNormalHitDetect = false; if(data != null) { if(player.isDead || data.team == Team.spectators) { continue; } if(player == playerToIgnore && !canHitSelf) continue; int snapshotToTry = pingOfShooter / 50; if(snapshotToTry >= data.snapshots.length) snapshotToTry = data.snapshots.length - 1; PlayerSnapshot snapshot = data.snapshots[snapshotToTry]; if(snapshot == null) snapshot = data.snapshots[0]; //DEBUG //snapshot = new PlayerSnapshot(player); //Check one last time for a null snapshot. If this is the case, fall back to normal hit detection if(snapshot == null) shouldDoNormalHitDetect = true; else { //Raytrace ArrayList<BulletHit> playerHits = snapshot.raytrace(origin, motion); hits.addAll(playerHits); } } } if(shouldDoNormalHitDetect) { Entity entity = (Entity)obj; if(entity != entityToIgnore && entity != playerToIgnore && !entity.isDead && (entity instanceof EntityLivingBase || entity instanceof EntityAAGun || entity instanceof EntityGrenade) && entity.getEntityBoundingBox() != null) { MovingObjectPosition mop = entity.getEntityBoundingBox().calculateIntercept(origin.toVec3(), new Vec3(origin.x + motion.x, origin.y + motion.y, origin.z + motion.z)); if(mop != null) { Vector3f hitPoint = new Vector3f(mop.hitVec.xCoord - origin.x, mop.hitVec.yCoord - origin.y, mop.hitVec.zCoord - origin.z); float hitLambda = 1F; if(motion.x != 0F) hitLambda = hitPoint.x / motion.x; else if(motion.y != 0F) hitLambda = hitPoint.y / motion.y; else if(motion.z != 0F) hitLambda = hitPoint.z / motion.z; if(hitLambda < 0) hitLambda = -hitLambda; hits.add(new EntityHit(entity, hitLambda)); } } } } //Ray trace the bullet by comparing its next position to its current position Vec3 posVec = origin.toVec3(); Vec3 nextPosVec = motion.toVec3().add(posVec); MovingObjectPosition hit = world.rayTraceBlocks(posVec, nextPosVec, false, true, true); posVec = origin.toVec3(); if(hit != null) { //Calculate the lambda value of the intercept Vec3 hitVec = posVec.subtract(hit.hitVec); float lambda = 1; //Try each co-ordinate one at a time. if(motion.x != 0) lambda = (float)(hitVec.xCoord / motion.x); else if(motion.y != 0) lambda = (float)(hitVec.yCoord / motion.y); else if(motion.z != 0) lambda = (float)(hitVec.zCoord / motion.z); if(lambda < 0) lambda = -lambda; hits.add(new BlockHit(hit, lambda)); } //We hit something if(!hits.isEmpty()) { //Sort the hits according to the intercept position Collections.sort(hits); } return hits; } public static Vector3f GetPlayerMuzzlePosition(EntityPlayer player, boolean isOffHand) { PlayerSnapshot snapshot = new PlayerSnapshot(player); PlayerData data = PlayerHandler.getPlayerData(player); ItemStack itemstack = (isOffHand && data != null && data.offHandGunSlot != 0 ) ? player.inventory.getStackInSlot(data.offHandGunSlot - 1) : player.getCurrentEquippedItem(); if(itemstack != null && itemstack.getItem() instanceof ItemGun) { GunType gunType = ((ItemGun)itemstack.getItem()).GetType(); AttachmentType barrelType = gunType.getBarrel(itemstack); return Vector3f.add(new Vector3f(player.posX, player.posY, player.posZ), snapshot.GetMuzzleLocation(gunType, barrelType, isOffHand), null); } return new Vector3f(player.getPositionEyes(0.0f)); } public static abstract class BulletHit implements Comparable<BulletHit> { /** The time along the ray that the intersection happened. Between 0 and 1 */ public float intersectTime; public BulletHit(float f) { intersectTime = f; } @Override public int compareTo(BulletHit other) { if(intersectTime < other.intersectTime) return -1; else if(intersectTime > other.intersectTime) return 1; return 0; } public abstract Entity GetEntity(); } public static class BlockHit extends BulletHit { public MovingObjectPosition raytraceResult; public BlockHit(MovingObjectPosition mop, float f) { super(f); raytraceResult = mop; } @Override public Entity GetEntity() { return null; } } public static class EntityHit extends BulletHit { public Entity entity; public EntityHit(Entity e, float f) { super(f); entity = e; } @Override public Entity GetEntity() { return entity; } } public static class DriveableHit extends BulletHit { public EntityDriveable driveable; public EnumDriveablePart part; public DriveableHit(EntityDriveable d, EnumDriveablePart p, float f) { super(f); part = p; driveable = d; } @Override public Entity GetEntity() { return driveable; } } /** Raytracing will return a load of these objects containing hit data. These will then be compared against each other and against any block hits * The hit that occurs first along the path of the bullet is the one that is acted upon. Unless the bullet has penetration of course */ public static class PlayerBulletHit extends BulletHit { /** The hitbox hit */ public PlayerHitbox hitbox; public PlayerBulletHit(PlayerHitbox box, float f) { super(f); hitbox = box; } @Override public Entity GetEntity() { return hitbox.player; } } private static byte GetClassType(BulletHit hit) { if(hit instanceof BlockHit) return 0; if(hit instanceof EntityHit) return 1; if(hit instanceof DriveableHit) return 2; if(hit instanceof PlayerBulletHit) return 3; return -1; } public static void WriteToBuffer(BulletHit hit, ByteBuf buffer) { buffer.writeByte(GetClassType(hit)); if(hit != null) { buffer.writeFloat(hit.intersectTime); if(hit instanceof BlockHit) { BlockHit blockHit = (BlockHit)hit; buffer.writeByte(blockHit.raytraceResult.sideHit.ordinal()); buffer.writeInt(blockHit.raytraceResult.getBlockPos().getX()); buffer.writeInt(blockHit.raytraceResult.getBlockPos().getY()); buffer.writeInt(blockHit.raytraceResult.getBlockPos().getZ()); } if(hit instanceof EntityHit) { EntityHit entityHit = (EntityHit)hit; buffer.writeInt(entityHit.entity.getEntityId()); } if(hit instanceof DriveableHit) { DriveableHit driveableHit = (DriveableHit)hit; buffer.writeInt(driveableHit.driveable.getEntityId()); buffer.writeByte(driveableHit.part.ordinal()); } if(hit instanceof PlayerBulletHit) { PlayerBulletHit playerBulletHit = (PlayerBulletHit)hit; buffer.writeInt(playerBulletHit.hitbox.player.getEntityId()); buffer.writeByte(playerBulletHit.hitbox.type.ordinal()); } } } public static BulletHit ReadFromBuffer(ByteBuf buffer) { byte type = buffer.readByte(); switch(type) { default: case -1: // No hit { return null; } case 0: // BlockHit { float intersectTime = buffer.readFloat(); EnumFacing facing = EnumFacing.VALUES[buffer.readByte()]; BlockPos blockPos = new BlockPos(buffer.readInt(), buffer.readInt(), buffer.readInt()); return new BlockHit(new MovingObjectPosition(new Vec3(0d, 0d, 0d), facing, blockPos), intersectTime); } case 1: // EntityHit { float intersectTime = buffer.readFloat(); Entity entity = GetEntityByID(buffer.readInt()); return new EntityHit(entity, intersectTime); } case 2: // DriveableHit { float intersectTime = buffer.readFloat(); Entity entity = GetEntityByID(buffer.readInt()); if(entity instanceof EntityDriveable) { return new DriveableHit((EntityDriveable)entity, EnumDriveablePart.values()[buffer.readByte()], intersectTime); } else { FlansMod.log("Entity was not a driveable"); return null; } } case 3: // PlayerBulletHit { float intersectTime = buffer.readFloat(); Entity entity = GetEntityByID(buffer.readInt()); if(entity instanceof EntityPlayer) { EntityPlayer player = (EntityPlayer)entity; PlayerSnapshot snapshot = new PlayerSnapshot(player); return new PlayerBulletHit(snapshot.GetHitbox(EnumHitboxType.values()[buffer.readByte()]), intersectTime); } else { FlansMod.log("Entity was not a player"); return null; } } } } public static Entity GetEntityByID(int id) { if(FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { for(World world : MinecraftServer.getServer().worldServers) { Entity entity = world.getEntityByID(id); if(entity != null) return entity; } } else { return Minecraft.getMinecraft().theWorld.getEntityByID(id); } return null; } }