Split mod into core, recording, replay, render[todo], paths[todo] and extras[wip] modules

Move everything to com.replaymod package
Add KeyBindingRegistry and SettingsRegistry
Recreate settings GUI with new GUI API and dynamically from SettingsRegistry
Use ReplayFile from ReplayStudio
ReplayHandler is now object oriented
Add GuiOverlay, GuiSlider and GuiTexturedButton to GUI API
Rewrite both overlays to use new GUI API
Fix size capping in vertical and horizontal layout
Allow CustomLayouts to have parents
Fix tooltip rendering when close to screen border
Allow changing of columns in GridLayout
This commit is contained in:
johni0702
2015-10-03 17:36:03 +02:00
parent 8bd051eb27
commit f925d56ca2
141 changed files with 6294 additions and 5582 deletions

View File

@@ -0,0 +1,276 @@
package com.replaymod.replay;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import eu.crushedpixel.replaymod.replay.LesserDataWatcher;
import net.minecraft.block.material.Material;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.entity.Entity;
import net.minecraft.stats.StatFileWriter;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MathHelper;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import org.lwjgl.Sys;
import java.util.UUID;
public class CameraEntity extends EntityPlayerSP {
public static final double SPEED_CHANGE = 0.5;
public static final double LOWER_SPEED = 2;
public static final double UPPER_SPEED = 20;
private static double MAX_SPEED = 10;
private static double THRESHOLD = MAX_SPEED / 20;
private static double DECAY = MAX_SPEED/3;
public static void modifyCameraSpeed(boolean increase) {
setCameraMaximumSpeed(getCameraMaximumSpeed() + (increase ? 1 : -1) * SPEED_CHANGE);
}
public static void setCameraMaximumSpeed(double maxSpeed) {
if(maxSpeed < LOWER_SPEED || maxSpeed > UPPER_SPEED) return;
MAX_SPEED = maxSpeed;
THRESHOLD = MAX_SPEED / 20;
DECAY = 5;
}
public static double getCameraMaximumSpeed() {
return MAX_SPEED;
}
private Vec3 direction;
private Vec3 dirBefore;
private double motion;
public float roll;
private long lastCall = 0;
private boolean speedup = false;
public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatFileWriter statFileWriter) {
super(mcIn, worldIn, netHandlerPlayClient, statFileWriter);
}
//frac = time since last tick
public void updateMovement() {
long frac = Sys.getTime() - lastCall;
if(frac == 0) return;
double decFac = Math.max(0, 1 - (DECAY * (frac / 1000D)));
if(speedup) {
if(motion < THRESHOLD) motion = THRESHOLD;
motion /= decFac;
} else {
motion *= decFac;
}
motion = Math.min(motion, MAX_SPEED);
lastCall = Sys.getTime();
if(direction == null || motion < THRESHOLD) {
return;
}
Vec3 movement = direction.normalize();
double factor = motion * (frac / 1000D);
moveRelative(movement.xCoord * factor, movement.yCoord * factor, movement.zCoord * factor);
}
public void speedUp() {
speedup = true;
}
public void stopSpeedUp() {
speedup = false;
}
public void setMovement(MoveDirection dir) {
switch(dir) {
case BACKWARD:
direction = this.getVectorForRotation(-rotationPitch, rotationYaw - 180);
break;
case DOWN:
direction = this.getVectorForRotation(90, 0);
break;
case FORWARD:
direction = this.getVectorForRotation(rotationPitch, rotationYaw);
break;
case LEFT:
direction = this.getVectorForRotation(0, rotationYaw - 90);
break;
case RIGHT:
direction = this.getVectorForRotation(0, rotationYaw + 90);
break;
case UP:
direction = this.getVectorForRotation(-90, 0);
break;
}
Vec3 dbf = direction;
if(dirBefore != null) {
direction = dirBefore.normalize().add(direction);
}
dirBefore = dbf;
updateMovement();
}
public void moveAbsolute(AdvancedPosition pos) {
this.moveAbsolute(pos.getX(), pos.getY(), pos.getZ());
rotationPitch = prevRotationPitch = (float)pos.getPitch();
rotationYaw = prevRotationYaw = (float)pos.getYaw();
roll = (float) pos.getRoll();
updateBoundingBox();
}
public void moveAbsolute(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();
}
public void moveRelative(double x, double y, double z) {
this.lastTickPosX = this.prevPosX = this.posX = this.posX + x;
this.lastTickPosY = this.prevPosY = this.posY = this.posY + y;
this.lastTickPosZ = this.prevPosZ = this.posZ = this.posZ + z;
updateBoundingBox();
}
public void movePath(AdvancedPosition pos) {
this.prevRotationPitch = this.rotationPitch = (float)pos.getPitch();
this.prevRotationYaw = this.rotationYaw = (float)pos.getYaw();
this.roll = (float) pos.getRoll();
this.lastTickPosX = this.prevPosX = this.posX = pos.getX();
this.lastTickPosY = this.prevPosY = this.posY = pos.getY();
this.lastTickPosZ = this.prevPosZ = this.posZ = pos.getZ();
updateBoundingBox();
}
private void updateBoundingBox() {
this.setEntityBoundingBox(new AxisAlignedBB(posX - width / 2, posY, posZ - width / 2,
posX + width / 2, posY + height, posZ + width / 2));
}
protected void updatePos(Entity to) {
prevPosX = to.prevPosX;
prevPosY = to.prevPosY;
prevPosZ = to.prevPosZ;
prevRotationYaw = to.prevRotationYaw;
prevRotationPitch = to.prevRotationPitch;
posX = to.posX;
posY = to.posY;
posZ = to.posZ;
rotationYaw = to.rotationYaw;
rotationPitch = to.rotationPitch;
}
@Override
protected void entityInit() {
this.dataWatcher = new LesserDataWatcher(this);
this.setSize(0.6f, 1.8f);
updateBoundingBox();
}
@Override
public void setAngles(float yaw, float pitch) {
this.rotationYaw = (float) ((double) this.rotationYaw + (double) yaw * 0.15D);
this.rotationPitch = (float) ((double) this.rotationPitch - (double) pitch * 0.15D);
this.rotationPitch = MathHelper.clamp_float(this.rotationPitch, -90.0F, 90.0F);
this.prevRotationPitch = this.rotationPitch;
this.prevRotationYawHead = this.rotationYawHead = this.prevRotationYaw = this.rotationYaw;
}
@Override
public void onUpdate() {
Entity view = mc.getRenderViewEntity();
if (view != null) {
UUID spectating = ReplayModReplay.instance.getReplayHandler().spectating;
if (spectating != null && (view.getUniqueID() != spectating || view.worldObj != worldObj)) {
view = worldObj.getPlayerEntityByUUID(spectating);
if (view != null) {
mc.setRenderViewEntity(view);
} else {
mc.setRenderViewEntity(this);
return;
}
}
if (view != this) {
updatePos(view);
}
}
}
@Override
public void preparePlayerToSpawn() {
if (mc.theWorld != null) {
worldObj = mc.theWorld;
}
super.preparePlayerToSpawn();
}
@Override
public boolean isEntityInsideOpaqueBlock() {
return false;
}
@Override
public boolean isInsideOfMaterial(Material materialIn) {
return false;
}
@Override
public boolean isInLava() {
return false;
}
@Override
public boolean isInWater() {
return false;
}
@Override
public boolean canBePushed() {
return false;
}
@Override
protected void createRunningParticles() {}
@Override
public boolean canBeCollidedWith() {
return false;
}
@Override
public boolean isSpectator() {
return true;
}
public enum MoveDirection {
UP, DOWN, LEFT, RIGHT, FORWARD, BACKWARD
}
@Override
public MovingObjectPosition rayTrace(double p_174822_1_, float p_174822_3_) {
MovingObjectPosition pos = super.rayTrace(p_174822_1_, 1f);
if(pos != null && pos.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
pos.typeOfHit = MovingObjectPosition.MovingObjectType.MISS;
}
return pos;
}
}

View File

