Files
ReplayModCinematic/src/main/java/com/replaymod/replay/ReplayHandler.java
Jonas Herzig 357501e5db Qualify some (remapped) field references which the preprocesser ignored
The preprocessor should have complained about these but it didn't for
some reason. This commit isn't fixing any imminent bug, it's just
precautionary (and required to compile once the preprocessor is fixed).
2019-06-17 12:04:51 +02:00

708 lines
25 KiB
Java
Executable File

package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.mixin.MinecraftAccessor;
import com.replaymod.core.mixin.TimerAccessor;
import com.replaymod.core.utils.Restrictions;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.camera.SpectatorCameraController;
import com.replaymod.replay.events.ReplayClosedCallback;
import com.replaymod.replay.events.ReplayClosingCallback;
import com.replaymod.replay.events.ReplayOpenedCallback;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.util.Location;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.network.ClientLoginNetworkHandler;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.ClientConnection;
import java.io.IOException;
import java.util.*;
//#if MC>=11300
import com.replaymod.replay.mixin.EntityLivingBaseAccessor;
import net.minecraft.client.util.Window;
import net.minecraft.entity.LivingEntity;
//#else
//$$ import com.replaymod.replay.mixin.EntityOtherPlayerMPAccessor;
//$$ import net.minecraft.client.entity.EntityOtherPlayerMP;
//$$ import net.minecraft.client.gui.ScaledResolution;
//$$ import org.lwjgl.opengl.Display;
//#endif
//#if MC>=11200
//#else
//$$ import io.netty.channel.ChannelOutboundHandlerAdapter;
//#endif
//#if MC<10904
//$$ import de.johni0702.minecraft.gui.element.GuiLabel;
//$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
//$$ import de.johni0702.minecraft.gui.utils.Colors;
//#endif
//#if MC>=10800
import net.minecraft.network.NetworkSide;
//#if MC>=11400
//#else
//#if MC>=11300
//$$ import net.minecraftforge.fml.network.NetworkHooks;
//#else
//$$ import com.mojang.authlib.GameProfile;
//$$ import net.minecraft.client.network.NetHandlerPlayClient;
//$$ import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
//#endif
//#endif
//#else
//$$ import cpw.mods.fml.client.FMLClientHandler;
//$$ import cpw.mods.fml.common.Loader;
//$$ import cpw.mods.fml.common.network.internal.FMLNetworkHandler;
//$$ import com.replaymod.replay.gui.screen.GuiOpeningReplay;
//$$ import net.minecraft.entity.EntityLivingBase;
//$$
//$$ import java.net.InetSocketAddress;
//$$ import java.net.SocketAddress;
//#endif
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.replaymod.core.versions.MCVer.*;
import static com.replaymod.replay.ReplayModReplay.LOGGER;
import static com.mojang.blaze3d.platform.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class ReplayHandler {
private static MinecraftClient mc = getMinecraft();
/**
* The file currently being played.
*/
private final ReplayFile replayFile;
/**
* Decodes and sends packets into channel.
*/
private final FullReplaySender fullReplaySender;
//#if MC>=10904
private final QuickReplaySender quickReplaySender;
private boolean quickMode = false;
//#else
//$$ private static final String QUICK_MODE_MIN_MC = "1.9.4";
//#endif
/**
* Currently active replay restrictions.
*/
private Restrictions restrictions = new Restrictions();
/**
* Whether camera movements by user input and/or server packets should be suppressed.
*/
private boolean suppressCameraMovements;
private Set<Marker> markers;
private final GuiReplayOverlay overlay;
private EmbeddedChannel channel;
private int replayDuration;
/**
* The position at which the camera should be located after the next jump.
*/
private Location targetCameraPosition;
private UUID spectating;
public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOException {
Preconditions.checkState(isOnMainThread(), "Must be called from Minecraft thread.");
this.replayFile = replayFile;
replayDuration = replayFile.getMetaData().getDuration();
markers = replayFile.getMarkers().or(Collections.emptySet());
fullReplaySender = new FullReplaySender(this, replayFile, false);
//#if MC>=10904
quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile);
//#endif
setup();
overlay = new GuiReplayOverlay(this);
overlay.setVisible(true);
ReplayOpenedCallback.EVENT.invoker().replayOpened(this);
fullReplaySender.setAsyncMode(asyncMode);
}
void restartedReplay() {
channel.close();
// Force re-creation of camera entity by unloading the previous world
ReplayMod.instance.runSync(() -> {
//#if MC>=11300
mc.mouse.unlockCursor();
//#else
//$$ mc.setIngameNotInFocus();
//#endif
mc.joinWorld(null);
});
restrictions = new Restrictions();
setup();
}
public void endReplay() throws IOException {
Preconditions.checkState(isOnMainThread(), "Must be called from Minecraft thread.");
ReplayClosingCallback.EVENT.invoker().replayClosing(this);
fullReplaySender.terminateReplay();
//#if MC>=10904
if (quickMode) {
quickReplaySender.unregister();
}
//#endif
replayFile.save();
replayFile.close();
channel.close().awaitUninterruptibly();
if (mc.player instanceof CameraEntity) {
//#if MC>=11300
mc.player.remove();
//#else
//$$ mc.player.setDead();
//#endif
}
if (mc.world != null) {
//#if MC>=11400
mc.disconnect();
//#else
//$$ mc.world.sendQuittingDisconnectingPacket();
//$$ mc.loadWorld(null);
//#endif
}
TimerAccessor timer = (TimerAccessor) ((MinecraftAccessor) mc).getTimer();
//#if MC>=11200
timer.setTickLength(WrappedTimer.DEFAULT_MS_PER_TICK);
//#else
//$$ timer.setTimerSpeed(1);
//#endif
overlay.setVisible(false);
ReplayModReplay.instance.replayHandler = null;
mc.openScreen(null);
ReplayClosedCallback.EVENT.invoker().replayClosed(this);
}
private void setup() {
//#if MC>=11100
mc.inGameHud.getChatHud().clear(false);
//#else
//$$ mc.ingameGUI.getChatGUI().clearChatMessages();
//#endif
//#if MC>=10800
ClientConnection networkManager = new ClientConnection(NetworkSide.CLIENTBOUND) {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
t.printStackTrace();
}
};
//#else
//$$ NetworkManager networkManager = new NetworkManager(true) {
//$$ @Override
//$$ public SocketAddress getRemoteAddress() {
//$$ // See https://github.com/Dyonovan/TCNodeTracker/issues/37
//$$ if (Loader.isModLoaded("tcnodetracker")) {
//$$ StackTraceElement elem = Thread.currentThread().getStackTrace()[2];
//$$ if ("com.dyonovan.tcnodetracker.events.ClientConnectionEvent".equals(elem.getClassName())) {
//$$ LOGGER.debug("TCNodeTracker crash workaround applied");
//$$ return new InetSocketAddress("replaymod.dummy", 0);
//$$ }
//$$ }
//$$ return super.getRemoteAddress();
//$$ }
//$$
//$$ @Override
//$$ public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
//$$ t.printStackTrace();
//$$ }
//$$ };
//$$ mc.displayGuiScreen(new GuiOpeningReplay(networkManager));
//$$ FMLClientHandler.instance().connectToRealmsServer(null, 0); // just to init the playClientBlock latch
//#endif
networkManager.setPacketListener(new ClientLoginNetworkHandler(
networkManager,
mc,
null
//#if MC>=11300
, it -> {}
//#endif
));
//#if MC>=11200
channel = new EmbeddedChannel();
//#else
//$$ ChannelOutboundHandlerAdapter dummyHandler = new ChannelOutboundHandlerAdapter();
//$$ channel = new EmbeddedChannel(dummyHandler);
//$$ channel.pipeline().remove(dummyHandler);
//#endif
//#if MC>=10904
channel.pipeline().addLast("ReplayModReplay_quickReplaySender", quickReplaySender);
//#endif
channel.pipeline().addLast("ReplayModReplay_replaySender", fullReplaySender);
channel.pipeline().addLast("packet_handler", networkManager);
channel.pipeline().fireChannelActive();
//#if MC>=11300 && MC<11400
//$$ NetworkHooks.registerClientLoginChannel(networkManager);
//#endif
}
public ReplayFile getReplayFile() {
return replayFile;
}
public Restrictions getRestrictions() {
return restrictions;
}
public ReplaySender getReplaySender() {
//#if MC>=10904
return quickMode ? quickReplaySender : fullReplaySender;
//#else
//$$ return fullReplaySender;
//#endif
}
public GuiReplayOverlay getOverlay() {
return overlay;
}
//#if MC>=10904
public void ensureQuickModeInitialized(Runnable andThen) {
ListenableFuture<Void> future = quickReplaySender.getInitializationPromise();
if (future == null) {
InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay);
future = quickReplaySender.initialize(progress -> popup.progressBar.setProgress(progress.floatValue()));
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
popup.close();
}
@Override
public void onFailure(@Nonnull Throwable t) {
String message = "Failed to initialize quick mode. It will not be available.";
Utils.error(LOGGER, overlay, CrashReport.create(t, message), popup::close);
}
});
}
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
andThen.run();
}
@Override
public void onFailure(@Nonnull Throwable t) {
// Exception already printed in callback added above
}
});
}
private class InitializingQuickModePopup extends AbstractGuiPopup<InitializingQuickModePopup> {
private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20)
.setI18nLabel("replaymod.gui.loadquickmode");
public InitializingQuickModePopup(GuiContainer container) {
super(container);
open();
}
@Override
public void close() {
super.close();
}
@Override
protected InitializingQuickModePopup getThis() {
return this;
}
}
public void setQuickMode(boolean quickMode) {
if (quickMode == this.quickMode) return;
if (quickMode && fullReplaySender.isAsyncMode()) {
// If this method is called via runLater, then it cannot switch to sync mode by itself as there might be
// some rogue packets in the task queue after it. Instead the caller must switch to sync mode first and
// use runLater until all packets have been processed (when using setAsyncModeAndWait, one runLater should
// be sufficient).
throw new IllegalStateException("Cannot switch to quick mode while in async mode.");
}
this.quickMode = quickMode;
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new Location(cam.x, cam.y, cam.z, cam.yaw, cam.pitch);
} else {
targetCameraPosition = null;
}
if (quickMode) {
quickReplaySender.register();
quickReplaySender.restart();
quickReplaySender.sendPacketsTill(fullReplaySender.currentTimeStamp());
} else {
quickReplaySender.unregister();
fullReplaySender.sendPacketsTill(0);
fullReplaySender.sendPacketsTill(quickReplaySender.currentTimeStamp());
}
moveCameraToTargetPosition();
}
public boolean isQuickMode() {
return quickMode;
}
//#else
//$$ public void ensureQuickModeInitialized(@SuppressWarnings("unused") Runnable andThen) {
//$$ GuiInfoPopup.open(overlay,
//$$ new GuiLabel().setI18nText("replaymod.gui.noquickmode", QUICK_MODE_MIN_MC).setColor(Colors.BLACK));
//$$ }
//$$
//$$ public void setQuickMode(@SuppressWarnings("unused") boolean quickMode) {
//$$ throw new UnsupportedOperationException("Quick Mode not supported on this version.");
//$$ }
//$$
//$$ public boolean isQuickMode() {
//$$ return false;
//$$ }
//#endif
public int getReplayDuration() {
return replayDuration;
}
/**
* Return whether camera movement by user inputs and/or server packets should be suppressed.
* @return {@code true} if these kinds of movement should be suppressed
*/
public boolean shouldSuppressCameraMovements() {
return suppressCameraMovements;
}
/**
* Set whether camera movement by user inputs and/or server packets should be suppressed.
* @param suppressCameraMovements {@code true} to suppress these kinds of movement, {@code false} to allow them
*/
public void setSuppressCameraMovements(boolean suppressCameraMovements) {
this.suppressCameraMovements = suppressCameraMovements;
}
/**
* Spectate the specified entity.
* When the entity is {@code null} or the camera entity, the camera becomes the view entity.
* @param e The entity to spectate
*/
//#if MC>=10800
public void spectateEntity(Entity e) {
//#else
//$$ public void spectateEntity(EntityLivingBase e) {
//#endif
CameraEntity cameraEntity = getCameraEntity();
if (cameraEntity == null) {
return; // Cannot spectate if we have no camera
}
if (e == null || e == cameraEntity) {
spectating = null;
e = cameraEntity;
} else if (e instanceof PlayerEntity) {
spectating = e.getUuid();
}
if (e == cameraEntity) {
cameraEntity.setCameraController(ReplayModReplay.instance.createCameraController(cameraEntity));
} else {
cameraEntity.setCameraController(new SpectatorCameraController(cameraEntity));
}
if (getRenderViewEntity(mc) != e) {
setRenderViewEntity(mc, e);
cameraEntity.setCameraPosRot(e);
}
}
/**
* Set the camera as the view entity.
* This is equivalent to {@code spectateEntity(null)}.
*/
public void spectateCamera() {
spectateEntity(null);
}
/**
* Returns whether the current view entity is the camera entity.
* @return {@code true} if the camera is the view entity, {@code false} otherwise
*/
public boolean isCameraView() {
return mc.player instanceof CameraEntity && mc.player == getRenderViewEntity(mc);
}
/**
* Returns the camera entity.
* @return The camera entity or {@code null} if it does not yet exist
*/
public CameraEntity getCameraEntity() {
return mc.player instanceof CameraEntity ? (CameraEntity) mc.player : null;
}
public UUID getSpectatedUUID() {
return spectating;
}
public void setTargetPosition(Location pos) {
targetCameraPosition = pos;
}
public void moveCameraToTargetPosition() {
CameraEntity cam = getCameraEntity();
if (cam != null && targetCameraPosition != null) {
cam.setCameraPosRot(targetCameraPosition);
}
}
public void doJump(int targetTime, boolean retainCameraPosition) {
//#if MC>=10904
if (getReplaySender() == quickReplaySender) {
// Always round to full tick
targetTime = targetTime + targetTime % 50;
if (targetTime >= 50) {
// Jump to time of previous tick first
quickReplaySender.sendPacketsTill(targetTime - 50);
}
// Update all entity positions (especially prev/lastTick values)
for (Entity entity : loadedEntityList(mc.world)) {
skipTeleportInterpolation(entity);
entity.prevRenderX = entity.prevX = entity.x;
entity.prevRenderY = entity.prevY = entity.y;
entity.prevRenderZ = entity.prevZ = entity.z;
entity.prevYaw = entity.yaw;
entity.prevPitch = entity.pitch;
}
// Run previous tick
//#if MC>=11300
mc.tick();
//#else
//$$ try {
//$$ mc.runTick();
//$$ } catch (IOException e) {
//$$ throw new RuntimeException(e);
//$$ }
//#endif
// Jump to target tick
quickReplaySender.sendPacketsTill(targetTime);
// Immediately apply player teleport interpolation
for (Entity entity : loadedEntityList(mc.world)) {
skipTeleportInterpolation(entity);
}
return;
}
//#endif
FullReplaySender replaySender = fullReplaySender;
if (replaySender.isHurrying()) {
return; // When hurrying, no Timeline jumping etc. is possible
}
if (targetTime < replaySender.currentTimeStamp()) {
mc.openScreen(null);
}
if (retainCameraPosition) {
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new Location(cam.x, cam.y, cam.z,
cam.yaw, cam.pitch);
} else {
targetCameraPosition = null;
}
}
long diff = targetTime - replaySender.getDesiredTimestamp();
if (diff != 0) {
if (diff > 0 && diff < 5000) { // Small difference and no time travel
replaySender.jumpToTime(targetTime);
} else { // We either have to restart the replay or send a significant amount of packets
// Render our please-wait-screen
Screen guiScreen = new Screen(
//#if MC>=11400
null
//#endif
) {
@Override
//#if MC>=11300
public void render(int mouseX, int mouseY, float partialTicks) {
//#else
//$$ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
//#endif
renderDirtBackground(0);
drawCenteredString(this.minecraft.textRenderer, I18n.translate("replaymod.gui.pleasewait"),
width / 2, height / 2, 0xffffffff);
}
};
// Make sure that the replaysender changes into sync mode
replaySender.setSyncModeAndWait();
// Perform the rendering using OpenGL
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
//#if MC>=11400
, true
//#endif
);
enableTexture();
mc.getFramebuffer().beginWrite(true);
//#if MC>=11400
mc.window.method_4493(true);
//#else
//#if MC>=11300
//$$ mc.mainWindow.setupOverlayRendering();
//#else
//$$ mc.entityRenderer.setupOverlayRendering();
//#endif
//#endif
//#if MC>=11300
Window
//#else
//$$ ScaledResolution
//#endif
resolution = newScaledResolution(mc);
guiScreen.init(mc, resolution.getScaledWidth(), resolution.getScaledHeight());
//#if MC>=11300
guiScreen.render(0, 0, 0);
//#else
//$$ guiScreen.drawScreen(0, 0, 0);
//#endif
mc.getFramebuffer().endWrite();
popMatrix();
pushMatrix();
//#if MC>=11300
mc.getFramebuffer().draw(mc.window.getFramebufferWidth(), mc.window.getFramebufferHeight());
//#else
//$$ mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
//#endif
popMatrix();
//#if MC>=11300
mc.window.setFullscreen(true);
//#else
//$$ Display.update();
//#endif
// Send the packets
replaySender.sendPacketsTill(targetTime);
replaySender.setAsyncMode(true);
replaySender.setReplaySpeed(0);
//#if MC<10800
//$$ while (mc.currentScreen instanceof GuiOpeningReplay) {
//$$ mc.currentScreen.handleInput();
//$$ }
//#endif
mc.getNetworkHandler().getClientConnection()
//#if MC>=11300
.tick();
//#else
//$$ .processReceivedPackets();
//#endif
for (Entity entity : loadedEntityList(mc.world)) {
skipTeleportInterpolation(entity);
entity.prevRenderX = entity.prevX = entity.x;
entity.prevRenderY = entity.prevY = entity.y;
entity.prevRenderZ = entity.prevZ = entity.z;
entity.prevYaw = entity.yaw;
entity.prevPitch = entity.pitch;
}
//#if MC>=10800 && MC<11300
//$$ try {
//$$ mc.runTick();
//$$ } catch (IOException e) {
//$$ e.printStackTrace(); // This should never be thrown but whatever
//$$ }
//#else
mc.tick();
//#endif
//finally, updating the camera's position (which is not done by the sync jumping)
moveCameraToTargetPosition();
// No need to remove our please-wait-screen. It'll vanish with the next
// render pass as it's never been a real GuiScreen in the first place.
}
}
}
private void skipTeleportInterpolation(Entity entity) {
//#if MC>=11300
if (entity instanceof LivingEntity && !(entity instanceof CameraEntity)) {
LivingEntity e = (LivingEntity) entity;
EntityLivingBaseAccessor ea = (EntityLivingBaseAccessor) e;
e.setPosition(ea.getInterpTargetX(), ea.getInterpTargetY(), ea.getInterpTargetZ());
e.yaw = (float) ea.getInterpTargetYaw();
e.pitch = (float) ea.getInterpTargetPitch();
}
//#else
//$$ if (entity instanceof EntityOtherPlayerMP) {
//$$ EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity;
//$$ EntityOtherPlayerMPAccessor ea = (EntityOtherPlayerMPAccessor) e;
//$$ e.setPosition(ea.getOtherPlayerMPX(), ea.getOtherPlayerMPY(), ea.getOtherPlayerMPZ());
//$$ e.rotationYaw = (float) ea.getOtherPlayerMPYaw();
//$$ e.rotationPitch = (float) ea.getOtherPlayerMPPitch();
//$$ }
//#endif
}
}