package com.replaymod.replay.camera; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.core.utils.Utils; import com.replaymod.core.versions.MCVer; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.Setting; import com.replaymod.replay.events.ReplayChatMessageEvent; import com.replaymod.replaystudio.util.Location; import lombok.Getter; import lombok.Setter; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.AbstractClientPlayer; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.item.EntityItemFrame; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.text.ITextComponent; import net.minecraft.world.World; import net.minecraftforge.client.event.EntityViewRenderEvent; import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; //#if MC>=11300 import net.minecraft.util.math.RayTraceFluidMode; //#else //$$ import net.minecraft.block.material.Material; //$$ import net.minecraft.entity.EntityLivingBase; //#endif //#if MC>=10904 import net.minecraft.inventory.EntityEquipmentSlot; //#if MC>=11200 //#if MC>=11300 import net.minecraft.client.util.RecipeBookClient; //#else //$$ import net.minecraft.stats.RecipeBook; //#endif //#endif import net.minecraft.stats.StatisticsManager; import net.minecraft.util.EnumHand; //#else //$$ import net.minecraft.stats.StatFileWriter; //#endif //#if MC>=10800 import net.minecraft.client.entity.EntityPlayerSP; //#else //$$ import net.minecraft.client.entity.EntityClientPlayerMP; //$$ import net.minecraft.util.Session; //#endif import java.util.Map; import java.util.UUID; import java.util.function.Function; import static com.replaymod.core.versions.MCVer.*; /** * The camera entity used as the main player entity during replay viewing. * During a replay the player should be an instance of this class. * Camera movement is controlled by a separate {@link CameraController}. */ public class CameraEntity //#if MC>=10800 extends EntityPlayerSP //#else //$$ extends EntityClientPlayerMP //#endif { /** * Roll of this camera in degrees. */ public float roll; @Getter @Setter private CameraController cameraController; private long lastControllerUpdate = System.currentTimeMillis(); /** * The entity whose hand was the last one rendered. */ private Entity lastHandRendered = null; /** * The hashCode and equals methods of Entity are not stable. * Therefore we cannot register any event handlers directly in the CameraEntity class and * instead have this inner class. */ private final EventHandler eventHandler = new EventHandler(); //#if MC>=11300 public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBookClient recipeBookClient) { super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBookClient); //#else //#if MC>=11200 //$$ public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBook recipeBook) { //$$ super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBook); //#else //#if MC>=10904 //$$ public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager) { //$$ super(mcIn, worldIn, netHandlerPlayClient, statisticsManager); //#else //#if MC>=10800 //$$ public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatFileWriter statFileWriter) { //$$ super(mcIn, worldIn, netHandlerPlayClient, statFileWriter); //#else //$$ public CameraEntity(Minecraft mcIn, World worldIn, Session session, NetHandlerPlayClient netHandlerPlayClient, StatFileWriter statFileWriter) { //$$ super(mcIn, worldIn, session, netHandlerPlayClient, statFileWriter); //#endif //#endif //#endif //#endif FORGE_BUS.register(eventHandler); FML_BUS.register(eventHandler); if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) { cameraController = ReplayModReplay.instance.createCameraController(this); } else { cameraController = new SpectatorCameraController(this); } } /** * Moves the camera by the specified delta. * @param x Delta in X direction * @param y Delta in Y direction * @param z Delta in Z direction */ public void moveCamera(double x, double y, double z) { setCameraPosition(posX + x, posY + y, posZ + z); } /** * Set the camera position. * @param x X coordinate * @param y Y coordinate * @param z Z coordinate */ public void setCameraPosition(double x, double y, double z) { this.lastTickPosX = this.prevPosX = this.posX = x; this.lastTickPosY = this.prevPosY = this.posY = y; this.lastTickPosZ = this.prevPosZ = this.posZ = z; updateBoundingBox(); } /** * Sets the camera rotation. * @param yaw Yaw in degrees * @param pitch Pitch in degrees * @param roll Roll in degrees */ public void setCameraRotation(float yaw, float pitch, float roll) { this.prevRotationYaw = this.rotationYaw = yaw; this.prevRotationPitch = this.rotationPitch = pitch; this.roll = roll; } /** * Sets the camera position and rotation to that of the specified AdvancedPosition * @param pos The position and rotation to set */ public void setCameraPosRot(Location pos) { setCameraRotation(pos.getYaw(), pos.getPitch(), roll); setCameraPosition(pos.getX(), pos.getY(), pos.getZ()); } /** * Sets the camera position and rotation to that of the specified entity. * @param to The entity whose position to copy */ public void setCameraPosRot(Entity to) { if (to == this) return; //#if MC>=10800 float yOffset = 0; //#else //$$ float yOffset = 1.62f; // Magic value (eye height) from EntityRenderer#orientCamera //#endif prevPosX = to.prevPosX; prevPosY = to.prevPosY + yOffset; prevPosZ = to.prevPosZ; prevRotationYaw = to.prevRotationYaw; prevRotationPitch = to.prevRotationPitch; posX = to.posX; posY = to.posY + yOffset; posZ = to.posZ; rotationYaw = to.rotationYaw; rotationPitch = to.rotationPitch; lastTickPosX = to.lastTickPosX; lastTickPosY = to.lastTickPosY + yOffset; lastTickPosZ = to.lastTickPosZ; updateBoundingBox(); } private void updateBoundingBox() { //#if MC>=10800 //#if MC>=11300 setBoundingBox(new AxisAlignedBB( //#else //$$ setEntityBoundingBox(new AxisAlignedBB( //#endif //#else //$$ this.boundingBox.setBB(AxisAlignedBB.getBoundingBox( //#endif posX - width / 2, posY, posZ - width / 2, posX + width / 2, posY + height, posZ + width / 2)); } @Override //#if MC>=11300 public void tick() { //#else //$$ public void onUpdate() { //#endif //#if MC>=10800 Entity view = getRenderViewEntity(mc); //#else //$$ EntityLivingBase view = getRenderViewEntity(mc); //#endif if (view != null) { // Make sure we're always spectating the right entity // This is important if the spectated player respawns as their // entity is recreated and we have to spectate a new entity UUID spectating = ReplayModReplay.instance.getReplayHandler().getSpectatedUUID(); if (spectating != null && (view.getUniqueID() != spectating || view.world != world) || world.getEntityByID(view.getEntityId()) != view) { if (spectating == null) { // Entity (non-player) died, stop spectating ReplayModReplay.instance.getReplayHandler().spectateEntity(this); return; } view = world.getPlayerEntityByUUID(spectating); if (view != null) { setRenderViewEntity(mc, view); } else { setRenderViewEntity(mc, this); return; } } // Move cmera to their position so when we exit the first person view // we don't jump back to where we entered it if (view != this) { setCameraPosRot(view); } } } @Override public void preparePlayerToSpawn() { // Make sure our world is up-to-date in case of world changes if (mc.world != null) { world = mc.world; } super.preparePlayerToSpawn(); } @Override public void setRotation(float yaw, float pitch) { if (getRenderViewEntity(mc) == this) { // Only update camera rotation when the camera is the view super.setRotation(yaw, pitch); } } @Override public boolean isEntityInsideOpaqueBlock() { return falseUnlessSpectating(Entity::isEntityInsideOpaqueBlock); // Make sure no suffocation overlay is rendered } //#if MC<11300 //$$ @Override //$$ public boolean isInsideOfMaterial(Material materialIn) { //$$ return falseUnlessSpectating(e -> e.isInsideOfMaterial(materialIn)); // Make sure no overlays are rendered //$$ } //#endif //#if MC>=10800 @Override public boolean isInLava() { return falseUnlessSpectating(Entity::isInLava); // Make sure no lava overlay is rendered } //#else //$$ @Override //$$ public boolean handleLavaMovement() { //$$ return falseUnlessSpectating(Entity::handleLavaMovement); // Make sure no lava overlay is rendered //$$ } //#endif @Override public boolean isInWater() { return falseUnlessSpectating(Entity::isInWater); // Make sure no water overlay is rendered } @Override public boolean isBurning() { return falseUnlessSpectating(Entity::isBurning); // Make sure no fire overlay is rendered } private boolean falseUnlessSpectating(Function property) { Entity view = getRenderViewEntity(mc); if (view != null && view != this) { return property.apply(view); } return false; } @Override public boolean canBePushed() { return false; // We are in full control of ourselves } //#if MC>=10800 @Override protected void createRunningParticles() { // We do not produce any particles, we are a camera } //#endif @Override public boolean canBeCollidedWith() { return false; // We are a camera, we cannot collide } //#if MC>=10800 @Override public boolean isSpectator() { return ReplayModReplay.instance.getReplayHandler().isCameraView(); // Make sure we're treated as spectator } //#endif @Override public boolean shouldRenderInPass(int pass) { // Never render the camera // This is necessary to hide the player head in third person mode and to not // cause any unwanted shadows when rendering with shaders. return false; } @Override public boolean isInvisible() { Entity view = getRenderViewEntity(mc); if (view != this) { return view.isInvisible(); } return super.isInvisible(); } @Override public ResourceLocation getLocationSkin() { Entity view = getRenderViewEntity(mc); if (view != this && view instanceof EntityPlayer) { return Utils.getResourceLocationForPlayerUUID(view.getUniqueID()); } return super.getLocationSkin(); } //#if MC>=10800 @Override public String getSkinType() { Entity view = mc.getRenderViewEntity(); if (view != this && view instanceof AbstractClientPlayer) { return ((AbstractClientPlayer) view).getSkinType(); } return super.getSkinType(); } //#endif @Override public float getSwingProgress(float renderPartialTicks) { Entity view = getRenderViewEntity(mc); if (view != this && view instanceof EntityPlayer) { return ((EntityPlayer) view).getSwingProgress(renderPartialTicks); } return 0; } //#if MC>=10904 @Override public float getCooldownPeriod() { Entity view = mc.getRenderViewEntity(); if (view != this && view instanceof EntityPlayer) { return ((EntityPlayer) view).getCooldownPeriod(); } return 1; } @Override public float getCooledAttackStrength(float adjustTicks) { Entity view = mc.getRenderViewEntity(); if (view != this && view instanceof EntityPlayer) { return ((EntityPlayer) view).getCooledAttackStrength(adjustTicks); } // Default to 1 as to not render the cooldown indicator (renders for < 1) return 1; } @Override public EnumHand getActiveHand() { Entity view = mc.getRenderViewEntity(); if (view != this && view instanceof EntityPlayer) { return ((EntityPlayer) view).getActiveHand(); } return super.getActiveHand(); } @Override public boolean isHandActive() { Entity view = mc.getRenderViewEntity(); if (view != this && view instanceof EntityPlayer) { return ((EntityPlayer) view).isHandActive(); } return super.isHandActive(); } //#if MC>=11300 @Override public RayTraceResult rayTrace(double blockReachDistance, float partialTicks, RayTraceFluidMode p_174822_4_) { RayTraceResult pos = super.rayTrace(blockReachDistance, partialTicks, p_174822_4_); // Make sure we can never look at blocks (-> no outline) if(pos != null && pos.type == RayTraceResult.Type.BLOCK) { pos.type = RayTraceResult.Type.MISS; } return pos; } //#else //$$ @Override //$$ public RayTraceResult rayTrace(double p_174822_1_, float p_174822_3_) { //$$ RayTraceResult pos = super.rayTrace(p_174822_1_, 1f); //$$ //$$ // Make sure we can never look at blocks (-> no outline) //$$ if(pos != null && pos.typeOfHit == RayTraceResult.Type.BLOCK) { //$$ pos.typeOfHit = RayTraceResult.Type.MISS; //$$ } //$$ //$$ return pos; //$$ } //#endif //#else //$$ @Override //$$ public MovingObjectPosition rayTrace(double p_174822_1_, float p_174822_3_) { //$$ MovingObjectPosition pos = super.rayTrace(p_174822_1_, 1f); //$$ //$$ // Make sure we can never look at blocks (-> no outline) //$$ if(pos != null && pos.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { //$$ pos.typeOfHit = MovingObjectPosition.MovingObjectType.MISS; //$$ } //$$ //$$ return pos; //$$ } //#endif //#if MC<11300 //$$ @Override //$$ public void openGui(Object mod, int modGuiId, World world, int x, int y, int z) { //$$ // Do not open any block GUIs for the camera entities //$$ // Note: Vanilla GUIs are filtered out on a packet level, this only applies to mod GUIs //$$ } //#endif @Override //#if MC>=11300 public void remove() { super.remove(); //#else //$$ public void setDead() { //$$ super.setDead(); //#endif FORGE_BUS.unregister(eventHandler); FML_BUS.unregister(eventHandler); } private void update() { long now = System.currentTimeMillis(); long timePassed = now - lastControllerUpdate; cameraController.update(timePassed / 50f); lastControllerUpdate = now; if (mc.gameSettings.keyBindAttack.isPressed() || mc.gameSettings.keyBindUseItem.isPressed()) { if (canSpectate(mc.pointedEntity)) { ReplayModReplay.instance.getReplayHandler().spectateEntity( //#if MC<=10710 //$$ (EntityLivingBase) //#endif mc.pointedEntity); // Make sure we don't exit right away mc.gameSettings.keyBindSneak.pressTime = 0; } } Map keyBindings = ReplayMod.instance.getKeyBindingRegistry().getKeyBindings(); if (keyBindings.get("replaymod.input.rollclockwise").isKeyDown()) { roll += Utils.isCtrlDown() ? 0.2 : 1; } if (keyBindings.get("replaymod.input.rollcounterclockwise").isKeyDown()) { roll -= Utils.isCtrlDown() ? 0.2 : 1; } } private void updateArmYawAndPitch() { prevRenderArmYaw = renderArmYaw; prevRenderArmPitch = renderArmPitch; renderArmPitch = renderArmPitch + (rotationPitch - renderArmPitch) * 0.5f; renderArmYaw = renderArmYaw + (rotationYaw - renderArmYaw) * 0.5f; } public boolean canSpectate(Entity e) { return e != null && !e.isInvisible() //#if MC>=10800 && (e instanceof EntityPlayer || e instanceof EntityLiving || e instanceof EntityItemFrame); //#else //$$ && e instanceof EntityPlayer; // cannot be more generic since 1.7.10 has no concept of eye height //#endif } //#if MC>=11102 @Override public void sendMessage(ITextComponent message) { if (MinecraftForge.EVENT_BUS.post(new ReplayChatMessageEvent(this))) return; super.sendMessage(message); } //#else //$$ @Override //$$ public void addChatMessage(ITextComponent message) { //$$ if (MinecraftForge.EVENT_BUS.post(new ReplayChatMessageEvent(this))) return; //$$ super.addChatMessage(message); //$$ } //#endif //#if MC>=10800 private //#else //$$ public // All event handlers need to be public in 1.7.10 //#endif class EventHandler { private EventHandler() {} @SubscribeEvent public void onPreClientTick(TickEvent.ClientTickEvent event) { if (event.phase == TickEvent.Phase.START) { update(); updateArmYawAndPitch(); } } @SubscribeEvent public void onRenderUpdate(TickEvent.RenderTickEvent event) { if (event.phase == TickEvent.Phase.START) { update(); } } @SubscribeEvent public void preCrosshairRender(RenderGameOverlayEvent.Pre event) { // The crosshair should only render if targeted entity can actually be spectated if (MCVer.getType(event) == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { event.setCanceled(!canSpectate(mc.pointedEntity)); } // Hotbar should never be rendered if (MCVer.getType(event) == RenderGameOverlayEvent.ElementType.HOTBAR) { event.setCanceled(true); } } @SubscribeEvent public void onSettingsChanged(SettingsChangedEvent event) { if (event.getKey() == Setting.CAMERA) { if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) { cameraController = ReplayModReplay.instance.createCameraController(CameraEntity.this); } else { cameraController = new SpectatorCameraController(CameraEntity.this); } } } @SubscribeEvent public void onRenderHand(RenderHandEvent event) { // Unless we are spectating another player, don't render our hand if (getRenderViewEntity(mc) == CameraEntity.this || !(getRenderViewEntity(mc) instanceof EntityPlayer)) { event.setCanceled(true); } } @SubscribeEvent(priority = EventPriority.LOWEST) public void onRenderHandMonitor(RenderHandEvent event) { Entity view = getRenderViewEntity(mc); if (view instanceof EntityPlayer) { EntityPlayer player = (EntityPlayer) view; // When the spectated player has changed, force equip their items to prevent the equip animation if (lastHandRendered != player) { lastHandRendered = player; //#if MC>=10904 mc.entityRenderer.itemRenderer.prevEquippedProgressMainHand = 1; mc.entityRenderer.itemRenderer.prevEquippedProgressOffHand = 1; mc.entityRenderer.itemRenderer.equippedProgressMainHand = 1; mc.entityRenderer.itemRenderer.equippedProgressOffHand = 1; mc.entityRenderer.itemRenderer.itemStackMainHand = player.getItemStackFromSlot(EntityEquipmentSlot.MAINHAND); mc.entityRenderer.itemRenderer.itemStackOffHand = player.getItemStackFromSlot(EntityEquipmentSlot.OFFHAND); //#else //$$ mc.entityRenderer.itemRenderer.prevEquippedProgress = 1; //$$ mc.entityRenderer.itemRenderer.equippedProgress = 1; //$$ mc.entityRenderer.itemRenderer.itemToRender = player.inventory.getCurrentItem(); //$$ mc.entityRenderer.itemRenderer.equippedItemSlot = player.inventory.currentItem; //#endif mc.player.renderArmYaw = mc.player.prevRenderArmYaw = player.rotationYaw; mc.player.renderArmPitch = mc.player.prevRenderArmPitch = player.rotationPitch; } } } //#if MC>=10800 @SubscribeEvent public void onEntityViewRenderEvent(EntityViewRenderEvent.CameraSetup event) { if (mc.getRenderViewEntity() == CameraEntity.this) { //#if MC>=10904 event.setRoll(roll); //#else //$$ event.roll = roll; //#endif } } //#endif private boolean heldItemTooltipsWasTrue; @SubscribeEvent public void preRenderGameOverlay(RenderGameOverlayEvent.Pre event) { switch (MCVer.getType(event)) { case ALL: heldItemTooltipsWasTrue = mc.gameSettings.heldItemTooltips; mc.gameSettings.heldItemTooltips = false; break; case ARMOR: case HEALTH: case FOOD: case AIR: case HOTBAR: case EXPERIENCE: case HEALTHMOUNT: case JUMPBAR: //#if MC>=10904 case POTION_ICONS: //#endif event.setCanceled(true); break; case HELMET: case PORTAL: case CROSSHAIRS: case BOSSHEALTH: //#if MC>=10904 case BOSSINFO: case SUBTITLES: //#endif case TEXT: case CHAT: case PLAYER_LIST: case DEBUG: break; } } @SubscribeEvent public void postRenderGameOverlay(RenderGameOverlayEvent.Post event) { if (MCVer.getType(event) != RenderGameOverlayEvent.ElementType.ALL) return; mc.gameSettings.heldItemTooltips = heldItemTooltipsWasTrue; } } }