@@ -0,0 +1,349 @@
package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.events.ReplayCloseEvent;
import com.replaymod.replay.events.ReplayOpenEvent;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import de.johni0702.replaystudio.data.Marker;
import de.johni0702.replaystudio.replay.ReplayFile;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry;
import eu.crushedpixel.replaymod.replay.Restrictions;
import eu.crushedpixel.replaymod.utils.MouseUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityOtherPlayerMP;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.network.EnumPacketDirection;
import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.Point;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static net.minecraft.client.renderer.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 Minecraft mc = Minecraft.getMinecraft();
/**
* The file currently being played.
*/
private final ReplayFile replayFile;
/**
* Decodes and sends packets into channel.
*/
private final ReplaySender replaySender;
/**
* 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 final Set<Marker> markers;
private final GuiReplayOverlay overlay;
private EmbeddedChannel channel;
/**
* The position at which the camera should be located after the next jump.
*/
private AdvancedPosition targetCameraPosition;
protected UUID spectating;
public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOException {
Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread.");
this.replayFile = replayFile;
FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Pre(this));
markers = replayFile.getMarkers().or(Collections.<Marker>emptySet());
// TODO: get rid of chat message handler
ReplayMod.chatMessageHandler.initialize();
replaySender = new ReplaySender(this, replayFile, asyncMode);
setup();
overlay = new GuiReplayOverlay(this);
overlay.setVisible(true);
FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Post(this));
}
void restartedReplay() {
channel.close();
restrictions = new Restrictions();
setup();
}
public void endReplay() throws IOException {
Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread.");
FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Pre(this));
replaySender.terminateReplay();
replayFile.save();
replayFile.close();
channel.close().awaitUninterruptibly();
if (mc.theWorld != null) {
mc.theWorld.sendQuittingDisconnectingPacket();
mc.loadWorld(null);
}
overlay.setVisible(false);
// These might have been hidden by the overlay, so we have to make sure they're visible again
ReplayGuiRegistry.show();
FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Post(this));
}
private void setup() {
mc.ingameGUI.getChatGUI().clearChatMessages();
NetworkManager networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
t.printStackTrace();
}
};
networkManager.setNetHandler(new NetHandlerPlayClient(
mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")));
channel = new EmbeddedChannel(networkManager);
channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager));
channel.pipeline().addFirst(replaySender);
channel.pipeline().fireChannelActive();
}
public ReplayFile getReplayFile() {
return replayFile;
}
public Restrictions getRestrictions() {
return restrictions;
}
public ReplaySender getReplaySender() {
return replaySender;
}
public GuiReplayOverlay getOverlay() {
return overlay;
}
/**
* 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;
}
/**
* Returns all markers.
* When changed, {@link #saveMarkers()} should be called to save the changes.
* @return Set of markers
*/
public Set<Marker> getMarkers() {
return markers;
}
/**
* Saves all markers.
*/
public void saveMarkers() {
try {
replayFile.writeMarkers(markers);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 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
*/
public void spectateEntity(Entity e) {
CameraEntity cameraEntity = getCameraEntity();
if (e == null || e == cameraEntity) {
spectating = null;
e = cameraEntity;
} else if (e instanceof EntityPlayer) {
spectating = e.getUniqueID();
}
if (mc.getRenderViewEntity() != e) {
mc.setRenderViewEntity(e);
cameraEntity.updatePos(e);
}
}
/**
* Set the camera as the view entity.
* This is equivalent to {@link #spectateEntity(Entity) 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.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity();
}
/**
* Returns the camera entity.
* @return The camera entity or {@code null} if it does not yet exist
*/
public CameraEntity getCameraEntity() {
return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null;
}
public void setTargetPosition(AdvancedPosition pos) {
targetCameraPosition = pos;
}
public void moveCameraToTargetPosition() {
CameraEntity cam = getCameraEntity();
if (cam != null && targetCameraPosition != null) {
cam.moveAbsolute(targetCameraPosition);
}
}
public void doJump(int targetTime, boolean retainCameraPosition) {
if (replaySender.isHurrying()) {
return; // When hurrying, no Timeline jumping etc. is possible
}
if (targetTime < replaySender.currentTimeStamp()) {
mc.displayGuiScreen(null);
}
if (retainCameraPosition) {
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new AdvancedPosition(cam.posX, cam.posY, cam.posZ,
cam.rotationPitch, cam.rotationYaw, cam.roll);
} 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
GuiScreen guiScreen = new GuiScreen() {
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
drawBackground(0);
drawCenteredString(fontRendererObj, I18n.format("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);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
Point point = MouseUtils.getScaledDimensions();
guiScreen.setWorldAndResolution(mc, point.getX(), point.getY());
guiScreen.drawScreen(0, 0, 0);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
Display.update();
// Send the packets
ReplayMod.replaySender.sendPacketsTill(targetTime);
ReplayMod.replaySender.setAsyncMode(true);
ReplayMod.replaySender.setReplaySpeed(0);
mc.getNetHandler().getNetworkManager().processReceivedPackets();
@SuppressWarnings("unchecked")
List<Entity> entities = (List<Entity>) mc.theWorld.loadedEntityList;
for (Entity entity : entities) {
if (entity instanceof EntityOtherPlayerMP) {
EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity;
e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ);
e.rotationYaw = (float) e.otherPlayerMPYaw;
e.rotationPitch = (float) e.otherPlayerMPPitch;
}
entity.lastTickPosX = entity.prevPosX = entity.posX;
entity.lastTickPosY = entity.prevPosY = entity.posY;
entity.lastTickPosZ = entity.prevPosZ = entity.posZ;
entity.prevRotationYaw = entity.rotationYaw;
entity.prevRotationPitch = entity.rotationPitch;
}
try {
mc.runTick();
} catch (IOException e) {
e.printStackTrace(); // This should never be thrown but whatever
}
//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.
}
}
}
}

View File

@@ -0,0 +1,58 @@
package com.replaymod.replay;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.handler.GuiHandler;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ZipReplayFile;
import de.johni0702.replaystudio.studio.ReplayStudio;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard;
import java.io.File;
import java.io.IOException;
@Mod(modid = ReplayModReplay.MOD_ID, useMetadata = true)
public class ReplayModReplay {
public static final String MOD_ID = "replaymod-replay";
@Mod.Instance(MOD_ID)
public static ReplayModReplay instance;
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
private Logger logger;
private ReplayHandler replayHandler;
public ReplayHandler getReplayHandler() {
return replayHandler;
}
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
core.getSettingsRegistry().register(Setting.class);
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() {
@Override
public void run() {
}
});
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
new GuiHandler(this).register();
}
public void startReplay(File file) throws IOException {
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file);
replayHandler = new ReplayHandler(replayFile, true);
}
}

View File

