package com.replaymod.simplepathing.gui; import com.replaymod.core.ReplayMod; import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.SPTimeline.SPPath; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; import de.johni0702.minecraft.gui.function.Click; import de.johni0702.minecraft.gui.function.Draggable; import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector2f; import net.minecraft.client.render.BufferBuilder; import net.minecraft.client.render.Tessellator; import net.minecraft.client.render.VertexFormats; import net.minecraft.client.util.math.MatrixStack; import org.apache.commons.lang3.tuple.Pair; import de.johni0702.minecraft.gui.utils.lwjgl.Point; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import org.lwjgl.opengl.GL11; import java.util.Comparator; import java.util.Optional; import static com.replaymod.core.versions.MCVer.emitLine; import static de.johni0702.minecraft.gui.versions.MCVer.popScissorState; import static de.johni0702.minecraft.gui.versions.MCVer.pushScissorState; import static de.johni0702.minecraft.gui.versions.MCVer.setScissorDisabled; //#if MC>=12111 //$$ import net.minecraft.client.render.RenderLayers; //#endif //#if MC>=12106 //$$ import com.replaymod.replay.mixin.DrawContextAccessor; //$$ import com.replaymod.render.mixin.GameRendererAccessor; //$$ import net.minecraft.client.MinecraftClient; //$$ import net.minecraft.client.gui.ScreenRect; //$$ import net.minecraft.client.gui.render.SpecialGuiElementRenderer; //$$ import net.minecraft.client.gui.render.state.special.SpecialGuiElementRenderState; //$$ import net.minecraft.client.util.ClosableFactory; //$$ import net.minecraft.client.util.Pool; //$$ import net.minecraft.client.render.RenderLayer; //$$ import net.minecraft.client.render.VertexConsumerProvider; //$$ import org.jetbrains.annotations.Nullable; //$$ import java.util.ArrayList; //$$ import java.util.List; //#endif //#if MC>=12105 //$$ import net.minecraft.client.render.RenderLayer; //$$ import net.minecraft.client.render.VertexConsumer; //$$ import net.minecraft.client.render.VertexConsumerProvider; //#endif //#if MC>=12102 //#if MC<12105 //$$ import net.minecraft.client.gl.ShaderProgramKeys; //#endif //#endif //#if MC>=11700 //$$ import com.mojang.blaze3d.systems.RenderSystem; //$$ import net.minecraft.client.render.GameRenderer; //#endif public class GuiKeyframeTimeline extends AbstractGuiTimeline implements Draggable { protected static final int KEYFRAME_SIZE = 5; protected static final int KEYFRAME_TEXTURE_X = 74; protected static final int KEYFRAME_TEXTURE_Y = 20; private static final int DOUBLE_CLICK_INTERVAL = 250; private static final int DRAGGING_THRESHOLD = KEYFRAME_SIZE; private final GuiPathing gui; /** * The keyframe (time on timeline) that was last clicked on using the left mouse button. */ private long lastClickedKeyframe; /** * Path of {@link #lastClickedKeyframe}. */ private SPPath lastClickedPath; /** * The time at which {@link #lastClickedKeyframe} was updated. * According to {@link MCVer#milliTime()}. */ private long lastClickedTime; /** * Whether to handle dragging events. */ private boolean dragging; /** * Whether we have surpassed the initial threshold and are actually dragging the keyframe. */ private boolean actuallyDragging; /** * Where the mouse was when {@link #dragging} started. */ private int draggingStartX; /** * Change caused by dragging. Whenever the user moves the keyframe further, the previous change is undone * and a new one is created. This way when the mouse is released, only one change is in the undo history. */ private Change draggingChange; public GuiKeyframeTimeline(GuiPathing gui) { this.gui = gui; } @Override protected void drawTimelineCursor(GuiRenderer renderer, ReadableDimension size) { ReplayModSimplePathing mod = gui.getMod(); int width = size.getWidth(); int visibleWidth = width - BORDER_LEFT - BORDER_RIGHT; int startTime = getOffset(); int visibleTime = (int) (getZoom() * getLength()); int endTime = getOffset() + visibleTime; renderer.bindTexture(ReplayMod.TEXTURE); //#if MC>=12106 //$$ TimeTimelineLinesRenderState linesRenderState = new TimeTimelineLinesRenderState(); //#endif SPTimeline timeline = mod.getCurrentTimeline(); timeline.getTimeline().getPaths().stream().flatMap(path -> path.getKeyframes().stream()).forEach(keyframe -> { if (keyframe.getTime() >= startTime && keyframe.getTime() <= endTime) { double relativeTime = keyframe.getTime() - startTime; int positonX = BORDER_LEFT + (int) (relativeTime / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2; int u = KEYFRAME_TEXTURE_X + (mod.isSelected(keyframe) ? KEYFRAME_SIZE : 0); int v = KEYFRAME_TEXTURE_Y; if (keyframe.getValue(CameraProperties.POSITION).isPresent()) { if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) { v += 2 * KEYFRAME_SIZE; } renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); } Optional timeProperty = keyframe.getValue(TimestampProperty.PROPERTY); if (timeProperty.isPresent()) { v += KEYFRAME_SIZE; renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); GuiMarkerTimeline replayTimeline = gui.overlay.timeline; GuiKeyframeTimeline keyframeTimeline = this; ReadableDimension replayTimelineSize = replayTimeline.getLastSize(); ReadableDimension keyframeTimelineSize = this.getLastSize(); if (replayTimelineSize == null || keyframeTimelineSize == null) { return; } // Determine absolute positions for both timelines Point replayTimelinePos = new Point(0, 0); Point keyframeTimelinePos = new Point(0, 0); replayTimeline.getContainer().convertFor(replayTimeline, replayTimelinePos); keyframeTimeline.getContainer().convertFor(keyframeTimeline, keyframeTimelinePos); replayTimelinePos.setLocation(-replayTimelinePos.getX(), -replayTimelinePos.getY()); keyframeTimelinePos.setLocation(-keyframeTimelinePos.getX(), -keyframeTimelinePos.getY()); int replayTimelineLeft = replayTimelinePos.getX(); int replayTimelineRight = replayTimelinePos.getX() + replayTimelineSize.getWidth(); int replayTimelineTop = replayTimelinePos.getY(); int replayTimelineBottom = replayTimelinePos.getY() + replayTimelineSize.getHeight(); int replayTimelineWidth = replayTimelineRight - replayTimelineLeft - BORDER_LEFT - BORDER_RIGHT; int keyframeTimelineLeft = keyframeTimelinePos.getX(); int keyframeTimelineTop = keyframeTimelinePos.getY(); float positionXReplayTimeline = BORDER_LEFT + timeProperty.get() / (float) replayTimeline.getLength() * replayTimelineWidth; float positionXKeyframeTimeline = positonX + KEYFRAME_SIZE / 2f; final int color = 0xff0000ff; final float lineWidth = 2f; //#if MC>=12105 //$$ VertexConsumerProvider.Immediate immediate = getMinecraft().getBufferBuilders().getEntityVertexConsumers(); //$$ immediate.draw(); //#if MC>=12111 //$$ VertexConsumer buffer = immediate.getBuffer(RenderLayers.LINES); //#else //$$ VertexConsumer buffer = immediate.getBuffer(RenderLayer.LINE_STRIP); //#endif //#else Tessellator tessellator = Tessellator.getInstance(); //#if MC>=12100 //$$ BufferBuilder buffer = tessellator.begin(net.minecraft.client.render.VertexFormat.DrawMode.LINE_STRIP, VertexFormats.LINES); //#else BufferBuilder buffer = tessellator.getBuffer(); buffer.begin(GL11.GL_LINE_STRIP, VertexFormats.POSITION_COLOR); //#endif //#endif // Start just below the top border of the replay timeline Vector2f p1 = new Vector2f(replayTimelineLeft + positionXReplayTimeline, replayTimelineTop + BORDER_TOP); // Draw vertically over the replay timeline, including its bottom border Vector2f p2 = new Vector2f(replayTimelineLeft + positionXReplayTimeline, replayTimelineBottom); // Now for the important part: connecting to the keyframe timeline Vector2f p3 = new Vector2f(keyframeTimelineLeft + positionXKeyframeTimeline, keyframeTimelineTop); // And finally another vertical bit (the timeline is already crammed enough, so only the border) Vector2f p4 = new Vector2f(keyframeTimelineLeft + positionXKeyframeTimeline, keyframeTimelineTop + BORDER_TOP); //#if MC>=12106 //$$ linesRenderState.color = color; //$$ linesRenderState.lineWidth = lineWidth; //$$ linesRenderState.line(p1, p2); //$$ linesRenderState.line(p2, p3); //$$ linesRenderState.line(p3, p4); //#else MatrixStack matrixStack = renderer.getMatrixStack(); emitLine(matrixStack, buffer, p1, p2, color, lineWidth); emitLine(matrixStack, buffer, p2, p3, color, lineWidth); emitLine(matrixStack, buffer, p3, p4, color, lineWidth); pushScissorState(); setScissorDisabled(); //#if MC>=12105 //$$ immediate.draw(); //#else //#if MC>=12102 //$$ RenderSystem.setShader(ShaderProgramKeys.RENDERTYPE_LINES); //#elseif MC>=11700 //$$ RenderSystem.setShader(GameRenderer::getRenderTypeLinesShader); //#else GL11.glEnable(GL11.GL_LINE_SMOOTH); GL11.glDisable(GL11.GL_TEXTURE_2D); //#endif //#if MC>=12100 //$$ try (var builtBuffer = buffer.end()) { //$$ net.minecraft.client.render.BufferRenderer.drawWithGlobalProgram(builtBuffer); //$$ } //#else tessellator.draw(); //#endif //#if MC<11700 GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glDisable(GL11.GL_LINE_SMOOTH); //#endif //#endif popScissorState(); //#endif } } }); //#if MC>=12106 //$$ if (!linesRenderState.lines.isEmpty()) { //$$ MinecraftClient mc = getMinecraft(); //$$ int scale = mc.getWindow().getScaleFactor(); //$$ // MC's special rendering code has multiple issues, we'll use a size matching the screen to avoid some //$$ linesRenderState.x1 = 0; //$$ linesRenderState.y1 = 0; //$$ linesRenderState.x2 = mc.getWindow().getFramebufferWidth() / scale; //$$ linesRenderState.y2 = mc.getWindow().getFramebufferHeight() / scale; //$$ //$$ Pool pool = ((GameRendererAccessor) mc.gameRenderer).getPool(); //$$ TimeTimelineLinesRenderer linesRenderer = pool.acquire(TimeTimelineLinesRenderer.FACTORY); //$$ pushScissorState(); //$$ setScissorDisabled(); //$$ linesRenderer.render(linesRenderState, ((DrawContextAccessor) renderer.getContext()).getState(), scale); //$$ popScissorState(); //$$ pool.release(TimeTimelineLinesRenderer.FACTORY, linesRenderer); // Note: Assumes we only render one per frame //$$ } //#endif // Draw colored quads on spectator path segments for (PathSegment segment : timeline.getPositionPath().getSegments()) { if (segment.getInterpolator() == null || !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) { continue; // Not a spectator segment } drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + 1, 0xFF0088FF); } // Draw red quads on time path segments that would require time going backwards for (PathSegment segment : timeline.getTimePath().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); } //#if MC>=12106 //$$ private static class TimeTimelineLinesRenderState implements SpecialGuiElementRenderState { //$$ int x1, x2, y1, y2; //$$ ScreenRect scissorState; //$$ //$$ List> lines = new ArrayList<>(); //$$ int color; //$$ float lineWidth; //$$ //$$ public void line(Vector2f p1, Vector2f p2) { //$$ lines.add(Pair.of(p1, p2)); //$$ } //$$ //$$ @Override public int x1() {return x1;} //$$ @Override public int x2() {return x2;} //$$ @Override public int y1() {return y1;} //$$ @Override public int y2() {return y2;} //$$ @Override public float comp_4133() {return 1; /* scale */} //$$ @Override public @Nullable ScreenRect comp_4128() {return scissorState;} //$$ @Override public @Nullable ScreenRect comp_4274() {return SpecialGuiElementRenderState.createBounds(x1, y1, x2, y2, scissorState);} //$$ } //$$ //$$ private static class TimeTimelineLinesRenderer extends SpecialGuiElementRenderer { //$$ private static ClosableFactory FACTORY = new ClosableFactory<>() { //$$ @Override //$$ public TimeTimelineLinesRenderer create() { //$$ return new TimeTimelineLinesRenderer(MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers()); //$$ } //$$ //$$ @Override //$$ public void close(TimeTimelineLinesRenderer object) { //$$ object.close(); //$$ } //$$ }; //$$ //$$ protected TimeTimelineLinesRenderer(VertexConsumerProvider.Immediate immediate) { //$$ super(immediate); //$$ } //$$ //$$ @Override //$$ public Class getElementClass() { //$$ return TimeTimelineLinesRenderState.class; //$$ } //$$ //$$ @Override //$$ protected void render(TimeTimelineLinesRenderState state, MatrixStack matrixStack) { //$$ matrixStack.translate(-state.x2 / 2f, -state.y2, 100); //$$ for (Pair line : state.lines) { //#if MC>=12111 //$$ emitLine(matrixStack, this.vertexConsumers.getBuffer(RenderLayers.LINES), line.getLeft(), line.getRight(), state.color, state.lineWidth); //#else //$$ emitLine(matrixStack, this.vertexConsumers.getBuffer(RenderLayer.LINES), line.getLeft(), line.getRight(), state.color, state.lineWidth); //#endif //$$ } //$$ } //$$ //$$ @Override //$$ protected String getName() { //$$ return "time_timeline_lines"; //$$ } //$$ } //#endif 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 * @return Pair of path id and keyframe or null when no keyframe was clicked */ private Pair getKeyframe(ReadablePoint position) { int time = getTimeAt(position.getX(), position.getY()); if (time != -1) { Point mouse = new Point(position); getContainer().convertFor(this, mouse); int mouseY = mouse.getY(); if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) { SPPath path; if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) { // Position keyframe path = SPPath.POSITION; } else { // Time keyframe path = SPPath.TIME; } int visibleTime = (int) (getZoom() * getLength()); int tolerance = visibleTime * KEYFRAME_SIZE / (getLastSize().getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2; Optional keyframe = gui.getMod().getCurrentTimeline().getPath(path).getKeyframes().stream() .filter(k -> Math.abs(k.getTime() - time) <= tolerance) .sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time))) .findFirst(); return Pair.of(path, keyframe.map(Keyframe::getTime).orElse(null)); } } return Pair.of(null, null); } @Override public boolean mouseClick(Click click) { int time = getTimeAt(click.x, click.y); Pair pathKeyframePair = getKeyframe(click); if (pathKeyframePair.getRight() != null) { SPPath path = pathKeyframePair.getLeft(); // Clicked on keyframe long keyframeTime = pathKeyframePair.getRight(); if (click.button == 0) { // Left click long now = MCVer.milliTime(); if (lastClickedKeyframe == keyframeTime) { // Clicked the same keyframe again, potentially a double click if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) { // Yup, double click, open the edit keyframe gui gui.openEditKeyframePopup(path, keyframeTime); return true; } } // Not a double click, just update the click time and selection lastClickedTime = now; lastClickedKeyframe = keyframeTime; lastClickedPath = path; gui.getMod().setSelected(lastClickedPath, lastClickedKeyframe); // We might be dragging draggingStartX = click.x; dragging = true; } else if (click.button == 1) { // Right click Keyframe keyframe = gui.getMod().getCurrentTimeline().getKeyframe(path, keyframeTime); for (Property property : keyframe.getProperties()) { applyPropertyToGame(property, keyframe); } } return true; } else if (time != -1) { // Clicked on timeline but not on any keyframe if (click.button == 0) { // Left click setCursorPosition(time); gui.getMod().setSelected(null, 0); } else if (click.button == 1) { // Right click if (pathKeyframePair.getLeft() != null) { // Apply the value of the clicked path at the clicked position Path path = gui.getMod().getCurrentTimeline().getPath(pathKeyframePair.getLeft()); path.getKeyframes().stream().flatMap(k -> k.getProperties().stream()).distinct().forEach( p -> applyPropertyToGame(p, path, time)); } } return true; } // Missed timeline return false; } // Helper method because generics cannot be defined on blocks private void applyPropertyToGame(Property property, Path path, long time) { Optional value = path.getValue(property, time); if (value.isPresent()) { property.applyToGame(value.get(), ReplayModReplay.instance.getReplayHandler()); } } // Helper method because generics cannot be defined on blocks private void applyPropertyToGame(Property property, Keyframe keyframe) { Optional value = keyframe.getValue(property); if (value.isPresent()) { property.applyToGame(value.get(), ReplayModReplay.instance.getReplayHandler()); } } @Override public boolean mouseDrag(Click click) { if (!dragging) { if (click.button == 0) { // Left click, the user might try to move the cursor by clicking and holding int time = getTimeAt(click.x, click.y); if (time != -1) { // and they are still on the timeline, so update the time appropriately setCursorPosition(time); return true; } } return false; } if (!actuallyDragging) { // Check if threshold has been passed by now if (Math.abs(click.x - draggingStartX) >= DRAGGING_THRESHOLD) { actuallyDragging = true; } } if (actuallyDragging) { if (!gui.loadEntityTracker(() -> mouseDrag(click))) return true; // Threshold passed SPTimeline timeline = gui.getMod().getCurrentTimeline(); Point mouse = new Point(click); getContainer().convertFor(this, mouse); int mouseX = mouse.getX(); int width = getLastSize().getWidth(); int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; double segmentLength = getLength() * getZoom(); double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth; int newTime = Math.min(Math.max((int) Math.round(getOffset() + segmentTime), 0), getLength()); if (newTime < 0) { return true; } // If there already is a keyframe at the target time, then increase the time by one until there is none while (timeline.getKeyframe(lastClickedPath, newTime) != null) { newTime++; } // First undo any previous changes if (draggingChange != null) { draggingChange.undo(timeline.getTimeline()); } // Move keyframe to new position and // store change for later undoing / pushing to history draggingChange = timeline.moveKeyframe(lastClickedPath, lastClickedKeyframe, newTime); // Selected keyframe has been replaced gui.getMod().setSelected(lastClickedPath, newTime); } return true; } @Override public boolean mouseRelease(Click click) { if (dragging) { if (actuallyDragging) { gui.getMod().getCurrentTimeline().getTimeline().pushChange(draggingChange); draggingChange = null; actuallyDragging = false; } dragging = false; return true; } return false; } @Override protected GuiKeyframeTimeline getThis() { return this; } }