diff --git a/ReplayStudio b/ReplayStudio index aa48888e..66ab0010 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit aa48888e154ac3d7399a944f64eb5f228aff66bf +Subproject commit 66ab00102ff3578884339ebd64eb45ade44801f0 diff --git a/jGui b/jGui index 46e47874..7849fb3b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 46e47874a46ab4cca5156297c391fde0b25dedca +Subproject commit 7849fb3bb7983c029b0da1962446f9a2d75eac19 diff --git a/src/main/java/com/replaymod/compat/ReplayModCompat.java b/src/main/java/com/replaymod/compat/ReplayModCompat.java new file mode 100644 index 00000000..737039ce --- /dev/null +++ b/src/main/java/com/replaymod/compat/ReplayModCompat.java @@ -0,0 +1,23 @@ +package com.replaymod.compat; + +import com.replaymod.compat.shaders.ShaderBeginRender; +import com.replaymod.core.ReplayMod; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.EventBus; + +@Mod(modid = ReplayModCompat.MOD_ID, useMetadata = true) +public class ReplayModCompat { + public static final String MOD_ID = "replaymod-compat"; + + @Mod.Instance(ReplayMod.MOD_ID) + private static ReplayMod core; + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + EventBus bus = MinecraftForge.EVENT_BUS; + bus.register(new ShaderBeginRender()); + } + +} diff --git a/src/main/java/com/replaymod/compat/shaders/ShaderBeginRender.java b/src/main/java/com/replaymod/compat/shaders/ShaderBeginRender.java new file mode 100644 index 00000000..8404801b --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/ShaderBeginRender.java @@ -0,0 +1,39 @@ +package com.replaymod.compat.shaders; + +import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.Minecraft; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import java.lang.reflect.InvocationTargetException; + +public class ShaderBeginRender { + + private final Minecraft mc = Minecraft.getMinecraft(); + + /** + * Invokes Shaders#beginRender when rendering a video, + * as this would usually get called by EntityRenderer#renderWorld, + * which we're not calling during rendering. + */ + @SubscribeEvent + public void onRenderTickStart(TickEvent.RenderTickEvent event) { + if (event.phase != TickEvent.Phase.START) return; + if (ShaderReflection.shaders_beginRender == null) return; + if (ShaderReflection.config_isShaders == null) return; + + try { + // check if video is being rendered + if (((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler() == null) + return; + + // check if Shaders are enabled + if (!(boolean) (ShaderReflection.config_isShaders.invoke(null))) return; + + ShaderReflection.shaders_beginRender.invoke(null, mc, mc.timer.elapsedPartialTicks, 0); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java b/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java new file mode 100644 index 00000000..55d785b2 --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java @@ -0,0 +1,102 @@ +package com.replaymod.compat.shaders; + +import net.minecraft.client.Minecraft; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class ShaderReflection { + + // Shaders.frameTimeCounter + public static Field shaders_frameTimeCounter; + + // Shaders.isShadowPass + public static Field shaders_isShadowPass; + + // Shaders.beginRender() + public static Method shaders_beginRender; + + // RenderGlobal.chunksToUpdateForced (Optifine only) + public static Field renderGlobal_chunksToUpdateForced; + + // Config.isShaders() (Optifine only) + public static Method config_isShaders; + + static { + initFrameTimeCounter(); + + initIsShadowPass(); + + initBeginRender(); + + initChunksToUpdateForced(); + + initConfigIsShaders(); + } + + private static void initFrameTimeCounter() { + try { + shaders_frameTimeCounter = Class.forName("shadersmod.client.Shaders") + .getDeclaredField("frameTimeCounter"); + shaders_frameTimeCounter.setAccessible(true); + } catch (ClassNotFoundException ignore) { + // no shaders mod installed + } catch (NoSuchFieldException e) { + // the field wasn't found. Has it been renamed? + e.printStackTrace(); + } + } + + private static void initIsShadowPass() { + try { + shaders_isShadowPass = Class.forName("shadersmod.client.Shaders") + .getDeclaredField("isShadowPass"); + shaders_isShadowPass.setAccessible(true); + } catch (ClassNotFoundException ignore) { + // no shaders mod installed + } catch (NoSuchFieldException e) { + // the field wasn't found. Has it been renamed? + e.printStackTrace(); + } + } + + private static void initBeginRender() { + try { + shaders_beginRender = Class.forName("shadersmod.client.Shaders") + .getDeclaredMethod("beginRender", Minecraft.class, float.class, long.class); + shaders_frameTimeCounter.setAccessible(true); + } catch (ClassNotFoundException ignore) { + // no shaders mod installed + } catch (NoSuchMethodException e) { + // the method wasn't found. Has it been renamed? + e.printStackTrace(); + } + } + + private static void initChunksToUpdateForced() { + try { + renderGlobal_chunksToUpdateForced = Class.forName("net.minecraft.client.renderer.RenderGlobal") + .getDeclaredField("chunksToUpdateForced"); + renderGlobal_chunksToUpdateForced.setAccessible(true); + } catch (ClassNotFoundException ignore) { + // no shaders mod installed + } catch (NoSuchFieldException e) { + // the field wasn't found. Has it been renamed? + e.printStackTrace(); + } + } + + private static void initConfigIsShaders() { + try { + config_isShaders = Class.forName("Config") + .getDeclaredMethod("isShaders"); + config_isShaders.setAccessible(true); + } catch (ClassNotFoundException ignore) { + // no shaders mod installed + } catch (NoSuchMethodException e) { + // the method wasn't found. Has it been renamed? + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderEntityRenderer.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderEntityRenderer.java new file mode 100644 index 00000000..358ff118 --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderEntityRenderer.java @@ -0,0 +1,28 @@ +package com.replaymod.compat.shaders.mixin; + +import com.replaymod.compat.shaders.ShaderReflection; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.client.renderer.EntityRenderer; +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(EntityRenderer.class) +public abstract class MixinShaderEntityRenderer { + + @Inject(method = "renderWorldPass", at = @At("HEAD")) + private void replayModCompat_updateShaderFrameTimeCounter(CallbackInfo ignore) { + if (ReplayModReplay.instance.getReplayHandler() == null) return; + if (ShaderReflection.shaders_frameTimeCounter == null) return; + + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + float timestamp = replayHandler.getReplaySender().currentTimeStamp() / 1000f % 3600f; + try { + ShaderReflection.shaders_frameTimeCounter.set(null, timestamp); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java new file mode 100644 index 00000000..3b4e9bc2 --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java @@ -0,0 +1,28 @@ +package com.replaymod.compat.shaders.mixin; + +import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.chunk.RenderChunk; +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.CallbackInfoReturnable; + +@Mixin(RenderChunk.class) +public abstract class MixinShaderRenderChunk { + + private final Minecraft mc = Minecraft.getMinecraft(); + + /** + * Changes the RenderChunk#isPlayerUpdate method that Optifine adds + * to always return true while rendering so no chunks are being added + * to a separate rendering queue + */ + @Inject(method = "isPlayerUpdate", at = @At("HEAD"), cancellable = true) + private void replayModCompat_disableIsPlayerUpdate(CallbackInfoReturnable ci) { + if (((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler() == null) return; + ci.setReturnValue(true); + } + + +} diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderGlobal.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderGlobal.java new file mode 100644 index 00000000..be69cf00 --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderGlobal.java @@ -0,0 +1,40 @@ +package com.replaymod.compat.shaders.mixin; + +import com.replaymod.compat.shaders.ShaderReflection; +import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.entity.Entity; +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; + +@Mixin(RenderGlobal.class) +public abstract class MixinShaderRenderGlobal { + + private final Minecraft mc = Minecraft.getMinecraft(); + + @Shadow + public boolean displayListEntitiesDirty; + + @Inject(method = "setupTerrain", at = @At("HEAD"), cancellable = true) + public void replayModCompat_setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, + int frameCount, boolean playerSpectator, CallbackInfo ci) { + if (((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler() == null) return; + if (ShaderReflection.shaders_isShadowPass == null) return; + + // when called by the shadow pass, displayListEntitiesDirty can't be set to false, as no chunk updates + // are being processed. As it's being set to true by ChunkLoadingRenderGlobal#updateChunks, we have to + // set it to false manually to exit the loop imposed by MixinRenderGlobal#replayModRender_setupTerrain. + try { + if ((boolean) ShaderReflection.shaders_isShadowPass.get(null) == true) { + displayListEntitiesDirty = false; + } + } catch (IllegalAccessException ignore) {} + + } + +} diff --git a/src/main/java/com/replaymod/core/LoadingPlugin.java b/src/main/java/com/replaymod/core/LoadingPlugin.java index a4badbe0..5e7042f9 100755 --- a/src/main/java/com/replaymod/core/LoadingPlugin.java +++ b/src/main/java/com/replaymod/core/LoadingPlugin.java @@ -19,6 +19,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin { Mixins.addConfiguration("mixins.recording.replaymod.json"); Mixins.addConfiguration("mixins.render.replaymod.json"); Mixins.addConfiguration("mixins.replay.replaymod.json"); + Mixins.addConfiguration("mixins.compat.shaders.replaymod.json"); CodeSource codeSource = getClass().getProtectionDomain().getCodeSource(); if (codeSource != null) { diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index 41e13c8c..de95d13e 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -34,6 +34,7 @@ import java.util.List; @Mod(modid = ReplayMod.MOD_ID, useMetadata = true, + updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", guiFactory = "com.replaymod.core.gui.GuiFactory") public class ReplayMod { diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java index 54a7eca4..750111a6 100644 --- a/src/main/java/com/replaymod/extras/ReplayModExtras.java +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -29,8 +29,7 @@ public class ReplayModExtras { FullBrightness.class, HotkeyButtons.class, LocalizationExtra.class, - OpenEyeExtra.class, - VersionChecker.class + OpenEyeExtra.class ); private Logger logger; diff --git a/src/main/java/com/replaymod/extras/VersionChecker.java b/src/main/java/com/replaymod/extras/VersionChecker.java deleted file mode 100644 index 8c0fd12f..00000000 --- a/src/main/java/com/replaymod/extras/VersionChecker.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.replaymod.extras; - -import com.replaymod.core.ReplayMod; -import com.replaymod.online.ReplayModOnline; -import net.minecraft.client.gui.Gui; -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.Mod; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; - -import java.awt.*; -import java.util.List; - - -public class VersionChecker implements Extra { - @Mod.Instance(ReplayModOnline.MOD_ID) - private static ReplayModOnline module; - - @Override - public void register(ReplayMod mod) throws Exception { - final String currentVersion = mod.getVersion(); - new Thread(new Runnable() { - @Override - public void run() { - try { - boolean upToDate = module.getApiClient().isVersionUpToDate(currentVersion); - if (!upToDate) { - MinecraftForge.EVENT_BUS.register(VersionChecker.this); - } - } catch(Exception e) { - e.printStackTrace(); - } - } - }, "ReplayMod-VersionChecker").start(); - } - - @SubscribeEvent - public void onDrawScreen(GuiScreenEvent.DrawScreenEvent.Post event) { - if (!(event.getGui() instanceof GuiMainMenu)) { - return; - } - - int width = Math.max(100, event.getGui().width / 2 - 100 - 10); - - @SuppressWarnings("unchecked") List lines = - event.getGui().mc.fontRendererObj.listFormattedStringToWidth(I18n.format("replaymod.gui.outdated"), width); - - int maxLineWidth = 0; - for(String line : lines) { - int lineWidth = event.getGui().mc.fontRendererObj.getStringWidth(line); - if(lineWidth > maxLineWidth) { - maxLineWidth = lineWidth; - } - } - - Gui.drawRect(2, 77, 5 + maxLineWidth + 3, 80 + (lines.size() * 10), 0x80FF0000); - - int i = 0; - for(String line : lines) { - event.getGui().mc.fontRendererObj.drawStringWithShadow(line, 5, 80 + (i * 10), Color.WHITE.getRGB()); - i++; - } - } -} diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java index 7ecfa148..9a39e54d 100644 --- a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java +++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java @@ -11,15 +11,18 @@ import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.element.GuiToggleButton; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import net.minecraft.client.resources.I18n; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; import java.util.ArrayList; import java.util.List; -public class GuiReplayCenterSearch extends AbstractGuiPopup { +public class GuiReplayCenterSearch extends AbstractGuiPopup implements Typeable { private final GuiReplayCenter replayCenter; private final ApiClient apiClient; public final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.center.search.filters").setColor(Colors.BLACK); @@ -100,4 +103,13 @@ public class GuiReplayCenterSearch extends AbstractGuiPopup future; @@ -33,6 +34,11 @@ public abstract class AbstractTimelinePlayer { this.replayHandler = replayHandler; } + public ListenableFuture start(Timeline timeline, long from) { + startOffset = from; + return start(timeline); + } + public ListenableFuture start(Timeline timeline) { this.timeline = timeline; diff --git a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java index eca409f4..4271ddbf 100644 --- a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java +++ b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java @@ -46,6 +46,6 @@ public class RealtimeTimelinePlayer extends AbstractTimelinePlayer { @Override public long getTimePassed() { - return firstFrame ? 0 : System.currentTimeMillis() - startTime; + return startOffset + (firstFrame ? 0 : System.currentTimeMillis() - startTime); } } diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/VideoWriter.java index 6fcce914..66665768 100755 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/VideoWriter.java @@ -8,6 +8,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; import org.apache.commons.exec.CommandLine; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; import org.lwjgl.util.ReadableDimension; @@ -34,6 +35,7 @@ public class VideoWriter implements FrameConsumer { this.settings = settings; File outputFolder = settings.getOutputFile().getParentFile(); + FileUtils.forceMkdir(outputFolder); String fileName = settings.getOutputFile().getName(); commandArgs = settings.getExportArguments() diff --git a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java index 54cab003..8c1d96a0 100644 --- a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java @@ -6,11 +6,11 @@ import com.replaymod.render.frame.ODSOpenGlFrame; import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.rendering.FrameCapturer; import com.replaymod.render.shader.Program; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.crash.CrashReport; import net.minecraft.util.ReportedException; import net.minecraft.util.ResourceLocation; -import org.lwjgl.util.Dimension; import org.lwjgl.util.ReadableDimension; import java.io.IOException; @@ -31,6 +31,8 @@ public class ODSFrameCapturer implements FrameCapturer { private final BooleanState[] previousStates = new BooleanState[3]; private final BooleanState previousFogState; + private final Minecraft mc = Minecraft.getMinecraft(); + public ODSFrameCapturer(WorldRenderer worldRenderer, final RenderInfo renderInfo, int frameSize) { RenderInfo fakeInfo = new RenderInfo() { private int call; @@ -143,6 +145,8 @@ public class ODSFrameCapturer implements FrameCapturer { @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { + resize(getFrameWidth(), getFrameHeight()); + pushMatrix(); frameBuffer().bindFramebuffer(true); @@ -150,7 +154,7 @@ public class ODSFrameCapturer implements FrameCapturer { enableTexture2D(); directionVariable.set(captureData.ordinal()); - worldRenderer.renderWorld(new Dimension(getFrameWidth(), getFrameHeight()), partialTicks, null); + worldRenderer.renderWorld(partialTicks, null); frameBuffer().unbindFramebuffer(); popMatrix(); diff --git a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java index 4513f2ae..d0968582 100644 --- a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java @@ -4,6 +4,7 @@ import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.rendering.Frame; import com.replaymod.render.rendering.FrameCapturer; import com.replaymod.render.utils.ByteBufferPool; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.shader.Framebuffer; import org.lwjgl.opengl.GL11; @@ -24,6 +25,8 @@ public abstract class OpenGlFrameCapturer bitRateUnit = new GuiDropdownMenu() @@ -130,7 +130,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { new GuiPanel().addElements(null, bitRateField, bitRateUnit).setLayout(new HorizontalLayout()), frameRateSlider).setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(3)), new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton) - .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(3)); + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(5)); public final GuiCheckbox nametagCheckbox = new GuiCheckbox() .setI18nLabel("replaymod.gui.rendersettings.nametags"); @@ -284,6 +284,15 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { videoHeight.setTextColor(Colors.RED); } + // Enable/Disable bitrate input field and dropdown + if (encodingPresetDropdown.getSelectedValue().hasBitrateSetting()) { + bitRateField.setEnabled(); + bitRateUnit.setEnabled(); + } else { + bitRateField.setDisabled(); + bitRateUnit.setDisabled(); + } + // Enable/Disable camera stabilization checkboxes switch (renderMethodDropdown.getSelectedValue()) { case CUBIC: diff --git a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java index 7777bf6f..0f60f3b4 100644 --- a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java +++ b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java @@ -14,7 +14,6 @@ import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.client.renderer.texture.TextureUtil; import net.minecraft.client.resources.I18n; import net.minecraft.util.ResourceLocation; import org.lwjgl.util.Dimension; @@ -23,8 +22,6 @@ import org.lwjgl.util.ReadablePoint; import java.nio.ByteBuffer; -import static net.minecraft.client.renderer.GlStateManager.bindTexture; - public class GuiVideoRenderer extends GuiScreen { private static final ResourceLocation NO_PREVIEW_TEXTURE = new ResourceLocation("replaymod", "logo.jpg"); @@ -232,13 +229,7 @@ public class GuiVideoRenderer extends GuiScreen { final int videoHeight = videoSize.getHeight(); if (previewTexture == null) { - previewTexture = new DynamicTexture(videoWidth, videoHeight) { - @Override - public void updateDynamicTexture() { - bindTexture(getGlTextureId()); - TextureUtil.uploadTextureSub(0, getTextureData(), videoWidth, videoHeight, 0, 0, true, false, false); - } - }; + previewTexture = new DynamicTexture(videoWidth, videoHeight); } if (previewTextureDirty) { @@ -273,7 +264,10 @@ public class GuiVideoRenderer extends GuiScreen { buffer.mark(); synchronized (this) { int[] data = previewTexture.getTextureData(); - for (int i = 0; i < data.length; i++) { + // Optifine changes the texture data array to be three times as long (for use by shaders), + // we only want to initialize the first third which is why we use the length of the buffer instead + // of the length of the data array + for (int i = 0; buffer.remaining() > 0; i++) { data[i] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | (buffer.get() & 0xff); } previewTextureDirty = true; diff --git a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java index e3c73168..f27ada50 100644 --- a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java +++ b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java @@ -5,14 +5,11 @@ import com.replaymod.render.capturer.CaptureData; import com.replaymod.render.capturer.WorldRenderer; import lombok.Getter; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.culling.ClippingHelper; +import net.minecraft.client.renderer.GlStateManager; import net.minecraftforge.fml.common.FMLCommonHandler; -import org.lwjgl.util.ReadableDimension; import java.io.IOException; -import static net.minecraft.client.renderer.GlStateManager.*; - public class EntityRendererHandler implements WorldRenderer { public final Minecraft mc = Minecraft.getMinecraft(); @@ -29,27 +26,10 @@ public class EntityRendererHandler implements WorldRenderer { ((IEntityRenderer) mc.entityRenderer).replayModRender_setHandler(this); } - public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) { - final int prevWidth = mc.displayWidth; - final int prevHeight = mc.displayHeight; - mc.displayWidth = displayWidth; - mc.displayHeight = displayHeight; - - runnable.run(); - - mc.displayWidth = prevWidth; - mc.displayHeight = prevHeight; - } - @Override - public void renderWorld(ReadableDimension displaySize, final float partialTicks, CaptureData data) { + public void renderWorld(final float partialTicks, CaptureData data) { this.data = data; - withDisplaySize(displaySize.getWidth(), displaySize.getHeight(), new Runnable() { - @Override - public void run() { - renderWorld(partialTicks, 0); - } - }); + renderWorld(partialTicks, 0); } public void renderWorld(float partialTicks, long finishTimeNano) { @@ -57,9 +37,9 @@ public class EntityRendererHandler implements WorldRenderer { mc.entityRenderer.updateLightmap(partialTicks); - enableDepth(); - enableAlpha(); - alphaFunc(516, 0.5F); + GlStateManager.enableDepth(); + GlStateManager.enableAlpha(); + GlStateManager.alphaFunc(516, 0.5F); mc.entityRenderer.renderWorldPass(2, partialTicks, finishTimeNano); @@ -76,13 +56,6 @@ public class EntityRendererHandler implements WorldRenderer { this.omnidirectional = omnidirectional; } - public static final class NoCullingClippingHelper extends ClippingHelper { - @Override - public boolean isBoxInFrustum(double p_78553_1_, double p_78553_3_, double p_78553_5_, double p_78553_7_, double p_78553_9_, double p_78553_11_) { - return true; - } - } - public interface GluPerspective { void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar); } diff --git a/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java b/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java index 5ca6dd0f..8c792b4c 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java +++ b/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java @@ -9,7 +9,6 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.EntityRenderer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.culling.Frustum; import net.minecraft.client.settings.GameSettings; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -49,14 +48,6 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti } } - @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/culling/Frustum;setPosition(DDD)V")) - public void replayModRender_createNoCullingFrustum(Frustum frustum, double x, double y, double z) { - if (replayModRender_handler != null) { - frustum.clippingHelper = new EntityRendererHandler.NoCullingClippingHelper(); - } - frustum.setPosition(x, y, z); - } - @Inject(method = "orientCamera", at = @At("HEAD")) private void replayModRender_resetRotationIfNeeded(float partialTicks, CallbackInfo ci) { if (replayModRender_handler != null) { diff --git a/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java index 7319adb7..e388aec9 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java +++ b/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java @@ -38,6 +38,8 @@ public abstract class MixinRenderGlobal { replayModRender_hook.updateChunks(); } while (displayListEntitiesDirty); + displayListEntitiesDirty = true; + replayModRender_passThroughSetupTerrain = false; ci.cancel(); } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 0cccb448..2d017740 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -19,6 +19,7 @@ import com.replaymod.replaystudio.pathing.path.Timeline; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; import net.minecraft.util.SoundCategory; import net.minecraft.util.Timer; import org.lwjgl.input.Mouse; @@ -62,6 +63,9 @@ public class VideoRenderer implements RenderInfo { private boolean paused; private boolean cancelled; + private Framebuffer guiFramebuffer; + private int displayWidth, displayHeight; + public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException { this.settings = settings; this.replayHandler = replayHandler; @@ -130,6 +134,13 @@ public class VideoRenderer implements RenderInfo { @Override public float updateForNextFrame() { + // because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily + int displayWidthBefore = mc.displayWidth; + int displayHeightBefore = mc.displayHeight; + + mc.displayWidth = displayWidth; + mc.displayHeight = displayHeight; + if (!settings.isHighPerformance() || framesDone % fps == 0) { drawGui(); } @@ -142,6 +153,10 @@ public class VideoRenderer implements RenderInfo { tick(); } + // change Minecraft's display size back + mc.displayWidth = displayWidthBefore; + mc.displayHeight = displayHeightBefore; + framesDone++; return mc.timer.renderPartialTicks; } @@ -192,10 +207,12 @@ public class VideoRenderer implements RenderInfo { totalFrames = (int) (duration*fps/1000); - ScaledResolution scaled = new ScaledResolution(mc); - gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); + updateDisplaySize(); chunkLoadingRenderGlobal = new ChunkLoadingRenderGlobal(mc.renderGlobal); + + // Set up our own framebuffer to render the GUI to + guiFramebuffer = new Framebuffer(displayWidth, displayHeight, true); } private void finish() { @@ -219,6 +236,9 @@ public class VideoRenderer implements RenderInfo { new SoundHandler().playRenderSuccessSound(); new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display(); + + // Finally, resize the Minecraft framebuffer to the actual width/height of the window + mc.resize(displayWidth, displayHeight); } private void tick() { @@ -238,12 +258,22 @@ public class VideoRenderer implements RenderInfo { public void drawGui() { do { + // Resize the GUI framebuffer if the display size changed + if (!settings.isHighPerformance() && displaySizeChanged()) { + updateDisplaySize(); + guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight); + } + pushMatrix(); clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); enableTexture2D(); - mc.getFramebuffer().bindFramebuffer(true); + guiFramebuffer.bindFramebuffer(true); + mc.entityRenderer.setupOverlayRendering(); + ScaledResolution scaled = new ScaledResolution(mc); + gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); + try { gui.toMinecraft().handleInput(); } catch (IOException e) { @@ -252,18 +282,17 @@ public class VideoRenderer implements RenderInfo { throw new RuntimeException(e); } - ScaledResolution scaled = new ScaledResolution(mc); int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth; int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1; + gui.toMinecraft().drawScreen(mouseX, mouseY, 0); - mc.getFramebuffer().unbindFramebuffer(); + guiFramebuffer.unbindFramebuffer(); popMatrix(); pushMatrix(); - mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + guiFramebuffer.framebufferRender(displayWidth, displayHeight); popMatrix(); - // if not in high performance mode, update the gui size if screen size changed // otherwise just swap the progress gui to screen if (settings.isHighPerformance()) { @@ -285,6 +314,15 @@ public class VideoRenderer implements RenderInfo { } while (paused); } + private boolean displaySizeChanged() { + return displayWidth != Display.getWidth() || displayHeight != Display.getHeight(); + } + + private void updateDisplaySize() { + displayWidth = Display.getWidth(); + displayHeight = Display.getHeight(); + } + public int getFramesDone() { return framesDone; } diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 936395ab..cb8689ee 100755 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -29,10 +29,7 @@ import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; import org.lwjgl.opengl.Display; import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static net.minecraft.client.renderer.GlStateManager.*; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; @@ -62,7 +59,7 @@ public class ReplayHandler { */ private boolean suppressCameraMovements; - private final Set markers; + private final List markers; private final GuiReplayOverlay overlay; @@ -81,7 +78,7 @@ public class ReplayHandler { MinecraftForge.EVENT_BUS.post(new ReplayOpenEvent.Pre(this)); - markers = new HashSet<>(replayFile.getMarkers().or(Collections.emptySet())); + markers = new ArrayList<>(replayFile.getMarkers().or(Collections.emptySet())); replaySender = new ReplaySender(this, replayFile, asyncMode); @@ -188,9 +185,9 @@ public class ReplayHandler { /** * Returns all markers. * When changed, {@link #saveMarkers()} should be called to save the changes. - * @return Set of markers + * @return Collection of markers in no particular order */ - public Set getMarkers() { + public Collection getMarkers() { return markers; } @@ -199,7 +196,7 @@ public class ReplayHandler { */ public void saveMarkers() { try { - replayFile.writeMarkers(markers); + replayFile.writeMarkers(new HashSet<>(markers)); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 3701f4e8..e038ab92 100755 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -10,6 +10,7 @@ import lombok.Getter; import lombok.Setter; import net.minecraft.block.material.Material; import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.AbstractClientPlayer; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.settings.KeyBinding; @@ -237,6 +238,14 @@ public class CameraEntity extends EntityPlayerSP { return ReplayModReplay.instance.getReplayHandler().isCameraView(); // Make sure we're treated as spectator } + @Override + public boolean shouldRenderInPass(int pass) { + // Never render the camera + // This is necessary to hide the player head in third person mode and to not + // cause any unwanted shadows when rendering with shaders. + return false; + } + @Override public ResourceLocation getLocationSkin() { Entity view = mc.getRenderViewEntity(); @@ -246,6 +255,15 @@ public class CameraEntity extends EntityPlayerSP { return super.getLocationSkin(); } + @Override + public String getSkinType() { + Entity view = mc.getRenderViewEntity(); + if (view != this && view instanceof AbstractClientPlayer) { + return ((AbstractClientPlayer) view).getSkinType(); + } + return super.getSkinType(); + } + @Override public float getSwingProgress(float renderPartialTicks) { Entity view = mc.getRenderViewEntity(); diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java index 27c46da6..23e9e5b5 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java @@ -5,14 +5,17 @@ import com.replaymod.replay.ReplayHandler; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.*; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import com.replaymod.replaystudio.data.Marker; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; -public class GuiEditMarkerPopup extends AbstractGuiPopup { +public class GuiEditMarkerPopup extends AbstractGuiPopup implements Typeable { private final ReplayHandler replayHandler; private final Marker marker; @@ -107,4 +110,13 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup { protected GuiEditMarkerPopup getThis() { return this; } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_ESCAPE) { + cancelButton.onClick(); + return true; + } + return false; + } } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java index 229237f0..04cf7391 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java @@ -133,6 +133,8 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im replayHandler.doJump(marker.getTime(), false); } return true; + } else { + selectedMarker = null; } return super.mouseClick(position, button); } 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 fd2be83e..eeba30db 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -7,8 +7,10 @@ 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.GuiElement; import de.johni0702.minecraft.gui.element.GuiSlider; import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; @@ -27,8 +29,20 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { 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 GuiTexturedButton playPauseButton = new GuiTexturedButton() { + @Override + public GuiElement getTooltip(RenderInfo renderInfo) { + GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); + if (tooltip != null) { + if (getTextureNormal().getY() == 0) { // Play button + tooltip.setI18nText("replaymod.gui.ingame.menu.unpause"); + } else { // Pause button + tooltip.setI18nText("replaymod.gui.ingame.menu.pause"); + } + } + return tooltip; + } + }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE).setTooltip(new GuiTooltip()); public final GuiSlider speedSlider = new GuiSlider().setSize(100, 20).setSteps(37); // 0.0 is not included public final GuiMarkerTimeline timeline; diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index 307238f3..b9ba3961 100755 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -101,6 +101,8 @@ public class GuiReplayViewer extends GuiScreen { }).onSelectionDoubleClicked(() -> { if (this.loadButton.isEnabled()) { this.loadButton.onClick(); + // Disable load button to prevent the player from opening the replay twice at the same time + this.loadButton.setDisabled(); } }).setDrawShadow(true).setDrawSlider(true); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 335d79cf..edb160a5 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -13,16 +13,20 @@ import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiNumberField; import de.johni0702.minecraft.gui.element.IGuiLabel; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.Consumer; import org.apache.commons.lang3.tuple.Triple; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; import static de.johni0702.minecraft.gui.utils.Utils.link; -public abstract class GuiEditKeyframe> extends AbstractGuiPopup { +public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { private static GuiNumberField newGuiNumberField() { return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true); } @@ -68,9 +72,10 @@ public abstract class GuiEditKeyframe> extends Abst this.path = path; long time = keyframe.getTime(); - timeMinField.setValue(time / 1000 / 60); - timeSecField.setValue(time / 1000 % 60); - timeMSecField.setValue(time % 1000); + Consumer updateSaveButtonState = s -> saveButton.setEnabled(canSave()); + timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState); + timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState); + timeMSecField.setValue(time % 1000).onTextChanged(updateSaveButtonState); title.setI18nText("replaymod.gui.editkeyframe.title." + type); saveButton.onClick(() -> { @@ -84,6 +89,23 @@ public abstract class GuiEditKeyframe> extends Abst }); } + private boolean canSave() { + long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger(); + if (newTime != keyframe.getTime() && path.getKeyframe(newTime) != null) { + return false; + } + return true; + } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_ESCAPE) { + cancelButton.onClick(); + return true; + } + return false; + } + @Override public void open() { super.open(); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index e9017265..72f1368c 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -121,24 +121,42 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline= endTime || endFrameTime <= startTime) { - continue; // Segment out of display range - } + drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + 1, 0xFF0088FF); + } - double relativeStart = startFrameTime - startTime; - double relativeEnd = endFrameTime - startTime; - int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1); - int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2); - if (startX < endX) { - renderer.drawRect(startX + 1, BORDER_TOP + 1, endX - startX - 2, KEYFRAME_SIZE - 2, 0xFF0088FF); + // Draw red quads on time path segments that would require time going backwards + for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.TIME_PATH).getSegments()) { + long startTimestamp = segment.getStartKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + long endTimestamp = segment.getEndKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + if (endTimestamp >= startTimestamp) { + continue; // All is fine, time is not moving backwards } + drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + KEYFRAME_SIZE + 1, 0xFFFF0000); } super.drawTimelineCursor(renderer, size); } + private void drawQuadOnSegment(GuiRenderer renderer, int visibleWidth, PathSegment segment, int y, int color) { + int startTime = getOffset(); + int visibleTime = (int) (getZoom() * getLength()); + int endTime = getOffset() + visibleTime; + + long startFrameTime = segment.getStartKeyframe().getTime(); + long endFrameTime = segment.getEndKeyframe().getTime(); + if (startFrameTime >= endTime || endFrameTime <= startTime) { + return; // Segment out of display range + } + + double relativeStart = startFrameTime - startTime; + double relativeEnd = endFrameTime - startTime; + int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1); + int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2); + if (startX < endX) { + renderer.drawRect(startX + 1, y, endX - startX - 2, KEYFRAME_SIZE - 2, color); + } + } + /** * Returns the keyframe at the specified position. * @param position The raw position @@ -243,6 +261,15 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline future = player.start(timeline); + // Start from cursor time unless the control key is pressed (then start from beginning) + int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition(); + ListenableFuture future = player.start(timeline, startTime); overlay.setCloseable(false); overlay.setMouseVisible(true); Futures.addCallback(future, new FutureCallback() { @@ -403,6 +455,15 @@ public class GuiPathing { }); }); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> { + if (!overlay.isVisible()) { + return; + } + if (mod.getSelectedKeyframe() != null) { + updateKeyframe(mod.getSelectedKeyframe().getValue(TimestampProperty.PROPERTY).isPresent()); + } + }); + // Start loading entity tracker entityTrackerFuture = SettableFuture.create(); new Thread(() -> { @@ -429,10 +490,27 @@ public class GuiPathing { }).start(); } - private void preparePathsForPlayback() { + private boolean preparePathsForPlayback() { Timeline timeline = mod.getCurrentTimeline(); timeline.getPaths().get(TIME_PATH).updateAll(); timeline.getPaths().get(POSITION_PATH).updateAll(); + + // Make sure time keyframes's values are monotonically increasing + int lastTime = 0; + for (Keyframe keyframe : timeline.getPaths().get(TIME_PATH).getKeyframes()) { + int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + if (time < lastTime) { + // We are going backwards in time + GuiInfoPopup.open(replayHandler.getOverlay(), + "replaymod.error.negativetime1", + "replaymod.error.negativetime2", + "replaymod.error.negativetime3"); + return false; + } + lastTime = time; + } + + return true; } public void zoomTimeline(double factor) { diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 1c660296..ecdf54ef 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -485,6 +485,9 @@ replaymod.gui.objects=Custom Objects #Errors replaymod.error.unknownrestriction1=This replay cannot be played with your current version. replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown. +replaymod.error.negativetime1=Some of your time keyframes are out of order. +replaymod.error.negativetime2=Going backwards in time is not supported. +replaymod.error.negativetime3=The invalid parts are marked in red. #Replay Mod Incompatibility Warning replaymod.gui.modwarning.title=Incompatibility detected diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 144f7a72..f6ac199a 100755 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -48,7 +48,7 @@ "parent": "replaymod", "screenshots": [], "dependencies": [] - }/*, + }, { "modid": "replaymod-online", "name": "Replay Mod - Online", @@ -116,5 +116,22 @@ "parent": "replaymod", "screenshots": [], "dependencies": [] - }*/ + }, + { + "modid": "replaymod-compat", + "name": "Replay Mod - Compatibility", + "description": "Compatibility Module of the ReplayMod - Adds compatibility with other mods", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": [] + } ] diff --git a/src/main/resources/mixins.compat.shaders.replaymod.json b/src/main/resources/mixins.compat.shaders.replaymod.json new file mode 100644 index 00000000..eea17922 --- /dev/null +++ b/src/main/resources/mixins.compat.shaders.replaymod.json @@ -0,0 +1,12 @@ +{ + "required": false, + "package": "com.replaymod.compat.shaders.mixin", + "mixins": [], + "server": [], + "client": [ + "MixinShaderEntityRenderer", + "MixinShaderRenderChunk", + "MixinShaderRenderGlobal" + ], + "refmap": "mixins.replaymod.refmap.json" +} \ No newline at end of file