@@ -0,0 +1,820 @@
package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFutureTask;
import de.johni0702.replaystudio.replay.ReplayFile;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.holders.PacketData;
import eu.crushedpixel.replaymod.replay.Restrictions;
import eu.crushedpixel.replaymod.settings.ReplaySettings;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiDownloadTerrain;
import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.Entity;
import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.*;
import net.minecraft.util.IChatComponent;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.world.World;
import net.minecraft.world.WorldSettings.GameType;
import net.minecraft.world.WorldType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Sends replay packets to netty channels.
* Even though {@link Sharable}, this should never be added to multiple pipes at once, it may however be re-added when
* the replay restart from the beginning.
*/
@Sharable
public class ReplaySender extends ChannelInboundHandlerAdapter {
/**
* Previously packets for the client player were inserted using one fixed entity id (this one).
* This is no longer the case however to provide backwards compatibility, we have to convert
* these old packets to use the normal entity id.
* Need to punch someone? -> CrushedPixel
*/
public static final int LEGACY_ENTITY_ID = Integer.MIN_VALUE + 9001;
/**
* These packets are ignored completely during replay.
*/
private static final List<Class> BAD_PACKETS = Arrays.<Class>asList(
S06PacketUpdateHealth.class,
S2DPacketOpenWindow.class,
S2EPacketCloseWindow.class,
S2FPacketSetSlot.class,
S30PacketWindowItems.class,
S36PacketSignEditorOpen.class,
S37PacketStatistics.class,
S1FPacketSetExperience.class,
S43PacketCamera.class,
S39PacketPlayerAbilities.class,
S45PacketTitle.class);
/**
* The replay handler responsible for the current replay.
*/
private final ReplayHandler replayHandler;
/**
* Whether to work in async mode.
*
* When in async mode, a separate thread send packets and waits according to their delays.
* This is default in normal playback mode.
*
* When in sync mode, no packets will be sent until {@link #sendPacketsTill(int)} is called.
* This is used during path playback and video rendering.
*/
protected boolean asyncMode;
/**
* Timestamp of the last packet sent in milliseconds since the start.
*/
protected int lastTimeStamp;
/**
* The replay file.
*/
protected ReplayFile replayFile;
/**
* The channel handler context used to send packets to minecraft.
*/
protected ChannelHandlerContext ctx;
/**
* The data input stream from which new packets are read.
* When accessing this stream make sure to synchronize on {@code this} as it's used from multiple threads.
*/
protected DataInputStream dis;
/**
* The next packet that should be sent.
* This is required as some actions such as jumping to a specified timestamp have to peek at the next packet.
*/
protected PacketData nextPacket;
/**
* Whether we need to restart the current replay. E.g. when jumping backwards in time
*/
protected boolean startFromBeginning = true;
/**
* Whether to terminate the replay. This only has an effect on the async mode and is {@code true} during sync mode.
*/
protected boolean terminate;
/**
* The speed of the replay. 1 is normal, 2 is twice as fast, 0.5 is half speed and 0 is frozen
*/
protected double replaySpeed = 1f;
/**
* Whether the world has been loaded and the dirt-screen should go away.
*/
protected boolean hasWorldLoaded;
/**
* The minecraft instance.
*/
protected Minecraft mc = Minecraft.getMinecraft();
/**
* The total length of this replay in milliseconds.
*/
protected final int replayLength;
/**
* Our actual entity id that the server gave to us.
*/
protected int actualID = -1;
/**
* Whether to allow (process) the next player movement packet.
*/
protected boolean allowMovement;
/**
* Directory to which resource packs are extracted.
*/
private final File tempResourcePackFolder = Files.createTempDir();
/**
* Create a new replay sender.
* @param file The replay file
* @param asyncMode {@code true} for async mode, {@code false} otherwise
* @see #asyncMode
*/
public ReplaySender(ReplayHandler replayHandler, ReplayFile file, boolean asyncMode) throws IOException {
this.replayHandler = replayHandler;
this.replayFile = file;
this.asyncMode = asyncMode;
this.replayLength = file.getMetaData().getDuration();
if (asyncMode) {
new Thread(asyncSender, "replaymod-async-sender").start();
}
}
/**
* Set whether this replay sender operates in async mode.
* When in async mode, it will send packets timed from a separate thread.
* When not in async mode, it will send packets when {@link #sendPacketsTill(int)} is called.
* @param asyncMode {@code true} to enable async mode
*/
public void setAsyncMode(boolean asyncMode) {
if (this.asyncMode == asyncMode) return;
this.asyncMode = asyncMode;
if (asyncMode) {
this.terminate = false;
new Thread(asyncSender, "replaymod-async-sender").start();
} else {
this.terminate = true;
}
}
/**
* Set whether this replay sender to operate in sync mode.
* When in sync mode, it will send packets when {@link #sendPacketsTill(int)} is called.
* This call will block until the async worker thread has stopped.
*/
public void setSyncModeAndWait() {
if (!this.asyncMode) return;
this.asyncMode = false;
this.terminate = true;
synchronized (this) {
// This will wait for the worker thread to leave the synchronized code part
}
}
/**
* Return the timestamp of the last packet sent.
* @return The timestamp in milliseconds since the start of the replay
*/
public int currentTimeStamp() {
return lastTimeStamp;
}
/**
* Return the total length of the replay played.
* @return Total length in milliseconds
*/
public int replayLength() {
return replayLength;
}
/**
* Terminate this replay sender.
*/
public void terminateReplay() {
terminate = true;
try {
channelInactive(ctx);
ctx.channel().pipeline().close();
} catch(Exception e) {
e.printStackTrace();
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// When in async mode and the replay sender shut down, then don't send packets
if(terminate && asyncMode) {
return;
}
// When a packet is sent directly, perform no filtering
if(msg instanceof Packet) {
super.channelRead(ctx, msg);
}
if (msg instanceof byte[]) {
try {
Packet p = ReplayFileIO.deserializePacket((byte[]) msg);
if (p != null) {
p = processPacket(p);
if (p != null) {
super.channelRead(ctx, p);
}
// If we do not give minecraft time to tick, there will be dead entity artifacts left in the world
// Therefore we have to remove all loaded, dead entities manually if we are in sync mode.
// We do this after every SpawnX packet and after the destroy entities packet.
if (!asyncMode && mc.theWorld != null) {
if (p instanceof S0CPacketSpawnPlayer
|| p instanceof S0EPacketSpawnObject
|| p instanceof S0FPacketSpawnMob
|| p instanceof S2CPacketSpawnGlobalEntity
|| p instanceof S10PacketSpawnPainting
|| p instanceof S11PacketSpawnExperienceOrb
|| p instanceof S13PacketDestroyEntities) {
World world = mc.theWorld;
for (int i = 0; i < world.loadedEntityList.size(); ++i) {
Entity entity = (Entity) world.loadedEntityList.get(i);
if (entity.isDead) {
int chunkX = entity.chunkCoordX;
int chunkY = entity.chunkCoordZ;
if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) {
world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity);
}
world.loadedEntityList.remove(i--);
world.onEntityRemoved(entity);
}
}
}
}
}
} catch (Exception e) {
// We'd rather not have a failure parsing one packet screw up the whole replay process
e.printStackTrace();
}
}
}
/**
* Process a packet and return the result.
* @param p The packet to process
* @return The processed packet or {@code null} if no packet shall be sent
*/
protected Packet processPacket(Packet p) throws Exception {
if (p instanceof S3FPacketCustomPayload) {
S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p;
if (Restrictions.PLUGIN_CHANNEL.equals(packet.getChannelName())) {
final String unknown = replayHandler.getRestrictions().handle(packet);
if (unknown == null) {
return null;
} else {
// Failed to parse options, make sure that under no circumstances further packets are parsed
terminateReplay();
// Then end replay and show error GUI
mc.addScheduledTask(new Runnable() {
@Override
public void run() {
try {
replayHandler.endReplay();
} catch (IOException e) {
e.printStackTrace();
}
mc.displayGuiScreen(new GuiErrorScreen(
I18n.format("replaymod.error.unknownrestriction1"),
I18n.format("replaymod.error.unknownrestriction2", unknown)
));
}
});
}
}
}
if (p instanceof S40PacketDisconnect) {
IChatComponent reason = ((S40PacketDisconnect) p).func_149165_c();
if ("Please update to view this replay.".equals(reason.getUnformattedText())) {
// This version of the mod supports replay restrictions so we are allowed
// to remove this packet.
return null;
}
}
if(BAD_PACKETS.contains(p.getClass())) return null;
convertLegacyEntityIds(p);
if(p instanceof S48PacketResourcePackSend) {
S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p;
String url = packet.func_179783_a();
if (url.startsWith("replay://")) {
int id = Integer.parseInt(url.substring("replay://".length()));
Map<Integer, String> index = replayFile.getResourcePackIndex();
if (index != null) {
String hash = index.get(id);
if (hash != null) {
File file = new File(tempResourcePackFolder, hash + ".zip");
if (!file.exists()) {
IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file));
}
mc.getResourcePackRepository().func_177319_a(file);
}
}
return null;
}
}
if(p instanceof S01PacketJoinGame) {
S01PacketJoinGame packet = (S01PacketJoinGame) p;
allowMovement = true;
int entId = packet.getEntityId();
actualID = entId;
entId = -1789435; // Camera entity id should be negative which is an invalid id and can't be used by servers
int dimension = packet.getDimension();
EnumDifficulty difficulty = packet.getDifficulty();
int maxPlayers = packet.getMaxPlayers();
WorldType worldType = packet.getWorldType();
p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension,
difficulty, maxPlayers, worldType, false);
}
if(p instanceof S07PacketRespawn) {
S07PacketRespawn respawn = (S07PacketRespawn) p;
p = new S07PacketRespawn(respawn.func_149082_c(),
respawn.func_149081_d(), respawn.func_149080_f(), GameType.SPECTATOR);
allowMovement = true;
}
if(p instanceof S08PacketPlayerPosLook) {
if(!hasWorldLoaded) hasWorldLoaded = true;
final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p;
if (mc.currentScreen instanceof GuiDownloadTerrain) {
// Close the world loading screen manually in case we swallow the packet
mc.displayGuiScreen(null);
}
if(replayHandler.shouldSuppressCameraMovements()) return null;
CameraEntity cent = replayHandler.getCameraEntity();
for (Object relative : ppl.func_179834_f()) {
if (relative == S08PacketPlayerPosLook.EnumFlags.X
|| relative == S08PacketPlayerPosLook.EnumFlags.Y
|| relative == S08PacketPlayerPosLook.EnumFlags.Z) {
return null; // At least one of the coordinates is relative, so we don't care
}
}
if(cent != null) {
if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > ReplayMod.TP_DISTANCE_LIMIT) ||
(Math.abs(cent.posZ - ppl.func_148933_e()) > ReplayMod.TP_DISTANCE_LIMIT))) {
return null;
} else {
allowMovement = false;
}
}
new Callable<Void>() {
@Override
@SuppressWarnings("unchecked")
public Void call() {
if (mc.theWorld == null || !mc.isCallingFromMinecraftThread()) {
synchronized(mc.scheduledTasks) {
mc.scheduledTasks.add(ListenableFutureTask.create(this));
}
return null;
}
CameraEntity cent = replayHandler.getCameraEntity();
cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e());
return null;
}
}.call();
}
if(p instanceof S2BPacketChangeGameState) {
S2BPacketChangeGameState pg = (S2BPacketChangeGameState)p;
int reason = pg.func_149138_c();
// only allow the following packets:
// 1 - End raining
// 2 - Begin raining
//
// The following values are to control sky color (e.g. if thunderstorm)
// 7 - Fade value
// 8 - Fade time
if(!(reason == 1 || reason == 2 || reason == 7 || reason == 8)) {
return null;
}
}
if (p instanceof S02PacketChat) {
if (ReplaySettings.ReplayOptions.showChat.getValue() != Boolean.TRUE) {
return null;
}
}
return asyncMode ? processPacketAsync(p) : processPacketSync(p);
}
@Override
@SuppressWarnings("unchecked")
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
ctx.attr(NetworkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
FileUtils.deleteDirectory(tempResourcePackFolder);
super.channelInactive(ctx);
}
/**
* Whether the replay is currently paused.
* @return {@code true} if it is paused, {@code false} otherwise
*/
public boolean paused() {
return mc.timer.timerSpeed == 0;
}
/**
* Returns the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast.
* If 0 is returned, the replay is paused.
* @return speed multiplier
*/
public double getReplaySpeed() {
if(!paused()) return replaySpeed;
else return 0;
}
/**
* Set the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast.
* The speed may not be set to 0 nor to negative values.
* @param d Speed multiplier
*/
public void setReplaySpeed(final double d) {
if(d != 0) this.replaySpeed = d;
mc.timer.timerSpeed = (float) d;
}
/////////////////////////////////////////////////////////
// Asynchronous packet processing //
/////////////////////////////////////////////////////////
/**
* The real time at which the last packet was sent in milliseconds.
*/
private long lastPacketSent;
/**
* There is no waiting performed until a packet with at least this timestamp is reached (but not yet sent).
* If this is -1, then timing is normal.
*/
private long desiredTimeStamp = -1;
/**
* Runnable which performs timed dispatching of packets from the input stream.
*/
private Runnable asyncSender = new Runnable() {
public void run() {
try {
while (ctx == null && !terminate) {
Thread.sleep(10);
}
REPLAY_LOOP:
while (!terminate) {
synchronized (ReplaySender.this) {
if (dis == null) {
dis = new DataInputStream(replayFile.getPacketData());
}
// Packet loop
while (true) {
try {
// When playback is paused and the world has loaded (we don't want any dirt-screens) we sleep
while (paused() && hasWorldLoaded) {
// Unless we are going to terminate, restart or jump
if (terminate || startFromBeginning || desiredTimeStamp != -1) {
break;
}
Thread.sleep(10);
}
if (terminate) {
break REPLAY_LOOP;
}
if (startFromBeginning) {
// In case we need to restart from the beginning
// break out of the loop sending all packets which will
// cause the replay to be restarted by the outer loop
break;
}
// Read the next packet if we don't already have one
if (nextPacket == null) {
nextPacket = ReplayFileIO.readPacketData(dis);
}
int nextTimeStamp = nextPacket.getTimestamp();
// If we aren't jumping and the world has already been loaded (no dirt-screens) then wait
// the required amount to get proper packet timing
if (!isHurrying() && hasWorldLoaded) {
// How much time should have passed
int timeWait = (int) Math.round((nextTimeStamp - lastTimeStamp) / replaySpeed);
// How much time did pass
long timeDiff = System.currentTimeMillis() - lastPacketSent;
// How much time we need to wait to make up for the difference
long timeToSleep = Math.max(0, timeWait - timeDiff);
Thread.sleep(timeToSleep);
lastPacketSent = System.currentTimeMillis();
}
// Process packet
channelRead(ctx, nextPacket.getByteArray());
nextPacket = null;
lastTimeStamp = nextTimeStamp;
// In case we finished jumping
// We need to check that we aren't planing to restart so we don't accidentally run this
// code before we actually restarted
if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) {
desiredTimeStamp = -1;
replayHandler.moveCameraToTargetPosition();
// Pause after jumping
setReplaySpeed(0);
}
} catch (EOFException eof) {
// Reached end of file
// Pause the replay which will cause it to freeze before getting restarted
setReplaySpeed(0);
// Then wait until the user tells us to continue
while (paused() && hasWorldLoaded && desiredTimeStamp == -1 && !terminate) {
Thread.sleep(10);
}
break;
} catch (IOException e) {
e.printStackTrace();
}
}
// Restart the replay.
hasWorldLoaded = false;
lastTimeStamp = 0;
startFromBeginning = false;
nextPacket = null;
lastPacketSent = System.currentTimeMillis();
replayHandler.restartedReplay();
if (dis != null) {
dis.close();
dis = null;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
/**
* Return whether this replay sender is currently rushing. When rushing, all packets are sent without waiting until
* a specified timestamp is passed.
* @return {@code true} if currently rushing, {@code false} otherwise
*/
public boolean isHurrying() {
return desiredTimeStamp != -1;
}
/**
* Cancels the hurrying.
*/
public void stopHurrying() {
desiredTimeStamp = -1;
}
/**
* Return the timestamp to which this replay sender is currently rushing. All packets with an lower or equal
* timestamp will be sent out without any sleeping.
* @return The timestamp in milliseconds since the start of the replay
*/
public long getDesiredTimestamp() {
return desiredTimeStamp;
}
/**
* Jumps to the specified timestamp when in async mode by rushing all packets until one with a timestamp greater
* than the specified timestamp is found.
* If the timestamp has already passed, this causes the replay to restart and then rush all packets.
* @param millis Timestamp in milliseconds since the start of the replay
*/
public void jumpToTime(int millis) {
Preconditions.checkState(asyncMode, "Can only jump in async mode. Use sendPacketsTill(int) instead.");
if(millis < lastTimeStamp && !isHurrying()) {
startFromBeginning = true;
}
desiredTimeStamp = millis;
}
protected Packet processPacketAsync(Packet p) {
//If hurrying, ignore some packets, except for short durations
if(desiredTimeStamp - lastTimeStamp > 1000) {
if(p instanceof S2APacketParticles) return null;
if(p instanceof S0EPacketSpawnObject) {
S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p;
int type = pso.func_148993_l();
if(type == 76) { // Firework rocket
return null;
}
}
}
return p;
}
/////////////////////////////////////////////////////////
// Synchronous packet processing //
/////////////////////////////////////////////////////////
/**
* Sends all packets until the specified timestamp is reached (inclusive).
* If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning.
* @param timestamp The timestamp in milliseconds since the beginning of this replay
*/
public void sendPacketsTill(int timestamp) {
Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead.");
try {
while (ctx == null && !terminate) { // Make sure channel is ready
Thread.sleep(10);
}
synchronized (this) {
if (timestamp < lastTimeStamp) { // Restart the replay if we need to go backwards in time
hasWorldLoaded = false;
lastTimeStamp = 0;
if (dis != null) {
dis.close();
dis = null;
}
startFromBeginning = false;
nextPacket = null;
replayHandler.restartedReplay();
}
if (dis == null) {
dis = new DataInputStream(replayFile.getPacketData());
}
while (true) { // Send packets
try {
PacketData pd;
if (nextPacket != null) {
// If there is still a packet left from before, use it first
pd = nextPacket;
nextPacket = null;
} else {
// Otherwise read one from the input stream
pd = ReplayFileIO.readPacketData(dis);
}
int nextTimeStamp = pd.getTimestamp();
if (nextTimeStamp > timestamp) {
// We are done sending all packets
nextPacket = pd;
break;
}
// Process packet
channelRead(ctx, pd.getByteArray());
// Store last timestamp
lastTimeStamp = nextTimeStamp;
} catch (EOFException eof) {
// Shit! We hit the end before finishing our job! What shall we do now?
// well, let's just pretend we're done...
dis = null;
break;
} catch (IOException e) {
e.printStackTrace();
}
}
// This might be required if we change to async mode anytime soon
lastPacketSent = System.currentTimeMillis();
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected Packet processPacketSync(Packet p) {
return p; // During synchronous playback everything is sent normally
}
/**
* This is necessary to convert packets from old replays to new replays.
* @param packet The packet to be transformed.
* @see #LEGACY_ENTITY_ID
*/
private void convertLegacyEntityIds(Packet packet) {
if (packet instanceof S0CPacketSpawnPlayer) {
S0CPacketSpawnPlayer p = (S0CPacketSpawnPlayer) packet;
if (p.field_148957_a == LEGACY_ENTITY_ID) {
p.field_148957_a = actualID;
}
} else if (packet instanceof S18PacketEntityTeleport) {
S18PacketEntityTeleport p = (S18PacketEntityTeleport) packet;
if (p.field_149458_a == LEGACY_ENTITY_ID) {
p.field_149458_a = actualID;
}
} else if (packet instanceof S14PacketEntity.S17PacketEntityLookMove) {
S14PacketEntity.S17PacketEntityLookMove p = (S14PacketEntity.S17PacketEntityLookMove) packet;
if (p.field_149074_a == LEGACY_ENTITY_ID) {
p.field_149074_a = actualID;
}
} else if (packet instanceof S19PacketEntityHeadLook) {
S19PacketEntityHeadLook p = (S19PacketEntityHeadLook) packet;
if (p.field_149384_a == LEGACY_ENTITY_ID) {
p.field_149384_a = actualID;
}
} else if (packet instanceof S12PacketEntityVelocity) {
S12PacketEntityVelocity p = (S12PacketEntityVelocity) packet;
if (p.field_149417_a == LEGACY_ENTITY_ID) {
p.field_149417_a = actualID;
}
} else if (packet instanceof S0BPacketAnimation) {
S0BPacketAnimation p = (S0BPacketAnimation) packet;
if (p.entityId == LEGACY_ENTITY_ID) {
p.entityId = actualID;
}
} else if (packet instanceof S04PacketEntityEquipment) {
S04PacketEntityEquipment p = (S04PacketEntityEquipment) packet;
if (p.field_149394_a == LEGACY_ENTITY_ID) {
p.field_149394_a = actualID;
}
} else if (packet instanceof S1BPacketEntityAttach) {
S1BPacketEntityAttach p = (S1BPacketEntityAttach) packet;
if (p.field_149408_a == LEGACY_ENTITY_ID) {
p.field_149408_a = actualID;
}
if (p.field_149406_b == LEGACY_ENTITY_ID) {
p.field_149406_b = actualID;
}
} else if (packet instanceof S0DPacketCollectItem) {
S0DPacketCollectItem p = (S0DPacketCollectItem) packet;
if (p.field_149356_b == LEGACY_ENTITY_ID) {
p.field_149356_b = actualID;
}
} else if (packet instanceof S13PacketDestroyEntities) {
S13PacketDestroyEntities p = (S13PacketDestroyEntities) packet;
if (p.field_149100_a.length == 1 && p.field_149100_a[0] == LEGACY_ENTITY_ID) {
p.field_149100_a[0] = actualID;
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.replaymod.replay;
import com.replaymod.core.SettingsRegistry;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true);
public static final Setting<Boolean> RECORD_SERVER = make("recordServer", "recordserver", true);
public static final Setting<Boolean> INDICATOR = make("indicator", "indicator", true);
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>(key, displayName, defaultValue);
}
public Setting(String key, String displayString, T defaultValue) {
super("recording", key, "replaymod.gui.settings." + displayString, defaultValue);
}
}

View File

@@ -0,0 +1,24 @@
package com.replaymod.replay.events;
import com.replaymod.replay.ReplayHandler;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minecraftforge.fml.common.eventhandler.Event;
@RequiredArgsConstructor
public abstract class ReplayCloseEvent extends Event {
@Getter
private final ReplayHandler replayHandler;
public static class Pre extends ReplayCloseEvent {
public Pre(ReplayHandler replayHandler) {
super(replayHandler);
}
}
public static class Post extends ReplayCloseEvent {
public Post(ReplayHandler replayHandler) {
super(replayHandler);
}
}
}

View File

@@ -0,0 +1,24 @@
package com.replaymod.replay.events;
import com.replaymod.replay.ReplayHandler;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minecraftforge.fml.common.eventhandler.Event;
@RequiredArgsConstructor
public abstract class ReplayOpenEvent extends Event {
@Getter
private final ReplayHandler replayHandler;
public static class Pre extends ReplayOpenEvent {
public Pre(ReplayHandler replayHandler) {
super(replayHandler);
}
}
public static class Post extends ReplayOpenEvent {
public Post(ReplayHandler replayHandler) {
super(replayHandler);
}
}
}

View File

@@ -0,0 +1,182 @@
package com.replaymod.replay.gui.overlay;
import de.johni0702.replaystudio.data.Marker;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.gui.elements.timelines.GuiTimeline;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import com.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.utils.MouseUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import org.lwjgl.util.Point;
import java.awt.*;
public class GuiMarkerTimeline extends GuiTimeline {
private static final int KEYFRAME_MARKER_X = 109;
private static final int KEYFRAME_MARKER_Y = 20;
private final ReplayHandler replayHandler;
private Marker selectedMarker;
private Marker clickedMarker;
private long clickTime;
private boolean dragging;
public GuiMarkerTimeline(ReplayHandler replayHandler, int positionX, int positionY, int width, int height) {
super(positionX, positionY, width, height);
this.replayHandler = replayHandler;
this.showMarkers = false;
}
@Override
public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) {
if(!enabled) return false;
long time = getTimeAt(mouseX, mouseY);
if(time == -1) {
return false;
}
int tolerance = (int) (2 * Math.round(zoom * timelineLength / width));
Marker closest = null;
if(mouseY >= positionY + BORDER_TOP + 10) {
long distance = tolerance;
for (Marker m : replayHandler.getMarkers()) {
long d = Math.abs(m.getTime() - time);
if (d <= distance) {
closest = m;
distance = d;
}
}
}
//left mouse button
if(button == 0) {
if(closest == null) { //if no keyframe clicked, jump in time
replayHandler.doJump((int) getTimeAt(mouseX, mouseY), true);
} else {
selectedMarker = closest;
// If we clicked on a key frame, then continue monitoring the mouse for movements
long currentTime = System.currentTimeMillis();
if(currentTime - clickTime < 500) { // if double clicked then open GUI instead
// mc.displayGuiScreen(GuiEditKeyframe.create(closest));
// TODO Edit Gui
this.clickedMarker = null;
} else {
this.clickedMarker = closest;
this.dragging = false;
}
this.clickTime = currentTime;
}
} else if(button == 1) {
if(closest != null) {
//Jump to clicked Marker Keyframe (explicitly force to jump to this position)
replayHandler.setTargetPosition(new AdvancedPosition(
closest.getX(), closest.getY(), closest.getZ(),
closest.getPitch(), closest.getYaw(), closest.getRoll()
));
//perform the jump, telling the Overlay not to override the last position value
replayHandler.doJump(closest.getTime(), false);
}
}
return isHovering(mouseX, mouseY);
}
@Override
public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) {
if(!enabled) return;
long time = getTimeAt(mouseX, mouseY);
if (time != -1) {
if (clickedMarker != null) {
int tolerance = (int) (2 * Math.round(zoom * timelineLength / width));
if (dragging || Math.abs(clickedMarker.getTime() - time) > tolerance) {
clickedMarker.setTime((int) time);
dragging = true;
}
}
}
}
@Override
public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) {
mouseDrag(mc, mouseX, mouseY, button);
clickedMarker = null;
if (dragging) {
replayHandler.saveMarkers();
dragging = false;
}
}
@Override
public void draw(Minecraft mc, int mouseX, int mouseY) {
super.draw(mc, mouseX, mouseY);
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
long leftTime = Math.round(timeStart * timelineLength);
long rightTime = Math.round((timeStart + zoom) * timelineLength);
double segmentLength = timelineLength * zoom;
drawTimelineCursor(leftTime, rightTime, bodyWidth);
//Draw Keyframe logos
for (Marker marker : replayHandler.getMarkers()) {
drawMarker(marker, bodyWidth, leftTime, rightTime, segmentLength);
}
}
private int getMarkerX(int timestamp, long leftTime, int bodyWidth, double segmentLength) {
long positionInSegment = timestamp - leftTime;
double fractionOfSegment = positionInSegment / segmentLength;
return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth);
}
@Override
public void drawOverlay(Minecraft mc, int mouseX, int mouseY) {
boolean drawn = false;
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
long leftTime = Math.round(timeStart * timelineLength);
double segmentLength = timelineLength * zoom;
for (Marker marker : replayHandler.getMarkers()) {
int markerX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength);
if(MouseUtils.isMouseWithinBounds(markerX - 2, this.positionY + BORDER_TOP + 10 + 1, 5, 5)) {
Point mouse = MouseUtils.getMousePos();
String markerName = marker.getName();
if(markerName == null || markerName.isEmpty()) markerName = I18n.format("replaymod.gui.ingame.unnamedmarker");
ReplayMod.tooltipRenderer.drawTooltip(mouse.getX(), mouse.getY(), markerName, null, Color.WHITE);
drawn = true;
}
}
if(!drawn) {
super.drawOverlay(mc, mouseX, mouseY);
}
}
private void drawMarker(Marker marker, int bodyWidth, long leftTime, long rightTime, double segmentLength) {
if (marker.getTime() <= rightTime && marker.getTime() >= leftTime) {
int textureX = KEYFRAME_MARKER_X;
int textureY = KEYFRAME_MARKER_Y;
int y = positionY+10;
int keyframeX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength);
if (selectedMarker == marker) {
textureX += 5;
}
rect(keyframeX - 2, y + BORDER_TOP, textureX, textureY, 5, 5);
}
}
}

View File

@@ -0,0 +1,140 @@
package com.replaymod.replay.gui.overlay;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplaySender;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiSlider;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.advanced.GuiTimeline;
import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.GameSettings;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.InputEvent;
import org.lwjgl.util.ReadableDimension;
import org.lwjgl.util.ReadablePoint;
import org.lwjgl.util.WritablePoint;
import static com.replaymod.core.ReplayMod.TEXTURE_SIZE;
public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
private final ReplayHandler replayHandler;
public final GuiPanel topPanel = new GuiPanel(this)
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5));
public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE);
public final GuiSlider speedSlider = new GuiSlider().setSize(100, 20).setSteps(37); // 0.0 is not included
public final GuiTimeline timeline = new GuiTimeline(){
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
setCursorPosition(replayHandler.getReplaySender().currentTimeStamp());
super.draw(renderer, size, renderInfo);
}
}.setSize(Integer.MAX_VALUE, 20);
public GuiReplayOverlay(final ReplayHandler replayHandler) {
this.replayHandler = replayHandler;
topPanel.addElements(null, playPauseButton, speedSlider, timeline);
setLayout(new CustomLayout<GuiReplayOverlay>() {
@Override
protected void layout(GuiReplayOverlay container, int width, int height) {
pos(topPanel, 10, 10);
size(topPanel, width - 20, 20);
}
});
playPauseButton.setTexturePosH(new ReadablePoint() {
@Override
public int getX() {
return 0;
}
@Override
public int getY() {
return replayHandler.getReplaySender().paused() ? 0 : 20;
}
@Override
public void getLocation(WritablePoint dest) {
dest.setLocation(getX(), getY());
}
}).onClick(new Runnable() {
@Override
public void run() {
ReplaySender replaySender = replayHandler.getReplaySender();
// If currently paused
if (replaySender.paused()) {
// then play
replaySender.setReplaySpeed(getSpeed());
} else {
// else pause
replaySender.setReplaySpeed(0);
}
}
});
speedSlider.onValueChanged(new Runnable() {
@Override
public void run() {
double speed = getSpeed();
speedSlider.setText(I18n.format("replaymod.gui.speed") + ": " + speed + "x");
ReplaySender replaySender = replayHandler.getReplaySender();
if (!replaySender.paused()) {
replaySender.setReplaySpeed(speed);
}
}
}).setValue(9);
timeline.onClick(new IGuiTimeline.OnClick() {
@Override
public void run(int time) {
replayHandler.doJump(time, true);
}
}).setLength(replayHandler.getReplaySender().replayLength());
}
private double getSpeed() {
int value = speedSlider.getValue() + 1;
if (value <= 9) {
return value / 10d;
} else {
return 1 + (0.25d * (value - 10));
}
}
@Override
public void setVisible(boolean visible) {
if (isVisible() != visible) {
if (visible) {
FMLCommonHandler.instance().bus().register(this);
} else {
FMLCommonHandler.instance().bus().unregister(this);
}
}
super.setVisible(visible);
}
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event) {
GameSettings gameSettings = getMinecraft().gameSettings;
while (gameSettings.keyBindChat.isPressed() || gameSettings.keyBindCommand.isPressed()) {
if (!isMouseVisible()) {
setMouseVisible(true);
}
}
}
@Override
protected GuiReplayOverlay getThis() {
return this;
}
}

