From 3df1bdcfc8b3514c718771ae7db3f3cb2c3cee99 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Mon, 15 Jun 2015 11:32:34 +0200 Subject: [PATCH] Added PathPreviewRenderer to draw a Preview of the current Camera Path into the World Added KeyframesModifyEvent to be dispatched whenever the ReplayHandler's Keyframe List is changed Refactored Event package Reworked Settings GUI --- .../eu/crushedpixel/replaymod/ReplayMod.java | 19 +- .../events/KeyframesModifyEvent.java | 15 ++ .../{ => handlers}/GuiEventHandler.java | 2 +- .../{ => handlers}/KeyInputHandler.java | 2 +- .../{ => handlers}/MinecraftTicker.java | 2 +- .../{ => handlers}/MouseInputHandler.java | 2 +- .../{ => handlers}/RecordingHandler.java | 2 +- .../{ => handlers}/TickAndRenderListener.java | 2 +- .../replaymod/gui/GuiEditKeyframe.java | 3 + .../replaymod/gui/GuiReplaySettings.java | 144 ++++------- .../gui/elements/GuiOnOffButton.java | 24 ++ .../gui/elements/GuiSettingsOnOffButton.java | 39 +++ .../gui/elements/GuiToggleButton.java | 7 + .../gui/overlay/GuiReplayOverlay.java | 2 +- .../replaymod/holders/Position.java | 8 + .../renderer/PathPreviewRenderer.java | 231 ++++++++++++++++++ .../renderer/SafeEntityRenderer.java | 1 + .../replaymod/replay/ReplayHandler.java | 11 + .../replaymod/replay/ReplaySender.java | 2 +- .../replaymod/settings/ReplaySettings.java | 52 +++- .../replaymod/video/ReplayScreenshot.java | 2 +- .../assets/replaymod/lang/en_US.lang | 1 + .../resources/assets/replaymod/replay_gui.png | Bin 6640 -> 6833 bytes 23 files changed, 447 insertions(+), 126 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/KeyframesModifyEvent.java rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/GuiEventHandler.java (99%) rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/KeyInputHandler.java (99%) rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/MinecraftTicker.java (99%) rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/MouseInputHandler.java (95%) rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/RecordingHandler.java (99%) rename src/main/java/eu/crushedpixel/replaymod/events/{ => handlers}/TickAndRenderListener.java (98%) create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiOnOffButton.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java index 85c264e0..f18155a4 100755 --- a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java +++ b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java @@ -3,7 +3,7 @@ package eu.crushedpixel.replaymod; import com.google.common.util.concurrent.ListenableFutureTask; import eu.crushedpixel.replaymod.api.ApiClient; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; -import eu.crushedpixel.replaymod.events.*; +import eu.crushedpixel.replaymod.events.handlers.*; import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.localization.LocalizedResourcePack; @@ -11,6 +11,8 @@ import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.registry.*; import eu.crushedpixel.replaymod.renderer.InvisibilityRender; +import eu.crushedpixel.replaymod.renderer.PathPreviewRenderer; +import eu.crushedpixel.replaymod.renderer.SafeEntityRenderer; import eu.crushedpixel.replaymod.renderer.SpectatorRenderer; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.replay.ReplayProcess; @@ -69,6 +71,7 @@ public class ReplayMod { public static RatedFileHandler ratedFileHandler; public static SpectatorRenderer spectatorRenderer; public static TooltipRenderer tooltipRenderer; + public static PathPreviewRenderer pathPreviewRenderer; // The instance of your mod that Forge uses. @Instance(value = "ReplayModID") @@ -122,13 +125,17 @@ public class ReplayMod { spectatorRenderer = new SpectatorRenderer(); + pathPreviewRenderer = new PathPreviewRenderer(); + FMLCommonHandler.instance().bus().register(pathPreviewRenderer); + MinecraftForge.EVENT_BUS.register(pathPreviewRenderer); + KeybindRegistry.initialize(); -// try { -// mc.entityRenderer = new SafeEntityRenderer(mc, mc.entityRenderer); -// } catch(Exception e) { -// e.printStackTrace(); -// } + try { + mc.entityRenderer = new SafeEntityRenderer(mc, mc.entityRenderer); + } catch(Exception e) { + e.printStackTrace(); + } tooltipRenderer = new TooltipRenderer(); diff --git a/src/main/java/eu/crushedpixel/replaymod/events/KeyframesModifyEvent.java b/src/main/java/eu/crushedpixel/replaymod/events/KeyframesModifyEvent.java new file mode 100644 index 00000000..86dc5195 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/KeyframesModifyEvent.java @@ -0,0 +1,15 @@ +package eu.crushedpixel.replaymod.events; + +import eu.crushedpixel.replaymod.holders.Keyframe; +import net.minecraftforge.fml.common.eventhandler.Event; + +import java.util.List; + +public class KeyframesModifyEvent extends Event { + + public List keyframes; + + public KeyframesModifyEvent(List keyframes) { + this.keyframes = keyframes; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java similarity index 99% rename from src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java index 0863fb55..d442ab40 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.gui.GuiConstants; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java similarity index 99% rename from src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java index 1d2e6dab..3c21b024 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity.MoveDirection; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java similarity index 99% rename from src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java index 1ccc8c03..a2ea2193 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.ReplayMod; import net.minecraft.client.Minecraft; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/MouseInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java similarity index 95% rename from src/main/java/eu/crushedpixel/replaymod/events/MouseInputHandler.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java index e9b66dc9..e0ffcf3b 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/MouseInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.entities.CameraEntity; import eu.crushedpixel.replaymod.replay.ReplayHandler; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java similarity index 99% rename from src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java index 620f6875..6475769d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import io.netty.buffer.ByteBuf; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java similarity index 98% rename from src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java rename to src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java index c80b52ba..e5855e9a 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.events; +package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java index 85d9744c..14dde1ba 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java @@ -1,6 +1,7 @@ package eu.crushedpixel.replaymod.gui; import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.events.KeyframesModifyEvent; import eu.crushedpixel.replaymod.gui.elements.GuiArrowButton; import eu.crushedpixel.replaymod.gui.elements.GuiNumberInput; import eu.crushedpixel.replaymod.holders.*; @@ -10,6 +11,7 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.resources.I18n; +import net.minecraftforge.fml.common.FMLCommonHandler; import org.lwjgl.input.Keyboard; import java.awt.*; @@ -195,6 +197,7 @@ public class GuiEditKeyframe extends GuiScreen { ((MarkerKeyframe)keyframe).setName(markerNameInput.getText().trim()); } } + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(ReplayHandler.getKeyframes())); Keyboard.enableRepeatEvents(false); } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java index 6b46643c..3a129ff7 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java @@ -1,9 +1,8 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.settings.ReplaySettings; +import eu.crushedpixel.replaymod.gui.elements.GuiSettingsOnOffButton; +import eu.crushedpixel.replaymod.gui.elements.GuiToggleButton; import eu.crushedpixel.replaymod.settings.ReplaySettings.RecordingOptions; -import eu.crushedpixel.replaymod.settings.ReplaySettings.RenderOptions; import eu.crushedpixel.replaymod.settings.ReplaySettings.ReplayOptions; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -16,20 +15,18 @@ import java.io.IOException; public class GuiReplaySettings extends GuiScreen { //TODO: Move to GuiConstants - private static final int QUALITY_SLIDER_ID = 9003; private static final int RECORDSERVER_ID = 9004; private static final int RECORDSP_ID = 9005; private static final int SEND_CHAT = 9006; private static final int FORCE_LINEAR = 9007; private static final int ENABLE_LIGHTING = 9008; - private static final int FRAMERATE_SLIDER_ID = 9009; private static final int RESOURCEPACK_ID = 9010; - private static final int WAITFORCHUNKS_ID = 9011; private static final int INDICATOR_ID = 9012; + private static final int PATHPREVIEW_ID = 9013; protected String screenTitle = I18n.format("replaymod.gui.settings.title"); private GuiScreen parentGuiScreen; - private GuiButton recordServerButton, recordSPButton, sendChatButton, linearButton, lightingButton, - resourcePackButton, waitForChunksButton, showIndicatorButton; + private GuiToggleButton recordServerButton, recordSPButton, sendChatButton, linearButton, lightingButton, + resourcePackButton, showIndicatorButton, pathPreviewButton; public GuiReplaySettings(GuiScreen parentGuiScreen) { this.parentGuiScreen = parentGuiScreen; @@ -40,32 +37,31 @@ public class GuiReplaySettings extends GuiScreen { this.buttonList.clear(); this.buttonList.add(new GuiButton(200, this.width / 2 - 100, this.height - 27, I18n.format("gui.done"))); - ReplaySettings settings = ReplayMod.replaySettings; - - int k = 0; int i = 0; for(RecordingOptions o : RecordingOptions.values()) { + + int xPos = this.width / 2 - 155 + i % 2 * 160; + int yPos = this.height / 6 + 24 * (i >> 1); + if(o == RecordingOptions.notifications) { - this.buttonList.add(sendChatButton = new GuiButton(SEND_CHAT, - this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, - I18n.format("replaymod.gui.settings.notifications")+": " + onOff(settings.isShowNotifications()))); + sendChatButton = new GuiSettingsOnOffButton(SEND_CHAT, xPos, yPos, 150, 20, o); + this.buttonList.add(sendChatButton); + } else if(o == RecordingOptions.recordServer) { - this.buttonList.add(recordServerButton = new GuiButton(RECORDSERVER_ID, - this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, - I18n.format("replaymod.gui.settings.recordserver")+": " - + onOff(settings.isEnableRecordingServer()))); + recordServerButton = new GuiSettingsOnOffButton(RECORDSERVER_ID, xPos, yPos, 150, 20, o); + this.buttonList.add(recordServerButton); + } else if(o == RecordingOptions.recordSingleplayer) { - this.buttonList.add(recordSPButton = new GuiButton(RECORDSP_ID, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.recordsingleplayer")+": " - + onOff(settings.isEnableRecordingSingleplayer()))); + recordSPButton = new GuiSettingsOnOffButton(RECORDSP_ID, xPos, yPos, 150, 20, o); + this.buttonList.add(recordSPButton); + } else if(o == RecordingOptions.indicator) { - this.buttonList.add(showIndicatorButton = new GuiButton(INDICATOR_ID, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.indicator")+": "+ onOff(settings.showRecordingIndicator()))); + showIndicatorButton = new GuiSettingsOnOffButton(INDICATOR_ID, xPos, yPos, 150, 20, o); + this.buttonList.add(showIndicatorButton); } ++i; - ++k; } @@ -74,48 +70,34 @@ public class GuiReplaySettings extends GuiScreen { } for(ReplayOptions o : ReplayOptions.values()) { + + int xPos = this.width / 2 - 155 + i % 2 * 160; + int yPos = this.height / 6 + 24 * (i >> 1); + if(o == ReplayOptions.lighting) { - this.buttonList.add(lightingButton = new GuiButton(ENABLE_LIGHTING, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.lighting")+": " + onOff(settings.isLightingEnabled()))); + lightingButton = new GuiSettingsOnOffButton(ENABLE_LIGHTING, xPos, yPos, 150, 20, o); + this.buttonList.add(lightingButton); + } else if(o == ReplayOptions.linear) { - this.buttonList.add(linearButton = new GuiButton(FORCE_LINEAR, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.interpolation")+": " + linearOnOff(settings.isLinearMovement()))); + linearButton = new GuiSettingsOnOffButton(FORCE_LINEAR, xPos, yPos, 150, 20, o, + I18n.format("replaymod.gui.settings.interpolation.linear"), I18n.format("replaymod.gui.settings.interpolation.cubic")); + this.buttonList.add(linearButton); + } else if(o == ReplayOptions.useResources) { - this.buttonList.add(resourcePackButton = new GuiButton(RESOURCEPACK_ID, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.resources")+": " + onOff(settings.getUseResourcePacks()))); + resourcePackButton = new GuiSettingsOnOffButton(RESOURCEPACK_ID, xPos, yPos, 150, 20, o); + this.buttonList.add(resourcePackButton); + + } else if(o == ReplayOptions.previewPath) { + pathPreviewButton = new GuiSettingsOnOffButton(PATHPREVIEW_ID, xPos, yPos, 150, 20, o); + this.buttonList.add(pathPreviewButton); } ++i; - ++k; } if(i % 2 == 1) { ++i; } - - for(RenderOptions o : RenderOptions.values()) { - if(o == RenderOptions.videoFramerate) { - this.buttonList.add(new GuiVideoFramerateSlider(FRAMERATE_SLIDER_ID, - this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), settings.getVideoFramerate(), I18n.format("replaymod.gui.settings.framerate"))); - } else if(o == RenderOptions.videoQuality) { - this.buttonList.add(new GuiVideoQualitySlider(QUALITY_SLIDER_ID, - this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), (float) settings.getVideoQuality(), I18n.format("replaymod.gui.settings.quality"))); - } else if(o == RenderOptions.waitForChunks) { - this.buttonList.add(waitForChunksButton = new GuiButton(WAITFORCHUNKS_ID, this.width / 2 - 155 + i % 2 * 160, - this.height / 6 + 24 * (i >> 1), 150, 20, I18n.format("replaymod.gui.settings.forcechunks")+": " + onOff(settings.getWaitForChunks()))); - } - - ++i; - ++k; - } - } - - private String onOff(boolean on) { - return on ? I18n.format("options.on") : I18n.format("options.off"); - } - - private String linearOnOff(boolean on) { - return on ? I18n.format("replaymod.gui.settings.interpolation.linear") : I18n.format("replaymod.gui.settings.interpolation.cubic"); } @Override @@ -136,54 +118,10 @@ public class GuiReplaySettings extends GuiScreen { case 200: this.mc.displayGuiScreen(this.parentGuiScreen); break; - case RECORDSERVER_ID: - boolean enabled = ReplayMod.replaySettings.isEnableRecordingServer(); - enabled = !enabled; - recordServerButton.displayString = I18n.format("replaymod.gui.settings.recordserver")+": " + onOff(enabled); - ReplayMod.replaySettings.setEnableRecordingServer(enabled); - break; - case RECORDSP_ID: - enabled = ReplayMod.replaySettings.isEnableRecordingSingleplayer(); - enabled = !enabled; - recordSPButton.displayString = I18n.format("replaymod.gui.settings.recordsingleplayer")+": " + onOff(enabled); - ReplayMod.replaySettings.setEnableRecordingSingleplayer(enabled); - break; - case SEND_CHAT: - enabled = ReplayMod.replaySettings.isShowNotifications(); - enabled = !enabled; - sendChatButton.displayString = I18n.format("replaymod.gui.settings.notifications")+": " + onOff(enabled); - ReplayMod.replaySettings.setShowNotifications(enabled); - break; - case FORCE_LINEAR: - enabled = ReplayMod.replaySettings.isLinearMovement(); - enabled = !enabled; - linearButton.displayString = I18n.format("replaymod.gui.settings.interpolation")+": " + linearOnOff(enabled); - ReplayMod.replaySettings.setLinearMovement(enabled); - break; - case ENABLE_LIGHTING: - enabled = ReplayMod.replaySettings.isLightingEnabled(); - enabled = !enabled; - lightingButton.displayString = I18n.format("replaymod.gui.settings.lighting")+": " + onOff(enabled); - ReplayMod.replaySettings.setLightingEnabled(enabled); - break; - case RESOURCEPACK_ID: - enabled = ReplayMod.replaySettings.getUseResourcePacks(); - enabled = !enabled; - resourcePackButton.displayString = I18n.format("replaymod.gui.settings.resources")+": " + onOff(enabled); - ReplayMod.replaySettings.setUseResourcePacks(enabled); - break; - case WAITFORCHUNKS_ID: - enabled = ReplayMod.replaySettings.getWaitForChunks(); - enabled = !enabled; - waitForChunksButton.displayString = I18n.format("replaymod.gui.settings.forcechunks")+": " + onOff(enabled); - ReplayMod.replaySettings.setWaitForChunks(enabled); - break; - case INDICATOR_ID: - enabled = ReplayMod.replaySettings.showRecordingIndicator(); - enabled = !enabled; - showIndicatorButton.displayString = I18n.format("replaymod.gui.settings.indicator")+": " + onOff(enabled); - ReplayMod.replaySettings.setEnableIndicator(enabled); - break; + } + + if(button instanceof GuiToggleButton) { + ((GuiToggleButton) button).toggle(); } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiOnOffButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiOnOffButton.java new file mode 100644 index 00000000..3c9e0cba --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiOnOffButton.java @@ -0,0 +1,24 @@ +package eu.crushedpixel.replaymod.gui.elements; + +import net.minecraft.client.resources.I18n; + +public class GuiOnOffButton extends GuiToggleButton { + + private static final String[] values = new String[] {I18n.format("options.on"), I18n.format("options.off")}; + + public GuiOnOffButton(int buttonId, int x, int y, String buttonText) { + super(buttonId, x, y, buttonText, values); + } + + public GuiOnOffButton(int buttonId, int x, int y, int width, int height, String buttonText) { + super(buttonId, x, y, width, height, buttonText, values); + } + + public GuiOnOffButton(int buttonId, int x, int y, int width, int height, String buttonText, String onValue, String offValue) { + super(buttonId, x, y, width, height, buttonText, new String[] {onValue, offValue}); + } + + public boolean isOn() { + return getValue() == 0; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java new file mode 100644 index 00000000..1c9386fa --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java @@ -0,0 +1,39 @@ +package eu.crushedpixel.replaymod.gui.elements; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.settings.ReplaySettings; + +public class GuiSettingsOnOffButton extends GuiOnOffButton { + + private ReplaySettings.ValueEnum toChange; + + public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, ReplaySettings.ValueEnum toChange) { + super(buttonId, x, y, width, height, toChange.getName()+": "); + this.toChange = toChange; + if(toChange.getValue() instanceof Boolean) { + this.setValue((Boolean) toChange.getValue() == true ? 0 : 1); + } + } + + public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, ReplaySettings.ValueEnum toChange, String onValue, String offValue) { + super(buttonId, x, y, width, height, toChange.getName()+": ", onValue, offValue); + this.toChange = toChange; + if(toChange.getValue() instanceof Boolean) { + this.setValue((Boolean) toChange.getValue() == true ? 0 : 1); + } + } + + @Override + public void toggle() { + super.toggle(); + toChange.setValue(isOn()); + ReplayMod.replaySettings.rewriteSettings(); + } + + @Override + public void setValue(int value) { + super.setValue(value); + toChange.setValue(isOn()); + ReplayMod.replaySettings.rewriteSettings(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiToggleButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiToggleButton.java index ca66d04e..f4105dc6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiToggleButton.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiToggleButton.java @@ -20,6 +20,12 @@ public class GuiToggleButton extends GuiAdvancedButton { this.displayString = baseText+values[value]; } + public GuiToggleButton(int buttonId, int x, int y, int width, int height, String buttonText, String[] values) { + this(buttonId, x, y, buttonText, values); + this.width = width; + this.height = height; + } + @Override public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { boolean success = super.mousePressed(mc, mouseX, mouseY); @@ -41,6 +47,7 @@ public class GuiToggleButton extends GuiAdvancedButton { public void setValue(int value) { this.value = value; + this.displayString = baseText+values[value]; } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java index 74ac6e73..cbd712a5 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java @@ -180,7 +180,7 @@ public class GuiReplayOverlay extends Gui { @Override public GuiElement delegate() { boolean selected = ReplayHandler.getSelectedKeyframe() instanceof PositionKeyframe; - boolean camera = true; + boolean camera; if(selected) { camera = ((PositionKeyframe)ReplayHandler.getSelectedKeyframe()).getSpectatedEntityID() == null; } else { diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Position.java b/src/main/java/eu/crushedpixel/replaymod/holders/Position.java index 94b4a89a..ac7b6096 100755 --- a/src/main/java/eu/crushedpixel/replaymod/holders/Position.java +++ b/src/main/java/eu/crushedpixel/replaymod/holders/Position.java @@ -82,6 +82,14 @@ public class Position { public void setRoll(float roll) { this.roll = roll; } + public double distanceTo(double x, double y, double z) { + return distanceTo(new Position(x, y, z, 0, 0)); + } + + public double distanceTo(Position p2) { + return Math.sqrt(Math.pow((p2.getX() - getX()), 2) + Math.pow((p2.getY() - getY()), 2) + Math.pow((p2.getZ() - getZ()), 2)); + } + @Override public String toString() { return "X=" + x + ", Y=" + y + ", Z=" + z + ", Yaw=" + yaw + ", Pitch=" + pitch + ", Roll="+roll; diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java new file mode 100644 index 00000000..edcff6f5 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java @@ -0,0 +1,231 @@ +package eu.crushedpixel.replaymod.renderer; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.events.KeyframesModifyEvent; +import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.Position; +import eu.crushedpixel.replaymod.holders.PositionKeyframe; +import eu.crushedpixel.replaymod.interpolation.SplinePoint; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.entity.Entity; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.lwjgl.opengl.GL11; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class PathPreviewRenderer { + + private static final Minecraft mc = Minecraft.getMinecraft(); + + private SplinePoint spline = new SplinePoint(); + + private DistanceComparator distanceComparator = new DistanceComparator(); + + private List keyframes = new ArrayList(); + + @SubscribeEvent + public void renderCameraPath(RenderWorldLastEvent event) { + if(!ReplayHandler.isInReplay() || ReplayHandler.isInPath() || !ReplayMod.replaySettings.showPathPreview()) return; + + Entity entity = ReplayHandler.getCameraEntity(); + if(entity == null) return; + + double doubleX = entity.posX; + double doubleY = entity.posY; + double doubleZ = entity.posZ; + + GlStateManager.pushAttrib(); + GlStateManager.pushMatrix(); + + GlStateManager.disableLighting(); + GlStateManager.enableBlend(); + + GlStateManager.disableTexture2D(); + + if(spline.getPoints().size() > 1) { + + Position prev = null; + + if(ReplayMod.replaySettings.isLinearMovement()) { + + for(int i = 0; i < spline.getPoints().size(); i++) { + Position point = spline.getPoints().get(i); + + if(prev != null) { + drawConnection(doubleX, doubleY, doubleZ, prev, point, Color.RED.getRGB()); + } + + prev = point; + } + + } else { + + float max = spline.getPoints().size() * 50; + for(int i = 0; i < max; i++) { + Position point = spline.getPoint(i / max); + + if(prev != null) { + drawConnection(doubleX, doubleY, doubleZ, prev, point, Color.RED.getRGB()); + } + + prev = point; + } + } + } + + + distanceComparator.setPlayerPos(doubleX, doubleY + 1.4, doubleZ); + + Collections.sort(keyframes, distanceComparator); + + for(PositionKeyframe kf : keyframes) { + drawPoint(doubleX, doubleY, doubleZ, kf); + } + + GlStateManager.enableTexture2D(); + GlStateManager.enableLighting(); + + GlStateManager.disableBlend(); + GlStateManager.popAttrib(); + GlStateManager.popMatrix(); + } + + @SubscribeEvent + public void recalcSpline(KeyframesModifyEvent event) { + keyframes = new ArrayList(); + spline = new SplinePoint(); + for(Keyframe kf : event.keyframes) { + if(kf instanceof PositionKeyframe) { + PositionKeyframe pkf = (PositionKeyframe)kf; + Position pos = pkf.getPosition(); + spline.addPoint(pos); + keyframes.add(pkf); + } + } + + if(spline.getPoints().size() > 1) { + spline.prepare(); + } + } + + private class DistanceComparator implements Comparator { + + private double playerX, playerY, playerZ; + + public void setPlayerPos(double playerX, double playerY, double playerZ) { + this.playerX = playerX; + this.playerY = playerY; + this.playerZ = playerZ; + } + + @Override + public int compare(PositionKeyframe o1, PositionKeyframe o2) { + return -(new Double(o1.getPosition().distanceTo(playerX, playerY, playerZ)).compareTo(o2.getPosition().distanceTo(playerX, playerY, playerZ))); + } + + @Override + public boolean equals(Object obj) { + return false; + } + } + + private void drawConnection(double playerX, double playerY, double playerZ, Position pos1, Position pos2, int color) { + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer renderer = tessellator.getWorldRenderer(); + + GL11.glLineWidth(3.0F); + + double x = pos1.getX() - playerX; + double y = pos1.getY() - playerY; + double z = pos1.getZ() - playerZ; + + renderer.setTranslation(x, y+1.4, z); + + renderer.startDrawing(1); + + renderer.setColorRGBA_I(color, 50); + + renderer.addVertex(pos2.getX() - pos1.getX(), pos2.getY() - pos1.getY(), pos2.getZ() - pos1.getZ()); + renderer.addVertex(0, 0, 0); + + tessellator.draw(); + renderer.setTranslation(0, 0, 0); + } + + private void drawPoint(double playerX, double playerY, double playerZ, PositionKeyframe kf) { + GlStateManager.pushMatrix(); + GlStateManager.enableTexture2D(); + GlStateManager.enableLighting(); + GlStateManager.disableLighting(); + GlStateManager.enableAlpha(); + + GlStateManager.enableBlend(); + + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer renderer = tessellator.getWorldRenderer(); + + mc.renderEngine.bindTexture(GuiReplayOverlay.replay_gui); + + Position pos1 = kf.getPosition(); + + double x = pos1.getX() - playerX; + double y = pos1.getY() - playerY; + double z = pos1.getZ() - playerZ; + + GlStateManager.translate(x, y+1.4, z); + + float pitch = mc.getRenderManager().playerViewX; + float yaw = mc.getRenderManager().playerViewY; + + GL11.glNormal3f(0, 1, 0); + + GlStateManager.rotate(-yaw, 0, 1, 0); + GlStateManager.rotate(pitch, 1, 0, 0); + + renderer.setColorRGBA_F(1, 1, 1, 0.5f); + + renderer.startDrawingQuads(); + + float posX = 80/128f; + float posY = 0; + float size = 10/128f; + + + if(kf.equals(ReplayHandler.getSelectedKeyframe())) { + posY += size; + } + + if(kf.getSpectatedEntityID() != null) { + posX += size; + } + + renderer.addVertexWithUV(-0.5, 0.5, 0, posX+size, posY); + renderer.addVertexWithUV(0.5, 0.5, 0, posX+size, posY+size); + renderer.addVertexWithUV(-0.5, -0.5, 0, posX, posY); + renderer.addVertexWithUV(0.5, -0.5, 0, posX, posY+size); + + renderer.addVertexWithUV(0.5, -0.5, 0, posX+size, posY); + renderer.addVertexWithUV(-0.5, -0.5, 0, posX, posY); + renderer.addVertexWithUV(0.5, 0.5, 0, posX+size, posY+size); + renderer.addVertexWithUV(-0.5, 0.5, 0, posX, posY + size); + + tessellator.draw(); + renderer.setTranslation(0, 0, 0); + + GlStateManager.disableAlpha(); + GlStateManager.disableTexture2D(); + GlStateManager.disableBlend(); + GlStateManager.enableLighting(); + GlStateManager.popMatrix(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/SafeEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/renderer/SafeEntityRenderer.java index 5bc9bf28..f5c0439e 100755 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/SafeEntityRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/SafeEntityRenderer.java @@ -14,6 +14,7 @@ public class SafeEntityRenderer extends EntityRenderer { try { super.updateCameraAndRender(partialTicks); } catch(Exception e) { + e.printStackTrace(); } //This is plain easier than doing proper error prevention. //If Johni reads this, don't think I'm a bad programmer... Just a lazy one :P } diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 153d0ef9..f4479001 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -3,6 +3,7 @@ package eu.crushedpixel.replaymod.replay; import com.mojang.authlib.GameProfile; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.events.KeyframesModifyEvent; import eu.crushedpixel.replaymod.holders.*; import eu.crushedpixel.replaymod.registry.PlayerHandler; import eu.crushedpixel.replaymod.settings.RenderOptions; @@ -92,6 +93,8 @@ public class ReplayHandler { e.printStackTrace(); } } + + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(keyframes)); } public static void useKeyframePresetFromRepository(int index) { @@ -104,6 +107,8 @@ public class ReplayHandler { } if(!(selectedKeyframe instanceof MarkerKeyframe)) selectedKeyframe = null; + + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(keyframes)); } public static void spectateEntity(Entity e) { @@ -214,6 +219,8 @@ public class ReplayHandler { a = b; } } + + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(keyframes)); } public static void removeKeyframe(Keyframe keyframe) { @@ -223,6 +230,8 @@ public class ReplayHandler { } else { sortKeyframes(); } + + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(keyframes)); } public static int getKeyframeIndex(TimeKeyframe timeKeyframe) { @@ -430,6 +439,8 @@ public class ReplayHandler { } else { selectKeyframe(null); } + + FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(keyframes)); } public static boolean isSelected(Keyframe kf) { diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java index 8aedbb20..8d97ec95 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java @@ -5,7 +5,7 @@ import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.events.RecordingHandler; +import eu.crushedpixel.replaymod.events.handlers.RecordingHandler; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.holders.Position; import eu.crushedpixel.replaymod.timer.MCTimerHandler; diff --git a/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java index 6f2540d9..d7431991 100755 --- a/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java @@ -2,6 +2,7 @@ package eu.crushedpixel.replaymod.settings; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.registry.LightingHandler; +import net.minecraft.client.resources.I18n; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; @@ -149,6 +150,13 @@ public class ReplaySettings { rewriteSettings(); } + public boolean showPathPreview() { return (Boolean) ReplayOptions.previewPath.getValue(); }; + + public void setShowPathPreview(boolean show) { + ReplayOptions.previewPath.setValue(show); + rewriteSettings(); + } + public void rewriteSettings() { ReplayMod.instance.config.load(); @@ -218,12 +226,17 @@ public class ReplaySettings { } public enum RecordingOptions implements ValueEnum { - recordServer(true), recordSingleplayer(true), notifications(true), indicator(true); + recordServer(true, "replaymod.gui.settings.recordserver"), + recordSingleplayer(true, "replaymod.gui.settings.recordsingleplayer"), + notifications(true, "replaymod.gui.settings.notifications"), + indicator(true, "replaymod.gui.settings.indicator"); private Object value; + private String name; - RecordingOptions(Object value) { + RecordingOptions(Object value, String name) { this.value = value; + this.name = name; } public Object getValue() { @@ -233,15 +246,22 @@ public class ReplaySettings { public void setValue(Object value) { this.value = value; } + + public String getName() { return I18n.format(name); }; } public enum ReplayOptions implements ValueEnum { - linear(false), lighting(false), useResources(true); + linear(false, "replaymod.gui.settings.interpolation"), + lighting(false, "replaymod.gui.settings.lighting"), + useResources(true, "replaymod.gui.settings.resources"), + previewPath(true, "replaymod.gui.settings.pathpreview"); private Object value; + private String name; - ReplayOptions(Object value) { + ReplayOptions(Object value, String name) { this.value = value; + this.name = name; } public Object getValue() { @@ -251,15 +271,21 @@ public class ReplaySettings { public void setValue(Object value) { this.value = value; } + + public String getName() { return I18n.format(name); }; } public enum RenderOptions implements ValueEnum { - videoQuality(0.5f), videoFramerate(30), waitForChunks(true); + videoQuality(0.5f, "replaymod.gui.settings.quality"), + videoFramerate(30, "replaymod.gui.settings.framerate"), + waitForChunks(true, "replaymod.gui.settings.forcechunks"); private Object value; + private String name; - RenderOptions(Object value) { + RenderOptions(Object value, String name) { this.value = value; + this.name = name; } public Object getValue() { @@ -269,15 +295,21 @@ public class ReplaySettings { public void setValue(Object value) { this.value = value; } + + public String getName() { return I18n.format(name); }; } public enum AdvancedOptions implements ValueEnum { - recordingPath("./replay_recordings/"), renderPath("./replay_videos/"), downloadPath("./replay_downloads"); + recordingPath("./replay_recordings/", ""), + renderPath("./replay_videos/", ""), + downloadPath("./replay_downloads", ""); private Object value; + private String name; - AdvancedOptions(Object value) { + AdvancedOptions(Object value, String name) { this.value = value; + this.name = name; } public Object getValue() { @@ -287,11 +319,15 @@ public class ReplaySettings { public void setValue(Object value) { this.value = value; } + + public String getName() { return I18n.format(name); }; } public interface ValueEnum { Object getValue(); void setValue(Object value); + + String getName(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java b/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java index 32b6a645..d1e43296 100755 --- a/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java @@ -2,7 +2,7 @@ package eu.crushedpixel.replaymod.video; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; -import eu.crushedpixel.replaymod.events.TickAndRenderListener; +import eu.crushedpixel.replaymod.events.handlers.TickAndRenderListener; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.ImageUtils; import net.minecraft.client.Minecraft; diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 03d82d55..985ac5ce 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -210,6 +210,7 @@ replaymod.gui.settings.lighting=Ambient Lighting replaymod.gui.settings.forcechunks=Force Render Chunks replaymod.gui.settings.resources=Server Resource Packs replaymod.gui.settings.interpolation=Path Interpolation +replaymod.gui.settings.pathpreview=Show Path Preview replaymod.gui.settings.videoquality.draft=Draft replaymod.gui.settings.videoquality.normal=Normal diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 526925d95a2dfdc311c9deb17d236f2cfcb26f58..64648ab26315dc3b8c838d0b278491440138ecc1 100755 GIT binary patch delta 6822 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 N8Y()EzA9M;{~xUJ03`qb delta 6627 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 Y(VTazYSQJfg2