diff --git a/ReplayStudio b/ReplayStudio index 66ab0010..be55f610 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit 66ab00102ff3578884339ebd64eb45ade44801f0 +Subproject commit be55f610ac8ccdbc4b476a0c713344c3712a6275 diff --git a/jGui b/jGui index 7849fb3b..5c0ca7d7 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 7849fb3bb7983c029b0da1962446f9a2d75eac19 +Subproject commit 5c0ca7d7d800c52898eccb6b854c7dec17a34338 diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java index 3537ca21..9d792864 100644 --- a/src/main/java/com/replaymod/core/KeyBindingRegistry.java +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -9,6 +9,7 @@ import net.minecraft.util.ReportedException; import net.minecraftforge.fml.client.registry.ClientRegistry; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.InputEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; import org.lwjgl.input.Keyboard; import java.util.Collection; @@ -19,16 +20,25 @@ import java.util.Map; public class KeyBindingRegistry { private Map keyBindings = new HashMap(); private Multimap keyBindingHandlers = ArrayListMultimap.create(); + private Multimap repeatedKeyBindingHandlers = ArrayListMultimap.create(); private Multimap rawHandlers = ArrayListMultimap.create(); public void registerKeyBinding(String name, int keyCode, Runnable whenPressed) { + keyBindingHandlers.put(registerKeyBinding(name, keyCode), whenPressed); + } + + public void registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed) { + repeatedKeyBindingHandlers.put(registerKeyBinding(name, keyCode), whenPressed); + } + + private KeyBinding registerKeyBinding(String name, int keyCode) { KeyBinding keyBinding = keyBindings.get(name); if (keyBinding == null) { keyBinding = new KeyBinding(name, keyCode, "replaymod.title"); keyBindings.put(name, keyBinding); ClientRegistry.registerKeyBinding(keyBinding); } - keyBindingHandlers.put(keyBinding, whenPressed); + return keyBinding; } public void registerRaw(int keyCode, Runnable whenPressed) { @@ -45,20 +55,38 @@ public class KeyBindingRegistry { handleRaw(); } + @SubscribeEvent + public void onTick(TickEvent.RenderTickEvent event) { + if (event.phase != TickEvent.Phase.START) return; + handleRepeatedKeyBindings(); + } + + public void handleRepeatedKeyBindings() { + for (Map.Entry> entry : repeatedKeyBindingHandlers.asMap().entrySet()) { + if (entry.getKey().isKeyDown()) { + invokeKeyBindingHandlers(entry.getKey(), entry.getValue()); + } + } + } + public void handleKeyBindings() { for (Map.Entry> entry : keyBindingHandlers.asMap().entrySet()) { while (entry.getKey().isPressed()) { - for (final Runnable runnable : entry.getValue()) { - try { - runnable.run(); - } catch (Throwable cause) { - CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding"); - CrashReportCategory category = crashReport.makeCategory("Key Binding"); - category.addCrashSection("Key Binding", entry.getKey()); - category.setDetail("Handler", runnable::toString); - throw new ReportedException(crashReport); - } - } + invokeKeyBindingHandlers(entry.getKey(), entry.getValue()); + } + } + } + + private void invokeKeyBindingHandlers(KeyBinding keyBinding, Collection handlers) { + for (final Runnable runnable : handlers) { + try { + runnable.run(); + } catch (Throwable cause) { + CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding"); + CrashReportCategory category = crashReport.makeCategory("Key Binding"); + category.addCrashSection("Key Binding", keyBinding); + category.setDetail("Handler", runnable::toString); + throw new ReportedException(crashReport); } } } diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index de95d13e..d028b85b 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -82,7 +82,8 @@ public class ReplayMod { } public File getReplayFolder() throws IOException { - File folder = new File(getSettingsRegistry().get(Setting.RECORDING_PATH)); + String path = getSettingsRegistry().get(Setting.RECORDING_PATH); + File folder = new File(path.startsWith("./") ? getMinecraft().mcDataDir : null, path); FileUtils.forceMkdir(folder); return folder; } diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java index e4b54459..9bf4f3ee 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java @@ -3,6 +3,7 @@ package com.replaymod.extras.playeroverview; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.extras.Extra; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.camera.CameraEntity; @@ -39,6 +40,16 @@ public class PlayerOverview implements Extra { return !(input instanceof CameraEntity); // Exclude the camera entity } }); + if (!Utils.isCtrlDown()) { + // Hide all players that have an UUID v2 (commonly used for NPCs) + Iterator iter = players.iterator(); + while (iter.hasNext()) { + UUID uuid = iter.next().getGameProfile().getId(); + if (uuid != null && uuid.version() == 2) { + iter.remove(); + } + } + } new PlayerOverviewGui(PlayerOverview.this, players).display(); } } diff --git a/src/main/java/com/replaymod/online/ReplayModOnline.java b/src/main/java/com/replaymod/online/ReplayModOnline.java index e2119e60..70071ac7 100644 --- a/src/main/java/com/replaymod/online/ReplayModOnline.java +++ b/src/main/java/com/replaymod/online/ReplayModOnline.java @@ -24,6 +24,8 @@ import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; +import static net.minecraft.client.Minecraft.getMinecraft; + @Mod(modid = ReplayModOnline.MOD_ID, useMetadata = true) public class ReplayModOnline { public static final String MOD_ID = "replaymod-online"; @@ -101,7 +103,8 @@ public class ReplayModOnline { } public File getDownloadsFolder() { - return new File(core.getSettingsRegistry().get(Setting.DOWNLOAD_PATH)); + String path = core.getSettingsRegistry().get(Setting.DOWNLOAD_PATH); + return new File(path.startsWith("./") ? getMinecraft().mcDataDir : null, path); } public File getDownloadedFile(int id) { diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java index 8cdc8b5c..b7db8327 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java @@ -3,6 +3,7 @@ package com.replaymod.recording.mixin; import com.replaymod.recording.handler.RecordingEventHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.client.network.NetworkPlayerInfo; import net.minecraft.network.play.server.SPacketPlayerListItem; import net.minecraft.network.play.server.SPacketRespawn; import org.spongepowered.asm.mixin.Mixin; @@ -11,12 +12,18 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Map; +import java.util.UUID; + @Mixin(NetHandlerPlayClient.class) public abstract class MixinNetHandlerPlayClient { @Shadow private Minecraft gameController; + @Shadow + private Map playerInfoMap; + public RecordingEventHandler getRecordingEventHandler() { return ((RecordingEventHandler.RecordingEventSender) gameController.renderGlobal).getRecordingEventHandler(); } @@ -28,12 +35,17 @@ public abstract class MixinNetHandlerPlayClient { * @param packet The packet * @param ci Callback info */ - @Inject(method = "handlePlayerListItem", at=@At("RETURN")) + @Inject(method = "handlePlayerListItem", at=@At("HEAD")) public void recordOwnJoin(SPacketPlayerListItem packet, CallbackInfo ci) { + if (gameController.thePlayer == null) return; + RecordingEventHandler handler = getRecordingEventHandler(); if (handler != null && packet.getAction() == SPacketPlayerListItem.Action.ADD_PLAYER) { for (SPacketPlayerListItem.AddPlayerData data : packet.getEntries()) { - if (data.getProfile().getId().equals(gameController.thePlayer.getGameProfile().getId())) { + if (data.getProfile() == null || data.getProfile().getId() == null) continue; + // Only add spawn packet for our own player and only if he isn't known yet + if (data.getProfile().getId().equals(Minecraft.getMinecraft().thePlayer.getGameProfile().getId()) + && !playerInfoMap.containsKey(data.getProfile().getId())) { handler.onPlayerJoin(); } } diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index ca213b29..d74bc125 100755 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -132,6 +132,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter { synchronized (replayFile) { try { replayFile.save(); + replayFile.close(); } catch (IOException e) { logger.error("Saving replay file:", e); } diff --git a/src/main/java/com/replaymod/render/ReplayModRender.java b/src/main/java/com/replaymod/render/ReplayModRender.java index ade88210..6ddb36ae 100644 --- a/src/main/java/com/replaymod/render/ReplayModRender.java +++ b/src/main/java/com/replaymod/render/ReplayModRender.java @@ -1,11 +1,17 @@ package com.replaymod.render; import com.replaymod.core.ReplayMod; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Logger; +import java.io.File; +import java.io.IOException; + @Mod(modid = ReplayModRender.MOD_ID, useMetadata = true) public class ReplayModRender { public static final String MOD_ID = "replaymod-render"; @@ -32,6 +38,17 @@ public class ReplayModRender { core.getSettingsRegistry().register(Setting.class); } + public File getVideoFolder() { + String path = core.getSettingsRegistry().get(Setting.RENDER_PATH); + File folder = new File(path.startsWith("./") ? core.getMinecraft().mcDataDir : null, path); + try { + FileUtils.forceMkdir(folder); + } catch (IOException e) { + throw new ReportedException(CrashReport.makeCrashReport(e, "Cannot create video folder.")); + } + return folder; + } + public Configuration getConfiguration() { return configuration; } diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/VideoWriter.java index 66665768..1e98ab37 100755 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/VideoWriter.java @@ -49,7 +49,8 @@ public class VideoWriter implements FrameConsumer { System.out.println("Starting " + settings.getExportCommand() + " with args: " + commandArgs); String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings(); process = new ProcessBuilder(cmdline).directory(outputFolder).start(); - OutputStream exportLogOut = new TeeOutputStream(new FileOutputStream("export.log"), ffmpegLog); + File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log"); + OutputStream exportLogOut = new TeeOutputStream(new FileOutputStream(exportLogFile), ffmpegLog); new StreamPipe(process.getInputStream(), exportLogOut).start(); new StreamPipe(process.getErrorStream(), exportLogOut).start(); outputStream = process.getOutputStream(); diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index a9be9391..247db259 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -7,7 +7,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; -import com.replaymod.render.Setting; import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.replay.ReplayHandler; import com.replaymod.replaystudio.pathing.path.Timeline; @@ -410,7 +409,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { private File generateOutputFile(RenderSettings.EncodingPreset encodingPreset) { String fileName = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss").format(new Date()); - File folder = new File(ReplayModRender.instance.getCore().getSettingsRegistry().get(Setting.RENDER_PATH)); + File folder = ReplayModRender.instance.getVideoFolder(); return new File(folder, fileName + "." + encodingPreset.getFileExtension()); } diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index cb8689ee..e2b53b55 100755 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -280,7 +280,7 @@ public class ReplayHandler { CameraEntity cam = getCameraEntity(); if (cam != null) { targetCameraPosition = new Location(cam.posX, cam.posY, cam.posZ, - cam.rotationPitch, cam.rotationYaw); + cam.rotationYaw, cam.rotationPitch); } else { targetCameraPosition = null; } diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index e038ab92..b834936d 100755 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -69,7 +69,11 @@ public class CameraEntity extends EntityPlayerSP { public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager) { super(mcIn, worldIn, netHandlerPlayClient, statisticsManager); MinecraftForge.EVENT_BUS.register(eventHandler); - cameraController = ReplayModReplay.instance.createCameraController(this); + if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) { + cameraController = ReplayModReplay.instance.createCameraController(this); + } else { + cameraController = new SpectatorCameraController(this); + } } /** @@ -151,7 +155,9 @@ public class CameraEntity extends EntityPlayerSP { // This is important if the spectated player respawns as their // entity is recreated and we have to spectate a new entity UUID spectating = ReplayModReplay.instance.getReplayHandler().getSpectatedUUID(); - if (spectating != null && (view.getUniqueID() != spectating || view.worldObj != worldObj)) { + if (spectating != null && (view.getUniqueID() != spectating + || view.worldObj != worldObj) + || worldObj.getEntityByID(view.getEntityId()) != view) { view = worldObj.getPlayerEntityByUUID(spectating); if (view != null) { mc.setRenderViewEntity(view); @@ -322,6 +328,12 @@ public class CameraEntity extends EntityPlayerSP { return pos; } + @Override + public void openGui(Object mod, int modGuiId, World world, int x, int y, int z) { + // Do not open any block GUIs for the camera entities + // Note: Vanilla GUIs are filtered out on a packet level, this only applies to mod GUIs + } + @Override public void setDead() { super.setDead(); @@ -394,7 +406,11 @@ public class CameraEntity extends EntityPlayerSP { @SubscribeEvent public void onSettingsChanged(SettingsChangedEvent event) { if (event.getKey() == Setting.CAMERA) { - cameraController = ReplayModReplay.instance.createCameraController(CameraEntity.this); + if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) { + cameraController = ReplayModReplay.instance.createCameraController(CameraEntity.this); + } else { + cameraController = new SpectatorCameraController(CameraEntity.this); + } } } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java index eeba30db..8e6ac252 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -19,6 +19,7 @@ import net.minecraft.client.settings.GameSettings; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.InputEvent; +import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.WritablePoint; @@ -153,6 +154,23 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { setMouseVisible(true); } } + if (Keyboard.getEventKeyState()) { + // Handle the F1 key binding while the overlay is opened as a gui screen + if (isMouseVisible() && Keyboard.getEventKey() == Keyboard.KEY_F1) { + gameSettings.hideGUI = !gameSettings.hideGUI; + } + } + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + // Do not render overlay when user pressed F1 and we are not currently in some popup + if (getMinecraft().gameSettings.hideGUI && isAllowUserInput()) { + // Note that this only applies to when the mouse is visible, otherwise + // the draw method isn't called in the first place + return; + } + super.draw(renderer, size, renderInfo); } @Override diff --git a/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java b/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java new file mode 100644 index 00000000..ff4dc3d9 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java @@ -0,0 +1,31 @@ +package com.replaymod.replay.mixin; + +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleManager; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Queue; + +@Mixin(ParticleManager.class) +public abstract class MixinParticleManager { + @Shadow + private Queue queueEntityFX; + + /** + * This method additionally clears the queue of particles to be added when the world is changed. + * Otherwise particles from the previous world might show up in this one if they were spawned after + * the last tick in the previous world. + * + * @param world The new world + * @param ci Callback info + */ + @Inject(method = "clearEffects", at = @At("HEAD")) + public void replayModReplay_clearParticleQueue(World world, CallbackInfo ci) { + queueEntityFX.clear(); + } +} diff --git a/src/main/java/com/replaymod/replay/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/replay/mixin/MixinWorldClient.java new file mode 100644 index 00000000..643ac038 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/MixinWorldClient.java @@ -0,0 +1,31 @@ +package com.replaymod.replay.mixin; + +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WorldClient.class) +public abstract class MixinWorldClient { + + /** + * Fixes a bug in vanilla Minecraft that leaves entities remaining in the entityList even after respawn. + * The entityList in WorldClient is a Set that assumes that the entity ID of its entities does not change. + * However in {@link WorldClient#addEntityToWorld(int, Entity)}, the entity passed in is first added to the + * set and subsequently its id is changed, leaving it stuck in he Set. That is the buggy method. + * This wouldn't be too much of a problem if the entity had the correct id to begin with, however the handler for + * the spawn player packet creates an EntityOtherPlayerMP which takes its id from a counter and then passes it to + * this method with the wrong id. + * This mixin fixes the id of the entity before it is added to the set instead of right after. + * The original id change right after is not changed, however it should not have any effect. + * @param entityId The id to be set for the entity + * @param entity The entity to be added + * @param ci Callback info + */ + @Inject(method = "addEntityToWorld", at=@At("HEAD")) + public void replayModReplay_fix_addEntityToWorld(int entityId, Entity entity, CallbackInfo ci) { + entity.setEntityId(entityId); + } +} diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index c1a4e69b..91815070 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -431,7 +431,7 @@ public class GuiPathing { }); }); - core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { + core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { // Current replay time int time = replayHandler.getReplaySender().currentTimeStamp(); // Position of the cursor @@ -452,6 +452,8 @@ public class GuiPathing { int cursorPassed = (int) (timePassed / speed); // Move cursor to new position timeline.setCursorPosition(keyframeCursor + cursorPassed); + // Deselect keyframe to allow the user to add a new one right away + mod.setSelectedKeyframe(null); }); }); diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index 94ef40e6..38ac52e1 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -3,6 +3,7 @@ "package": "com.replaymod.replay.mixin", "mixins": [ "MixinGuiSpectator", + "MixinParticleManager", "MixinPlayerControllerMP", "MixinRenderArmorStand", "MixinRenderArrow", @@ -10,7 +11,8 @@ "MixinRenderLivingBase", "MixinRenderManager", "MixinTileEntityEndPortalRenderer", - "MixinViewFrustum" + "MixinViewFrustum", + "MixinWorldClient" ], "server": [], "client": [],