View File

@@ -0,0 +1,100 @@
package com.replaymod.replay.gui.screen;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.client.resources.I18n;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.lwjgl.input.Keyboard;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class GuiRenameReplay extends GuiScreen {
private GuiScreen parent;
private GuiTextField replayNameInput;
private File file;
public GuiRenameReplay(GuiScreen parent, File file) {
this.parent = parent;
this.file = file;
}
@Override
public void updateScreen() {
this.replayNameInput.updateCursorCounter();
}
@Override
public void initGui() {
Keyboard.enableRepeatEvents(true);
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = this.buttonList;
buttonList.clear();
buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 96 + 12, I18n.format("replaymod.gui.rename")));
buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 4 + 120 + 12, I18n.format("replaymod.gui.cancel")));
String s = FilenameUtils.getBaseName(file.getAbsolutePath());
this.replayNameInput = new GuiTextField(2, this.fontRendererObj, this.width / 2 - 100, 60, 200, 20);
this.replayNameInput.setFocused(true);
this.replayNameInput.setText(s);
}
@Override
public void onGuiClosed() {
Keyboard.enableRepeatEvents(false);
}
@Override
protected void actionPerformed(GuiButton button) throws IOException {
if(button.enabled) {
if(button.id == 1) {
this.mc.displayGuiScreen(this.parent);
} else if(button.id == 0) {
File folder = ReplayFileIO.getReplayFolder();
File initRenamed = new File(folder, (this.replayNameInput.getText().trim() + ".zip").replaceAll("[^a-zA-Z0-9\\.\\- ]", "_"));
File renamed = ReplayFileIO.getNextFreeFile(initRenamed);
try {
FileUtils.moveFile(file, renamed);
} catch (IOException e) {
e.printStackTrace();
mc.displayGuiScreen(new GuiErrorScreen(
I18n.format("replaymod.gui.viewer.delete.failed1"),
I18n.format("replaymod.gui.viewer.delete.failed2")
));
return;
}
this.mc.displayGuiScreen(this.parent);
}
}
}
@Override
protected void keyTyped(char typedChar, int keyCode) throws IOException {
this.replayNameInput.textboxKeyTyped(typedChar, keyCode);
((GuiButton) this.buttonList.get(0)).enabled = this.replayNameInput.getText().trim().length() > 0;
if(keyCode == 28 || keyCode == 156) {
this.actionPerformed((GuiButton) this.buttonList.get(0));
}
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
super.mouseClicked(mouseX, mouseY, mouseButton);
this.replayNameInput.mouseClicked(mouseX, mouseY, mouseButton);
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
this.drawDefaultBackground();
this.drawCenteredString(this.fontRendererObj, I18n.format("replaymod.gui.viewer.rename.title"), this.width / 2, 20, 16777215);
this.drawString(this.fontRendererObj, I18n.format("replaymod.gui.viewer.rename.name"), this.width / 2 - 100, 47, 10526880);
this.replayNameInput.drawTextBox();
super.drawScreen(mouseX, mouseY, partialTicks);
}
}

View File

@@ -0,0 +1,360 @@
package com.replaymod.replay.gui.screen;
import com.google.common.base.Optional;
import com.mojang.realmsclient.util.Pair;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.replay.ReplayModReplay;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ReplayMetaData;
import de.johni0702.replaystudio.replay.ZipReplayFile;
import de.johni0702.replaystudio.studio.ReplayStudio;
import eu.crushedpixel.replaymod.api.replay.holders.FileInfo;
import eu.crushedpixel.replaymod.gui.elements.GuiLoadingListEntry;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListEntry;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended;
import eu.crushedpixel.replaymod.gui.online.GuiUploadFile;
import eu.crushedpixel.replaymod.registry.ResourceHelper;
import eu.crushedpixel.replaymod.utils.ImageUtils;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiYesNo;
import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.Util;
import net.minecraftforge.fml.common.FMLLog;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback {
private static final int LOAD_BUTTON_ID = 9001;
private static final int UPLOAD_BUTTON_ID = 9002;
private static final int FOLDER_BUTTON_ID = 9003;
private static final int RENAME_BUTTON_ID = 9004;
private static final int DELETE_BUTTON_ID = 9005;
private static final int SETTINGS_BUTTON_ID = 9006;
private static final int CANCEL_BUTTON_ID = 9007;
private final ReplayModReplay mod;
private boolean initialized;
private GuiReplayListExtended replayGuiList;
private List<Pair<Pair<File, ReplayMetaData>, File>> replayFileList = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>();
private GuiButton loadButton;
private GuiButton uploadButton;
private GuiButton renameButton;
private GuiButton deleteButton;
private boolean delete_file = false;
private Queue<Runnable> loadedReplaysQueue = new ConcurrentLinkedQueue<Runnable>();
private Thread fileReloader;
public GuiReplayViewer(ReplayModReplay mod) {
this.mod = mod;
}
private class FileReloaderThread extends Thread {
private final GuiLoadingListEntry loadingListEntry = new GuiLoadingListEntry();
@Override
public synchronized void start() {
replayFileList = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>();
replayGuiList.clearEntries();
replayGuiList.addEntry(loadingListEntry);
super.start();
}
@Override
public void run() {
for(final File file : ReplayFileIO.getAllReplayFiles()) {
if(interrupted()) break;
try {
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file);
final ReplayMetaData metaData = replayFile.getMetaData();
Optional<BufferedImage> thumb = replayFile.getThumb();
replayFile.close();
File tmp = null;
if(thumb.isPresent()) {
BufferedImage img = ImageUtils.scaleImage(thumb.get(), new Dimension(1280, 720));
tmp = File.createTempFile(FilenameUtils.getBaseName(file.getAbsolutePath())+"_THUMBNAIL", "jpg");
tmp.deleteOnExit();
ImageIO.write(img, "jpg", tmp);
}
final File thumbFile = tmp;
loadedReplaysQueue.offer(new Runnable() {
@Override
public void run() {
addEntry(file, metaData, thumbFile);
}
});
} catch(Exception e) {
FMLLog.getLogger().error("Could not load Replay File "+file.getName(), e);
}
}
loadedReplaysQueue.offer(new Runnable() {
@Override
public void run() {
replayGuiList.removeEntry(loadingListEntry);
}
});
}
}
public static GuiYesNo getYesNoGui(GuiYesNoCallback p_152129_0_, String file, int p_152129_2_) {
String s1 = I18n.format("replaymod.gui.viewer.delete.linea");
String s2 = "\'" + file + "\' " + I18n.format("replaymod.gui.viewer.delete.lineb");
String s3 = I18n.format("replaymod.gui.delete");
String s4 = I18n.format("replaymod.gui.cancel");
return new GuiYesNo(p_152129_0_, s1, s2, s3, s4, p_152129_2_);
}
@Override
public void onGuiClosed() {
ResourceHelper.freeAllResources();
super.onGuiClosed();
}
@Override
@SuppressWarnings("deprecation")
public void initGui() {
Keyboard.enableRepeatEvents(true);
if(!this.initialized) {
replayGuiList = new ReplayList(this, this.mc, this.width, this.height, 32, this.height - 64, 36);
this.initialized = true;
} else {
this.replayGuiList.setDimensions(this.width, this.height, 32, this.height - 64);
}
try {
if(fileReloader != null) {
fileReloader.interrupt();
fileReloader.join();
}
fileReloader = new FileReloaderThread();
fileReloader.start();
} catch(Exception e) {
e.printStackTrace();
}
this.createButtons();
}
private void createButtons() {
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = this.buttonList;
buttonList.add(loadButton = new GuiButton(LOAD_BUTTON_ID, this.width / 2 - 154, this.height - 52, 73, 20, I18n.format("replaymod.gui.load")));
buttonList.add(uploadButton = new GuiButton(UPLOAD_BUTTON_ID, this.width / 2 - 154 + 78, this.height - 52, 73, 20, I18n.format("replaymod.gui.upload")));
buttonList.add(new GuiButton(FOLDER_BUTTON_ID, this.width / 2 + 4, this.height - 52, 150, 20, I18n.format("replaymod.gui.viewer.replayfolder")));
buttonList.add(renameButton = new GuiButton(RENAME_BUTTON_ID, this.width / 2 - 154, this.height - 28, 72, 20, I18n.format("replaymod.gui.rename")));
buttonList.add(deleteButton = new GuiButton(DELETE_BUTTON_ID, this.width / 2 - 76, this.height - 28, 72, 20, I18n.format("replaymod.gui.delete")));
buttonList.add(new GuiButton(SETTINGS_BUTTON_ID, this.width / 2 + 4, this.height - 28, 72, 20, I18n.format("replaymod.gui.settings")));
buttonList.add(new GuiButton(CANCEL_BUTTON_ID, this.width / 2 + 4 + 78, this.height - 28, 72, 20, I18n.format("replaymod.gui.cancel")));
setButtonsEnabled(false);
}
@Override
public void handleMouseInput() throws IOException {
super.handleMouseInput();
this.replayGuiList.handleMouseInput();
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
super.mouseClicked(mouseX, mouseY, mouseButton);
this.replayGuiList.mouseClicked(mouseX, mouseY, mouseButton);
}
@Override
protected void mouseReleased(int mouseX, int mouseY, int state) {
super.mouseReleased(mouseX, mouseY, state);
this.replayGuiList.mouseReleased(mouseX, mouseY, state);
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
this.drawDefaultBackground();
this.replayGuiList.drawScreen(mouseX, mouseY, partialTicks);
this.drawCenteredString(this.fontRendererObj, I18n.format("replaymod.gui.replayviewer"), this.width / 2, 20, 16777215);
super.drawScreen(mouseX, mouseY, partialTicks);
if(uploadButton.isMouseOver() && !uploadButton.enabled && loadButton.enabled) {
if(currentFileUploaded) {
ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.viewer.alreadyuploaded"), this, Color.RED);
}
}
}
@Override
protected void actionPerformed(GuiButton button) throws IOException {
if(button.enabled) {
if(button.id == LOAD_BUTTON_ID) {
loadReplay(replayGuiList.selected);
} else if(button.id == CANCEL_BUTTON_ID) {
mc.displayGuiScreen(null);
} else if(button.id == DELETE_BUTTON_ID) {
String s = ((GuiReplayListEntry)replayGuiList.getListEntry(replayGuiList.selected)).getFileInfo().getName();
if(s != null) {
delete_file = true;
GuiYesNo guiyesno = getYesNoGui(this, s, 1);
this.mc.displayGuiScreen(guiyesno);
}
} else if(button.id == SETTINGS_BUTTON_ID) {
new GuiReplaySettings(this, ReplayMod.instance.getSettingsRegistry()).display();
} else if(button.id == RENAME_BUTTON_ID) {
File file = replayFileList.get(replayGuiList.selected).first().first();
this.mc.displayGuiScreen(new GuiRenameReplay(this, file));
} else if(button.id == UPLOAD_BUTTON_ID) {
File file = replayFileList.get(replayGuiList.selected).first().first();
this.mc.displayGuiScreen(new GuiUploadFile(file, this));
} else if(button.id == FOLDER_BUTTON_ID) {
File file1 = ReplayFileIO.getReplayFolder();
String s = file1.getAbsolutePath();
if(Util.getOSType() == Util.EnumOS.OSX) {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", s});
return;
} catch (IOException e) {
LogManager.getLogger().error("Cannot open file", e);
}
} else if(Util.getOSType() == Util.EnumOS.WINDOWS) {
String s1 = String.format("cmd.exe /C start \"Open file\" \"%s\"", s);
try {
Runtime.getRuntime().exec(s1);
return;
} catch(IOException e) {
LogManager.getLogger().error("Cannot open file", e);
}
}
boolean flag = false;
try {
Desktop.getDesktop().browse(file1.toURI());
} catch(Throwable throwable) {
flag = true;
}
if(flag) {
Sys.openURL("file://" + s);
}
}
}
}
@Override
public void confirmClicked(boolean result, int id) {
if(this.delete_file) {
this.delete_file = false;
if(result) {
try {
FileUtils.forceDelete(replayFileList.get(replayGuiList.selected).first().first());
} catch (IOException e) {
e.printStackTrace();
}
replayFileList.remove(replayGuiList.selected);
replayGuiList.selected = -1;
}
this.mc.displayGuiScreen(this);
}
}
@Override
public void updateScreen() {
super.updateScreen();
while (!loadedReplaysQueue.isEmpty()) {
loadedReplaysQueue.poll().run();
}
}
private boolean currentFileUploaded = false;
public void setButtonsEnabled(boolean b) {
loadButton.enabled = b;
if(b) {
currentFileUploaded = ReplayMod.uploadedFileHandler.isUploaded(replayFileList.get(replayGuiList.selected).first().first());
uploadButton.enabled = !currentFileUploaded;
} else {
uploadButton.enabled = false;
}
renameButton.enabled = b;
deleteButton.enabled = b;
}
public void loadReplay(int id) {
mc.displayGuiScreen(null);
try {
mod.startReplay(replayFileList.get(id).first().first());
} catch(Exception e) {
e.printStackTrace();
}
}
private void addEntry(File file, ReplayMetaData metaData, File thumb) {
final Pair<Pair<File, ReplayMetaData>, File> p = Pair.of(Pair.of(file, metaData), thumb);
final int index = getInsertionIndex(p, replayFileList);
replayFileList.add(index, p);
final FileInfo fileInfo = new FileInfo(-1, p.first().second(), null, null,
-1, -1, -1, FilenameUtils.getBaseName(p.first().first().getName()), true, -1);
replayGuiList.addEntry(index, new GuiReplayListEntry(replayGuiList, fileInfo, p.second()));
}
private static FileAgeComparator fileAgeComparator = new FileAgeComparator();
public static class FileAgeComparator implements Comparator<Pair<Pair<File, ReplayMetaData>, File>> {
@Override
public int compare(Pair<Pair<File, ReplayMetaData>, File> o1, Pair<Pair<File, ReplayMetaData>, File> o2) {
try {
return new Date(o2.first().second().getDate()).compareTo(new Date(o1.first().second().getDate()));
} catch(Exception e) {
return 0;
}
}
}
private int getInsertionIndex(Pair<Pair<File, ReplayMetaData>, File> p, List<Pair<Pair<File, ReplayMetaData>, File>> list) {
List<Pair<Pair<File, ReplayMetaData>, File>> nl = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>(list);
nl.add(p);
Collections.sort(nl, fileAgeComparator);
return nl.indexOf(p);
}
}

View File

@@ -0,0 +1,29 @@
package com.replaymod.replay.gui.screen;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended;
import net.minecraft.client.Minecraft;
public class ReplayList extends GuiReplayListExtended {
private GuiReplayViewer parent;
public ReplayList(GuiReplayViewer parent, Minecraft mcIn,
int p_i45010_2_, int p_i45010_3_, int p_i45010_4_, int p_i45010_5_,
int p_i45010_6_) {
super(mcIn, p_i45010_2_, p_i45010_3_, p_i45010_4_, p_i45010_5_,
p_i45010_6_);
this.parent = parent;
}
@Override
protected void elementClicked(int slotIndex, boolean isDoubleClick,
int mouseX, int mouseY) {
super.elementClicked(slotIndex, isDoubleClick, mouseX, mouseY);
parent.setButtonsEnabled(true);
if(isDoubleClick) {
parent.loadReplay(slotIndex);
}
}
}

View File

@@ -0,0 +1,111 @@
package com.replaymod.replay.handler;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class GuiHandler {
private static final int BUTTON_EXIT_SERVER = 1;
private static final int BUTTON_RETURN_TO_GAME = 4;
private static final int BUTTON_ACHIEVEMENTS = 5;
private static final int BUTTON_STATS = 6;
private static final int BUTTON_OPEN_TO_LAN = 7;
private static final int BUTTON_REPLAY_VIEWER = 17890234;
private static final int BUTTON_EXIT_REPLAY = 17890235;
private static final Minecraft mc = Minecraft.getMinecraft();
private final ReplayModReplay mod;
public GuiHandler(ReplayModReplay mod) {
this.mod = mod;
}
public void register() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void injectIntoIngameMenu(GuiScreenEvent.InitGuiEvent.Post event) {
if (!(event.gui instanceof GuiIngameMenu)) {
return;
}
if (mod.getReplayHandler() != null) {
// Pause replay when menu is opened
mod.getReplayHandler().getReplaySender().setReplaySpeed(0);
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
for(GuiButton b : new ArrayList<>(buttonList)) {
switch (b.id) {
// Replace "Exit Server" button with "Exit Replay" button
case BUTTON_EXIT_SERVER:
b.displayString = I18n.format("replaymod.gui.exit");
b.id = BUTTON_EXIT_REPLAY;
break;
// Remove "Achievements", "Stats" and "Open to LAN" buttons
case BUTTON_ACHIEVEMENTS:
case BUTTON_STATS:
case BUTTON_OPEN_TO_LAN:
buttonList.remove(b);
}
// Move all buttons except the "Return to game" button upwards
if (b.id != BUTTON_RETURN_TO_GAME) {
b.yPosition -= 48;
}
}
}
}
@SubscribeEvent
public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
if (!(event.gui instanceof GuiMainMenu)) {
return;
}
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
GuiButton button = new GuiButton(BUTTON_REPLAY_VIEWER, event.gui.width / 2 - 100,
event.gui.height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayviewer"));
button.width = button.width / 2 - 2;
buttonList.add(button);
}
@SubscribeEvent
public void onButton(GuiScreenEvent.ActionPerformedEvent event) {
if(!event.button.enabled) return;
if (event.gui instanceof GuiMainMenu) {
if (event.button.id == BUTTON_REPLAY_VIEWER) {
mc.displayGuiScreen(new GuiReplayViewer(mod));
}
}
if (event.gui instanceof GuiIngameMenu && mod.getReplayHandler() != null) {
if (event.button.id == BUTTON_EXIT_REPLAY) {
event.button.enabled = false;
try {
mod.getReplayHandler().endReplay();
} catch (IOException e) {
e.printStackTrace();
}
mc.displayGuiScreen(new GuiMainMenu());
}
}
}
}