From 4e59189eab11d708cadfbae85d90b2eff9071ee1 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 24 Aug 2021 20:20:29 +0200 Subject: [PATCH 001/132] Add Kotlin --- build.gradle | 18 ++++++++++++++++++ root.gradle.kts | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 192236b5..6ad951d7 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ if (['1.10.2', '1.11', '1.11.2'].contains(jGuiVersion)) jGuiVersion = '1.9.4' if (['1.12.1', '1.12.2'].contains(jGuiVersion)) jGuiVersion = '1.12' def jGui = project.evaluationDependsOn(":jGui:$jGuiVersion") +apply plugin: 'kotlin' apply plugin: 'com.github.johnrengelman.shadow' if (mcVersion >= 10800) { @@ -112,6 +113,12 @@ sourceCompatibility = targetCompatibility = mcVersion >= 11700 ? 16 : 1.8 tasks.withType(JavaCompile).configureEach { options.release = mcVersion >= 11700 ? 16 : 8 } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = mcVersion >= 11700 ? 16 : 1.8 + freeCompilerArgs = ["-Xjvm-default=all"] + } +} if (mcVersion >= 11400) { sourceSets { @@ -302,6 +309,10 @@ dependencies { annotationProcessor 'org.ow2.asm:asm-tree:6.2' annotationProcessor 'org.apache.logging.log4j:log4j-core:2.0-beta9' } + + shadow platform('org.jetbrains.kotlin:kotlin-bom') + shadow 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + shadow 'com.googlecode.mp4parser:isoparser:1.1.7' shadow 'org.apache.commons:commons-exec:1.3' shadow 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', shadeExclusions @@ -442,6 +453,10 @@ task configureRelocation() { } } } + // Kotlin will be filtered out because of the string constants shadow bug workaround, but we definitely need + // it and luckily that package is fairly distinct and unlikely to be in random string constants. + pkgs << 'kotlin' + pkgs }.flatten().unique() configureRelocationOutput.write(pkgs.join('\n')) @@ -495,6 +510,9 @@ tasks.register('bundleJar', com.github.jengelman.gradle.plugins.shadow.tasks.Sha exclude 'it/unimi/dsi/fastutil/**' } + // These are only required for kotlin-reflect which we are not going to use + exclude '**/*.kotlin_metadata' + minimize { exclude(dependency('.*spongepowered:mixin:.*')) } diff --git a/root.gradle.kts b/root.gradle.kts index 870e71bc..dcf6fb49 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -2,8 +2,9 @@ import groovy.json.JsonOutput import java.io.ByteArrayOutputStream plugins { + kotlin("jvm") version "1.5.21" apply false id("fabric-loom") version "0.8-SNAPSHOT" apply false - id("com.replaymod.preprocess") version "123fb7a" + id("com.replaymod.preprocess") version "ff216cd" id("com.github.hierynomus.license") version "0.15.0" } From afdd5b81a2d7e899de8e802f99d3a02389998c2e Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 3 Sep 2021 22:50:24 +0200 Subject: [PATCH 002/132] Separate shadow from implementation gradle configuration This is necessary so we can add mod dependencies via modImplementation. --- build.gradle | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 6ad951d7..1f621a81 100644 --- a/build.gradle +++ b/build.gradle @@ -227,10 +227,8 @@ repositories { configurations { // Include dep in fat jar without relocation and, when forge supports it, without exploding (TODO) shade - implementation.extendsFrom shade // Include dep in fat jar with relocation and minimization shadow - implementation.extendsFrom shadow } def shadeExclusions = { @@ -299,9 +297,9 @@ dependencies { def mixinVersion = mcVersion >= 11200 ? '0.8.2' : '0.7.11-SNAPSHOT' annotationProcessor "org.spongepowered:mixin:$mixinVersion" compileOnly "org.spongepowered:mixin:$mixinVersion" - shade("org.spongepowered:mixin:$mixinVersion") { + implementation(shade("org.spongepowered:mixin:$mixinVersion") { transitive = false // deps should all be bundled with MC - } + }) // Mixin needs these (and depends on them but for some reason that's not enough. FG, did you do that?) annotationProcessor 'com.google.code.gson:gson:2.2.4' @@ -310,45 +308,47 @@ dependencies { annotationProcessor 'org.apache.logging.log4j:log4j-core:2.0-beta9' } - shadow platform('org.jetbrains.kotlin:kotlin-bom') - shadow 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation(shadow(platform('org.jetbrains.kotlin:kotlin-bom'))) + implementation(shadow('org.jetbrains.kotlin:kotlin-stdlib-jdk8')) - shadow 'com.googlecode.mp4parser:isoparser:1.1.7' - shadow 'org.apache.commons:commons-exec:1.3' - shadow 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', shadeExclusions - shadow 'com.google.api-client:google-api-client-gson:1.20.0', shadeExclusions - shadow 'com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions - shadow 'com.google.oauth-client:google-oauth-client-jetty:1.20.0' + implementation(shadow('com.googlecode.mp4parser:isoparser:1.1.7')) + implementation(shadow('org.apache.commons:commons-exec:1.3')) + implementation(shadow('com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', shadeExclusions)) + implementation(shadow('com.google.api-client:google-api-client-gson:1.20.0', shadeExclusions)) + implementation(shadow('com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions)) + implementation(shadow('com.google.oauth-client:google-oauth-client-jetty:1.20.0')) if (mcVersion >= 11400) { // need lwjgl 3 for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos']) { - shadow('org.lwjgl:lwjgl-tinyexr:3.2.2' + suffix) { + implementation(shadow('org.lwjgl:lwjgl-tinyexr:3.2.2' + suffix) { exclude group: 'org.lwjgl', module: 'lwjgl' // comes with MC - } + }) } } if (mcVersion < 11200) { // The version which MC ships is too old, we'll need to ship our own - shadow 'com.google.code.gson:gson:2.8.7' + implementation(shadow('com.google.code.gson:gson:2.8.7')) } - shadow 'com.github.javagl.JglTF:jgltf-model:3af6de4' + implementation(shadow('com.github.javagl.JglTF:jgltf-model:3af6de4')) if (FABRIC) { - shadow 'org.apache.maven:maven-artifact:3.6.1' + implementation(shadow('org.apache.maven:maven-artifact:3.6.1')) } - shadow 'org.aspectj:aspectjrt:1.8.2' + implementation(shadow('org.aspectj:aspectjrt:1.8.2')) + + implementation(shadow('com.github.ReplayMod.JavaBlend:2.79.0:a0696f8')) - shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' + implementation(shadow('com.udojava:EvalEx:2.6')) - shadow "com.github.ReplayMod:ReplayStudio:c9de2f5", shadeExclusions + implementation(shadow("com.github.ReplayMod:ReplayStudio:c9de2f5", shadeExclusions)) implementation(jGui){ transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those } - shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' + implementation(shadow('com.github.ReplayMod:lwjgl-utils:27dcd66')) if (FABRIC) { if (mcVersion >= 11700) { @@ -367,7 +367,6 @@ dependencies { } testImplementation 'junit:junit:4.11' - shadow 'com.udojava:EvalEx:2.6' } if (mcVersion <= 10710) { From e656d6e0cee538baadd127b0768786ce8484d037 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 3 Sep 2021 23:02:22 +0200 Subject: [PATCH 003/132] Add Elementa with a bunch of common components and utilities --- build.gradle | 27 +- .../replaymod/core/gui/common/GuiWindow.kt | 76 ++ .../com/replaymod/core/gui/common/UI4Slice.kt | 98 ++ .../com/replaymod/core/gui/common/UI9Slice.kt | 223 ++++ .../com/replaymod/core/gui/common/UIButton.kt | 127 +++ .../replaymod/core/gui/common/UITexture.kt | 121 ++ .../replaymod/core/gui/common/UITooltip.kt | 67 ++ .../gui/common/elementa/AbstractTextInput.kt | 1009 +++++++++++++++++ .../gui/common/elementa/UIScrollComponent.kt | 802 +++++++++++++ .../core/gui/common/elementa/UITextInput.kt | 176 +++ .../gui/common/input/UIAdvancedTextInput.kt | 85 ++ .../core/gui/common/input/UIInputField.kt | 82 ++ .../core/gui/common/input/UIIntegerInput.kt | 51 + .../core/gui/common/input/UITimeInput.kt | 79 ++ .../gui/common/scrollbar/UIFlatScrollBar.kt | 47 + .../common/scrollbar/UITexturedScrollBar.kt | 33 + .../com/replaymod/core/gui/common/state.kt | 25 + .../core/gui/common/timeline/UITimeline.kt | 151 +++ .../gui/common/timeline/UITimelineCursor.kt | 46 + .../common/timeline/UITimelineIndicators.kt | 71 ++ .../gui/common/timeline/UITimelineTime.kt | 42 + .../com/replaymod/core/gui/utils/events.kt | 61 + .../replaymod/core/gui/utils/extensions.kt | 25 + .../com/replaymod/core/gui/utils/focus.kt | 81 ++ .../com/replaymod/core/gui/utils/state.kt | 52 + .../com/replaymod/core/gui/utils/tooltip.kt | 52 + .../com/replaymod/core/utils/extensions.kt | 23 + 27 files changed, 3731 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/GuiWindow.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UITooltip.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/elementa/UIScrollComponent.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/elementa/UITextInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIAdvancedTextInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIInputField.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIIntegerInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UITimeInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UIFlatScrollBar.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UITexturedScrollBar.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/state.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineIndicators.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineTime.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/events.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/extensions.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/focus.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/state.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/tooltip.kt create mode 100644 src/main/kotlin/com/replaymod/core/utils/extensions.kt diff --git a/build.gradle b/build.gradle index 1f621a81..87dde884 100644 --- a/build.gradle +++ b/build.gradle @@ -116,7 +116,7 @@ tasks.withType(JavaCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { jvmTarget = mcVersion >= 11700 ? 16 : 1.8 - freeCompilerArgs = ["-Xjvm-default=all"] + freeCompilerArgs = ["-Xjvm-default=all", "-Xopt-in=kotlin.time.ExperimentalTime"] } } @@ -222,6 +222,13 @@ repositories { includeGroupByRegex 'com\\.github\\..*' } } + maven { + // url 'https://repo.essential.gg' + url 'https://repo.sk1er.club/repository/maven-public' + content { + includeGroup 'gg.essential' + } + } } configurations { @@ -311,6 +318,24 @@ dependencies { implementation(shadow(platform('org.jetbrains.kotlin:kotlin-bom'))) implementation(shadow('org.jetbrains.kotlin:kotlin-stdlib-jdk8')) + // TODO will need to port this to all the versions later, for now let's just guess the closest and hope for the best + String elementaMcVersion + if (mcVersion >= 11700) { + elementaMcVersion = '1.17.1' + } else if (mcVersion >= 11400) { + elementaMcVersion = '1.16.2' + } else if (mcVersion >= 11000) { + elementaMcVersion = '1.12.2' + } else { + elementaMcVersion = '1.8.9' + } + def elementaVersion = '391' + if (fabric) { + modImplementation(shadow("gg.essential:elementa-$elementaMcVersion-fabric:$elementaVersion")) + } else { + implementation(shadow("gg.essential:elementa-$elementaMcVersion-forge:$elementaVersion")) + } + implementation(shadow('com.googlecode.mp4parser:isoparser:1.1.7')) implementation(shadow('org.apache.commons:commons-exec:1.3')) implementation(shadow('com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', shadeExclusions)) diff --git a/src/main/kotlin/com/replaymod/core/gui/common/GuiWindow.kt b/src/main/kotlin/com/replaymod/core/gui/common/GuiWindow.kt new file mode 100644 index 00000000..2e5d748e --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/GuiWindow.kt @@ -0,0 +1,76 @@ +package com.replaymod.core.gui.common + +import de.johni0702.minecraft.gui.GuiRenderer +import de.johni0702.minecraft.gui.RenderInfo +import de.johni0702.minecraft.gui.container.GuiContainer +import de.johni0702.minecraft.gui.element.AbstractGuiElement +import de.johni0702.minecraft.gui.function.Clickable +import de.johni0702.minecraft.gui.function.Draggable +import de.johni0702.minecraft.gui.function.Scrollable +import de.johni0702.minecraft.gui.function.Typeable +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint +import gg.essential.elementa.components.Window +import gg.essential.universal.UMatrixStack + +class GuiWindow(parent: GuiContainer<*>, val window: Window) : AbstractGuiElement(parent), Clickable, Draggable, Scrollable, Typeable { + private var dragging = false + + override fun getThis(): GuiWindow = this + override fun calcMinSize(): ReadableDimension = Dimension() + + override fun draw(renderer: GuiRenderer, size: ReadableDimension, renderInfo: RenderInfo) { + window.draw(UMatrixStack()) + } + + private fun hasComponent(pos: ReadablePoint): Boolean = + window.hoveredFloatingComponent != null || window.hitTest(pos.x.toFloat(), pos.y.toFloat()) != window + + override fun mouseClick(position: ReadablePoint, button: Int): Boolean { + return if (hasComponent(position)) { + window.mouseClick(position.x.toDouble(), position.y.toDouble(), button) + true + } else { + false + }.also { dragging = it } + } + + override fun mouseDrag(position: ReadablePoint, button: Int, timeSinceLastCall: Long): Boolean { + return dragging + } + + override fun mouseRelease(position: ReadablePoint, button: Int): Boolean { + return if (dragging) { + dragging = false + window.mouseRelease() + true + } else { + false + } + } + + override fun scroll(position: ReadablePoint, dWheel: Int): Boolean { + return if (hasComponent(position)) { + window.mouseScroll(dWheel / 120.0) + true + } else { + false + } + } + + override fun typeKey( + mousePosition: ReadablePoint?, + keyCode: Int, + keyChar: Char, + ctrlDown: Boolean, + shiftDown: Boolean + ): Boolean { + return if (window.focusedComponent != null) { + window.keyType(keyChar, keyCode) + true + } else { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt b/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt new file mode 100644 index 00000000..2ecb88a4 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt @@ -0,0 +1,98 @@ +package com.replaymod.core.gui.common + +import com.replaymod.core.gui.common.UITexture.TextureData +import gg.essential.elementa.UIComponent +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.shader.BlendState +import net.minecraft.client.render.VertexFormats +import net.minecraft.util.Identifier +import java.awt.Color + +class UI4Slice( + private val texture: Identifier, + private var textureData: State, +) : UIComponent() { + + constructor(texture: Identifier, textureData: TextureData) : this(texture, BasicState(textureData)) + + fun bindTextureData(textureData: State) { + this.textureData = textureData + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft().toDouble() + val right = getRight().toDouble() + val top = getTop().toDouble() + val bottom = getBottom().toDouble() + + UGraphics.bindTexture(0, texture) + draw(matrixStack, left, right, top, bottom, getColor(), textureData.get()) + + super.draw(matrixStack) + } + + companion object { + fun draw( + matrixStack: UMatrixStack, + l: Double, + r: Double, + t: Double, + b: Double, + color: Color, + data: TextureData, + ) { + val oldBlendState = BlendState.active() + BlendState.NORMAL.activate() + + UGraphics.enableAlpha() + + val red = color.red.toFloat() / 255f + val green = color.green.toFloat() / 255f + val blue = color.blue.toFloat() / 255f + val alpha = color.alpha.toFloat() / 255f + val buffer = UGraphics.getFromTessellator() + + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + + fun UGraphics.texS(u: Double, v: Double) = tex(u / data.textureWidth, v / data.textureHeight) + + fun drawTexturedRect(x: Double, y: Double, u: Double, v: Double, width: Double, height: Double) { + buffer.pos(matrixStack, x, y + height, 0.0) + .color(red, green, blue, alpha) + .texS(u, v + height) + .endVertex() + buffer.pos(matrixStack, x + width, y + height, 0.0) + .color(red, green, blue, alpha) + .texS(u + width, v + height) + .endVertex() + buffer.pos(matrixStack, x + width, y, 0.0) + .color(red, green, blue, alpha) + .texS(u + width, v) + .endVertex() + buffer.pos(matrixStack, x, y, 0.0) + .color(red, green, blue, alpha) + .texS(u, v) + .endVertex() + } + + with(data) { + val wh = (r - l) / 2 + val hh = (b - t) / 2 + + drawTexturedRect(l, t, lt, tt, wh, hh) + drawTexturedRect(l, t + hh, lt, bt - hh, wh, hh) + drawTexturedRect(l + wh, t, rt - wh, tt, wh, hh) + drawTexturedRect(l + wh, t + hh, rt - wh, bt - hh, wh, hh) + } + + buffer.drawDirect() + + oldBlendState.activate() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt b/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt new file mode 100644 index 00000000..d33f94ea --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt @@ -0,0 +1,223 @@ +package com.replaymod.core.gui.common + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.shader.BlendState +import net.minecraft.client.render.VertexFormats +import net.minecraft.util.Identifier +import java.awt.Color + +class UI9Slice( + private val texture: Identifier, + private var textureData: State, +) : UIComponent() { + + constructor(texture: Identifier, textureData: TextureData) : this(texture, BasicState(textureData)) + + fun bindTextureData(textureData: State) { + this.textureData = textureData + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft().toDouble() + val right = getRight().toDouble() + val top = getTop().toDouble() + val bottom = getBottom().toDouble() + + UGraphics.bindTexture(0, texture) + draw(matrixStack, left, right, top, bottom, getColor(), textureData.get()) + + super.draw(matrixStack) + } + + companion object { + fun draw( + matrixStack: UMatrixStack, + l: Double, + r: Double, + t: Double, + b: Double, + color: Color, + data: TextureData, + ) { + val oldBlendState = BlendState.active() + BlendState.NORMAL.activate() + + UGraphics.enableAlpha() + + val red = color.red.toFloat() / 255f + val green = color.green.toFloat() / 255f + val blue = color.blue.toFloat() / 255f + val alpha = color.alpha.toFloat() / 255f + val buffer = UGraphics.getFromTessellator() + + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + + fun UGraphics.texS(u: Double, v: Double) = tex(u / data.textureWidth, v / data.textureHeight) + + fun drawTexturedRect(x: Double, y: Double, u: Double, v: Double, width: Double, height: Double) { + buffer.pos(matrixStack, x, y + height, 0.0) + .color(red, green, blue, alpha) + .texS(u, v + height) + .endVertex() + buffer.pos(matrixStack, x + width, y + height, 0.0) + .color(red, green, blue, alpha) + .texS(u + width, v + height) + .endVertex() + buffer.pos(matrixStack, x + width, y, 0.0) + .color(red, green, blue, alpha) + .texS(u + width, v) + .endVertex() + buffer.pos(matrixStack, x, y, 0.0) + .color(red, green, blue, alpha) + .texS(u, v) + .endVertex() + } + + with(data) { + // Corners + run { + drawTexturedRect(l, t, lt, tt, ls, ts) // Top left + drawTexturedRect(r - rs, t, rt - rs, tt, rs, ts) // Top right + drawTexturedRect(l, b - bs, lt, bt - bs, ls, bs) // Bottom left + drawTexturedRect(r - rs, b - bs, rt - rs, bt - bs, rs, bs) // Bottom right + } + + // Top and bottom edge + run { + var x0 = l + ls + val xMax = r - rs + while (x0 < xMax) { + val w = (xMax - x0).coerceAtMost(ws) + drawTexturedRect(x0, t, lt + ls, tt, w, ts) // Top + drawTexturedRect(x0, b - bs, lt + ls, bt - bs, w, bs) // Bottom + x0 += w + } + } + + // Left and right edge + run { + var y0 = t + ts + val yMax = b - bs + while (y0 < yMax) { + val h = (yMax - y0).coerceAtMost(hs) + drawTexturedRect(l, y0, lt, tt + ts, ls, h) // Left + drawTexturedRect(r - rs, y0, rt - rs, tt + ts, rs, h) // Right + y0 += h + } + } + + // Center + run { + var x0 = l + ls + val xMax = r - rs + while (x0 < xMax) { + val w = (xMax - x0).coerceAtMost(ws) + + var y0 = t + ts + val yMax = b - bs + while (y0 < yMax) { + val h = (yMax - y0).coerceAtMost(hs) + drawTexturedRect(x0, y0, lt + ls, tt + ts, w, h) + y0 += h + } + + x0 += w + } + } + } + + buffer.drawDirect() + + oldBlendState.activate() + } + } + + data class TextureData( + val leftTexture: Double, + val topTexture: Double, + val rightTexture: Double, + val bottomTexture: Double, + val leftSlice: Double, + val topSlice: Double, + val rightSlice: Double, + val bottomSlice: Double, + val textureWidth: Int = 256, + val textureHeight: Int = 256, + ) { + val lt = leftTexture + val tt = topTexture + val rt = rightTexture + val bt = bottomTexture + val wt = rightTexture - leftTexture + val ht = bottomTexture - topTexture + val ls = leftSlice + val ts = topSlice + val rs = rightSlice + val bs = bottomSlice + val ws = wt - leftSlice - rightSlice + val hs = ht - leftSlice - bottomSlice + + fun offset(x: Int, y: Int) = offset(x.toDouble(), y.toDouble()) + + fun offset(x: Double, y: Double) = TextureData( + leftTexture + x, + topTexture + y, + rightTexture + x, + bottomTexture + y, + leftSlice, + topSlice, + rightSlice, + bottomSlice, + textureWidth, + textureHeight + ) + + companion object { + fun ofSize( + width: Int, + height: Int, + border: Int, + ) = ofSize(width, height, border, border, border, border) + + fun ofSize( + width: Int, + height: Int, + leftSlice: Int, + topSlice: Int, + rightSlice: Int, + bottomSlice: Int, + ) = ofSize( + width.toDouble(), + height.toDouble(), + leftSlice.toDouble(), + topSlice.toDouble(), + rightSlice.toDouble(), + bottomSlice.toDouble() + ) + + fun ofSize( + width: Double, + height: Double, + leftSlice: Double, + topSlice: Double, + rightSlice: Double, + bottomSlice: Double, + ) = TextureData( + 0.0, + 0.0, + width, + height, + leftSlice, + topSlice, + rightSlice, + bottomSlice + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt new file mode 100644 index 00000000..251c1733 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt @@ -0,0 +1,127 @@ +package com.replaymod.core.gui.common + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIImage +import gg.essential.elementa.components.UIText +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.state.toConstraint +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import net.minecraft.client.MinecraftClient +import net.minecraft.client.sound.PositionedSoundInstance +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Identifier +import java.awt.Color +import java.awt.image.BufferedImage +import java.util.concurrent.CompletableFuture + +class UIButton : UIComponent() { + + private val hovered = BasicState(false) + private val enabled = BasicState(true) + + private val background = enabled.zip(hovered).map { (enabled, hovered) -> + val offset = when { + !enabled -> 0 + hovered -> 2 + else -> 1 + } + UITexture.TextureData.ofSize(0, 46 + offset * 20, 200, 20) + } + + init { + onMouseEnter { hovered.set(true) } + onMouseLeave { hovered.set(false) } + + onMouseClick { + playClickSound() + } + + constrain { + width = 200.pixels + height = 20.pixels + } + } + + fun label(configure: UIText.() -> Unit) = apply { + val component = UIText() childOf this + component.constrain { + x = CenterConstraint() + y = CenterConstraint() + color = enabled.zip(hovered).map { (enabled, hovered) -> when { + !enabled -> Color(160, 160, 160) + hovered -> Color(255, 255, 160) + else -> Color(224, 224, 224) + } }.toConstraint() + } + component.configure() + } + + fun image(imageFuture: CompletableFuture, configure: UIImage.() -> Unit = {}) = apply { + val component = UIImage(imageFuture) childOf this + component.constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 100.percent + height = 100.percent + } + component.configure() + } + + fun texture( + texture: Identifier, + data: UITexture.TextureData = UITexture.TextureData.full(), + configure: UITexture.() -> Unit = {}, + ) = texture(texture, BasicState(data), configure) + + fun texture(texture: Identifier, data: State, configure: UITexture.() -> Unit = {}) = apply { + val component = UITexture(texture, data) childOf this + component.constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 100.percent + height = 100.percent + } + component.configure() + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft().toDouble() + val right = getRight().toDouble() + val top = getTop().toDouble() + val bottom = getBottom().toDouble() + + UGraphics.bindTexture(0, WIDGETS_TEXTURE) + UI4Slice.draw(matrixStack, left, right, top, bottom, getColor(), background.get()) + + super.draw(matrixStack) + } + + companion object { + //#if MC>=11900 + private val BUTTON_SOUND = SoundEvents.UI_BUTTON_CLICK + //#else + //$$ private val BUTTON_SOUND = ResourceLocation("gui.button.press") + //#endif + + val WIDGETS_TEXTURE = Identifier("textures/gui/widgets.png") + + fun playClickSound() { + val mc = MinecraftClient.getInstance() + //#if MC>=11400 + mc.soundManager.play(PositionedSoundInstance.master(BUTTON_SOUND, 1.0f)) + //#elseif MC>=10904 + //$$ mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(BUTTON_SOUND, 1.0F)); + //#elseif MC>=10800 + //$$ mc.getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); + //#else + //$$ mc.getSoundHandler().playSound(PositionedSoundRecord.createPositionedSoundRecord(BUTTON_SOUND, 1.0F)); + //#endif + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt b/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt new file mode 100644 index 00000000..38809c87 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt @@ -0,0 +1,121 @@ +package com.replaymod.core.gui.common + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.shader.BlendState +import net.minecraft.client.render.VertexFormats +import net.minecraft.util.Identifier +import java.awt.Color + +class UITexture( + private val texture: Identifier, + private var textureData: State, +) : UIComponent() { + + constructor(texture: Identifier, textureData: TextureData) : this(texture, BasicState(textureData)) + + fun bindTextureData(textureData: State) { + this.textureData = textureData + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft().toDouble() + val right = getRight().toDouble() + val top = getTop().toDouble() + val bottom = getBottom().toDouble() + + UGraphics.bindTexture(0, texture) + draw(matrixStack, left, right, top, bottom, getColor(), textureData.get()) + + super.draw(matrixStack) + } + + companion object { + fun draw( + matrixStack: UMatrixStack, + l: Double, + r: Double, + t: Double, + b: Double, + color: Color, + data: TextureData, + ) { + val oldBlendState = BlendState.active() + BlendState.NORMAL.activate() + + UGraphics.enableAlpha() + + val buffer = UGraphics.getFromTessellator() + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + with(data) { + with(color) { + fun UGraphics.texS(u: Double, v: Double) = tex(u / textureWidth, v / textureHeight) + + buffer.pos(matrixStack, l, b, 0.0) + .color(red, green, blue, alpha) + .texS(lt, bt) + .endVertex() + buffer.pos(matrixStack, r, b, 0.0) + .color(red, green, blue, alpha) + .texS(rt, bt) + .endVertex() + buffer.pos(matrixStack, r, t, 0.0) + .color(red, green, blue, alpha) + .texS(rt, tt) + .endVertex() + buffer.pos(matrixStack, l, t, 0.0) + .color(red, green, blue, alpha) + .texS(lt, tt) + .endVertex() + } + } + buffer.drawDirect() + + oldBlendState.activate() + } + } + + data class TextureData( + val leftTexture: Double, + val topTexture: Double, + val rightTexture: Double, + val bottomTexture: Double, + val textureWidth: Int = 256, + val textureHeight: Int = 256, + ) { + val lt = leftTexture + val tt = topTexture + val rt = rightTexture + val bt = bottomTexture + + fun offset(x: Int, y: Int) = offset(x.toDouble(), y.toDouble()) + + fun offset(x: Double, y: Double) = TextureData( + leftTexture + x, + topTexture + y, + rightTexture + x, + bottomTexture + y, + textureWidth, + textureHeight + ) + + companion object { + fun full() = ofSize(256, 256) + + fun ofSize(width: Int, height: Int) = ofSize(0, 0, width, height) + + fun ofSize(width: Double, height: Double) = ofSize(0.0, 0.0, width, height) + + fun ofSize(left: Int, top: Int, width: Int, height: Int) = + ofSize(left.toDouble(), top.toDouble(), width.toDouble(), height.toDouble()) + + fun ofSize(left: Double, top: Double, width: Double, height: Double) = + TextureData(left, top, left + width, top + height) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UITooltip.kt b/src/main/kotlin/com/replaymod/core/gui/common/UITooltip.kt new file mode 100644 index 00000000..4efc170c --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UITooltip.kt @@ -0,0 +1,67 @@ +package com.replaymod.core.gui.common + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.GradientComponent.Companion.drawGradientBlock +import gg.essential.elementa.components.GradientComponent.GradientDirection.TOP_TO_BOTTOM +import gg.essential.elementa.components.UIBlock.Companion.drawBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.dsl.* +import gg.essential.universal.UMatrixStack +import java.awt.Color + +class UITooltip : UIComponent() { + + init { + constrain { + width = ChildBasedMaxSizeConstraint() + 8.pixels + height = ChildBasedSizeConstraint() + 8.pixels + } + } + + val content by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf this + + fun addLine(text: String = "", configure: UIText.() -> Unit = {}) = apply { + val component = UIText(text).constrain { + y = SiblingConstraint(padding = 3f) + } childOf content + component.configure() + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val l = getLeft().toDouble() + val r = getRight().toDouble() + val t = getTop().toDouble() + val b = getBottom().toDouble() + + // Draw background + drawBlock(matrixStack, BACKGROUND_COLOR, l + 1, t, r - 1, b) // Top to bottom + drawBlock(matrixStack, BACKGROUND_COLOR, l, t + 1, l + 1, b - 1) // Left pixel row + drawBlock(matrixStack, BACKGROUND_COLOR, r - 1, t + 1, r, b - 1) // Right pixel row + + // Draw the border, it gets darker from top to bottom + drawBlock(matrixStack, BORDER_LIGHT, l + 1, t + 1, r - 1, t + 2) // Top border + drawBlock(matrixStack, BORDER_DARK, l + 1, b - 2, r - 1, b - 1) // Bottom border + drawGradientBlock(matrixStack, l + 1, t + 2, l + 2, b - 2, BORDER_LIGHT, BORDER_DARK, TOP_TO_BOTTOM) // Left border + drawGradientBlock(matrixStack, r - 2, t + 2, r - 1, b - 2, BORDER_LIGHT, BORDER_DARK, TOP_TO_BOTTOM) // Right border + + super.draw(matrixStack) + } + + companion object { + private val BACKGROUND_COLOR = Color(16, 0, 16, 240) + private val BORDER_LIGHT = Color(80, 0, 255, 80) + private val BORDER_DARK = Color(40, 0, 127, 80) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt new file mode 100644 index 00000000..b7dd7c90 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt @@ -0,0 +1,1009 @@ +/** + * Based on Elementa's AbstractTextInput but with more stuff exposed and certain changes so we can get it to behave just + * like we need it to: + * - Remove the `private` from TextOperation implementations' fields + * + * MIT License + * + * Copyright (c) 2021 ReplayMod contributors + * Copyright (c) 2021 Sk1er LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.replaymod.core.gui.common.elementa + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.* +import gg.essential.elementa.effects.ScissorEffect +import gg.essential.elementa.utils.getStringSplitToWidth +import gg.essential.universal.UDesktop +import gg.essential.universal.UKeyboard +import gg.essential.universal.UMatrixStack +import net.minecraft.SharedConstants +import java.awt.Color +import java.util.* +import kotlin.math.abs + +abstract class AbstractTextInput( + var placeholder: String, + var shadow: Boolean, + protected val selectionBackgroundColor: Color, + protected val selectionForegroundColor: Color, + protected val allowInactiveSelection: Boolean, + protected val inactiveSelectionBackgroundColor: Color, + protected val inactiveSelectionForegroundColor: Color, + protected val cursorColor: Color +) : UIComponent() { + protected var active = false + var lineHeight = 9f + set(value) { + cursorComponent.setHeight(value.pixels()) + this.setHeight(value.pixels()) + field = value + } + protected var updateAction: (text: String) -> Unit = {} + protected var activateAction: (text: String) -> Unit = {} + + protected val textualLines = mutableListOf(TextualLine("", 0..0)) + protected val visualLines = mutableListOf(VisualLine("", 0)) + + protected var verticalScrollingOffset = 0f + protected var targetVerticalScrollingOffset = 0f + protected var horizontalScrollingOffset = 0f + protected var cursorNeedsRefocus = false + + protected var lastSelectionMoveTimestamp = System.currentTimeMillis() + protected var selectionMode = SelectionMode.None + protected var initiallySelectedLine = -1 + protected var initiallySelectedWord = LinePosition(0, 0, true) to LinePosition(0, 0, true) + + protected val undoStack = ArrayDeque() + protected val redoStack = ArrayDeque() + + protected var cursorComponent: UIComponent = UIBlock(Color(255, 255, 255, 0)).constrain { + y = CenterConstraint() - 0.5f.pixels() + width = 1.pixel() + height = lineHeight.pixels() + } childOf this + + protected var cursor = LinePosition(0, 0, isVisual = true) + set(value) { + field = value.toVisualPos() + } + + protected var otherSelectionEnd = LinePosition(0, 0, isVisual = true) + set(value) { + field = value.toVisualPos() + } + + enum class SelectionMode { + None, + Character, + Word, + Line, + } + + init { + + setHeight(lineHeight.pixels()) + + onKeyType { typedChar, keyCode -> + if (!active) return@onKeyType + + if (keyCode == UKeyboard.KEY_ESCAPE) { + releaseWindowFocus() + } else if (UKeyboard.isKeyComboCtrlA(keyCode)) { + selectAll() + } else if (UKeyboard.isKeyComboCtrlC(keyCode) && hasSelection()) { + copySelection() + } else if (UKeyboard.isKeyComboCtrlX(keyCode) && hasSelection()) { + copySelection() + deleteSelection() + } else if (UKeyboard.isKeyComboCtrlV(keyCode)) { + commitTextAddition(UDesktop.getClipboardString()) + } else if (UKeyboard.isKeyComboCtrlZ(keyCode)) { + if (undoStack.isEmpty()) + return@onKeyType + val operationToUndo = undoStack.pop() + operationToUndo.undo() + redoStack.push(operationToUndo) + } else if (UKeyboard.isKeyComboCtrlShiftZ(keyCode) || UKeyboard.isKeyComboCtrlY(keyCode)) { + if (redoStack.isEmpty()) + return@onKeyType + val operationToRedo = redoStack.pop() + operationToRedo.redo() + undoStack.push(operationToRedo) + } else if (SharedConstants.isValidChar(typedChar)) { // Most of the ASCII characters + commitTextAddition(typedChar.toString()) + } else if (keyCode == UKeyboard.KEY_LEFT) { + val holdingShift = UKeyboard.isShiftKeyDown() + val holdingCtrl = UKeyboard.isCtrlKeyDown() + + val newCursorPosition = when { + holdingCtrl -> getNearestWordBoundary(cursor, Direction.Left) + hasSelection() -> if (holdingShift) cursor.offsetColumn(-1) else selectionStart() + else -> cursor.offsetColumn(-1) + } + + if (!holdingShift) { + setCursorPosition(newCursorPosition) + return@onKeyType + } + + cursor = newCursorPosition + cursorNeedsRefocus = true + } else if (keyCode == UKeyboard.KEY_RIGHT) { + val holdingShift = UKeyboard.isShiftKeyDown() + val holdingCtrl = UKeyboard.isCtrlKeyDown() + + val newCursorPosition = when { + holdingCtrl -> getNearestWordBoundary(cursor, Direction.Right) + hasSelection() -> if (holdingShift) cursor.offsetColumn(1) else selectionEnd() + else -> cursor.offsetColumn(1) + } + + if (!holdingShift) { + setCursorPosition(newCursorPosition) + return@onKeyType + } + + cursor = newCursorPosition + cursorNeedsRefocus = true + } else if (keyCode == UKeyboard.KEY_UP) { + val newVisualPos = if (cursor.line == 0) { + LinePosition(0, 0, isVisual = true) + } else { + val (currX, currY) = cursor.toScreenPos() + screenPosToVisualPos(currX, currY - lineHeight) + } + + if (UKeyboard.isShiftKeyDown()) { + cursor = newVisualPos + cursorNeedsRefocus = true + } else { + setCursorPosition(newVisualPos) + } + } else if (keyCode == UKeyboard.KEY_DOWN) { + val newVisualPos = if (cursor.line == visualLines.lastIndex) { + LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true) + } else { + val (currX, currY) = cursor.toScreenPos() + screenPosToVisualPos(currX, currY + lineHeight) + } + + if (UKeyboard.isShiftKeyDown()) { + cursor = newVisualPos + cursorNeedsRefocus = true + } else { + setCursorPosition(newVisualPos) + } + } else if (keyCode == UKeyboard.KEY_BACKSPACE) { + if (hasSelection()) { + deleteSelection() + } else if (!cursor.isAtAbsoluteStart) { + val startPos = if (UKeyboard.isCtrlKeyDown()) { + getNearestWordBoundary(cursor, Direction.Left) + } else cursor.offsetColumn(-1).toTextualPos() + val endPos = cursor.toTextualPos() + + commitTextRemoval(startPos, endPos, selectAfterUndo = false) + } + } else if (keyCode == UKeyboard.KEY_DELETE) { + if (hasSelection()) { + deleteSelection() + } else if (!cursor.isAtAbsoluteEnd) { + val startPos = cursor.toTextualPos() + val endPos = if (UKeyboard.isCtrlKeyDown()) { + getNearestWordBoundary(cursor, Direction.Right) + } else cursor.offsetColumn(1).toTextualPos() + + commitTextRemoval(startPos, endPos, selectAfterUndo = false) + } + } else if (keyCode == UKeyboard.KEY_HOME) { + if (UKeyboard.isShiftKeyDown()) { + cursor = cursor.withColumn(0) + cursorNeedsRefocus = true + } else { + setCursorPosition(cursor.withColumn(0)) + } + } else if (keyCode == UKeyboard.KEY_END) { + cursor.withColumn(visualLines[cursor.line].length).also { + if (UKeyboard.isShiftKeyDown()) { + cursor = it + cursorNeedsRefocus = true + } else { + setCursorPosition(it) + } + } + } else if (keyCode == UKeyboard.KEY_ENTER) { // Enter + onEnterPressed() + } + } + + onMouseScroll { + val heightDifference = getHeight() - visualLines.size * lineHeight + if (heightDifference > 0) + return@onMouseScroll + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset + it.delta.toFloat() * lineHeight).coerceIn(heightDifference, 0f) + it.stopPropagation() + } + + onMouseClick { event -> + if (!active || event.mouseButton != 0) + return@onMouseClick + + val clickedVisualPos = screenPosToVisualPos(event.relativeX, event.relativeY) + + var clickCount = event.clickCount % 3 + if (clickCount == 0 && clickedVisualPos.line != cursor.line) + clickCount = 1 + else if (clickCount == 2 && cursor != clickedVisualPos) + clickCount = 1 + + when (clickCount) { + 0 -> { + selectionMode = SelectionMode.Line + otherSelectionEnd = clickedVisualPos.withColumn(visualLines[cursor.line].length) + initiallySelectedLine = cursor.line + } + 1 -> { + selectionMode = SelectionMode.Character + setCursorPosition(clickedVisualPos) + } + 2 -> { + selectionMode = SelectionMode.Word + cursor = getNearestWordBoundary( + clickedVisualPos, + Direction.Left + ) + cursorNeedsRefocus = true + otherSelectionEnd = getNearestWordBoundary( + clickedVisualPos, + Direction.Right + ) + initiallySelectedWord = cursor to otherSelectionEnd + } + } + } + + onMouseDrag { mouseX, mouseY, mouseButton -> + if (mouseButton != 0 || selectionMode == SelectionMode.None) + return@onMouseDrag + + val draggedVisualPos = screenPosToVisualPos(mouseX, mouseY) + + when (selectionMode) { + SelectionMode.Character -> otherSelectionEnd = draggedVisualPos + SelectionMode.Line -> if (initiallySelectedLine < draggedVisualPos.line) { + cursor = LinePosition(initiallySelectedLine, 0, isVisual = true) + otherSelectionEnd = draggedVisualPos.withColumn(visualLines[draggedVisualPos.line].length) + } else { + cursor = draggedVisualPos.withColumn(0) + otherSelectionEnd = LinePosition( + initiallySelectedLine, + visualLines[initiallySelectedLine].length, + isVisual = true + ) + } + SelectionMode.Word -> when { + draggedVisualPos < initiallySelectedWord.first -> { + cursor = getNearestWordBoundary( + draggedVisualPos, + Direction.Left + ) + otherSelectionEnd = initiallySelectedWord.second + } + draggedVisualPos > initiallySelectedWord.second -> { + cursor = initiallySelectedWord.first + otherSelectionEnd = getNearestWordBoundary( + draggedVisualPos, + Direction.Right + ) + } + else -> { + cursor = initiallySelectedWord.first + otherSelectionEnd = initiallySelectedWord.second + } + } + SelectionMode.None -> { + } + } + + val currentTime = System.currentTimeMillis() + if (currentTime - lastSelectionMoveTimestamp > 50) { + if (mouseY <= 0) { + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset + lineHeight * getTextScale()).coerceAtMost(0f) + lastSelectionMoveTimestamp = currentTime + } else if (mouseY >= getHeight()) { + val heightDifference = getHeight() - visualLines.size * lineHeight * getTextScale() + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset - lineHeight * getTextScale()).coerceIn(0f, heightDifference) + lastSelectionMoveTimestamp = currentTime + } else if (mouseX <= 0) { + scrollIntoView(draggedVisualPos.offsetColumn(-1)) + lastSelectionMoveTimestamp = currentTime + } else if (mouseX >= getWidth()) { + scrollIntoView(draggedVisualPos.offsetColumn(1)) + lastSelectionMoveTimestamp = currentTime + } + } + } + + onMouseRelease { + selectionMode = SelectionMode.None + } + + onFocus { + setActive(true) + } + + onFocusLost { + setActive(false) + } + + cursorComponent.animateAfterUnhide { + setColorAnimation(Animations.OUT_CIRCULAR, 0.5f, cursorColor.toConstraint()) + onComplete { + if (!active) return@onComplete + cursorComponent.animate { + setColorAnimation(Animations.IN_CIRCULAR, 0.5f, Color(255, 255, 255, 0).toConstraint()) + onComplete { + if (active) animateCursor() + } + } + } + } + + enableEffect(ScissorEffect()) + } + + override fun draw(matrixStack: UMatrixStack) { + cursorComponent.setHeight( + (lineHeight * getTextScale()).pixels() + ) + super.draw(matrixStack) + } + + abstract fun getText(): String + fun setText(text: String) { + val absoluteStart = LinePosition(0, 0, isVisual = true) + val replaceTextOperation = ReplaceTextOperation( + AddTextOperation(text, absoluteStart), + RemoveTextOperation( + absoluteStart, + LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true), + selectAfterUndo = true + ) + ) + commitTextOperation(replaceTextOperation) + } + + protected abstract fun scrollIntoView(pos: LinePosition) + protected abstract fun screenPosToVisualPos(x: Float, y: Float): LinePosition + protected abstract fun recalculateDimensions() + protected abstract fun textToLines(text: String): List + protected abstract fun onEnterPressed() + + fun setActive(isActive: Boolean) = apply { + active = isActive + + if (isActive) { + cursorComponent.unhide() + animateCursor() + } else { + cursorComponent.setColor(Color(255, 255, 255, 0).toConstraint()) + if (hasText() && (!allowInactiveSelection || !hasSelection())) { + setCursorPosition(LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true)) + } + } + } + + fun isActive() = active + + fun onUpdate(listener: (text: String) -> Unit) = apply { + updateAction = listener + } + + fun onActivate(listener: (text: String) -> Unit) = apply { + activateAction = listener + } + + protected open fun commitTextOperation(operation: TextOperation) { + operation.redo() + undoStack.push(operation) + redoStack.clear() + } + + protected fun commitTextAddition(newText: String) { + val addTextOperation = AddTextOperation(newText, cursor) + + if (hasSelection()) { + val removeTextOperation = RemoveTextOperation(selectionStart(), selectionEnd(), selectAfterUndo = true) + val replaceTextOperation = ReplaceTextOperation(addTextOperation, removeTextOperation) + commitTextOperation(replaceTextOperation) + return + } + + commitTextOperation(addTextOperation) + } + + protected fun addText(newText: String, position: LinePosition) { + val textPos = position.toTextualPos() + val textualLine = textualLines[textPos.line] + + val lines = textToLines(newText) + when { + lines.isEmpty() -> { + return + } + lines.size == 1 -> { + textualLine.addTextAt(lines.first(), textPos.column) + } + else -> { + val newTextualLines = lines.drop(1).map { TextualLine(it) } + + if (textPos.column < textualLine.text.length) { + val textAfterInsertion = textualLine.text.substring(textPos.column) + textualLine.text = textualLine.text.substring(0, textPos.column) + lines.first() + newTextualLines.last().text += textAfterInsertion + } else { + textualLine.addTextAt(lines.first(), textPos.column) + } + + textualLines.addAll(textPos.line + 1, newTextualLines) + } + } + + recalculateAllVisualLines() + setCursorPosition(textPos.offsetColumn(newText.length).toVisualPos()) + + updateAction(getText()) + } + + protected open fun recalculateVisualLinesFor(textualLineIndex: Int) { + val textualLine = textualLines[textualLineIndex] + val firstVisualIndex = textualLine.visualIndices.first + repeat(textualLine.visualIndices.count()) { + if (firstVisualIndex < visualLines.size) + visualLines.removeAt(firstVisualIndex) + } + val splitLines = splitTextForWrapping(textualLine.text, getWidth()) + + visualLines.addAll(firstVisualIndex, splitLines.map { VisualLine(it, textualLineIndex) }) + textualLine.visualIndices = firstVisualIndex until firstVisualIndex + splitLines.size + } + + // TODO: This probably isn't necessary. Remove when feeling not lazy :) + protected open fun recalculateAllVisualLines() { + visualLines.clear() + + for ((index, textualLine) in textualLines.withIndex()) { + val splitLines = splitTextForWrapping(textualLine.text, getWidth()) + textualLine.visualIndices = visualLines.size..visualLines.size + splitLines.size + visualLines.addAll(splitLines.map { VisualLine(it, index) }) + } + } + + // TODO: Look into optimization of this algorithm + protected open fun splitTextForWrapping(text: String, maxLineWidth: Float): List { + return getStringSplitToWidth(text, maxLineWidth, getTextScale(), processColorCodes = false) + } + + protected fun commitTextRemoval(startPos: LinePosition, endPos: LinePosition, selectAfterUndo: Boolean) { + val removeTextOperation = RemoveTextOperation(startPos, endPos, selectAfterUndo) + commitTextOperation(removeTextOperation) + } + + private fun removeText(startPos: LinePosition, endPos: LinePosition) { + val textualStartPos = startPos.toTextualPos() + val textualEndPos = endPos.toTextualPos() + + val startTextualLine = textualLines[textualStartPos.line] + val endTextualLine = textualLines[textualEndPos.line] + + startTextualLine.text = startTextualLine.text.substring( + 0, + textualStartPos.column + ) + endTextualLine.text.substring(textualEndPos.column) + + val firstItemToDelete = textualStartPos.line + 1 + repeat(textualEndPos.line - firstItemToDelete + 1) { + textualLines.removeAt(firstItemToDelete) + } + + recalculateAllVisualLines() + + val heightDifference = getHeight() - visualLines.size * lineHeight + if (verticalScrollingOffset < heightDifference) + targetVerticalScrollingOffset = heightDifference.coerceAtMost(0f) + + updateAction(getText()) + } + + private fun setCursorPosition(newPosition: LinePosition) { + newPosition.toVisualPos().run { + cursor = this + otherSelectionEnd = this + cursorNeedsRefocus = true + } + } + + protected open fun getTextBetween(startPos: LinePosition, endPos: LinePosition): String { + val textStart = startPos.toTextualPos() + val textEnd = endPos.toTextualPos() + + return if (textStart.line == textEnd.line) { + textualLines[textStart.line].text.substring(textStart.column, textEnd.column) + } else { + val lines = mutableListOf() + lines.add(textualLines[textStart.line].text.substring(textStart.column)) + + for (i in textStart.line + 1 until textEnd.line) + lines.add(textualLines[i].text) + + lines.add(textualLines[textEnd.line].text.substring(0, textEnd.column)) + lines.joinToString("\n") + } + } + + protected open fun selectAll() { + cursor = LinePosition(0, 0, isVisual = true) + otherSelectionEnd = LinePosition(visualLines.size - 1, visualLines.last().length, isVisual = true) + } + + protected open fun hasSelection() = cursor != otherSelectionEnd + protected open fun selectionStart() = minOf(cursor, otherSelectionEnd) + protected open fun selectionEnd() = maxOf(cursor, otherSelectionEnd) + protected open fun getSelection() = selectionStart() to selectionEnd() + + protected open fun deleteSelection() { + if (!hasSelection()) + return + + commitTextRemoval(selectionStart(), selectionEnd(), selectAfterUndo = true) + } + + protected open fun copySelection() { + val (visualSelectionStart, visualSelectionEnd) = getSelection() + if (visualSelectionStart == visualSelectionEnd) + return + + UDesktop.setClipboardString(getTextBetween(visualSelectionStart, visualSelectionEnd)) + } + + protected open fun charBefore(pos: LinePosition) = pos.toTextualPos().let { + when { + it.isAtAbsoluteStart -> null + it.isAtLineStart -> '\n' + else -> textualLines[it.line].text[it.column - 1] + } + } + + protected open fun charAfter(pos: LinePosition) = pos.toTextualPos().let { + when { + it.isAtAbsoluteEnd -> null + it.isAtLineEnd -> '\n' + else -> textualLines[it.line].text[it.column] + } + } + + enum class Direction { + Left, + Right + } + + protected open fun isBreakingCharacter(ch: Char): Boolean { + return !ch.isLetterOrDigit() && ch != '_' + } + + protected open fun getNearestWordBoundary(pos: LinePosition, direction: Direction): LinePosition { + /* + * Algorithm: + * 1. If we can't go further in the specified direction, return pos + * 2. First, ignore all breaking characters until a non-breaking character is found + * or the beginning is reached + * 3. Consume until a breaking character is found or the beginning is reached + * 4. If our direction is left and we are at the end of a visual line, and we are not + * at the last line, return the position at the beginning of the next visual line + * 5. if our direction is right and we are at the beginning of a visual + * line, and we are not the first line, return the position at the end of the + * last visual line + * 6. Return the position + * + * Other conditions: + * - If a newline is encountered, one of the following actions happens: + * - If this is the first character, return the position past that newline + * - Otherwise, return the position before that newline + * - If our direction is left and we are at the end of a visual line, and we are not + * at the last line, return the position at the beginning of the next visual line + * - If our direction is right and we are at the beginning of a visual line, and we + * are not the first line, return the position at the end of the last visual line + */ + + // Step 1 + val atEndOfDirection = if (direction == Direction.Left) pos::isAtAbsoluteStart else pos::isAtAbsoluteEnd + if (atEndOfDirection()) + return pos + + var textualPos = pos.toTextualPos() + val columnOffset = if (direction == Direction.Left) -1 else 1 + val nextChar = if (direction == Direction.Left) ::charBefore else ::charAfter + + if (direction == Direction.Left && textualPos.isAtLineStart) { + val previousLine = textualLines[textualPos.line - 1] + return LinePosition(textualPos.line - 1, previousLine.length, isVisual = false) + } else if (direction == Direction.Right && textualPos.isAtLineEnd) { + return LinePosition(textualPos.line + 1, 0, isVisual = false) + } + + var ch = nextChar(textualPos) + + // Step 2 + while (!atEndOfDirection() && ch?.let(::isBreakingCharacter) == true) { + textualPos = textualPos.offsetColumn(columnOffset) + ch = nextChar(textualPos) + if (ch == '\n') + return textualPos + } + + // Step 3 + while (!atEndOfDirection() && ch?.let(::isBreakingCharacter) == false) { + textualPos = textualPos.offsetColumn(columnOffset) + ch = nextChar(textualPos) + if (ch == '\n') + return textualPos + } + + // Note that if we go into either of the if cases below, we will end up returning + // a visual position rather than a textual position. This is intentional, as a + // textual position cannot distinguish a visual end of line from a visual start + // of line (in other words, if you call `.toTextualPos()` on a visual EOL on line 5 + // and on a visual start of line on line 6, they will give you the same position) + // + // In order to distinguish this, if either of these are true, we have to return a + // visual position. Fortunately, because of the way we handle visual vs textual + // lines, this does not cause a problem + val visualPos = textualPos.toVisualPos() + if (direction == Direction.Left && visualPos.isAtLineEnd && !visualPos.isInLastLine) { // Step 4 + textualPos = LinePosition(visualPos.line + 1, 0, isVisual = true) + } else if (direction == Direction.Right && visualPos.isAtLineStart && !visualPos.isInFirstLine) { // Step 5 + textualPos = LinePosition(visualPos.line - 1, visualLines[visualPos.line - 1].text.length, isVisual = true) + } + + // Step 6 + return textualPos + } + + protected open fun animateCursor() { + if (!active) return + + cursorComponent.animate { + setColorAnimation(Animations.OUT_CIRCULAR, 0.5f, cursorColor.toConstraint()) + onComplete { + if (!active) return@onComplete + cursorComponent.animate { + setColorAnimation(Animations.IN_CIRCULAR, 0.5f, Color(255, 255, 255, 0).toConstraint()) + onComplete { + if (active) animateCursor() + } + } + } + } + } + + protected open fun hasText() = textualLines.size > 1 || textualLines[0].text.isNotEmpty() + + @Deprecated(UMatrixStack.Compat.DEPRECATED, ReplaceWith("drawUnselectedText(matrixStack, text, left, row)")) + protected open fun drawUnselectedText(text: String, left: Float, row: Int) = + drawUnselectedText(UMatrixStack.Compat.get(), text, left, row) + + @Suppress("DEPRECATION") + protected fun drawUnselectedTextCompat(matrixStack: UMatrixStack, text: String, left: Float, row: Int) = + UMatrixStack.Compat.runLegacyMethod(matrixStack) { drawUnselectedText(text, left, row) } + + protected open fun drawUnselectedText(matrixStack: UMatrixStack, text: String, left: Float, row: Int) { + // TODO: Shadow color + getFontProvider().drawString( + matrixStack, + text, + getColor(), + left - horizontalScrollingOffset, + getTop() + ((lineHeight * row + 1) * getTextScale()) + verticalScrollingOffset, + 10f, + getTextScale(), + shadow = false + ) + } + + @Deprecated(UMatrixStack.Compat.DEPRECATED, ReplaceWith("drawSelectedText(matrixStack, text, left, right, row)")) + protected open fun drawSelectedText(text: String, left: Float, right: Float, row: Int) = + drawSelectedText(UMatrixStack.Compat.get(), text, left, right, row) + + @Suppress("DEPRECATION") + protected fun drawSelectedTextCompat(matrixStack: UMatrixStack, text: String, left: Float, right: Float, row: Int) = + UMatrixStack.Compat.runLegacyMethod(matrixStack) { drawSelectedText(text, left, right, row) } + + protected open fun drawSelectedText(matrixStack: UMatrixStack, text: String, left: Float, right: Float, row: Int) { + UIBlock.drawBlock( + matrixStack, + if (active) selectionBackgroundColor else inactiveSelectionBackgroundColor, + left.toDouble() - horizontalScrollingOffset, + getTop().toDouble() + (lineHeight * row * getTextScale()) + verticalScrollingOffset, + right.toDouble() - horizontalScrollingOffset, + getTop().toDouble() + (lineHeight * ((row + 1) * getTextScale())) + verticalScrollingOffset + ) + if (text.isNotEmpty()) { + getFontProvider().drawString( + matrixStack, + text, + if (active) selectionForegroundColor else inactiveSelectionForegroundColor, + left - horizontalScrollingOffset, + getTop() + ((lineHeight * row + 1) * getTextScale()) + verticalScrollingOffset, + 10f, + getTextScale(), + shadow = false + ) + } + } + + override fun animationFrame() { + super.animationFrame() + + val diff = (targetVerticalScrollingOffset - verticalScrollingOffset) * 0.1f + if (abs(diff) < .25f) + verticalScrollingOffset = targetVerticalScrollingOffset + verticalScrollingOffset += diff + + recalculateDimensions() + + if (cursorNeedsRefocus) { + scrollIntoView(cursor) + cursorNeedsRefocus = false + } + } + + protected inner class LinePosition(val line: Int, val column: Int, val isVisual: Boolean) : + Comparable { + val isAtLineStart: Boolean get() = column == 0 + val isAtLineEnd: Boolean get() = column == lines[line].length + + val isInFirstLine: Boolean get() = line == 0 + val isInLastLine: Boolean get() = line == lines.lastIndex + + val isAtAbsoluteStart: Boolean get() = isInFirstLine && isAtLineStart + val isAtAbsoluteEnd: Boolean get() = isInLastLine && isAtLineEnd + + private val lines: List = if (isVisual) visualLines else textualLines + + fun offsetColumn(amount: Int) = when { + amount > 0 -> offsetColumnPositive(amount, this) + amount < 0 -> offsetColumnNegative(-amount, this) + else -> this + } + + private tailrec fun offsetColumnNegative(amount: Int, pos: LinePosition): LinePosition { + if (amount == 0 || pos.isAtAbsoluteStart) + return pos + + return offsetColumnNegative(amount - 1, complexOffsetColumnNegative(pos)) + } + + private fun complexOffsetColumnNegative(pos: LinePosition): LinePosition { + if (!pos.isVisual) + return simpleOffsetColumnNegative(pos) + if (!pos.isAtLineStart) + return simpleOffsetColumnNegative(pos) + + val currentLine = visualLines[pos.line] + val previousLine = visualLines[pos.line - 1] + if (currentLine.textIndex != previousLine.textIndex) + return simpleOffsetColumnNegative(pos) + if (previousLine.text.last() != ' ') + return simpleOffsetColumnNegative(pos) + return LinePosition(pos.line - 1, previousLine.length - 1, isVisual = true) + } + + private fun simpleOffsetColumnNegative(pos: LinePosition) = if (pos.column == 0) { + LinePosition(pos.line - 1, pos.lines[pos.line - 1].length, pos.isVisual) + } else { + pos.withColumn(pos.column - 1) + } + + private tailrec fun offsetColumnPositive(amount: Int, pos: LinePosition): LinePosition { + if (amount == 0 || pos.isAtAbsoluteEnd) + return pos + + return offsetColumnPositive(amount - 1, complexOffsetColumnPositive(pos)) + } + + private fun complexOffsetColumnPositive(pos: LinePosition): LinePosition { + if (!pos.isVisual) + return simpleOffsetColumnPositive(pos) + + val currentLine = visualLines[pos.line] + if (pos.column < currentLine.length - 1) + return simpleOffsetColumnPositive(pos) + if (pos.line == visualLines.lastIndex) + return LinePosition(pos.line, currentLine.length, isVisual = true) + if (pos.column == currentLine.length - 1 && currentLine.text.last() != ' ') + return simpleOffsetColumnPositive(pos) + + val nextLine = visualLines[pos.line + 1] + if (currentLine.textIndex == nextLine.textIndex) + return LinePosition(pos.line + 1, 0, isVisual = true) + return simpleOffsetColumnPositive(pos) + } + + private fun simpleOffsetColumnPositive(pos: LinePosition) = if (pos.column >= pos.lines[pos.line].length) { + if (pos.line == pos.lines.lastIndex) { + LinePosition(pos.lines.lastIndex, pos.lines.last().length, pos.isVisual) + } else { + LinePosition(pos.line + 1, 0, pos.isVisual) + } + } else { + pos.withColumn(pos.column + 1) + } + + fun withColumn(newColumn: Int) = LinePosition(line, newColumn, isVisual) + + fun toTextualPos(): LinePosition { + if (!isVisual) + return this + + val visualLine = visualLines[line] + val textualLine = textualLines[visualLine.textIndex] + var totalVisualLength = 0 + + for (i in textualLine.visualIndices.first until line) + totalVisualLength += visualLines[i].length + + return LinePosition(visualLine.textIndex, totalVisualLength + column, isVisual = false) + } + + fun toVisualPos(): LinePosition { + if (isVisual) + return this + + val textualLine = textualLines[line] + var lengthRemaining = column + + for (visualLineIndex in textualLine.visualIndices) { + val visualLine = visualLines[visualLineIndex] + if (visualLine.length >= lengthRemaining) + return LinePosition(visualLineIndex, lengthRemaining, isVisual = true) + + lengthRemaining -= visualLine.length + } + + println("toTextualPos: Unexpected end of function") + return LinePosition(0, 0, isVisual = true) + } + + fun toScreenPos(): Pair { + val visualPos = toVisualPos() + val x = visualLines[visualPos.line].text.substring(0, visualPos.column) + .width(getTextScale()) - horizontalScrollingOffset + val y = (lineHeight * visualPos.line * getTextScale()) + verticalScrollingOffset + return x to y + } + + override operator fun compareTo(other: LinePosition): Int { + val thisVisual = toVisualPos() + val otherVisual = other.toVisualPos() + + return when { + thisVisual.line < otherVisual.line -> -1 + thisVisual.line > otherVisual.line -> 1 + thisVisual.column < otherVisual.column -> -1 + thisVisual.column > otherVisual.column -> 1 + else -> 0 + } + } + + override fun equals(other: Any?) = + other is LinePosition && line == other.line && column == other.column && isVisual == other.isVisual + + override fun hashCode(): Int { + var result = line + result = 31 * result + column + result = 31 * result + isVisual.hashCode() + return result + } + + override fun toString() = "LinePosition(line=$line, column=$column, isVisual=$isVisual)" + } + + protected open inner class Line(var text: String) { + val length: Int get() = text.length + } + + protected inner class TextualLine(text: String, var visualIndices: IntRange = 0..0) : Line(text) { + fun addTextAt(newText: String, column: Int) { + if (column >= text.length) { + text += newText + } else { + text = text.substring(0, column) + newText + text.substring(column) + } + } + + override fun toString() = "TextualLine(text=$text, visualIndices=$visualIndices)" + } + + protected inner class VisualLine(text: String, val textIndex: Int) : Line(text) { + override fun toString() = "VisualLine(text=$text, textIndex=$textIndex)" + } + + protected abstract inner class TextOperation { + abstract fun redo() + abstract fun undo() + } + + protected inner class AddTextOperation(val newText: String, val startPos: LinePosition) : + TextOperation() { + override fun redo() { + addText(newText, startPos) + } + + override fun undo() { + removeText(startPos, startPos.offsetColumn(newText.length)) + setCursorPosition(startPos.toVisualPos()) + } + } + + protected inner class RemoveTextOperation( + val startPos: LinePosition, val endPos: LinePosition, val selectAfterUndo: Boolean + ) : TextOperation() { + val text = getTextBetween(startPos, endPos) + + override fun redo() { + val textualStartPos = startPos.toTextualPos() + removeText(textualStartPos, endPos) + setCursorPosition(textualStartPos) + } + + override fun undo() { + addText(text, startPos) + if (selectAfterUndo) { + cursor = startPos + otherSelectionEnd = endPos + cursorNeedsRefocus = true + } + } + } + + protected inner class ReplaceTextOperation( + val addTextOperation: AddTextOperation, + val removeTextOperation: RemoveTextOperation + ) : TextOperation() { + override fun redo() { + removeTextOperation.redo() + addTextOperation.redo() + } + + override fun undo() { + addTextOperation.undo() + removeTextOperation.undo() + } + } +} diff --git a/src/main/kotlin/com/replaymod/core/gui/common/elementa/UIScrollComponent.kt b/src/main/kotlin/com/replaymod/core/gui/common/elementa/UIScrollComponent.kt new file mode 100644 index 00000000..c6f354a9 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/elementa/UIScrollComponent.kt @@ -0,0 +1,802 @@ +/** + * Based on Elementa's ScrollComponent but with more stuff exposed and certain changes so we can get it to behave just + * like we need it to: + * - New scrollTo method + * - Exposed onScroll method + * - Fix scrollbars when size or content size changes + * - Fix NaN as scrollPercentage when the offset range is zero in width + * - Ignore components going into the negative when calculating actual width/height, fixes scrollbar when zoomed out + * + * MIT License + * + * Copyright (c) 2021 ReplayMod contributors + * Copyright (c) 2021 Sk1er LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.replaymod.core.gui.common.elementa + +import gg.essential.elementa.components.* + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.constraints.* +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.dsl.* +import gg.essential.elementa.effects.ScissorEffect +import gg.essential.elementa.utils.bindLast +import gg.essential.universal.UKeyboard +import gg.essential.universal.UMatrixStack +import gg.essential.universal.UMouse +import gg.essential.universal.UResolution +import java.awt.Color +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.math.abs +import kotlin.math.max + +/** + * Basic scroll component that will only draw what is currently visible. + * + * Also prevents scrolling past what should be reasonable. + */ +class UIScrollComponent @JvmOverloads constructor( + emptyString: String = "", + private val innerPadding: Float = 0f, + private val scrollIconColor: Color = Color.WHITE, + private val horizontalScrollEnabled: Boolean = false, + private val verticalScrollEnabled: Boolean = true, + private val horizontalScrollOpposite: Boolean = false, + private val verticalScrollOpposite: Boolean = false, + private val pixelsPerScroll: Float = 15f, + private val scrollAcceleration: Float = 1.0f, + customScissorBoundingBox: UIComponent? = null +) : UIContainer() { + private var animationFPS: Int? = null + + private val actualHolder = UIContainer().constrain { + x = innerPadding.pixels() + y = innerPadding.pixels() + width = RelativeConstraint(1f) - innerPadding.pixels() + height = RelativeConstraint(1f) + } + + //Exposed so its position and value can be adjusted by user + val emptyText = UIWrappedText(emptyString, centered = true).constrain { + x = CenterConstraint() + y = SiblingConstraint() + 4.pixels() + } + + private val scrollSVGComponent = getScrollImage().constrain { + width = 24.pixels() + height = 24.pixels() + + color = scrollIconColor.toConstraint() + } + + var horizontalOffset = innerPadding + private set + var verticalOffset = innerPadding + private set + + private var horizontalScrollBarGrip: UIComponent? = null + private var horizontalHideScrollWhenUseless = false + private var verticalScrollBarGrip: UIComponent? = null + private var verticalHideScrollWhenUseless = false + + private var horizontalDragBeginPos = -1f + private var verticalDragBeginPos = -1f + + private val horizontalScrollAdjustEvents: MutableList<(Float, Float) -> Unit> = + mutableListOf(::updateScrollBar.bindLast(true)) + private val verticalScrollAdjustEvents: MutableList<(Float, Float) -> Unit> = + mutableListOf(::updateScrollBar.bindLast(false)) + private var needsUpdate = true + + private var isAutoScrolling = false + private var autoScrollBegin: Pair = -1f to -1f + private var currentScrollAcceleration: Float = 1.0f + + val allChildren = CopyOnWriteArrayList() + + /** + * Difference between the ScrollComponent's width and its contents' width. + * Will not be less than zero, even if the contents' width is less than the + * component's width. + */ + val horizontalOverhang: Float + get() = max(0f, calculateActualWidth() - getWidth()) + + /** + * Difference between the ScrollComponent's height and its contents' height. + * Will not be less than zero, even if the contents' height is less than the + * component's height. + */ + val verticalOverhang: Float + get() = max(0f, calculateActualHeight() - getHeight()) + + init { + this.constrain { + width = ScrollChildConstraint() coerceAtMost 100.percentOfWindow() + height = ScrollChildConstraint() coerceAtMost 100.percentOfWindow() + } + + if (!horizontalScrollEnabled && !verticalScrollEnabled) + throw IllegalArgumentException("ScrollComponent must have at least one direction of scrolling enabled") + + super.addChild(actualHolder) + actualHolder.addChild(emptyText) + this.enableEffects(ScissorEffect(customScissorBoundingBox)) + emptyText.setFontProvider(getFontProvider()) + super.addChild(scrollSVGComponent) + scrollSVGComponent.hide(instantly = true) + + onMouseScroll { + if (UKeyboard.isShiftKeyDown() && horizontalScrollEnabled) { + onScroll(it.delta.toFloat(), isHorizontal = true) + } else if (!UKeyboard.isShiftKeyDown() && verticalScrollEnabled) { + onScroll(it.delta.toFloat(), isHorizontal = false) + } + + it.stopPropagation() + } + + onMouseClick { event -> + onClick(event.relativeX, event.relativeY, event.mouseButton) + } + } + + private var lastHorizontalRange = 0f..0f + private var lastVerticalRange = 0f..0f + private var lastActualWidth = 0f + private var lastActualHeight = 0f + + override fun draw(matrixStack: UMatrixStack) { + val horizontalRange = calculateOffsetRange(isHorizontal = true) + val verticalRange = calculateOffsetRange(isHorizontal = false) + val actualWidth = calculateActualWidth() + val actualHeight = calculateActualHeight() + if (lastHorizontalRange != horizontalRange + || lastVerticalRange != verticalRange + || lastActualWidth != actualWidth + || lastActualHeight != actualHeight + ) { + lastHorizontalRange = horizontalRange + lastVerticalRange = verticalRange + lastActualWidth = actualWidth + lastActualHeight = actualHeight + needsUpdate = true + } + + if (needsUpdate) { + needsUpdate = false + + // Recalculate our scroll box and move the content inside if needed. + actualHolder.animate { + horizontalOffset = + if (horizontalRange.isEmpty()) innerPadding else horizontalOffset.coerceIn(horizontalRange) + verticalOffset = if (verticalRange.isEmpty()) innerPadding else verticalOffset.coerceIn(verticalRange) + + setXAnimation(Animations.IN_SIN, 0.1f, horizontalOffset.pixels()) + setYAnimation(Animations.IN_SIN, 0.1f, verticalOffset.pixels()) + } + // Run our scroll adjust event, normally updating [scrollBarGrip] + var percent = abs(horizontalOffset) / horizontalRange + var percentageOfParent = this.getWidth() / actualWidth + horizontalScrollAdjustEvents.forEach { it(percent, percentageOfParent) } + + percent = abs(verticalOffset) / verticalRange + percentageOfParent = this.getHeight() / actualHeight + verticalScrollAdjustEvents.forEach { it(percent, percentageOfParent) } + } + + super.draw(matrixStack) + } + + override fun afterInitialization() { + super.afterInitialization() + + animationFPS = Window.of(this).animationFPS + } + + /** + * Sets the text that appears when no items are shown + */ + fun setEmptyText(text: String) { + emptyText.setText(text) + } + + fun addScrollAdjustEvent( + isHorizontal: Boolean, + event: (scrollPercentage: Float, percentageOfParent: Float) -> Unit + ) { + if (isHorizontal) horizontalScrollAdjustEvents.add(event) else verticalScrollAdjustEvents.add(event) + } + + @JvmOverloads + fun setHorizontalScrollBarComponent(component: UIComponent, hideWhenUseless: Boolean = false) { + setScrollBarComponent(component, hideWhenUseless, isHorizontal = true) + } + + @JvmOverloads + fun setVerticalScrollBarComponent(component: UIComponent, hideWhenUseless: Boolean = false) { + setScrollBarComponent(component, hideWhenUseless, isHorizontal = false) + } + + /** + * A scroll bar component is an optional component that can visually display the scroll status + * of this scroll component (just like any scroll bar). + * + * The utility here is that it is automatically updated by this component with no extra work on the user-end. + * + * The hierarchy for this scrollbar grip component must be as follows: + * - Have a containing parent being the full height range of this scroll bar. + * + * [component]'s mouse events will all be overridden by this action. + * + * If [hideWhenUseless] is enabled, [component] will have [hide] called on it when the scrollbar is full height + * and dragging it would do nothing. + */ + fun setScrollBarComponent(component: UIComponent, hideWhenUseless: Boolean, isHorizontal: Boolean) { + if (isHorizontal) { + horizontalScrollBarGrip = component + horizontalHideScrollWhenUseless = hideWhenUseless + } else { + verticalScrollBarGrip = component + verticalHideScrollWhenUseless = hideWhenUseless + } + + component.onMouseScroll { + if (isHorizontal && horizontalScrollEnabled && UKeyboard.isShiftKeyDown()) { + onScroll(it.delta.toFloat(), isHorizontal = true) + } else if (!isHorizontal && verticalScrollEnabled) { + onScroll(it.delta.toFloat(), isHorizontal = false) + } + + it.stopPropagation() + } + + component.onMouseClick { event -> + if (isHorizontal) { + horizontalDragBeginPos = event.relativeX + } else { + verticalDragBeginPos = event.relativeY + } + } + + component.onMouseDrag { mouseX, mouseY, _ -> + if (isHorizontal) { + if (horizontalDragBeginPos == -1f) + return@onMouseDrag + updateGrip(component, mouseX, isHorizontal = true) + } else { + if (verticalDragBeginPos == -1f) + return@onMouseDrag + updateGrip(component, mouseY, isHorizontal = false) + } + } + + component.onMouseRelease { + if (isHorizontal) { + horizontalDragBeginPos = -1f + } else { + verticalDragBeginPos = -1f + } + } + + needsUpdate = true + } + + fun scrollToLeft(smoothScroll: Boolean = true) { + scrollTo(horizontalOffset = Float.POSITIVE_INFINITY, smoothScroll = smoothScroll) + } + + fun scrollToRight(smoothScroll: Boolean = true) { + scrollTo(horizontalOffset = Float.NEGATIVE_INFINITY, smoothScroll = smoothScroll) + } + + fun scrollToTop(smoothScroll: Boolean = true) { + scrollTo(verticalOffset = Float.POSITIVE_INFINITY, smoothScroll = smoothScroll) + } + + fun scrollToBottom(smoothScroll: Boolean = true) { + scrollTo(verticalOffset = Float.NEGATIVE_INFINITY, smoothScroll = smoothScroll) + } + + fun scrollTo( + horizontalOffset: Float = this.horizontalOffset, + verticalOffset: Float = this.verticalOffset, + smoothScroll: Boolean = true + ) { + val horizontalRange = calculateOffsetRange(isHorizontal = true) + val verticalRange = calculateOffsetRange(isHorizontal = false) + this.horizontalOffset = if (horizontalRange.isEmpty()) innerPadding else horizontalOffset.coerceIn(horizontalRange) + this.verticalOffset = if (verticalRange.isEmpty()) innerPadding else verticalOffset.coerceIn(verticalRange) + + if (smoothScroll) { + needsUpdate = true + return + } + + actualHolder.setX(this.horizontalOffset.pixels()) + actualHolder.setY(this.verticalOffset.pixels()) + val horizontalFraction = abs(horizontalOffset) / horizontalRange + val verticalFraction = abs(verticalOffset) / verticalRange + horizontalScrollAdjustEvents.forEach { it(horizontalFraction, this.getWidth() / calculateActualWidth()) } + verticalScrollAdjustEvents.forEach { it(verticalFraction, this.getHeight() / calculateActualHeight()) } + } + + fun filterChildren(filter: (component: UIComponent) -> Boolean) { + actualHolder.children.clear() + actualHolder.children.addAll(allChildren.filter(filter).ifEmpty { listOf(emptyText) }) + actualHolder.children.forEach { it.parent = actualHolder } + + needsUpdate = true + } + + @JvmOverloads + fun > sortChildren(descending: Boolean = false, comparator: (UIComponent) -> T) { + if (descending) { + actualHolder.children.sortByDescending(comparator) + } else { + actualHolder.children.sortBy(comparator) + } + } + + fun sortChildren(comparator: Comparator) { + actualHolder.children.sortWith(comparator) + } + + private fun updateGrip(component: UIComponent, mouseCoord: Float, isHorizontal: Boolean) { + if (isHorizontal) { + val minCoord = component.parent.getLeft() + val maxCoord = component.parent.getRight() + val dragDelta = mouseCoord - horizontalDragBeginPos + + horizontalOffset = if (horizontalScrollOpposite) { + val newPos = maxCoord - component.getRight() - dragDelta + val percentage = newPos / (maxCoord - minCoord) + + percentage * calculateActualWidth() + } else { + val newPos = component.getLeft() + dragDelta - minCoord + val percentage = newPos / (maxCoord - minCoord) + + -(percentage * calculateActualWidth()) + } + } else { + val minCoord = component.parent.getTop() + val maxCoord = component.parent.getBottom() + val dragDelta = mouseCoord - verticalDragBeginPos + + verticalOffset = if (verticalScrollOpposite) { + val newPos = maxCoord - component.getBottom() - dragDelta + val percentage = newPos / (maxCoord - minCoord) + + percentage * calculateActualHeight() + } else { + val newPos = component.getTop() + dragDelta - minCoord + val percentage = newPos / (maxCoord - minCoord) + + -(percentage * calculateActualHeight()) + } + } + + needsUpdate = true + } + + fun onScroll(delta: Float, isHorizontal: Boolean) { + if (isHorizontal) { + horizontalOffset += delta * pixelsPerScroll * currentScrollAcceleration + } else { + verticalOffset += delta * pixelsPerScroll * currentScrollAcceleration + } + + currentScrollAcceleration = + (currentScrollAcceleration + (scrollAcceleration - 1.0f) * 0.15f).coerceIn(0f, scrollAcceleration) + + needsUpdate = true + } + + private fun updateScrollBar(scrollPercentage: Float, percentageOfParent: Float, isHorizontal: Boolean) { + val component = if (isHorizontal) { + horizontalScrollBarGrip ?: return + } else { + verticalScrollBarGrip ?: return + } + + val clampedPercentage = percentageOfParent.coerceAtMost(1f) + + if ((isHorizontal && horizontalHideScrollWhenUseless) || (!isHorizontal && verticalHideScrollWhenUseless)) { + if (clampedPercentage == 1f) { + Window.enqueueRenderOperation(component::hide) + return + } else { + Window.enqueueRenderOperation(component::unhide) + } + } + + if (isHorizontal) { + component.setWidth(RelativeConstraint(clampedPercentage)) + } else { + component.setHeight(RelativeConstraint(clampedPercentage)) + } + + component.animate { + if (isHorizontal) { + setXAnimation( + Animations.IN_SIN, 0.1f, basicXConstraint { component -> + val offset = (component.parent.getWidth() - component.getWidth()) * scrollPercentage + + if (horizontalScrollOpposite) component.parent.getRight() - component.getHeight() - offset + else component.parent.getLeft() + offset + } + ) + } else { + setYAnimation( + Animations.IN_SIN, 0.1f, basicYConstraint { component -> + val offset = (component.parent.getHeight() - component.getHeight()) * scrollPercentage + + if (verticalScrollOpposite) component.parent.getBottom() - component.getHeight() - offset + else component.parent.getTop() + offset + } + ) + } + } + } + + private fun calculateActualWidth(): Float { + if (actualHolder.children.isEmpty()) return 0f + + return actualHolder.children.let { c -> + c.maxOf { it.getRight() } - c.minOf { it.getLeft() }.coerceAtLeast(actualHolder.getLeft()) + } + } + + private fun calculateActualHeight(): Float { + if (actualHolder.children.isEmpty()) return 0f + + return actualHolder.children.let { c -> + c.maxOf { it.getBottom() } - c.minOf { it.getTop() }.coerceAtLeast(actualHolder.getTop()) + } + } + + private fun calculateOffsetRange(isHorizontal: Boolean): ClosedFloatingPointRange { + return if (isHorizontal) { + val actualWidth = calculateActualWidth() + val maxNegative = this.getWidth() - actualWidth - innerPadding + if (horizontalScrollOpposite) (-innerPadding)..-maxNegative else maxNegative..(innerPadding) + } else { + val actualHeight = calculateActualHeight() + val maxNegative = this.getHeight() - actualHeight - innerPadding + if (verticalScrollOpposite) (-innerPadding)..-maxNegative else maxNegative..(innerPadding) + } + } + + private fun onClick(mouseX: Float, mouseY: Float, mouseButton: Int) { + if (isAutoScrolling) { + isAutoScrolling = false + scrollSVGComponent.hide() + return + } + + if (mouseButton == 2) { + // Middle click, begin the auto scroll + isAutoScrolling = true + autoScrollBegin = mouseX to mouseY + + scrollSVGComponent.constrain { + x = (mouseX - 12).pixels() + y = (mouseY - 12).pixels() + } + + scrollSVGComponent.unhide(useLastPosition = false) + } + } + + override fun animationFrame() { + super.animationFrame() + + currentScrollAcceleration = + (currentScrollAcceleration - ((scrollAcceleration - 1.0f) / (animationFPS ?: 244).toFloat())) + .coerceAtLeast(1.0f) + + if (!isAutoScrolling) return + + if (horizontalScrollEnabled) { + val xBegin = autoScrollBegin.first + getLeft() + val currentX = UMouse.Scaled.x + + if (currentX in getLeft()..getRight()) { + val deltaX = currentX - xBegin + val percentX = deltaX / (-getWidth() / 2) + horizontalOffset += (percentX.toFloat() * 5f) + needsUpdate = true + } + } + + if (verticalScrollEnabled) { + val yBegin = autoScrollBegin.second + getTop() + val currentY = UMouse.Scaled.y + + if (currentY in getTop()..getBottom()) { + val deltaY = currentY - yBegin + val percentY = deltaY / (-getHeight() / 2) + verticalOffset += (percentY.toFloat() * 5f) + needsUpdate = true + } + } + + needsUpdate = true + } + + override fun addChild(component: UIComponent) = apply { + actualHolder.removeChild(emptyText) + + actualHolder.addChild(component) + allChildren.add(component) + + needsUpdate = true + } + + override fun insertChildAt(component: UIComponent, index: Int) = apply { + if (index < 0 || index > allChildren.size) { + println("Bad index given to insertChildAt (index: $index, children size: ${allChildren.size}") + return@apply + } + + actualHolder.removeChild(emptyText) + + component.parent = actualHolder + actualHolder.children.add(index, component) + allChildren.add(index, component) + + needsUpdate = true + } + + override fun insertChildBefore(newComponent: UIComponent, targetComponent: UIComponent) = apply { + val indexOfExisting = allChildren.indexOf(targetComponent) + if (indexOfExisting == -1) { + println("targetComponent given to insertChildBefore is not a child of this component") + return@apply + } + + insertChildAt(newComponent, indexOfExisting) + } + + override fun insertChildAfter(newComponent: UIComponent, targetComponent: UIComponent) = apply { + val indexOfExisting = allChildren.indexOf(targetComponent) + if (indexOfExisting == -1) { + println("targetComponent given to insertChildAfter is not a child of this component") + return@apply + } + + insertChildAt(newComponent, indexOfExisting + 1) + } + + override fun replaceChild(newComponent: UIComponent, componentToReplace: UIComponent) = apply { + val indexOfExisting = allChildren.indexOf(componentToReplace) + if (indexOfExisting == -1) { + println("componentToReplace given to replaceChild is not a child of this component") + return@apply + } + + actualHolder.removeChild(emptyText) + + actualHolder.children.removeAt(indexOfExisting) + allChildren.removeAt(indexOfExisting) + + newComponent.parent = actualHolder + actualHolder.children.add(indexOfExisting, newComponent) + allChildren.add(indexOfExisting, newComponent) + + needsUpdate = true + } + + override fun removeChild(component: UIComponent) = apply { + if (component == scrollSVGComponent) { + super.removeChild(component) + return@apply + } + + actualHolder.removeChild(component) + allChildren.remove(component) + + if (allChildren.isEmpty()) + actualHolder.addChild(emptyText) + + needsUpdate = true + } + + override fun clearChildren() = apply { + allChildren.clear() + actualHolder.clearChildren() + actualHolder.addChild(emptyText) + + needsUpdate = true + } + + override fun alwaysDrawChildren(): Boolean { + return true + } + + override fun childrenOfType(clazz: Class): List { + return actualHolder.childrenOfType(clazz) + } + + override fun mouseClick(mouseX: Double, mouseY: Double, button: Int) { + actualHolder.mouseClick(mouseX, mouseY, button) + } + + override fun hitTest(x: Float, y: Float): UIComponent { + return actualHolder.hitTest(x, y) + } + + fun searchAndInsert(components: List, comparison: (UIComponent) -> Int) { + if (components.isEmpty()) return + + actualHolder.children.remove(emptyText) + val searchIndex = actualHolder.children.binarySearch(comparison = comparison) + + components.forEach { it.parent = actualHolder } + allChildren.addAll(components) + actualHolder.children.addAll( + if (searchIndex >= 0) searchIndex else -(searchIndex + 1), + components + ) + + needsUpdate = true + } + + fun setChildren(components: List) = apply { + actualHolder.children.clear() + actualHolder.children.addAll(components.ifEmpty { listOf(emptyText) }) + actualHolder.children.forEach { it.parent = actualHolder } + + allChildren.clear() + allChildren.addAll(actualHolder.children) + + needsUpdate = true + } + + private fun ClosedFloatingPointRange.width() = abs(this.start - this.endInclusive) + private fun ClosedFloatingPointRange.width() = abs(this.start - this.endInclusive) + private operator fun Float.div(range: ClosedFloatingPointRange): Float { + val width = range.width() + return if (width == 0f) 0f else this / width + } + + class DefaultScrollBar(isHorizontal: Boolean) : UIComponent() { + val grip: UIComponent + + init { + if (isHorizontal) { + constrain { + y = 2.pixels(alignOpposite = true) + width = 100.percent() + height = 10.pixels() + } + + val container = UIContainer().constrain { + x = 2.pixels() + y = CenterConstraint() + width = RelativeConstraint() - 4.pixels() + height = 4.pixels() + } childOf this + + grip = UIBlock(Color(70, 70, 70)).constrain { + x = 0.pixels(alignOpposite = true) + y = CenterConstraint() + width = 30.pixels() + height = 3.pixels() + } childOf container + } else { + constrain { + x = 2.pixels(alignOpposite = true) + width = 10.pixels() + height = 100.percent() + } + + val container = UIContainer().constrain { + x = CenterConstraint() + y = 2.pixels() + width = 4.pixels() + height = RelativeConstraint() - 4.pixels() + } childOf this + + grip = UIBlock(Color(70, 70, 70)).constrain { + x = CenterConstraint() + y = 0.pixels(alignOpposite = true) + width = 3.pixels() + height = 30.pixels() + } childOf container + } + } + } + + /** + * Constrains a scroll component to either the widest/tallest child or to the sum of their widths/heights and + * padding. If [horizontalScrollEnabled] or [verticalScrollEnabled] are true, they will change the constraint to + * use the total of their children's corresponding measurements and padding. Otherwise, it will use the maximum + * measurement for the corresponding direction. + * + * This is the default width and height constraint for scroll components. + * + * @param padding Pixels of padding to add to each component + */ + inner class ScrollChildConstraint(val padding: Float = 0f) : WidthConstraint, HeightConstraint { + override var cachedValue = 0f + override var recalculate = true + override var constrainTo: UIComponent? = null + + override fun getWidthImpl(component: UIComponent): Float { + if (horizontalScrollEnabled) { + val holder = constrainTo ?: actualHolder + var totalPadding = (holder.children.size - 1) * padding + holder.children.forEach { child -> + if (child.constraints.y is PaddingConstraint) { + totalPadding += (child.constraints.y as PaddingConstraint).getVerticalPadding(child) + } + } + return (holder.children + .sumOf { it.getHeight().toDouble() } + totalPadding).toFloat() + + } else { + return (constrainTo ?: actualHolder).children.maxByOrNull { + if(it.constraints.x is PaddingConstraint) + return@maxByOrNull it.getWidth() + (it.constraints.x as PaddingConstraint).getHorizontalPadding(it) + it.getWidth() }?.getWidth() ?: 0f + } + } + + override fun getHeightImpl(component: UIComponent): Float { + if (verticalScrollEnabled) { + val holder = constrainTo ?: actualHolder + var totalPadding = (holder.children.size - 1) * padding + holder.children.forEach { child -> + if (child.constraints.y is PaddingConstraint) { + totalPadding += (child.constraints.y as PaddingConstraint).getVerticalPadding(child) + } + } + return (holder.children + .sumOf { it.getHeight().toDouble() } + totalPadding).toFloat() + } else { + return (constrainTo ?: actualHolder).children.maxByOrNull { + if(it.constraints.y is PaddingConstraint) + return@maxByOrNull it.getHeight() + (it.constraints.y as PaddingConstraint).getVerticalPadding(it) + it.getHeight() }?.getHeight() ?: 0f + } + } + + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { + when (type) { + ConstraintType.WIDTH -> visitor.visitChildren(ConstraintType.WIDTH) + ConstraintType.HEIGHT -> visitor.visitChildren(ConstraintType.HEIGHT) + else -> throw IllegalArgumentException(type.prettyName) + } + } + + } + + companion object { + + fun getScrollImage(): UIImage { + return UIImage.ofResourceCached("/svg/scroll.png") + } + } +} diff --git a/src/main/kotlin/com/replaymod/core/gui/common/elementa/UITextInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/elementa/UITextInput.kt new file mode 100644 index 00000000..f37c546f --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/elementa/UITextInput.kt @@ -0,0 +1,176 @@ +/** + * Based on Elementa's UITextInput but with more stuff exposed and certain changes so we can get it to behave just + * like we need it to: + * - No changes except it is using our AbstractTextInput (which has changes) + * + * MIT License + * + * Copyright (c) 2021 ReplayMod contributors + * Copyright (c) 2021 Sk1er LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.replaymod.core.gui.common.elementa + +import gg.essential.elementa.constraints.WidthConstraint +import gg.essential.elementa.dsl.* +import gg.essential.universal.UMatrixStack +import java.awt.Color + +open class UITextInput @JvmOverloads constructor( + placeholder: String = "", + shadow: Boolean = true, + selectionBackgroundColor: Color = Color.WHITE, + selectionForegroundColor: Color = Color(64, 139, 229), + allowInactiveSelection: Boolean = false, + inactiveSelectionBackgroundColor: Color = Color(176, 176, 176), + inactiveSelectionForegroundColor: Color = Color.WHITE, + cursorColor: Color = Color.WHITE +) : AbstractTextInput( + placeholder, + shadow, + selectionBackgroundColor, + selectionForegroundColor, + allowInactiveSelection, + inactiveSelectionBackgroundColor, + inactiveSelectionForegroundColor, + cursorColor = Color.WHITE +) { + protected var minWidth: WidthConstraint? = null + protected var maxWidth: WidthConstraint? = null + + protected val placeholderWidth = placeholder.width() + + fun setMinWidth(constraint: WidthConstraint) = apply { + minWidth = constraint + } + + fun setMaxWidth(constraint: WidthConstraint) = apply { + maxWidth = constraint + } + + override fun getText() = textualLines.first().text + + protected open fun getTextForRender(): String = getText() + + protected open fun setCursorPos() { + cursorComponent.unhide() + val (cursorPosX, _) = cursor.toScreenPos() + cursorComponent.setX((cursorPosX).pixels()) + } + + override fun textToLines(text: String): List { + return listOf(text.replace('\n', ' ')) + } + + override fun scrollIntoView(pos: LinePosition) { + val column = pos.column + val lineText = getTextForRender() + if (column < 0 || column > lineText.length) + return + + val widthBeforePosition = lineText.substring(0, column).width(getTextScale()) + + when { + getTextForRender().width(getTextScale()) < getWidth() -> { + horizontalScrollingOffset = 0f + } + horizontalScrollingOffset > widthBeforePosition -> { + horizontalScrollingOffset = widthBeforePosition + } + widthBeforePosition - horizontalScrollingOffset > getWidth() -> { + horizontalScrollingOffset = widthBeforePosition - getWidth() + } + } + } + + override fun screenPosToVisualPos(x: Float, y: Float): LinePosition { + val targetXPos = x + horizontalScrollingOffset + var currentX = 0f + + val line = getTextForRender() + + for (i in line.indices) { + val charWidth = line[i].width(getTextScale()) + if (currentX + (charWidth / 2) >= targetXPos) return LinePosition(0, i, isVisual = true) + currentX += charWidth + } + + return LinePosition(0, line.length, isVisual = true) + } + + override fun recalculateDimensions() { + if (minWidth != null && maxWidth != null) { + val width = if (!hasText() && !this.active) { + placeholderWidth + } else { + getTextForRender().width(getTextScale()) + 1 /* cursor */ + } + setWidth(width.pixels().coerceIn(minWidth!!, maxWidth!!)) + } + } + + override fun splitTextForWrapping(text: String, maxLineWidth: Float): List { + return listOf(text) + } + + override fun onEnterPressed() { + activateAction(getText()) + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDrawCompat(matrixStack) + + if (!active && !hasText()) { + getFontProvider().drawString(matrixStack, placeholder, getColor(), getLeft(), getTop(), 10f, getTextScale()) + return super.draw(matrixStack) + } + + val lineText = getTextForRender() + + if (hasSelection()) { + var currentX = getLeft() + cursorComponent.hide(instantly = true) + + if (!selectionStart().isAtLineStart) { + val preSelectionText = lineText.substring(0, selectionStart().column) + drawUnselectedTextCompat(matrixStack, preSelectionText, currentX, row = 0) + currentX += preSelectionText.width(getTextScale()) + } + + val selectedText = lineText.substring(selectionStart().column, selectionEnd().column) + val selectedTextWidth = selectedText.width(getTextScale()) + drawSelectedTextCompat(matrixStack, selectedText, currentX, currentX + selectedTextWidth, row = 0) + currentX += selectedTextWidth + + if (!selectionEnd().isAtLineEnd) { + drawUnselectedTextCompat(matrixStack, lineText.substring(selectionEnd().column), currentX, row = 0) + } + } else { + cursorComponent.setY(basicYConstraint { + getTop() + }) + setCursorPos() + + drawUnselectedTextCompat(matrixStack, lineText, getLeft(), 0) + } + + super.draw(matrixStack) + } +} diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIAdvancedTextInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIAdvancedTextInput.kt new file mode 100644 index 00000000..2aa176d4 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIAdvancedTextInput.kt @@ -0,0 +1,85 @@ +package com.replaymod.core.gui.common.input + +import com.replaymod.core.gui.common.elementa.UITextInput +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.pixels + +open class UIAdvancedTextInput( + private val preferReplace: Boolean = true, + private val alignRight: Boolean = true, +) : UITextInput() { + + override fun afterInitialization() { + super.afterInitialization() + + if (alignRight) { + constrain { + x = 2.pixels(alignOpposite = true) + } + } + } + + protected open fun shouldReplaceExistingText(add: AddTextOperation) = preferReplace + protected open fun fixText(): TextOperation? = null + + override fun commitTextOperation(operation: TextOperation) { + var op = operation + + if (op is AddTextOperation && shouldReplaceExistingText(op)) { + op = CompoundOperation(op, with(op) { + applied { RemoveTextOperation(endPos, endPos.offsetColumn(newText.length), false) } + }) + } + + op.applied { + val fix = fixText() ?: return + if (fix !is NoOperation) { + op = CompoundOperation(op, fix) + } + } + + super.commitTextOperation(op) + } + + private inner class CompoundOperation( + val inner: TextOperation, + val fix: TextOperation, + ) : TextOperation() { + override fun redo() { + inner.redo() + + val orgCursor = cursor + val orgOtherSelectionEnd = otherSelectionEnd + fix.redo() + cursor = orgCursor.coerceInBounds() + otherSelectionEnd = orgOtherSelectionEnd.coerceInBounds() + } + + override fun undo() { + fix.undo() + inner.undo() + } + } + + protected inner class NoOperation : TextOperation() { + override fun redo() = Unit + override fun undo() = Unit + } + + private inline fun TextOperation?.applied(block: () -> T): T { + this?.redo() + try { + return block() + } finally { + this?.undo() + } + } + + private fun LinePosition.coerceInBounds(): LinePosition { + val lines = if (isVisual) visualLines else textualLines + val line = line.coerceIn(0, lines.lastIndex) + return LinePosition(line, column.coerceIn(0, lines[line].length), isVisual) + } + + protected val AddTextOperation.endPos get() = startPos.offsetColumn(newText.length) +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputField.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputField.kt new file mode 100644 index 00000000..874a9e97 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputField.kt @@ -0,0 +1,82 @@ +package com.replaymod.core.gui.common.input + +import com.replaymod.core.gui.common.elementa.UITextInput +import com.replaymod.core.gui.utils.enableTabFocusChange +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.dsl.* +import gg.essential.elementa.effects.OutlineEffect +import gg.essential.elementa.events.UIClickEvent +import gg.essential.elementa.state.BasicState +import java.awt.Color + +class UIInputField( + val input: T, +) : UIContainer() { + + private val focused = BasicState(false) + + val background by UIBlock(Color.BLACK).constrain { + width = 100.percent + height = 100.percent + }.effect(OutlineEffect(Color.WHITE, 1f, drawInsideChildren = true).bindColor(focused.map { focused -> + if (focused) { + Color.WHITE + } else { + Color(160, 160, 160) + } + })).onMouseClick { event -> + for (listener in input.mouseClickListeners) { + input.listener(event.projectOnto(input)) + } + } childOf this + + init { + constrain { + width = 200.pixels + height = 20.pixels + } + + input.constrain { + x = 3.pixels + y = CenterConstraint() + }.apply { + setMinWidth(1.pixel) + setMaxWidth(100.percent - 6.pixels) + enableTabFocusChange() + }.onFocus { + focused.set(true) + }.onFocusLost { + focused.set(false) + }.onMouseClick { event -> + input.grabWindowFocus() + + if (!input.isActive()) { + Window.enqueueRenderOperation { + if (input.isActive()) { + for (listener in input.mouseClickListeners) { + listener(event) + } + } + } + } + } childOf this + } + + companion object { + fun text( + placeholder: String = "", + ): UIInputField = UIInputField(UITextInput( + placeholder, + )) + } + + private fun UIClickEvent.projectOnto(component: UIComponent) = copy( + absoluteX = absoluteX.coerceIn(component.getLeft(), component.getRight()), + absoluteY = absoluteY.coerceIn(component.getTop(), component.getBottom()), + currentTarget = component, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIIntegerInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIIntegerInput.kt new file mode 100644 index 00000000..a51d4318 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIIntegerInput.kt @@ -0,0 +1,51 @@ +package com.replaymod.core.gui.common.input + +import java.util.* + +class UIIntegerInput( + preferReplace: Boolean = true, + alignRight: Boolean = true, + private val fixedDigits: Int? = null, +) : UIAdvancedTextInput( + preferReplace, + alignRight, +) { + var value: Int + get() = getText().toIntOrNull() ?: 0 + set(value) { + val str = "%0${fixedDigits ?: 1}d".format(Locale.ROOT, value) + if (getText() != str) { + setText(str) + } + } + + override fun shouldReplaceExistingText(add: AddTextOperation): Boolean = + super.shouldReplaceExistingText(add) && add.newText != "-" + + override fun fixText(): TextOperation? { + val text = getText() + + val validValue = text.toIntOrNull() != null + || text.isEmpty() // allow field to be completely emptied + || (fixedDigits == null && text == "-") // allow typing of negative numbers into empty field + if (!validValue) { + return null + } + + val targetLen = fixedDigits + if (targetLen != null) { + val newLen = text.length + val endOfLine = LinePosition(0, newLen, false) + when { + newLen < targetLen -> return AddTextOperation("0".repeat(targetLen - newLen), endOfLine) + newLen > targetLen -> return RemoveTextOperation( + endOfLine.offsetColumn(targetLen - newLen), + endOfLine, + false + ) + } + } + + return NoOperation() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UITimeInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UITimeInput.kt new file mode 100644 index 00000000..b5175830 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UITimeInput.kt @@ -0,0 +1,79 @@ +package com.replaymod.core.gui.common.input + +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class UITimeInput( + private val minuteDigits: Int? = 3, +) : UIAdvancedTextInput() { + + private fun Duration.formatString() = "%0${minuteDigits}d:%02d:%03d".format( + Locale.ROOT, + inWholeMinutes, + inWholeSeconds % 60, + inWholeMilliseconds % 1000 + ) + + private fun String.parseDuration(): Duration? = this + .split(":") + .also { if (it.size != 3) return null } + .zip(listOf(minuteDigits ?: 1, 2, 3)) + .map { (str, len) -> str.padEnd(len, '0').toIntOrNull() ?: return null } + .zip(listOf(DurationUnit.MINUTES, TimeUnit.SECONDS, TimeUnit.MILLISECONDS)) + .map { (int, unit) -> int.toDuration(unit) } + .reduce(Duration::plus) + + var value: Duration + get() = getText().parseDuration() ?: Duration.ZERO + set(value) { + val str = value.formatString() + if (getText() != str) { + setText(str) + } + } + + override fun fixText(): TextOperation? { + val unformatted = getText() + val duration = getText().parseDuration() ?: return null + val formatted = duration.formatString() + + if (formatted != unformatted) { + val start = LinePosition(0, 0, false) + return ReplaceTextOperation( + AddTextOperation(formatted, start), + RemoveTextOperation(start, LinePosition(0, unformatted.length, false), false) + ) + } + + return NoOperation() + } + + override fun commitTextOperation(operation: TextOperation) { + var op = operation + + // If they try to delete the separator, move the cursor before it instead + if (op is RemoveTextOperation && op.text == ":") { + cursor = op.startPos + otherSelectionEnd = cursor + return + } + + // If they add the final digit before a separator, move the cursor past the separator as well + if (op is AddTextOperation && getText().drop(op.endPos.column).startsWith(":")) { + op = AddTextOperation(op.newText + ":", op.startPos) + } + + // Replace any non-digits with the separator character, this allows us to skip the separator with any key + if (op is AddTextOperation && !getText().all { it.isDigit() || it == ':' }) { + op = AddTextOperation( + op.newText.map { if (it.isDigit()) it else ':' }.joinToString(), + op.startPos + ) + } + + super.commitTextOperation(op) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UIFlatScrollBar.kt b/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UIFlatScrollBar.kt new file mode 100644 index 00000000..229bd832 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UIFlatScrollBar.kt @@ -0,0 +1,47 @@ +package com.replaymod.core.gui.common.scrollbar + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.dsl.childOf +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.percent +import gg.essential.elementa.dsl.provideDelegate +import gg.essential.elementa.utils.withAlpha +import gg.essential.universal.UMatrixStack +import java.awt.Color + +class UIFlatScrollBar(transparent: Boolean) : UIBlock(Color.BLACK.withAlpha(if(transparent) 0.5f else 1f)) { + val grip by Grip().constrain { + width = 100.percent + height = 100.percent + } childOf this + + init { + constrain { + width = 100.percent + height = 100.percent + } + } + + class Grip : UIComponent() { + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val l = getLeft().toDouble() + val r = getRight().toDouble() + val t = getTop().toDouble() + val b = getBottom().toDouble() + + // Slider + drawBlock(matrixStack, Color.LIGHT_GRAY, l, t, r, b) + + // Right shadow + drawBlock(matrixStack, Color.GRAY, r - 1, t, r, b) + + // Bottom shadow + drawBlock(matrixStack, Color.GRAY, l, b - 1, r, b) + + super.draw(matrixStack) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UITexturedScrollBar.kt b/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UITexturedScrollBar.kt new file mode 100644 index 00000000..25104f1c --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/scrollbar/UITexturedScrollBar.kt @@ -0,0 +1,33 @@ +package com.replaymod.core.gui.common.scrollbar + +import com.replaymod.core.gui.common.UI9Slice +import com.replaymod.core.gui.common.timeline.UITimeline.Companion.TEXTURE +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.dsl.* + +class UITexturedScrollBar : UIContainer() { + val background by UI9Slice(TEXTURE, UI9Slice.TextureData.ofSize(64, 9, 2).offset(0, 7)).constrain { + width = 100.percent + height = 100.percent + } childOf this + + val inner by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 100.percent - 2.pixels + height = 100.percent - 2.pixels + } childOf background + + val grip by UI9Slice(TEXTURE, UI9Slice.TextureData.ofSize(62, 7, 2)).constrain { + width = 100.percent + height = 100.percent + } childOf inner + + init { + constrain { + width = 100.percent + height = 100.percent + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/state.kt b/src/main/kotlin/com/replaymod/core/gui/common/state.kt new file mode 100644 index 00000000..29f9cf2d --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/state.kt @@ -0,0 +1,25 @@ +package com.replaymod.core.gui.common + +import gg.essential.elementa.state.BasicState + +class LazyState(value: T) : BasicState(value) { + private var dirty = false + + override fun set(value: T) { + if (value == valueBacker) return + valueBacker = value + dirty = true + } + + fun flush() { + if (!dirty) return + val value = get() + listeners.forEach { it(value) } + } +} + +class BoundedState(value: T, private val constrain: (value: T) -> T) : BasicState(value) { + override fun set(value: T) { + super.set(constrain(value)) + } +} diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt new file mode 100644 index 00000000..0854772c --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt @@ -0,0 +1,151 @@ +package com.replaymod.core.gui.common.timeline + +import com.replaymod.core.gui.common.UI9Slice +import com.replaymod.core.gui.common.elementa.UIScrollComponent +import com.replaymod.core.gui.utils.actualHorizontalOffset +import com.replaymod.core.gui.utils.addTooltip +import com.replaymod.core.gui.utils.pollingState +import com.replaymod.core.gui.utils.selfOrParentOfType +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.ConstraintType +import gg.essential.elementa.constraints.WidthConstraint +import gg.essential.elementa.constraints.XConstraint +import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.state.pixels +import gg.essential.universal.UKeyboard +import net.minecraft.util.Identifier +import kotlin.math.pow +import kotlin.time.Duration + +class UITimeline : UIContainer() { + val length = BasicState(Duration.ZERO) + val offset = BasicState(Duration.ZERO) + val zoom = BasicState(1f) + private val widthOfView = pollingState(1f) { content.getWidth() } + private val widthOfContent = widthOfView.zip(zoom).map { (viewWidth, zoom) -> viewWidth / zoom } + val unit = length.zip(widthOfContent).map { (length, width) -> length / width.toDouble() } + + val lengthMillis get() = length.get().inWholeMilliseconds + + val background by UI9Slice(TEXTURE, UI9Slice.TextureData.ofSize(64, 22, 5, 5, 5, 5).offset(0, 16)).constrain { + width = 100.percent + height = 100.percent + } childOf this + + val content by UIScrollComponent(horizontalScrollEnabled = true, verticalScrollEnabled = false).constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 100.percent - 6.pixels + height = 100.percent - 6.pixels + } childOf this + + val contentSize by UIContainer().constrain { + width = widthOfContent.pixels() + } childOf content + + val indicators by UITimelineIndicators(this).constrain { + y = 0.pixels(alignOpposite = true) + width = widthOfContent.pixels() + height = 6.pixels + } + + val cursor by UITimelineCursor(this) childOf content + + init { + addTooltip { + addLine { + bindText(pollingState("") { + val time = getTimeAt(getMousePosition().first) + "%02d:%02d".format(time.inWholeMinutes, time.inWholeSeconds % 60) + }) + } + } + } + + fun getTimeAt(mouseX: Float): Duration { + val width = content.getWidth() + val innerX = (mouseX - content.getLeft()).coerceIn(0f, width) - content.actualHorizontalOffset + return Duration.milliseconds((unit.get() * innerX.toDouble()).inWholeMilliseconds) + } + + fun enableIndicators() = apply { + content.insertChildAt(indicators, 0) + } + + fun enableZooming() = apply { + content.mouseScrollListeners.clear() + onMouseScroll { event -> + event.stopImmediatePropagation() + + if (UKeyboard.isCtrlKeyDown()) { + val (absoluteMouseX, _) = getMousePosition() + val mouseX = absoluteMouseX - content.getLeft() + val fixedPos = mouseX - content.horizontalOffset + val oldZoom = zoom.get() + val newZoom = (oldZoom * (1.2).pow(-event.delta).toFloat()).coerceIn(0.001f, 1f) + val zoomedPos = fixedPos * oldZoom / newZoom + val newOffset = mouseX - zoomedPos + zoom.set(newZoom) + content.onWindowResize() + content.scrollTo(horizontalOffset = newOffset, smoothScroll = false) + } else { + content.onScroll(event.delta.toFloat(), isHorizontal = true) + } + } + } + + class Constraint(value: Duration) : XConstraint, WidthConstraint { + override var cachedValue = 0f + override var recalculate = true + override var constrainTo: UIComponent? = null + + private var valueState: State = BasicState(value) + + var value: Duration + get() = valueState.get() + set(value) { + valueState.set(value) + } + + fun bindValue(newState: State) = apply { + valueState = newState + } + + override fun getXPositionImpl(component: UIComponent): Float = getImpl(component, true) + override fun getWidthImpl(component: UIComponent): Float = getImpl(component, false) + + private fun getImpl(component: UIComponent, absolute: Boolean): Float { + val targetComponent = constrainTo ?: component.parent + val targetTimeline = targetComponent.selfOrParentOfType() + ?: throw IllegalStateException("$targetComponent needs to be the child of a timeline") + val targetContent = targetTimeline.content + val unit = targetTimeline.unit.get() + + val value = this.valueState.get() + + val offset = (value / unit).toFloat() + return if (absolute) { + targetContent.getLeft() + targetContent.actualHorizontalOffset + offset + } else { + offset + } + } + + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { + when (type) { + ConstraintType.X -> visitor.visitParent(ConstraintType.X) + ConstraintType.WIDTH -> visitor.visitParent(ConstraintType.WIDTH) + else -> throw IllegalArgumentException(type.prettyName) + } + } + } + + companion object { + internal val TEXTURE = Identifier("jgui", "gui.png") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt new file mode 100644 index 00000000..3c56b5c7 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt @@ -0,0 +1,46 @@ +package com.replaymod.core.gui.common.timeline + +import com.replaymod.core.gui.common.BoundedState +import com.replaymod.core.gui.common.UITexture +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.dsl.* +import kotlin.time.Duration + +class UITimelineCursor(val timeline: UITimeline) : UIContainer() { + val position = BoundedState(Duration.ZERO) { it.coerceIn(Duration.ZERO..timeline.length.get()) } + + val positionMillis get() = position.get().inWholeMilliseconds + + private val pin by UITexture(UITimeline.TEXTURE, UITexture.TextureData.ofSize(5, 4).offset(64, 0)).constrain { + width = 5.pixels + height = 4.pixels + } childOf this + + private val needle by UITexture(UITimeline.TEXTURE, UITexture.TextureData.ofSize(5, 11).offset(64, 4)).constrain { + y = 4.pixels + width = 5.pixels + height = 100.percent - 4.pixels + } childOf this + + init { + constrain { + x = UITimeline.Constraint(Duration.ZERO).bindValue(position) - (2.5).pixels + width = 5.pixels + height = 100.percent + } + } + + override fun isPointInside(x: Float, y: Float): Boolean = false // the cursor should never obstruct clicks, etc. + + fun ensureVisibleWithPadding() = ensureVisible(padding = timeline.content.getWidth() / 10) + + fun ensureVisible(padding: Float = 0f) = apply { + val scroller = timeline.content + val position = getLeft() - parent.getLeft() + if (position - padding + scroller.horizontalOffset < 0) { + scroller.scrollTo(horizontalOffset = -(position - padding), smoothScroll = false) + } else if (position + padding + scroller.horizontalOffset > scroller.getWidth()) { + scroller.scrollTo(horizontalOffset = -(position + padding - scroller.getWidth()), smoothScroll = false) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineIndicators.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineIndicators.kt new file mode 100644 index 00000000..bc15238a --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineIndicators.kt @@ -0,0 +1,71 @@ +package com.replaymod.core.gui.common.timeline + +import com.replaymod.core.gui.utils.actualHorizontalOffset +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.universal.UMatrixStack +import java.awt.Color +import kotlin.time.Duration + +class UITimelineIndicators(val timeline: UITimeline) : UIComponent() { + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val height = getHeight() + val bottom = getBottom() + + val unit = timeline.unit.get() + val visibleLength = unit * timeline.content.getWidth().toDouble() + val offset = unit * -timeline.content.actualHorizontalOffset.toDouble() + val interval = getInterval() + val smallInterval = interval / 5 + var time = interval * (offset / interval).toInt() + var counter = 0 + while (time <= offset + visibleLength) { + if (time >= offset) { + val big = counter % 5 == 0 + val color = if (big) Color.LIGHT_GRAY else Color.WHITE + val x = UITimeline.Constraint(time).getXPosition(this) - 0.5 + val h = height / if (big) 1.0 else 2.0 + UIBlock.drawBlockSized(matrixStack, color, x, bottom - h, 1.0, h) + } + counter++ + time += smallInterval + } + + super.draw(matrixStack) + } + + internal fun getInterval(): Duration { + val width = timeline.content.getWidth().toDouble() // Width of the drawn timeline + val segmentLength = timeline.unit.get() * width // Length of the drawn timeline + val minDistance = MIN_DISTANCE * if (timeline.length.get() > Duration.Companion.hours(1)) 1.2 else 1.0 + val maxIndicators = width / minDistance // Max. amount of indicators that can fit in the timeline + val minInterval = segmentLength / maxIndicators // Min. interval between those indicators + return SNAP_TO.firstOrNull { it > minInterval } ?: SNAP_TO.last() // find next greater snap, fallback to max one + } + + companion object { + private const val MIN_DISTANCE = 40 + + private val SNAP_TO = listOf( + Duration.seconds(1), + Duration.seconds(2), + Duration.seconds(5), + Duration.seconds(10), + Duration.seconds(15), + Duration.seconds(20), + Duration.seconds(30), + Duration.minutes(1), + Duration.minutes(2), + Duration.minutes(5), + Duration.minutes(10), + Duration.minutes(15), + Duration.minutes(30), + Duration.hours(1), + Duration.hours(2), + Duration.hours(5), + Duration.hours(10), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineTime.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineTime.kt new file mode 100644 index 00000000..06fee96b --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineTime.kt @@ -0,0 +1,42 @@ +package com.replaymod.core.gui.common.timeline + +import com.replaymod.core.gui.utils.actualHorizontalOffset +import gg.essential.elementa.UIComponent +import gg.essential.elementa.dsl.boundTo +import gg.essential.elementa.dsl.width +import gg.essential.universal.UMatrixStack +import kotlin.time.Duration + +class UITimelineTime(val timeline: UITimeline) : UIComponent() { + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft() + val right = getRight() + val top = getTop() + val fontProvider = getFontProvider() + val color = getColor() + + val unit = timeline.unit.get() + val visibleLength = unit * timeline.content.getWidth().toDouble() + val offset = unit * -timeline.content.actualHorizontalOffset.toDouble() + val interval = timeline.indicators.getInterval() + var time = interval * (offset / interval).toInt() + while (time <= offset + visibleLength) { + if (time >= offset) { + val str = if (timeline.length.get() > Duration.hours(1)) { + "%02d:%02d:%02d".format(time.inWholeHours, time.inWholeMinutes % 60, time.inWholeSeconds % 60) + } else { + "%02d:%02d".format(time.inWholeMinutes, time.inWholeSeconds % 60) + } + val halfStrWidth = str.width(fontProvider = fontProvider) / 2 + val x = (UITimeline.Constraint(time).boundTo(timeline).getXPosition(this) - 0.5f) + .coerceIn(left + halfStrWidth, right - halfStrWidth) + fontProvider.drawString(matrixStack, str, color, x - halfStrWidth, top, 1f, 1f) + } + time += interval + } + + super.draw(matrixStack) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/events.kt b/src/main/kotlin/com/replaymod/core/gui/utils/events.kt new file mode 100644 index 00000000..8d8abda7 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/events.kt @@ -0,0 +1,61 @@ +package com.replaymod.core.gui.utils + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.events.UIClickEvent +import gg.essential.elementa.utils.ObservableClearEvent +import gg.essential.elementa.utils.ObservableRemoveEvent +import java.util.Observer + +fun T.onAnimationFrame(block: T.() -> Unit) = apply { + enableEffect(object : Effect() { + override fun animationFrame() = block() + }) +} + +fun T.onLeftClick(clickCount: Int? = null, handler: T.(event: UIClickEvent) -> Unit) = apply { + onMouseClick { if (it.mouseButton == 0 && (clickCount == null || it.clickCount == clickCount)) handler(it) } +} + +fun T.onRightClick(clickCount: Int? = null, handler: T.(event: UIClickEvent) -> Unit) = apply { + onMouseClick { if (it.mouseButton == 1 && (clickCount == null || it.clickCount == clickCount)) handler(it) } +} + +fun T.onMiddleClick(clickCount: Int? = null, handler: T.(event: UIClickEvent) -> Unit) = apply { + onMouseClick { if (it.mouseButton == 2 && (clickCount == null || it.clickCount == clickCount)) handler(it) } +} + +private fun T.onMouse(mouseButton: Int, handler: T.(mouseX: Float, mouseY: Float) -> Unit) = apply { + var dragging = false + onMouseClick { + if (it.mouseButton == mouseButton) { + dragging = true + handler(it.absoluteX, it.absoluteY) + } + } + onMouseDrag { mouseX, mouseY, _ -> if (dragging) handler(getLeft() + mouseX, getTop() + mouseY) } + onMouseRelease { dragging = false } +} + +fun T.onLeftMouse(handler: T.(mouseX: Float, mouseY: Float) -> Unit) = onMouse(0, handler) +fun T.onRightMouse(handler: T.(mouseX: Float, mouseY: Float) -> Unit) = onMouse(1, handler) +fun T.onMiddleMouse(handler: T.(mouseX: Float, mouseY: Float) -> Unit) = onMouse(2, handler) + +// Elementa has no unmount event, so instead we listen for changes to the children list of all our parents. +fun UIComponent.onRemoved(listener: () -> Unit): () -> Unit { + if (parent == this) { + return {} + } + val parentUnregister = parent.onRemoved(listener) + + val observer = Observer { _, event -> + if (event is ObservableClearEvent<*> || event is ObservableRemoveEvent<*> && event.element.value == this) { + listener() + } + } + parent.children.addObserver(observer) + return { + parent.children.deleteObserver(observer) + parentUnregister() + } +} diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/extensions.kt b/src/main/kotlin/com/replaymod/core/gui/utils/extensions.kt new file mode 100644 index 00000000..314383ff --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/extensions.kt @@ -0,0 +1,25 @@ +package com.replaymod.core.gui.utils + +import com.replaymod.core.gui.common.elementa.UIScrollComponent +import gg.essential.elementa.UIComponent +import gg.essential.elementa.dsl.* + +infix fun T.hiddenChildOf(parent: UIComponent) = (this childOf parent).also { this.hide(true) } + +inline fun UIComponent.parentOfType(): T? = + if (hasParent && parent != this) parent.selfOrParentOfType() else null + +inline fun UIComponent.selfOrParentOfType(): T? { + var component: UIComponent = this + while (component.hasParent && component.parent != component) { + if (component is T) { + return component + } + component = component.parent + } + return null +} + +/** Like [UIScrollComponent.horizontalOffset] but including any animations. */ +val UIScrollComponent.actualHorizontalOffset + get() = children.first().getLeft() - getLeft() diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/focus.kt b/src/main/kotlin/com/replaymod/core/gui/utils/focus.kt new file mode 100644 index 00000000..ec2f8933 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/focus.kt @@ -0,0 +1,81 @@ +package com.replaymod.core.gui.utils + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.effects.Effect +import gg.essential.universal.UKeyboard + +/** Marks components which can receive focus via [enableTabFocusChange]. */ +private class CanReceiveFocus : Effect() + +/** Limits (tab) focus changes to stay within the children of the decorated component. */ +class FocusTrap : Effect() + +/** + * Enables tab-behavior for this component. Specifically this allows the user to switch away from this component to + * instead focus the next component by pressing Tab (with Shift to go backwards) and in turn allows this component to + * receive focus from another tab-enabled component. + * + * Use [FocusTrap] to limit tab switching to specific sub-tree of the component hierarchy. + */ +fun T.enableTabFocusChange() = apply { + enableEffect(CanReceiveFocus()) + onKeyType { _, keyCode -> + if (keyCode == UKeyboard.KEY_TAB) { + val backwards = UKeyboard.isShiftKeyDown() + val nextComponent = parent.findNextFocusableComponent(this, backwards) ?: return@onKeyType + nextComponent.grabWindowFocus() + } + } +} + +/** + * Returns the next focusable component after the given [fromComponent] as returned by a depth-first search of the + * entire component hierarchy (but without actually running the entire search, and wrapping around). + * If this or a parent component has the [FocusTrap] effect, then the search will stay within the sub-tree with the + * respective component at its root. + * + * When [backwards] is `true`, the entire search is performed in reverse, returning the previous focusable component. + * + * When [fromComponent] is given, then this will recursively search up the tree if the next element cannot be found in + * this subtree. If [fromComponent] is `null` (searching a sibling sub-tree of the original sub-tree), then `null` is + * return if the next element cannot be found in this subtree. + * + * If the search is unsuccessful (because there is no focusable component other than [fromComponent]), then `null` is + * returned. + */ +private fun UIComponent.findNextFocusableComponent(fromComponent: UIComponent?, backwards: Boolean): UIComponent? { + fun findNextInRange(range: IntProgression): UIComponent? { + for (index in range) { + val child = children[index] + return if (child.effects.any { it is CanReceiveFocus }) { + child + } else { + child.findNextFocusableComponent(null, backwards) ?: continue + } + } + return null + } + + if (fromComponent != null) { + val fromIndex = children.indexOf(fromComponent) + val higherRange = (fromIndex + 1)..children.lastIndex + val lowerRange = 0 until fromIndex + + // first check siblings in the respective direction + findNextInRange(if (backwards) lowerRange.reversed() else higherRange)?.let { return it } + + // then go one level up and check siblings there, recursively + if (parent != this && effects.none { it is FocusTrap }) { + parent.findNextFocusableComponent(this, backwards)?.let { return it } + } + + // and finally, back on this level, check the other half of siblings + findNextInRange(if (backwards) higherRange.reversed() else lowerRange)?.let { return it } + } else { + // check all children in the respective order + findNextInRange(if (backwards) children.indices.reversed() else children.indices)?.let { return it } + } + + // looped all the way round and still haven't found anything + return null +} diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/state.kt b/src/main/kotlin/com/replaymod/core/gui/utils/state.kt new file mode 100644 index 00000000..1a61b0fe --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/state.kt @@ -0,0 +1,52 @@ +package com.replaymod.core.gui.utils + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State + +fun State.onSetValueAndNow(block: (value: T) -> Unit) = onSetValue(block).also { block(get()) } + +fun UIComponent.pollingState(initialValue: T? = null, getter: () -> T): State { + val state = BasicState(initialValue ?: getter()) + enableEffect(object : Effect() { + override fun animationFrame() { + state.set(getter()) + } + }) + return state +} + +fun T.bindStateTransition( + state: State, + update: T.(done: () -> Unit, oldState: S, newState: S) -> Unit, +) = apply { + state.bindTransition { done, oldState, newState -> update(done, oldState, newState) } +} + +fun State.bindTransition(update: (done: () -> Unit, oldState: S, newState: S) -> Unit) { + var activeState = get() + var pendingState: S? = null + var inProgress = false + + fun onStateChanged(newState: S) { + if (inProgress) { + pendingState = newState + return + } + + if (newState == activeState) { + return + } + + val oldState = activeState + activeState = newState + + inProgress = true + update({ + inProgress = false + pendingState?.also { pendingState = null }?.let(::onStateChanged) + }, oldState, newState) + } + onSetValue(::onStateChanged) +} diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/tooltip.kt b/src/main/kotlin/com/replaymod/core/gui/utils/tooltip.kt new file mode 100644 index 00000000..d3f22dda --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/tooltip.kt @@ -0,0 +1,52 @@ +package com.replaymod.core.gui.utils + +import com.replaymod.core.gui.common.UITooltip +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.MousePositionConstraint +import gg.essential.elementa.dsl.* + +fun T.addTooltip(configure: UITooltip.() -> Unit) = addTooltip(UITooltip().apply(configure)) + +fun T.addTooltip(tooltip: UIComponent) = apply { + tooltip.constrain { + // Slightly right of the cursor but never off-screen + x = (MousePositionConstraint() + 8.pixels) + .coerceAtMost(100.percentOfWindow - basicXConstraint { tooltip.getWidth() }) + // Slightly below the cursor except when there is insufficient space, then slightly above it + y = basicYConstraint { + val mouseY = MousePositionConstraint().getYPosition(it) + val idealY = mouseY + 8 + val height = tooltip.getHeight() + if (idealY + height <= 100.percentOfWindow.getYPosition(it)) { + idealY + } else { + mouseY - 8 - height + } + } + } + + var unregister: (() -> Unit)? = null + + onMouseEnter { + Window.enqueueRenderOperation { + tooltip childOf Window.of(this) + tooltip.setFloating(true) + val unregisterOnRemoved = this.onRemoved { + unregister?.invoke() + unregister = null + } + unregister = { + unregisterOnRemoved.invoke() + Window.enqueueRenderOperation { + tooltip.setFloating(false) + tooltip.hide(true) + } + } + } + } + onMouseLeave { + unregister?.invoke() + unregister = null + } +} diff --git a/src/main/kotlin/com/replaymod/core/utils/extensions.kt b/src/main/kotlin/com/replaymod/core/utils/extensions.kt new file mode 100644 index 00000000..f542d659 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/utils/extensions.kt @@ -0,0 +1,23 @@ +package com.replaymod.core.utils + +import net.minecraft.client.resource.language.I18n +import java.util.* + +fun String.i18n(vararg args: String): String = I18n.translate(this, *args) + +val Optional?.orNull get() = this?.orElse(null) + +fun Pair?.transpose() = this ?: Pair(null, null) + +val org.apache.commons.lang3.tuple.Triple.kt: Triple + get() = Triple(left, middle, right) + +fun Triple.toDouble() = Triple(first.toDouble(), second.toDouble(), third.toDouble()) + +inline fun Iterable.associateNotNull(transform: (T) -> Pair?): Map { + val destination = LinkedHashMap((this as? Collection)?.size ?: 16) + for (element in this) { + destination += transform(element) ?: continue + } + return destination +} From 40d5525aa75e09ec72e1959b69d87653c5763f21 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 3 Sep 2021 23:06:54 +0200 Subject: [PATCH 004/132] Convert most of GuiPathing to Kotlin+Elementa --- .../replay/gui/overlay/GuiReplayOverlay.java | 7 + .../simplepathing/ReplayModSimplePathing.java | 2 +- .../simplepathing/gui/GuiEditKeyframe.java | 2 +- .../gui/GuiKeyframeTimeline.java | 408 ------------------ .../simplepathing/gui/GuiPathing.java | 383 +--------------- .../preview/PathPreviewRenderer.java | 2 +- .../core/gui/common/timeline/UITimeline.kt | 2 + .../replay/gui/overlay/GuiReplayOverlayKt.kt | 8 + .../simplepathing/gui/GuiPathingKt.kt | 297 +++++++++++++ .../simplepathing/gui/KeyframeState.kt | 90 ++++ .../simplepathing/gui/KeyframeType.kt | 40 ++ .../simplepathing/gui/UITimelineKeyframes.kt | 280 ++++++++++++ 12 files changed, 733 insertions(+), 788 deletions(-) delete mode 100644 src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java create mode 100644 src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeType.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt 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 14bef7b1..9c1f22ae 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -3,6 +3,7 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.events.KeyBindingEventCallback; import com.replaymod.core.events.KeyEventCallback; +import com.replaymod.core.gui.common.GuiWindow; import com.replaymod.core.versions.MCVer.Keyboard; import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayModReplay; @@ -31,6 +32,9 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { private final ReplayModReplay mod = ReplayModReplay.instance; + public final GuiReplayOverlayKt kt = new GuiReplayOverlayKt(); + public final GuiWindow guiWindow = new GuiWindow(this, kt.getWindow()); + public final GuiPanel topPanel = new GuiPanel(this) .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5)); public final GuiButton playPauseButton = new GuiButton() { @@ -80,6 +84,9 @@ protected void layout(GuiReplayOverlay container, int width, int height) { pos(statusIndicatorPanel, width / 2, height - 21); width(statusIndicatorPanel, width / 2 - 5); + + pos(guiWindow, 0, 0); + size(guiWindow, width, height); } }); diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index aaa839e8..00377aea 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -91,7 +91,7 @@ public void registerKeyBindings(KeyBindingRegistry registry) { if (guiPathing != null) guiPathing.clearKeyframesButtonPressed(); }, true); keySyncTime = core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { - if (guiPathing != null) guiPathing.syncTimeButtonPressed(); + if (guiPathing != null) guiPathing.kt.syncTimeButtonPressed(); }, true); SettingsRegistry settingsRegistry = core.getSettingsRegistry(); keySyncTime.registerAutoActivationSupport(settingsRegistry.get(Setting.AUTO_SYNC), active -> { diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index a9943528..d8f556cd 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -101,7 +101,7 @@ protected boolean canSave() { long newTime = (timeMin * 60 + timeSec) * 1000 + timeMSec; - if (newTime < 0 || newTime > guiPathing.timeline.getLength()) { + if (newTime < 0 || newTime > guiPathing.kt.getTimeline().getLengthMillis()) { return false; } return newTime == keyframe.getTime() || path.getKeyframe(newTime) == null; diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java deleted file mode 100644 index da17ba14..00000000 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ /dev/null @@ -1,408 +0,0 @@ -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.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 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>=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); - - 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; - Tessellator tessellator = Tessellator.getInstance(); - BufferBuilder buffer = tessellator.getBuffer(); - buffer.begin(GL11.GL_LINE_STRIP, VertexFormats.POSITION_COLOR); - - // 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); - - emitLine(buffer, p1, p2, color); - emitLine(buffer, p2, p3, color); - emitLine(buffer, p3, p4, color); - - //#if MC>=11700 - //$$ RenderSystem.setShader(GameRenderer::getRenderTypeLinesShader); - //#else - GL11.glEnable(GL11.GL_LINE_SMOOTH); - GL11.glDisable(GL11.GL_TEXTURE_2D); - //#endif - pushScissorState(); - setScissorDisabled(); - GL11.glLineWidth(2); - tessellator.draw(); - popScissorState(); - //#if MC<11700 - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glDisable(GL11.GL_LINE_SMOOTH); - //#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); - } - - 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(ReadablePoint position, int button) { - int time = getTimeAt(position.getX(), position.getY()); - Pair pathKeyframePair = getKeyframe(position); - if (pathKeyframePair.getRight() != null) { - SPPath path = pathKeyframePair.getLeft(); - // Clicked on keyframe - long keyframeTime = pathKeyframePair.getRight(); - if (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 = position.getX(); - dragging = true; - } else if (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 (button == 0) { // Left click - setCursorPosition(time); - gui.getMod().setSelected(null, 0); - } else if (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(ReadablePoint position, int button, long timeSinceLastCall) { - if (!dragging) { - if (button == 0) { - // Left click, the user might try to move the cursor by clicking and holding - int time = getTimeAt(position.getX(), position.getY()); - 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(position.getX() - draggingStartX) >= DRAGGING_THRESHOLD) { - actuallyDragging = true; - } - } - if (actuallyDragging) { - if (!gui.loadEntityTracker(() -> mouseDrag(position, button, timeSinceLastCall))) return true; - // Threshold passed - SPTimeline timeline = gui.getMod().getCurrentTimeline(); - Point mouse = new Point(position); - 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(ReadablePoint position, int button) { - 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; - } -} diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index d4031bbd..ee9ad830 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -3,18 +3,14 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Result; import com.replaymod.core.utils.Utils; import com.replaymod.pathing.gui.GuiKeyframeRepository; -import com.replaymod.pathing.player.RealtimeTimelinePlayer; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; -import com.replaymod.render.gui.GuiRenderQueue; -import com.replaymod.render.gui.GuiRenderSettings; import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; @@ -26,53 +22,25 @@ import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.SPTimeline.SPPath; -import com.replaymod.simplepathing.Setting; -import de.johni0702.minecraft.gui.GuiRenderer; -import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.GuiContainer; -import de.johni0702.minecraft.gui.container.GuiPanel; -import de.johni0702.minecraft.gui.container.GuiScreen; -import de.johni0702.minecraft.gui.element.GuiButton; -import de.johni0702.minecraft.gui.element.GuiElement; -import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar; import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; -import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; -import de.johni0702.minecraft.gui.layout.CustomLayout; -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.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; -import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; -import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; -import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint; import net.minecraft.client.resource.language.I18n; import net.minecraft.util.crash.CrashReport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -//#if MC>=11400 -//#else -//$$ import org.lwjgl.input.Keyboard; -//#if MC>=10800 -//$$ import net.minecraftforge.fml.common.Loader; -//#else -//$$ import cpw.mods.fml.common.Loader; -//#endif -//#endif - import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.util.Collections; -import java.util.concurrent.CancellationException; import java.util.function.Consumer; import static com.replaymod.core.utils.Utils.error; -import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER; @@ -82,148 +50,12 @@ public class GuiPathing { private static final Logger logger = LogManager.getLogger(); - public final GuiButton playPauseButton = new GuiButton() { - @Override - public GuiElement getTooltip(RenderInfo renderInfo) { - GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); - if (tooltip != null) { - if (player.isActive()) { - tooltip.setI18nText("replaymod.gui.ingame.menu.pausepath"); - } else if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { - tooltip.setI18nText("replaymod.gui.ingame.menu.playpathfromstart"); - } else { - tooltip.setI18nText("replaymod.gui.ingame.menu.playpath"); - } - } - return tooltip; - } - }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip()); - - public final GuiButton renderButton = new GuiButton().onClick(new Runnable() { - @Override - public void run() { - abortPathPlayback(); - GuiScreen screen = GuiRenderSettings.createBaseScreen(); - new GuiRenderQueue(screen, replayHandler, () -> preparePathsForPlayback(false)) { - @Override - protected void close() { - super.close(); - getMinecraft().openScreen(null); - } - }.open(); - screen.display(); - } - }).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 0) - .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.renderpath")); - - public final GuiButton positionKeyframeButton = new GuiButton() { - @Override - public GuiElement getTooltip(RenderInfo renderInfo) { - GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); - if (tooltip != null) { - String label; - if (getSpriteUV().getY() == 40) { // Add keyframe - if (getSpriteUV().getX() == 0) { // Position - label = "replaymod.gui.ingame.menu.addposkeyframe"; - } else { // Spectator - label = "replaymod.gui.ingame.menu.addspeckeyframe"; - } - } else { // Remove keyframe - if (getSpriteUV().getX() == 0) { // Position - label = "replaymod.gui.ingame.menu.removeposkeyframe"; - } else { // Spectator - label = "replaymod.gui.ingame.menu.removespeckeyframe"; - } - } - tooltip.setText(I18n.translate(label) + " (" + mod.keyPositionKeyframe.getBoundKey() + ")"); - } - return tooltip; - } - }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip()); - - public final GuiButton timeKeyframeButton = new GuiButton() { - @Override - public GuiElement getTooltip(RenderInfo renderInfo) { - GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); - if (tooltip != null) { - String label; - if (getSpriteUV().getY() == 80) { // Add time keyframe - label = "replaymod.gui.ingame.menu.addtimekeyframe"; - } else { // Remove time keyframe - label = "replaymod.gui.ingame.menu.removetimekeyframe"; - } - tooltip.setText(I18n.translate(label) + " (" + mod.keyTimeKeyframe.getBoundKey() + ")"); - } - return tooltip; - } - }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip()); - - public final GuiKeyframeTimeline timeline = new GuiKeyframeTimeline(this){ - @Override - public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - if (player.isActive()) { - setCursorPosition((int) player.getTimePassed()).ensureCursorVisibleWithPadding(); - } - super.draw(renderer, size, renderInfo); - } - }.setSize(Integer.MAX_VALUE, 20).setMarkers(); - - public final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(Integer.MAX_VALUE, 9); - {scrollbar.onValueChanged(new Runnable() { - @Override - public void run() { - timeline.setOffset((int) (scrollbar.getPosition() * timeline.getLength())); - timeline.setZoom(scrollbar.getZoom()); - } - }).setZoom(0.1);} - - public final GuiTimelineTime timelineTime = new GuiTimelineTime() - .setTimeline(timeline); - - public final GuiButton zoomInButton = new GuiButton().setSize(9, 9).onClick(new Runnable() { - @Override - public void run() { - zoomTimeline(2d / 3d); - } - }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 20) - .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin")); - - public final GuiButton zoomOutButton = new GuiButton().setSize(9, 9).onClick(new Runnable() { - @Override - public void run() { - zoomTimeline(3d / 2d); - } - }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 30) - .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout")); - - public final GuiPanel zoomButtonPanel = new GuiPanel() - .setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(2)) - .addElements(null, zoomInButton, zoomOutButton); - - public final GuiPanel timelinePanel = new GuiPanel().setSize(Integer.MAX_VALUE, 40) - .setLayout(new CustomLayout() { - @Override - protected void layout(GuiPanel container, int width, int height) { - pos(zoomButtonPanel, width - width(zoomButtonPanel), 10); - pos(timelineTime, 0, 2); - size(timelineTime, x(zoomButtonPanel), 8); - pos(timeline, 0, y(timelineTime) + height(timelineTime)); - size(timeline, x(zoomButtonPanel) - 2, 20); - pos(scrollbar, 0, y(timeline) + height(timeline) + 1); - size(scrollbar, x(zoomButtonPanel) - 2, 9); - } - }).addElements(null, timeline, timelineTime, scrollbar, zoomButtonPanel); - - public final GuiPanel panel = new GuiPanel() - .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)) - .addElements(new HorizontalLayout.Data(0.5), - playPauseButton, renderButton, positionKeyframeButton, timeKeyframeButton, timelinePanel); - private final ReplayMod core; private final ReplayModSimplePathing mod; private final ReplayHandler replayHandler; public final GuiReplayOverlay overlay; - private final RealtimeTimelinePlayer player; + + public final GuiPathingKt kt; // Whether any error which occured during entity tracker loading has already been shown to the user private boolean errorShown; @@ -236,164 +68,13 @@ public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final this.mod = mod; this.replayHandler = replayHandler; this.overlay = replayHandler.getOverlay(); - this.player = new RealtimeTimelinePlayer(replayHandler); - - timeline.setLength(core.getSettingsRegistry().get(Setting.TIMELINE_LENGTH) * 1000); - - playPauseButton.setSpriteUV(new ReadablePoint() { - @Override - public int getX() { - return 0; - } - - @Override - public int getY() { - return player.isActive() ? 20 : 0; - } - - @Override - public void getLocation(WritablePoint dest) { - dest.setLocation(getX(), getY()); - } - }).onClick(new Runnable() { - @Override - public void run() { - if (player.isActive()) { - player.getFuture().cancel(false); - } else { - boolean ignoreTimeKeyframes = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT); - - Timeline timeline = preparePathsForPlayback(ignoreTimeKeyframes).okOrElse(err -> { - GuiInfoPopup.open(overlay, err); - return null; - }); - if (timeline == null) return; - - Path timePath = new SPTimeline(timeline).getTimePath(); - timePath.setActive(!ignoreTimeKeyframes); - - // 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); - core.printInfoToChat("replaymod.chat.pathstarted"); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - if (future.isCancelled()) { - core.printInfoToChat("replaymod.chat.pathinterrupted"); - } else { - core.printInfoToChat("replaymod.chat.pathfinished"); - } - overlay.setCloseable(true); - } - - @Override - public void onFailure(Throwable t) { - if (!(t instanceof CancellationException)) { - t.printStackTrace(); - } - overlay.setCloseable(true); - } - }); - } - } - }); - - positionKeyframeButton.setSpriteUV(new ReadablePoint() { - @Override - public int getX() { - SPPath keyframePath = mod.getSelectedPath(); - long keyframeTime = mod.getSelectedTime(); - if (keyframePath != SPPath.POSITION) { - // No keyframe or wrong path - keyframeTime = timeline.getCursorPosition(); - keyframePath = mod.getCurrentTimeline().isPositionKeyframe(keyframeTime) ? SPPath.POSITION : null; - } - if (keyframePath != SPPath.POSITION) { - return replayHandler.isCameraView() ? 0 : 40; - } else { - return mod.getCurrentTimeline().isSpectatorKeyframe(keyframeTime) ? 40 : 0; - } - } - - @Override - public int getY() { - SPPath keyframePath = mod.getSelectedPath(); - if (keyframePath != SPPath.POSITION) { - // No keyframe selected but there might be one at exactly the position of the cursor - keyframePath = mod.getCurrentTimeline().isPositionKeyframe(timeline.getCursorPosition()) ? SPPath.POSITION : null; - } - return keyframePath == SPPath.POSITION ? 60 : 40; - } - - @Override - public void getLocation(WritablePoint dest) { - dest.setLocation(getX(), getY()); - } - }).onClick(new Runnable() { - @Override - public void run() { - toggleKeyframe(SPPath.POSITION, false); - } - }); - - timeKeyframeButton.setSpriteUV(new ReadablePoint() { - @Override - public int getX() { - return 0; - } - - @Override - public int getY() { - SPPath keyframePath = mod.getSelectedPath(); - if (keyframePath != SPPath.TIME) { - // No keyframe selected but there might be one at exactly the position of the cursor - keyframePath = mod.getCurrentTimeline().isTimeKeyframe(timeline.getCursorPosition()) ? SPPath.TIME : null; - } - return keyframePath == SPPath.TIME ? 100 : 80; - } - - @Override - public void getLocation(WritablePoint dest) { - dest.setLocation(getX(), getY()); - } - }).onClick(new Runnable() { - @Override - public void run() { - toggleKeyframe(SPPath.TIME, false); - } - }); - - overlay.addElements(null, panel); - overlay.setLayout(new CustomLayout(overlay.getLayout()) { - @Override - protected void layout(GuiReplayOverlay container, int width, int height) { - checkForAutoSync(); - pos(panel, 10, y(overlay.topPanel) + height(overlay.topPanel) + 3); - size(panel, width - 20, 40); - } - }); + this.kt = new GuiPathingKt(this, replayHandler); startLoadingEntityTracker(); } - private void abortPathPlayback() { - if (!player.isActive()) { - return; - } - - ListenableFuture future = player.getFuture(); - if (!future.isDone() && !future.isCancelled()) { - future.cancel(false); - } - // Tear down of the player might only happen the next tick after it was cancelled - player.onTick(); - } - public void keyframeRepoButtonPressed() { - abortPathPlayback(); + kt.abortPathPlayback(); try { GuiKeyframeRepository gui = new GuiKeyframeRepository( mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline()); @@ -429,54 +110,6 @@ public void clearKeyframesButtonPressed() { }); } - private int prevSpeed = -1; - private int prevTime = -1; - private void checkForAutoSync() { - if (!mod.keySyncTime.isAutoActivating()) { - prevSpeed = -1; - prevTime = -1; - return; - } - - int speed = overlay.speedSlider.getValue(); - if (prevSpeed != speed && prevSpeed != -1) { - syncTimeButtonPressed(); - } - prevSpeed = speed; - - int time = replayHandler.getReplaySender().currentTimeStamp(); - if (prevTime != time && prevTime != -1 && !player.isActive()) { - syncTimeButtonPressed(); - } - prevTime = time; - } - - public void syncTimeButtonPressed() { - // Current replay time - int time = replayHandler.getReplaySender().currentTimeStamp(); - // Position of the cursor - int cursor = timeline.getCursorPosition(); - // Get the last time keyframe before the cursor - mod.getCurrentTimeline().getTimePath().getKeyframes().stream() - .filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> { - // Cursor position at the keyframe - int keyframeCursor = (int) keyframe.getTime(); - // Replay time at the keyframe - // This is a keyframe from the time path, so it _should_ always have a time property - int keyframeTime = keyframe.getValue(TimestampProperty.PROPERTY).get(); - // Replay time passed - int timePassed = time - keyframeTime; - // Speed (set to 1 when shift is held) - double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : replayHandler.getOverlay().getSpeedSliderValue(); - // Cursor time passed - int cursorPassed = (int) (timePassed / speed); - // Move cursor to new position - timeline.setCursorPosition(keyframeCursor + cursorPassed).ensureCursorVisibleWithPadding(); - // Deselect keyframe to allow the user to add a new one right away - mod.setSelected(null, 0); - }); - } - public boolean deleteButtonPressed() { if (mod.getSelectedPath() != null) { toggleKeyframe(mod.getSelectedPath(), false); @@ -514,7 +147,7 @@ private void startLoadingEntityTracker() { }).start(); } - private Result preparePathsForPlayback(boolean ignoreTimeKeyframes) { + Result preparePathsForPlayback(boolean ignoreTimeKeyframes) { SPTimeline spTimeline = mod.getCurrentTimeline(); String[] errors = validatePathsForPlayback(spTimeline, ignoreTimeKeyframes); @@ -569,10 +202,6 @@ private String[] validatePathsForPlayback(SPTimeline timeline, boolean ignoreTim return null; } - public void zoomTimeline(double factor) { - scrollbar.setZoom(scrollbar.getZoom() * factor); - } - public boolean loadEntityTracker(Runnable thenRun) { if (entityTracker == null && !errorShown) { LOGGER.debug("Entity tracker not yet loaded, delaying..."); @@ -620,7 +249,7 @@ public void toggleKeyframe(SPPath path, boolean neverSpectator) { LOGGER.debug("Updating keyframe on path {}" + path); if (!loadEntityTracker(() -> toggleKeyframe(path, neverSpectator))) return; - int time = timeline.getCursorPosition(); + int time = (int) kt.getTimeline().getCursor().getPositionMillis(); SPTimeline timeline = mod.getCurrentTimeline(); if (timeline.getPositionPath().getKeyframes().isEmpty() && diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java index 50782934..e43fe0ca 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java @@ -180,7 +180,7 @@ private void renderCameraPath(MatrixStack matrixStack) { GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glEnable(GL11.GL_DEPTH_TEST); - int time = guiPathing.timeline.getCursorPosition(); + long time = guiPathing.kt.getTimeline().getCursor().getPositionMillis(); Optional entityId = path.getValue(SpectatorProperty.PROPERTY, time); if (entityId.isPresent()) { // Spectating an entity diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt index 0854772c..0dc1c42f 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimeline.kt @@ -137,11 +137,13 @@ class UITimeline : UIContainer() { } override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { + /** Can only visit the direct parent, but that's of no use to us and generates misleading errors. when (type) { ConstraintType.X -> visitor.visitParent(ConstraintType.X) ConstraintType.WIDTH -> visitor.visitParent(ConstraintType.WIDTH) else -> throw IllegalArgumentException(type.prettyName) } + */ } } diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt new file mode 100644 index 00000000..255b10ec --- /dev/null +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt @@ -0,0 +1,8 @@ +package com.replaymod.replay.gui.overlay + +import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.components.Window + +class GuiReplayOverlayKt { + val window = Window(ElementaVersion.V1, 60) +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt new file mode 100644 index 00000000..9a0152e0 --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -0,0 +1,297 @@ +package com.replaymod.simplepathing.gui + +import com.google.common.util.concurrent.FutureCallback +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.replaymod.core.ReplayMod +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.common.UITexture +import com.replaymod.core.gui.common.scrollbar.UITexturedScrollBar +import com.replaymod.core.gui.common.timeline.UITimeline +import com.replaymod.core.gui.common.timeline.UITimelineTime +import com.replaymod.core.gui.utils.* +import com.replaymod.core.utils.* +import com.replaymod.core.versions.MCVer.Keyboard +import com.replaymod.pathing.player.RealtimeTimelinePlayer +import com.replaymod.render.gui.GuiRenderQueue +import com.replaymod.render.gui.GuiRenderSettings +import com.replaymod.replay.ReplayHandler +import com.replaymod.simplepathing.SPTimeline +import com.replaymod.simplepathing.SPTimeline.SPPath +import com.replaymod.simplepathing.Setting +import de.johni0702.minecraft.gui.popup.GuiInfoPopup +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.State +import java.util.concurrent.CancellationException +import kotlin.time.Duration + +class GuiPathingKt( + val java: GuiPathing, + val replayHandler: ReplayHandler, +) { + private val state = KeyframeState(java.mod, this) + private val overlay = replayHandler.overlay + private val mod = java.mod + private val core = mod.core + private val window = overlay.kt.window + private val player = RealtimeTimelinePlayer(replayHandler) + + private val isCtrlDown = window.pollingState { Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) } + private val isShiftDown = window.pollingState { Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) } + + private val playing = window.pollingState { player.isActive } + + init { + window.onAnimationFrame { state.update() } + } + + private val secondRow by UIContainer().constrain { + x = CenterConstraint() + y = 30.pixels /* topPanel.bottom */ + 13.pixels + width = 100.percent - 20.pixels + height = 20.pixels + } childOf window + + private val playPauseButton by UIButton().constrain { + width = 20.pixels + height = 20.pixels + }.texture(ReplayMod.TEXTURE, playing.map { + UITexture.TextureData.ofSize(0, if (it) 20 else 0, 20, 20) + }).addTooltip { + addLine { + bindText(playing.zip(isCtrlDown).map { (playing, isCtrlDown) -> + when { + playing -> "replaymod.gui.ingame.menu.pausepath" + isCtrlDown -> "replaymod.gui.ingame.menu.playpathfromstart" + else -> "replaymod.gui.ingame.menu.playpath" + }.i18n() + }) + } + }.onMouseClick { + if (player.isActive) { + player.future.cancel(false) + } else { + val ignoreTimeKeyframes = isShiftDown.get() + + val timeline = java.preparePathsForPlayback(ignoreTimeKeyframes).okOrElse { err -> + GuiInfoPopup.open(overlay, *err) + null + } ?: return@onMouseClick + + val timePath = SPTimeline(timeline).timePath + timePath.isActive = !ignoreTimeKeyframes + + // Start from cursor time unless the control key is pressed (then start from beginning) + val startTime = if (isCtrlDown.get()) Duration.ZERO else this@GuiPathingKt.timeline.cursor.position.get() + val future: ListenableFuture = player.start(timeline, startTime.inWholeMilliseconds) + overlay.isCloseable = false + overlay.isMouseVisible = true + core.printInfoToChat("replaymod.chat.pathstarted") + Futures.addCallback(future, object : FutureCallback { + override fun onSuccess(result: Void?) { + if (future.isCancelled) { + core.printInfoToChat("replaymod.chat.pathinterrupted") + } else { + core.printInfoToChat("replaymod.chat.pathfinished") + } + overlay.isCloseable = true + } + + override fun onFailure(t: Throwable) { + if (t !is CancellationException) { + t.printStackTrace() + } + overlay.isCloseable = true + } + }) + } + } childOf secondRow + + private val renderButton by UIButton().constrain { + x = SiblingConstraint(5f) + width = 20.pixels + height = 20.pixels + }.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(40, 0, 20, 20)) { + }.addTooltip { + addLine("replaymod.gui.ingame.menu.renderpath".i18n()) + }.onMouseClick { + abortPathPlayback() + val screen = GuiRenderSettings.createBaseScreen() + object : GuiRenderQueue(screen, replayHandler, { java.preparePathsForPlayback(false) }) { + override fun close() { + super.close() + minecraft.openScreen(null) + } + }.open() + screen.display() + } childOf secondRow + + private data class PositionButtonType( + val type: KeyframeType = KeyframeType.POSITION, + val remove: Boolean = false, + ) + + private val positionKeyframeButtonType = window.pollingState(PositionButtonType()) { + val time = state.selectedPositionKeyframes.get().keys.firstOrNull() + ?: timeline.cursor.position.get() + val keyframe = state.positionKeyframes.get()[time] + PositionButtonType(when { + keyframe?.entityId != null -> KeyframeType.SPECTATOR + keyframe == null && !replayHandler.isCameraView -> KeyframeType.SPECTATOR + else -> KeyframeType.POSITION + }, keyframe != null) + } + + private val positionKeyframeButton by UIButton().constrain { + x = SiblingConstraint(5f) + width = 20.pixels + height = 20.pixels + }.texture(ReplayMod.TEXTURE, positionKeyframeButtonType.map { (type, remove) -> + type.buttonIcon.offset(0, if (remove) 20 else 0) + }).addTooltip { + addLine { + bindText(positionKeyframeButtonType.map { (type, remove) -> + type.tooltip(!remove).i18n() + " (" + mod.keyPositionKeyframe.boundKey + ")" + }) + } + }.onMouseClick { + java.toggleKeyframe(SPPath.POSITION, false) + } childOf secondRow + + private val timeKeyframePresent: State = window.pollingState(false) { + val time = state.selectedTimeKeyframes.get().keys.firstOrNull() + ?: timeline.cursor.position.get() + val keyframe = state.timeKeyframes.get()[time] + keyframe != null + } + + private val timeKeyframeButton by UIButton().constrain { + x = SiblingConstraint(5f) + width = 20.pixels + height = 20.pixels + }.texture(ReplayMod.TEXTURE, timeKeyframePresent.map { present -> + KeyframeType.TIME.buttonIcon.offset(0, if (present) 20 else 0) + }).addTooltip { + addLine { + bindText(timeKeyframePresent.map { present -> + KeyframeType.TIME.tooltip(!present).i18n() + " (" + mod.keyTimeKeyframe.boundKey + ")" + }) + } + }.onMouseClick { + java.toggleKeyframe(SPPath.TIME, false) + } childOf secondRow + + val timeline by UITimeline().constrain { + x = SiblingConstraint(5f) + width = FillConstraint(false) - basicWidthConstraint { zoomButtonPanel.getWidth() } - 2.pixels + height = 20.pixels + }.apply { + enableIndicators() + enableZooming() + + zoom.set(0.1f) + length.set(Duration.seconds(core.settingsRegistry.get(Setting.TIMELINE_LENGTH))) + + content.insertChildBefore(UITimelineKeyframes(state, overlay.timeline), cursor) + }.onLeftMouse { mouseX, _ -> + val time = getTimeAt(mouseX) + cursor.position.set(time) + mod.setSelected(null, 0) + }.onLeftClick { + it.stopImmediatePropagation() + }.onAnimationFrame { + if (player.isActive) { + cursor.position.set(Duration.milliseconds(player.timePassed)) + cursor.ensureVisibleWithPadding() + } + } childOf secondRow + + private val scrollbar by UITexturedScrollBar().constrain { + x = 0.pixels boundTo timeline + y = SiblingConstraint(1f) boundTo timeline + width = CopyConstraintFloat() boundTo timeline + height = 9.pixels + }.apply { + timeline.content.setHorizontalScrollBarComponent(grip) + } childOf window + + private val timelineTime by UITimelineTime(timeline).constrain { + x = 0.pixels boundTo timeline + y = SiblingConstraint(alignOpposite = true) boundTo timeline + width = CopyConstraintFloat() boundTo timeline + height = 8.pixels + } childOf window + + private val zoomButtonPanel by UIContainer().constrain { + x = 0.pixels(alignOpposite = true) + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf secondRow + + private val zoomInButton by UIButton().constrain { + width = 9.pixels + height = 9.pixels + }.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(40, 20, 9, 9)) { + }.addTooltip { + addLine("replaymod.gui.ingame.menu.zoomin".i18n()) + }.onMouseClick { + timeline.zoom.set { it * 2 / 3 } + } childOf zoomButtonPanel + + private val zoomOutButton by UIButton().constrain { + y = SiblingConstraint(2f) + width = 9.pixels + height = 9.pixels + }.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(40, 30, 9, 9)) { + }.addTooltip { + addLine("replaymod.gui.ingame.menu.zoomout".i18n()) + }.onMouseClick { + timeline.zoom.set { it * 3 / 2 } + } childOf zoomButtonPanel + + init { + val speedValue = window.pollingState { overlay.speedSlider.value } + val replayTime = window.pollingState { replayHandler.replaySender.currentTimeStamp() } + speedValue.zip(replayTime).onSetValue { + if (mod.keySyncTime.isAutoActivating && !player.isActive) { + syncTimeButtonPressed() + } + } + } + + fun abortPathPlayback() { + if (!player.isActive) { + return + } + val future = player.future + if (!future.isDone && !future.isCancelled) { + future.cancel(false) + } + // Tear down of the player might only happen the next tick after it was cancelled + player.onTick() + } + + fun syncTimeButtonPressed() { + // Current replay time + val currentReplayTime = Duration.milliseconds(replayHandler.replaySender.currentTimeStamp()) + // Position of the cursor + val cursor = timeline.cursor.position.get() + // Get the last time keyframe before the cursor + val (keyframeCursor, keyframe) = state.timeKeyframes.get().entries.findLast { (time, _) -> time <= cursor } + ?: return + val keyframeReplayTime = keyframe.replayTime + // Replay time passed + val replayTimePassed = currentReplayTime - keyframeReplayTime + // Speed (set to 1 when shift is held) + val speed = if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) 1.0 else overlay.speedSliderValue + // Cursor time passed + val cursorPassed = replayTimePassed / speed + // Move cursor to new position + timeline.cursor.position.set(keyframeCursor + cursorPassed) + timeline.cursor.ensureVisibleWithPadding() + // Deselect keyframe to allow the user to add a new one right away + mod.setSelected(null, 0) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt new file mode 100644 index 00000000..8874c357 --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt @@ -0,0 +1,90 @@ +package com.replaymod.simplepathing.gui + +import com.replaymod.core.gui.common.LazyState +import com.replaymod.core.utils.associateNotNull +import com.replaymod.core.utils.kt +import com.replaymod.core.utils.orNull +import com.replaymod.core.utils.toDouble +import com.replaymod.pathing.properties.CameraProperties +import com.replaymod.pathing.properties.SpectatorProperty +import com.replaymod.pathing.properties.TimestampProperty +import com.replaymod.replaystudio.pathing.change.Change +import com.replaymod.simplepathing.ReplayModSimplePathing +import com.replaymod.simplepathing.SPTimeline +import kotlin.time.Duration + +class KeyframeState( + val mod: ReplayModSimplePathing, + val gui: GuiPathingKt, +) { + private var lastTimeline: SPTimeline? = null + private var lastChange: Change? = null + + val selection = LazyState(Selection(emptySet(), emptySet())) + val timeKeyframes = LazyState(emptyMap()) + val positionKeyframes = LazyState(emptyMap()) + + val selectionPositionKeyframes = selection.map { it.positionKeyframes } + val selectionTimeKeyframes = selection.map { it.timeKeyframes } + + val selectedTimeKeyframes = timeKeyframes.zip(selectionTimeKeyframes) + .map { (keyframes, selection) -> keyframes.filterKeys { it in selection } } + val selectedPositionKeyframes = positionKeyframes.zip(selectionPositionKeyframes) + .map { (keyframes, selection) -> keyframes.filterKeys { it in selection } } + + fun update() { + val selectedTime = setOf(Duration.milliseconds(mod.selectedTime)) + + selection.set(Selection( + if (mod.selectedPath == SPTimeline.SPPath.TIME) selectedTime else emptySet(), + if (mod.selectedPath == SPTimeline.SPPath.POSITION) selectedTime else emptySet(), + )) + + val timeline = mod.currentTimeline + val change = timeline.timeline.peekUndoStack() + if (timeline != lastTimeline || change != lastChange) { + lastTimeline = timeline + lastChange = change + + timeKeyframes.set(timeline.timePath.keyframes.associateNotNull { + val replayTime = it.getValue(TimestampProperty.PROPERTY).orNull ?: return@associateNotNull null + Duration.milliseconds(it.time) to TimeKeyframe(Duration.milliseconds(replayTime)) + }) + positionKeyframes.set(timeline.positionPath.keyframes.associateNotNull { + val position = it.getValue(CameraProperties.POSITION).orNull ?: return@associateNotNull null + val rotation = it.getValue(CameraProperties.ROTATION).orNull ?: return@associateNotNull null + val entityId = it.getValue(SpectatorProperty.PROPERTY).orNull + Duration.milliseconds(it.time) to PositionKeyframe(position.kt, rotation.kt.toDouble(), entityId) + }) + } + + selection.flush() + timeKeyframes.flush() + positionKeyframes.flush() + } + + fun refreshKeyframes() { + lastTimeline = null + lastChange = null + } + + data class TimeKeyframe( + val replayTime: Duration, + ) + + data class PositionKeyframe( + val position: Triple, + val rotation: Triple, + val entityId: Int?, + ) + + data class Selection( + val timeKeyframes: Set, + val positionKeyframes: Set, + ) { + operator fun get(path: SPTimeline.SPPath) = when (path) { + SPTimeline.SPPath.TIME -> timeKeyframes + SPTimeline.SPPath.POSITION -> positionKeyframes + } + } +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeType.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeType.kt new file mode 100644 index 00000000..12e1ca8e --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeType.kt @@ -0,0 +1,40 @@ +package com.replaymod.simplepathing.gui + +import com.replaymod.core.gui.common.UITexture +import com.replaymod.simplepathing.SPTimeline + +private val buttonIcon = UITexture.TextureData.ofSize(20, 20) +private val timelineIcon = UITexture.TextureData.ofSize(5, 5) + +enum class KeyframeType( + val path: SPTimeline.SPPath, + val buttonIcon: UITexture.TextureData, + val timelineIcon: UITexture.TextureData, + val tooltipAdd: String, + val tooltipRemove: String, +) { + TIME( + SPTimeline.SPPath.TIME, + buttonIcon.offset(0, 80), + timelineIcon.offset(74, 25), + "replaymod.gui.ingame.menu.addtimekeyframe", + "replaymod.gui.ingame.menu.removetimekeyframe", + ), + POSITION( + SPTimeline.SPPath.POSITION, + buttonIcon.offset(0, 40), + timelineIcon.offset(74, 20), + "replaymod.gui.ingame.menu.addposkeyframe", + "replaymod.gui.ingame.menu.removeposkeyframe", + ), + SPECTATOR( + SPTimeline.SPPath.POSITION, + buttonIcon.offset(40, 40), + timelineIcon.offset(74, 30), + "replaymod.gui.ingame.menu.addspeckeyframe", + "replaymod.gui.ingame.menu.removespeckeyframe", + ), + ; + + fun tooltip(add: Boolean) = if (add) tooltipAdd else tooltipRemove +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt new file mode 100644 index 00000000..e65e8b3e --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt @@ -0,0 +1,280 @@ +package com.replaymod.simplepathing.gui + +import com.replaymod.core.ReplayMod +import com.replaymod.core.gui.common.UITexture +import com.replaymod.core.gui.common.timeline.UITimeline +import com.replaymod.core.gui.utils.* +import com.replaymod.core.utils.* +import com.replaymod.core.versions.MCVer +import com.replaymod.replay.ReplayModReplay +import com.replaymod.replay.gui.overlay.GuiMarkerTimeline +import com.replaymod.replaystudio.pathing.change.Change +import com.replaymod.simplepathing.SPTimeline +import de.johni0702.minecraft.gui.utils.lwjgl.Point +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector2f +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.effects.ScissorEffect +import gg.essential.universal.UMatrixStack +import net.minecraft.client.render.Tessellator +import net.minecraft.client.render.VertexFormats +import org.lwjgl.opengl.GL11 +import java.awt.Color +import kotlin.math.absoluteValue +import kotlin.time.Duration + +class UITimelineKeyframes( + private val state: KeyframeState, + replayTimeline: GuiMarkerTimeline, +) : UIContainer() { + private val rows = listOf(SPTimeline.SPPath.POSITION, SPTimeline.SPPath.TIME).map { path -> + Row(state, path).constrain { + y = SiblingConstraint() + width = ChildBasedRangeConstraint() + height = 5.pixels + } childOf this + } + + init { + constrain { + y = 1.pixels + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } + + state.timeKeyframes.zip(state.selectionTimeKeyframes).onSetValueAndNow { (keyframes, selection) -> + val row = rows[1] + row.clearChildren() + + var prevTime = Duration.ZERO + var prevReplayTime = Duration.ZERO + for ((time, keyframe) in keyframes) { + val replayTime = keyframe.replayTime + val lineEffect = LineToReplayTimelineEffect(replayTimeline, replayTime) + UIKeyframe(time, KeyframeType.TIME, time in selection) effect lineEffect childOf row + + // Draw red quads on time path segments that would require time going backwards + if (prevReplayTime > replayTime) { + UISegment(prevTime, time, Color.RED) childOf row + } + prevTime = time + prevReplayTime = replayTime + } + } + + state.positionKeyframes.zip(state.selectionPositionKeyframes).onSetValueAndNow { (keyframes, selection) -> + val row = rows[0] + row.clearChildren() + + var prevTime = Duration.ZERO + var prevType: KeyframeType? = null + for ((time, keyframe) in keyframes) { + val type = if (keyframe.entityId != null) KeyframeType.SPECTATOR else KeyframeType.POSITION + UIKeyframe(time, type, time in selection) childOf row + + // Draw colored quads on spectator path segments + if (type == KeyframeType.SPECTATOR && prevType == type) { + UISegment(prevTime, time, Color(0x00, 0x88, 0xff)) childOf row + } + prevTime = time + prevType = type + } + } + } + + class Row(state: KeyframeState, path: SPTimeline.SPPath) : UIContainer() { + var dragging: Dragging? = null + + init { + onRightMouse { mouseX, _ -> + val time = parentOfType()!!.getTimeAt(mouseX) + val pathObj = state.mod.currentTimeline.getPath(path) + for (property in pathObj.keyframes.flatMapTo(mutableSetOf()) { it.properties }) { + val value = pathObj.getValue(property, time.inWholeMilliseconds).orNull ?: continue + property.applyToGame(value, ReplayModReplay.instance.replayHandler) + } + } + onRightClick { + it.stopImmediatePropagation() + } + + onMouseDrag { relativeMouseX, _, _ -> + val dragging = dragging ?: return@onMouseDrag + val spTimeline = state.mod.currentTimeline ?: return@onMouseDrag + + val diff = (getLeft() + relativeMouseX) - dragging.mouseX + if (!dragging.passedThreshold) { + if (diff.absoluteValue < 5) { + return@onMouseDrag + } else { + dragging.passedThreshold = true + } + } + + // First undo any previous changes + dragging.change?.undo(spTimeline.timeline) + + // Compute new time + val oldTime = dragging.keyframe + var newTime = (oldTime + parentOfType()!!.unit.get() * diff.toDouble()) + .coerceAtLeast(Duration.ZERO) + + // If there already is a keyframe at the target time, then increase the time by one until there is none + while (spTimeline.getKeyframe(path, newTime.inWholeMilliseconds) != null) { + newTime += Duration.Companion.milliseconds(1) + } + + // Move keyframe to new position and + // store change for later undoing / pushing to history + dragging.change = + spTimeline.moveKeyframe(path, oldTime.inWholeMilliseconds, newTime.inWholeMilliseconds) + state.refreshKeyframes() + + // Selected keyframe has been moved + state.mod.setSelected(path, newTime.inWholeMilliseconds) + } + + onMouseRelease { + val dragging = dragging.also { dragging = null } ?: return@onMouseRelease + val change = dragging.change ?: return@onMouseRelease + state.mod.currentTimeline.timeline.pushChange(change) + } + } + + data class Dragging( + val keyframe: Duration, + val mouseX: Float, + var passedThreshold: Boolean = false, + /** + * 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. + */ + var change: Change? = null, + ) + } + + inner class UIKeyframe( + val time: Duration, + val type: KeyframeType, + val selected: Boolean, + ) : UIContainer() { + val icon by UITexture(ReplayMod.TEXTURE, type.timelineIcon.offset(if (selected) 5 else 0, 0)).constrain { + width = 100.percent + height = 100.percent + } childOf this + + init { + constrain { + x = UITimeline.Constraint(time) - (2.5).pixels + width = 5.pixels + height = 5.pixels + } + + onLeftClick(1) { event -> + event.stopImmediatePropagation() + state.mod.setSelected(type.path, time.inWholeMilliseconds) + parentOfType()?.dragging = Row.Dragging(time, event.absoluteX) + } + onLeftClick(2) { event -> + event.stopImmediatePropagation() + state.gui.java.openEditKeyframePopup(type.path, time.inWholeMilliseconds) + } + onRightClick { event -> + event.stopImmediatePropagation() + val keyframe = state.mod.currentTimeline.getKeyframe(type.path, time.inWholeMilliseconds) + ?: return@onRightClick + for (property in keyframe.properties) { + val value = keyframe.getValue(property).orNull ?: continue + property.applyToGame(value, ReplayModReplay.instance.replayHandler) + } + } + } + } + + class UISegment(left: Duration, right: Duration, color: Color) : UIBlock(color) { + init { + constrain { + val segmentWidth = UITimeline.Constraint(right) - UITimeline.Constraint(left) + val actualWidth = basicXConstraint { it.getWidth() } + x = UITimeline.Constraint(left) + (segmentWidth - actualWidth) / 2 + y = CenterConstraint() + width = (UITimeline.Constraint(right - left) - 5.pixels).coerceAtLeast(1.pixel) + height = 3.pixel + } + } + } + + class LineToReplayTimelineEffect( + private val replayTimeline: GuiMarkerTimeline, + private val replayTime: Duration, + ) : Effect() { + private val scissorEffect by lazy { ScissorEffect(Window.of(boundComponent), scissorIntersection = false) } + + override fun afterDraw(matrixStack: UMatrixStack) { + val keyframeTimeline = boundComponent.parentOfType() + ?: throw IllegalStateException("$boundComponent needs to be the child of a timeline") + val replayTimelineSize = replayTimeline.lastSize ?: return + + // Determine absolute positions for replay timeline + val replayTimelinePos = Point(0, 0) + replayTimeline.container.convertFor(replayTimeline, replayTimelinePos) + replayTimelinePos.setLocation(-replayTimelinePos.x, -replayTimelinePos.y) + + val replayTimelineLeft = replayTimelinePos.x + val replayTimelineRight = replayTimelinePos.x + replayTimelineSize.width + val replayTimelineTop = replayTimelinePos.y + val replayTimelineBottom = replayTimelinePos.y + replayTimelineSize.height + val replayTimelineWidth = + replayTimelineRight - replayTimelineLeft - BORDER_LEFT - BORDER_RIGHT + + val positionXReplayTimeline: Float = + BORDER_LEFT + replayTime.inWholeMilliseconds / replayTimeline.length.toFloat() * replayTimelineWidth + val keyframeX = boundComponent.getLeft() + boundComponent.getWidth() / 2 + + val color = -0xffff01 + val tessellator = Tessellator.getInstance() + val buffer = tessellator.buffer + buffer.begin(GL11.GL_LINE_STRIP, VertexFormats.POSITION_COLOR) + + // Start just below the top border of the replay timeline + val p1 = Vector2f(replayTimelineLeft + positionXReplayTimeline, (replayTimelineTop + BORDER_TOP).toFloat()) + // Draw vertically over the replay timeline, including its bottom border + val p2 = Vector2f(replayTimelineLeft + positionXReplayTimeline, replayTimelineBottom.toFloat()) + // Now for the important part: connecting to the keyframe timeline + val p3 = Vector2f(keyframeX, keyframeTimeline.getTop()) + // And finally another vertical bit (the timeline is already crammed enough, so only the border) + val p4 = Vector2f(keyframeX, keyframeTimeline.content.getTop()) + + MCVer.emitLine(buffer, p1, p2, color) + MCVer.emitLine(buffer, p2, p3, color) + MCVer.emitLine(buffer, p3, p4, color) + + //#if MC>=11700 + //$$ RenderSystem.setShader(GameRenderer::getRenderTypeLinesShader); + //#else + GL11.glEnable(GL11.GL_LINE_SMOOTH) + GL11.glDisable(GL11.GL_TEXTURE_2D) + //#endif + GL11.glLineWidth(2f) + + scissorEffect.beforeDraw(matrixStack) + tessellator.draw() + scissorEffect.afterDraw(matrixStack) + + //#if MC<11700 + GL11.glEnable(GL11.GL_TEXTURE_2D) + GL11.glDisable(GL11.GL_LINE_SMOOTH) + //#endif + } + + companion object { + private const val BORDER_LEFT = 4 + private const val BORDER_RIGHT = 4 + private const val BORDER_TOP = 4 + } + } +} From 4ec33c5d7969b48668cbc0fd90db7eeebd697a14 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 4 Sep 2021 08:51:22 +0200 Subject: [PATCH 005/132] Redesign hotkey buttons (closes #469) Also using Kotlin+Elementa instead of jGui now. The new button is on the right side, so the status indicators have temporarily been moved to the far left. Once converted to Elementa, they can be positioned properly. --- .../com/replaymod/extras/HotkeyButtons.java | 127 ----------------- .../com/replaymod/extras/ReplayModExtras.java | 3 +- .../replay/gui/overlay/GuiReplayOverlay.java | 4 +- .../replay/gui/overlay/GuiReplayOverlayKt.kt | 18 +++ .../overlay/panels/UIHotkeyButtonsPanel.kt | 132 ++++++++++++++++++ .../gui/overlay/panels/UIToggleablePanel.kt | 56 ++++++++ 6 files changed, 209 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/com/replaymod/extras/HotkeyButtons.java create mode 100644 src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIHotkeyButtonsPanel.kt create mode 100644 src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIToggleablePanel.kt diff --git a/src/main/java/com/replaymod/extras/HotkeyButtons.java b/src/main/java/com/replaymod/extras/HotkeyButtons.java deleted file mode 100644 index 41829207..00000000 --- a/src/main/java/com/replaymod/extras/HotkeyButtons.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.replaymod.extras; - -import com.replaymod.core.KeyBindingRegistry; -import com.replaymod.core.ReplayMod; -import com.replaymod.replay.events.ReplayOpenedCallback; -import com.replaymod.replay.gui.overlay.GuiReplayOverlay; -import de.johni0702.minecraft.gui.GuiRenderer; -import de.johni0702.minecraft.gui.RenderInfo; -import de.johni0702.minecraft.gui.container.GuiPanel; -import de.johni0702.minecraft.gui.element.GuiButton; -import de.johni0702.minecraft.gui.element.GuiElement; -import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiTooltip; -import de.johni0702.minecraft.gui.layout.CustomLayout; -import de.johni0702.minecraft.gui.layout.GridLayout; -import de.johni0702.minecraft.gui.layout.LayoutData; -import de.johni0702.minecraft.gui.utils.EventRegistrations; -import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.resource.language.I18n; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Map; - -public class HotkeyButtons extends EventRegistrations implements Extra { - private ReplayMod mod; - - @Override - public void register(ReplayMod mod) { - this.mod = mod; - - register(); - } - - { on(ReplayOpenedCallback.EVENT, replayHandler -> new Gui(mod, replayHandler.getOverlay())); } - public static final class Gui { - private final GuiButton toggleButton; - private final GridLayout panelLayout; - private final GuiPanel panel; - - private boolean open; - - public Gui(ReplayMod mod, GuiReplayOverlay overlay) { - toggleButton = new GuiButton(overlay).setSize(20, 20) - .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(0, 120) - .onClick(new Runnable() { - @Override - public void run() { - open = !open; - } - }); - - panel = new GuiPanel(overlay) { - @Override - public Collection getChildren() { - return open ? super.getChildren() : Collections.emptyList(); - } - - @Override - public Map getElements() { - return open ? super.getElements() : Collections.emptyMap(); - } - }.setLayout(panelLayout = new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(1)); - - final KeyBindingRegistry keyBindingRegistry = mod.getKeyBindingRegistry(); - keyBindingRegistry.getBindings().values().stream() - .sorted(Comparator.comparing(it -> I18n.translate(it.name))) - .forEachOrdered(keyBinding -> { - GuiButton button = new GuiButton(){ - @Override - public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - // There doesn't seem to be an KeyBindingUpdate event, so we'll just update it every time - setLabel(keyBinding.isBound() ? keyBinding.getBoundKey() : ""); - - if (keyBinding.supportsAutoActivation()) { - setTooltip(new GuiTooltip().setText(new String[]{ - I18n.translate("replaymod.gui.ingame.autoactivating"), - I18n.translate("replaymod.gui.ingame.autoactivating." - + (keyBinding.isAutoActivating() ? "disable" : "enable")), - })); - setLabelColor(keyBinding.isAutoActivating() ? 0x00ff00 : 0xe0e0e0); - } - - super.draw(renderer, size, renderInfo); - } - }.onClick(() -> { - if (keyBinding.supportsAutoActivation() && Screen.hasControlDown()) { - keyBinding.setAutoActivating(!keyBinding.isAutoActivating()); - } else { - keyBinding.trigger(); - } - }); - GuiLabel label = new GuiLabel().setI18nText(keyBinding.name); - panel.addElements(null, new GuiPanel().setLayout(new CustomLayout() { - @Override - protected void layout(GuiPanel container, int width, int height) { - width(button, Math.max(10 /* consistent min width */, width(button)) + 10 /* padding */); - height(button, 20); - - int textWidth = width(label); - - x(label, width(button) + 4); - width(label, width - x(label)); - - if (textWidth > width - x(label)) { - height(label, height(label) * 2); // split over two lines - } - y(label, (height - height(label)) / 2); - } - }).addElements(null, button, label).setSize(150, 20)); - }); - - overlay.setLayout(new CustomLayout(overlay.getLayout()) { - @Override - protected void layout(GuiReplayOverlay container, int width, int height) { - panelLayout.setColumns(Math.max(1, (width - 10) / 155)); - size(panel, panel.getMinSize()); - - pos(toggleButton, 5, height - 25); - pos(panel, 5, y(toggleButton) - 5 - height(panel)); - } - }); - } - } -} diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java index add19778..da993a1e 100644 --- a/src/main/java/com/replaymod/extras/ReplayModExtras.java +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -23,8 +23,7 @@ public class ReplayModExtras implements Module { PlayerOverview.class, YoutubeUpload.class, FullBrightness.class, - QuickMode.class, - HotkeyButtons.class + QuickMode.class ); private final Map, Extra> instances = new HashMap<>(); 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 9c1f22ae..7a91d189 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -61,7 +61,7 @@ public GuiElement getTooltip(RenderInfo renderInfo) { * when they're active. */ public final GuiPanel statusIndicatorPanel = new GuiPanel(this).setSize(100, 16) - .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(5)); + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5)); private final EventHandler eventHandler = new EventHandler(); private boolean hidden; @@ -82,7 +82,7 @@ protected void layout(GuiReplayOverlay container, int width, int height) { pos(topPanel, 10, 10); size(topPanel, width - 20, 20); - pos(statusIndicatorPanel, width / 2, height - 21); + pos(statusIndicatorPanel, 5, height - 21); width(statusIndicatorPanel, width / 2 - 5); pos(guiWindow, 0, 0); diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt index 255b10ec..471c0f46 100644 --- a/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt @@ -1,8 +1,26 @@ package com.replaymod.replay.gui.overlay +import com.replaymod.core.gui.utils.hiddenChildOf +import com.replaymod.replay.gui.overlay.panels.UIHotkeyButtonsPanel import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.dsl.* class GuiReplayOverlayKt { val window = Window(ElementaVersion.V1, 60) + + val bottomRightPanel by UIContainer().constrain { + x = 6.pixels(alignOpposite = true) + y = 6.pixels(alignOpposite = true) + // Children will be on top of each other + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf window + + val hotkeyButtonsPanel by UIHotkeyButtonsPanel().apply { + toggleButton childOf bottomRightPanel + } hiddenChildOf window } diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIHotkeyButtonsPanel.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIHotkeyButtonsPanel.kt new file mode 100644 index 00000000..29c210c7 --- /dev/null +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIHotkeyButtonsPanel.kt @@ -0,0 +1,132 @@ +package com.replaymod.replay.gui.overlay.panels + +import com.replaymod.core.KeyBindingRegistry +import com.replaymod.core.ReplayMod +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.common.scrollbar.UIFlatScrollBar +import com.replaymod.core.gui.common.UITexture +import com.replaymod.core.gui.common.elementa.UIScrollComponent +import com.replaymod.core.gui.utils.addTooltip +import com.replaymod.core.gui.utils.pollingState +import com.replaymod.core.utils.i18n +import com.replaymod.core.utils.* +import gg.essential.elementa.components.* +import gg.essential.elementa.components.input.UITextInput +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.resource.language.I18n +import java.awt.Color + +class UIHotkeyButtonsPanel( + keyBindings: Collection = ReplayMod.instance.keyBindingRegistry.bindings.values, +) : UIToggleablePanel() { + init { + toggleButton.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(0, 120, 20, 20)) + + constrain { + width = 200.pixels + height = 100.percent - 80.pixels + } + } + + val scrollContainer by UIContainer().constrain { + x = CenterConstraint() + y = 4.pixels + height = 100.percent - 20.pixels - 8.pixels + width = 100.percent - 8.pixels + } childOf this + + val scrollComponent by UIScrollComponent().constrain { + width = 100.percent - 10.pixels + height = 100.percent + }.apply { + for (keyBinding in keyBindings.sortedBy { I18n.translate(it.name) }) { + addChild(Row(keyBinding)) + } + } childOf scrollContainer + + val scrollBar by UIFlatScrollBar(transparent = true).constrain { + x = 0.pixels(alignOpposite = true) + width = 6.pixels + }.apply { + scrollComponent.setVerticalScrollBarComponent(grip) + } childOf scrollContainer + + val filter by UITextInput("Click to filter").constrain { + x = 10.pixels + y = (2 + 5).pixels(alignOpposite = true) + width = 100.percent - 10.pixels - 20.pixels - 2.pixels + height = 10.pixels + }.apply { + // When the panel is opened, immediately grant focus and clear it (so we can get straight to typing) + open.onSetValue { + if (it) { + grabWindowFocus() + setText("") + } + } + }.onUpdate { search -> + scrollComponent.filterChildren { (it as Row).label.getText().contains(search, ignoreCase = true) } + }.onActivate { + val row = scrollComponent.childrenOfType().singleOrNull() ?: return@onActivate + open.set(false) + row.keyBinding.trigger() + }.onMouseClick { + grabWindowFocus() + } childOf this + + inner class Row(val keyBinding: KeyBindingRegistry.Binding) : UIContainer() { + val button by UIButton().constrain { + y = CenterConstraint() + width = ChildBasedSizeConstraint().coerceAtLeast(10.pixels) + 10.pixels + height = 20.pixels + }.label { + bindText(pollingState { if (keyBinding.isBound) keyBinding.boundKey else "" }) + constrain { + val orgColor = color + color = basicColorConstraint { + if (keyBinding.isAutoActivating) Color.GREEN else orgColor.getColor(it) + } + } + }.onMouseClick { + it.stopImmediatePropagation() + + if (keyBinding.supportsAutoActivation() && Screen.hasControlDown()) { + keyBinding.isAutoActivating = !keyBinding.isAutoActivating + } else { + keyBinding.trigger() + } + }.apply { + if (keyBinding.supportsAutoActivation()) { + addTooltip { + addLine("replaymod.gui.ingame.autoactivating".i18n()) + addLine { + bindText(pollingState { + if (keyBinding.isAutoActivating) { + "replaymod.gui.ingame.autoactivating.disable".i18n() + } else { + "replaymod.gui.ingame.autoactivating.enable".i18n() + } + }) + } + } + } + } + + val label = UIWrappedText().constrain { + x = SiblingConstraint(padding = 4f) + y = CenterConstraint() + width = FillConstraint(useSiblings = false) + }.setText(keyBinding.name.i18n()) + + init { + constrain { + width = 100.percent + height = 25.pixels + y = SiblingConstraint() + } + addChildren(button, label) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIToggleablePanel.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIToggleablePanel.kt new file mode 100644 index 00000000..33fc5eaa --- /dev/null +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/panels/UIToggleablePanel.kt @@ -0,0 +1,56 @@ +package com.replaymod.replay.gui.overlay.panels + +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.utils.bindStateTransition +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.boundTo +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.pixels +import gg.essential.elementa.dsl.provideDelegate +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.transitions.ExpandFromTransition +import gg.essential.elementa.transitions.ShrinkToTransition +import gg.essential.elementa.utils.withAlpha +import java.awt.Color + +open class UIToggleablePanel : UIBlock(Color.BLACK.withAlpha(0.6f)) { + val open = BasicState(false) + + val toggleButton by UIButton().constrain { + x = 0.pixels(alignOpposite = true) + y = SiblingConstraint(4f) + width = 20.pixels + height = 20.pixels + }.onMouseClick { + open.set { !it } + } as UIButton + + init { + constrain { + x = (-2).pixels(alignOpposite = true) boundTo toggleButton + y = (-2).pixels(alignOpposite = true) boundTo toggleButton + } + + bindStateTransition(open) { done, _, open -> + if (open) { + unhide() + ExpandFromTransition.Bottom(0.5f, Animations.IN_OUT_EXP).transition(this) { + done() + } + + // Make sure the toggle button is on top of the panel + toggleButton.setFloating(true) + } else { + ShrinkToTransition.Bottom(0.5f, Animations.IN_OUT_EXP, true).transition(this) { + hide(true) + + toggleButton.setFloating(false) + + done() + } + } + } + } +} \ No newline at end of file From 21cd0724789e7356ea516024c9305f0f930104f7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 4 Sep 2021 09:10:18 +0200 Subject: [PATCH 006/132] Convert FullBright/QuickMode indicators to Elementa --- .../com/replaymod/extras/FullBrightness.java | 10 +++---- .../java/com/replaymod/extras/QuickMode.java | 9 +++--- .../replay/gui/overlay/GuiReplayOverlayKt.kt | 8 +++++ .../replay/gui/overlay/UIStatusIndicator.kt | 29 +++++++++++++++++++ 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/com/replaymod/replay/gui/overlay/UIStatusIndicator.kt diff --git a/src/main/java/com/replaymod/extras/FullBrightness.java b/src/main/java/com/replaymod/extras/FullBrightness.java index b6bef691..022070df 100644 --- a/src/main/java/com/replaymod/extras/FullBrightness.java +++ b/src/main/java/com/replaymod/extras/FullBrightness.java @@ -8,9 +8,7 @@ import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.events.ReplayOpenedCallback; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; -import de.johni0702.minecraft.gui.element.GuiImage; -import de.johni0702.minecraft.gui.element.IGuiImage; -import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import com.replaymod.replay.gui.overlay.UIStatusIndicator; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.effect.StatusEffectInstance; @@ -20,7 +18,7 @@ public class FullBrightness extends EventRegistrations implements Extra { private ReplayMod core; private ReplayModReplay module; - private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 16).setSize(19, 16); + private final UIStatusIndicator indicator = new UIStatusIndicator(90, 20); private MinecraftClient mc; private boolean active; @@ -108,9 +106,9 @@ private void postRender() { { on(ReplayOpenedCallback.EVENT, replayHandler -> updateIndicator(replayHandler.getOverlay())); } private void updateIndicator(GuiReplayOverlay overlay) { if (active) { - overlay.statusIndicatorPanel.addElements(new HorizontalLayout.Data(1), indicator); + overlay.kt.getBottomLeftPanel().addChild(indicator); } else { - overlay.statusIndicatorPanel.removeElement(indicator); + overlay.kt.getBottomLeftPanel().removeChild(indicator); } } diff --git a/src/main/java/com/replaymod/extras/QuickMode.java b/src/main/java/com/replaymod/extras/QuickMode.java index 454d20be..a6b5853a 100644 --- a/src/main/java/com/replaymod/extras/QuickMode.java +++ b/src/main/java/com/replaymod/extras/QuickMode.java @@ -6,14 +6,13 @@ import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.events.ReplayOpenedCallback; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; -import de.johni0702.minecraft.gui.element.GuiImage; -import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import com.replaymod.replay.gui.overlay.UIStatusIndicator; import de.johni0702.minecraft.gui.utils.EventRegistrations; public class QuickMode extends EventRegistrations implements Extra { private ReplayModReplay module; - private final GuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 40, 100, 16, 16).setSize(16, 16); + private final UIStatusIndicator indicator = new UIStatusIndicator(40, 100); @Override public void register(final ReplayMod mod) { @@ -44,9 +43,9 @@ public void register(final ReplayMod mod) { private void updateIndicator(GuiReplayOverlay overlay, boolean enabled) { if (enabled) { - overlay.statusIndicatorPanel.addElements(new HorizontalLayout.Data(1), indicator); + overlay.kt.getBottomLeftPanel().addChild(indicator); } else { - overlay.statusIndicatorPanel.removeElement(indicator); + overlay.kt.getBottomLeftPanel().removeChild(indicator); } } } diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt index 471c0f46..99cd67e2 100644 --- a/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/GuiReplayOverlayKt.kt @@ -12,6 +12,14 @@ import gg.essential.elementa.dsl.* class GuiReplayOverlayKt { val window = Window(ElementaVersion.V1, 60) + val bottomLeftPanel by UIContainer().constrain { + x = 6.pixels(alignOpposite = false) + y = 6.pixels(alignOpposite = true) + // Children will be next to each other + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + } childOf window + val bottomRightPanel by UIContainer().constrain { x = 6.pixels(alignOpposite = true) y = 6.pixels(alignOpposite = true) diff --git a/src/main/kotlin/com/replaymod/replay/gui/overlay/UIStatusIndicator.kt b/src/main/kotlin/com/replaymod/replay/gui/overlay/UIStatusIndicator.kt new file mode 100644 index 00000000..3fe31683 --- /dev/null +++ b/src/main/kotlin/com/replaymod/replay/gui/overlay/UIStatusIndicator.kt @@ -0,0 +1,29 @@ +package com.replaymod.replay.gui.overlay + +import com.replaymod.core.ReplayMod +import com.replaymod.core.gui.common.UITexture +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.dsl.childOf +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.pixels +import gg.essential.elementa.dsl.provideDelegate + +class UIStatusIndicator(u: Int, v: Int) : UIContainer() { + val icon by UITexture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(16, 16).offset(u, v)).constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 16.pixels + height = 16.pixels + } childOf this + + init { + constrain { + x = SiblingConstraint(4f) + y = 0.pixels(alignOpposite = true) + width = 20.pixels + height = 20.pixels + } + } +} \ No newline at end of file From 7bedc2a70325a2dccd21c63a6b20d901cebf3e64 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 4 Sep 2021 12:50:31 +0200 Subject: [PATCH 007/132] Add new position keyframe edit panel --- .../gui/common/elementa/AbstractTextInput.kt | 7 +- .../gui/common/input/UIExpressionInput.kt | 52 ++++ .../common/input/UIInputOrExpressionField.kt | 76 +++++ .../com/replaymod/core/gui/utils/Axis.kt | 11 + .../com/replaymod/core/gui/utils/resources.kt | 10 + .../simplepathing/gui/GuiPathingKt.kt | 5 + .../gui/panels/UIPositionKeyframePanel.kt | 263 ++++++++++++++++++ .../replaymod/icons/pos_keyframe_panel.png | Bin 0 -> 126 bytes 8 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIExpressionInput.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/resources.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt create mode 100644 src/main/resources/assets/replaymod/icons/pos_keyframe_panel.png diff --git a/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt index b7dd7c90..9a27b9d8 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/elementa/AbstractTextInput.kt @@ -1,7 +1,9 @@ /** * Based on Elementa's AbstractTextInput but with more stuff exposed and certain changes so we can get it to behave just * like we need it to: + * - Add `isCursorAtAbsoluteStart` * - Remove the `private` from TextOperation implementations' fields + * - Remove the `protected` from `activateAction` * * MIT License * @@ -61,7 +63,7 @@ abstract class AbstractTextInput( field = value } protected var updateAction: (text: String) -> Unit = {} - protected var activateAction: (text: String) -> Unit = {} + var activateAction: (text: String) -> Unit = {} protected val textualLines = mutableListOf(TextualLine("", 0..0)) protected val visualLines = mutableListOf(VisualLine("", 0)) @@ -767,6 +769,9 @@ abstract class AbstractTextInput( } } + val isCursorAtAbsoluteStart: Boolean + get() = cursor.isAtAbsoluteStart + override fun animationFrame() { super.animationFrame() diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIExpressionInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIExpressionInput.kt new file mode 100644 index 00000000..f068f26e --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIExpressionInput.kt @@ -0,0 +1,52 @@ +package com.replaymod.core.gui.common.input + +import com.replaymod.core.gui.common.elementa.UITextInput +import com.udojava.evalex.Expression +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.pixels +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.state.toConstraint +import java.awt.Color +import java.math.MathContext + +class UIExpressionInput : UITextInput() { + val value: Double? + get() = try { + expression.eval()?.toDouble() + } catch (e: Expression.ExpressionException) { + null + } catch (e: ArithmeticException) { + null + } catch (e: NumberFormatException) { + null + } + + val expression: Expression + get() = Expression(getText(), MathContext.DECIMAL64) + + val valid: State = BasicState(false) + + init { + constrain { + color = valid.map { if (it) Color.WHITE else Color.RED }.toConstraint() + } + + onUpdate { + valid.set(value != null) + } + } + + override fun afterInitialization() { + // Right-align + constrain { + x = 2.pixels(alignOpposite = true) + } + + super.afterInitialization() + } + + companion object { + val expressionChars = "+-*/%".toSet() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt new file mode 100644 index 00000000..cb14f5ee --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt @@ -0,0 +1,76 @@ +package com.replaymod.core.gui.common.input + +import com.replaymod.core.gui.common.elementa.UITextInput +import com.replaymod.core.gui.utils.hiddenChildOf +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.dsl.* +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +class UIInputOrExpressionField( + field: UIInputField, + getValue: T.() -> String, + setValue: T.(value: Double) -> Unit, + forwardActivation: Boolean = true, +) : UIContainer() { + + val input: T by field::input + + val inputField: UIInputField = field.constrain { + width = 100.percent + height = 100.percent + } childOf this + + val expressionField by UIInputField(UIExpressionInput()).constrain { + width = 100.percent + height = 100.percent + } hiddenChildOf this + + init { + // Switch from regular input to expression when certain keys are pressed (special case negative numbers) + input.keyTypedListeners.add(0) { typedChar, keyCode -> + if (typedChar in UIExpressionInput.expressionChars && (typedChar != '-' || !input.isCursorAtAbsoluteStart)) { + inputField.hide(instantly = true) + expressionField.unhide() + + with (expressionField.input) { + setText(inputField.input.getValue()) + grabWindowFocus() + setActive(true) + keyTypedListeners.forEach { it(typedChar, keyCode) } + } + } + } + + // Switch from expression to regular input when Enter is pressed (and the expression is valid) + expressionField.input.onActivate { + val value = expressionField.input.value ?: return@onActivate + + expressionField.hide(instantly = true) + inputField.unhide() + + with(inputField.input) { + setValue(value) + grabWindowFocus() + if (forwardActivation) { + activateAction(getText()) + } + } + } + + // Also switch back when the expression field looses focus, but do not apply the result in that case + expressionField.input.onFocusLost { + expressionField.hide(instantly = true) + inputField.unhide() + } + } + + companion object { + fun forTimeInput(input: UITimeInput = UITimeInput()) = UIInputOrExpressionField( + UIInputField(input), + { "%.3f".format(Locale.ROOT, value.toDouble(TimeUnit.SECONDS)) }, + { value = Duration.seconds(it) }, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt b/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt new file mode 100644 index 00000000..c7abccf6 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt @@ -0,0 +1,11 @@ +package com.replaymod.core.gui.utils + +import java.awt.Color + +enum class Axis( + val color: Color, +) { + X(Color.RED), + Y(Color.GREEN), + Z(Color.BLUE), +} diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/resources.kt b/src/main/kotlin/com/replaymod/core/gui/utils/resources.kt new file mode 100644 index 00000000..142d277b --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/resources.kt @@ -0,0 +1,10 @@ +package com.replaymod.core.gui.utils + +import com.replaymod.core.ReplayMod +import net.minecraft.util.Identifier + +object Resources { + private const val namespace = ReplayMod.MOD_ID + + fun icon(name: String) = Identifier(namespace, "icons/$name.png") +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 9a0152e0..bcd86e76 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -19,6 +19,7 @@ import com.replaymod.replay.ReplayHandler import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.SPTimeline.SPPath import com.replaymod.simplepathing.Setting +import com.replaymod.simplepathing.gui.panels.UIPositionKeyframePanel import de.johni0702.minecraft.gui.popup.GuiInfoPopup import gg.essential.elementa.components.UIContainer import gg.essential.elementa.constraints.* @@ -251,6 +252,10 @@ class GuiPathingKt( timeline.zoom.set { it * 3 / 2 } } childOf zoomButtonPanel + private val positionKeyframePanel by UIPositionKeyframePanel(state).apply { + overlay.kt.bottomRightPanel.insertChildAt(toggleButton, 0) + } hiddenChildOf window + init { val speedValue = window.pollingState { overlay.speedSlider.value } val replayTime = window.pollingState { replayHandler.replaySender.currentTimeStamp() } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt new file mode 100644 index 00000000..7f4af096 --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt @@ -0,0 +1,263 @@ +package com.replaymod.simplepathing.gui.panels + +import com.replaymod.core.gui.common.input.* +import com.replaymod.core.gui.utils.Axis +import com.replaymod.core.gui.utils.Resources +import com.replaymod.core.gui.utils.hiddenChildOf +import com.replaymod.core.gui.utils.onSetValueAndNow +import com.replaymod.core.utils.i18n +import com.replaymod.core.utils.transpose +import com.replaymod.replay.gui.overlay.panels.UIToggleablePanel +import com.replaymod.replaystudio.pathing.change.Change +import com.replaymod.replaystudio.pathing.change.CombinedChange +import com.replaymod.simplepathing.SPTimeline +import com.replaymod.simplepathing.gui.KeyframeState +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import java.util.* +import kotlin.math.* +import kotlin.time.Duration + +class UIPositionKeyframePanel( + val state: KeyframeState, +) : UIToggleablePanel() { + init { + toggleButton.texture(Resources.icon("pos_keyframe_panel")) + + constrain { + width = 230.pixels + height = 114.pixels + } + } + + val translateContainer by UIContainer().constrain { + width = 128.pixels + height = ChildBasedSizeConstraint() + } childOf this + + val translateLabel by UIContainer().constrain { + width = 100.percent + height = 20.pixels + }.addChild(UIText("Translate".i18n()).constrain { + x = CenterConstraint() + y = CenterConstraint() + }) childOf translateContainer + + val translateXYZ by UIContainer().constrain { + y = SiblingConstraint() + width = 100.percent + height = ChildBasedSizeConstraint() + } childOf translateContainer + + val translateX by FieldComponent(Axis.X) childOf translateXYZ + val translateY by FieldComponent(Axis.Y) childOf translateXYZ + val translateZ by FieldComponent(Axis.Z) childOf translateXYZ + + val rotateContainer by UIContainer().constrain { + x = SiblingConstraint() + width = 102.pixels + height = ChildBasedSizeConstraint() + } childOf this + + val rotateLabel by UIContainer().constrain { + width = 100.percent + height = 20.pixels + }.addChild(UIText("Rotate".i18n()).constrain { + x = CenterConstraint() + y = CenterConstraint() + }) childOf rotateContainer + + val rotateXYZ by UIContainer().constrain { + y = SiblingConstraint() + width = 100.percent + height = ChildBasedSizeConstraint() + } childOf rotateContainer + + val rotateX by FieldComponent(Axis.X) childOf rotateXYZ + val rotateY by FieldComponent(Axis.Y) childOf rotateXYZ + val rotateZ by FieldComponent(Axis.Z) childOf rotateXYZ + + val timeField by UIInputOrExpressionField.forTimeInput().constrain { + x = 0.pixels boundTo rotateZ.input.integer + y = SiblingConstraint(4f) + width = 60.pixels + height = 20.pixels + }.apply { + input.onActivate { apply() } + } childOf this + + val timeLabel by UIContainer().constrain { + x = 5.pixels(alignOutside = true) boundTo timeField + y = 0.pixels boundTo timeField + width = ChildBasedSizeConstraint() + height = CopyConstraintFloat() boundTo timeField + }.addChild(UIText("Video Time (min:sec:ms)".i18n()).constrain { + y = CenterConstraint() + }) childOf this + + init { + state.selectedPositionKeyframes.map { it.entries.firstOrNull() }.onSetValueAndNow { selection -> + val (time, keyframe) = selection?.toPair().transpose() + + timeField.input.value = time ?: Duration.ZERO + (keyframe?.position ?: Triple(0.0, 0.0, 0.0)).let { (x, y, z) -> + translateX.value = x + translateY.value = y + translateZ.value = z + } + (keyframe?.rotation ?: Triple(0.0, 0.0, 0.0)).let { (x, y, z) -> + rotateX.value = x + rotateY.value = y + rotateZ.value = z + } + } + } + + private fun apply() { + val time = state.selectedPositionKeyframes.get().keys.firstOrNull() ?: return + + val timeline = state.mod.currentTimeline + var change: Change = timeline.updatePositionKeyframe( + time.inWholeMilliseconds, + translateX.value, translateY.value, translateZ.value, + rotateX.value.toFloat(), rotateY.value.toFloat(), rotateZ.value.toFloat(), + ) + + val newTime = timeField.input.value + if (newTime != time) { + change = CombinedChange.createFromApplied( + change, + timeline.moveKeyframe(SPTimeline.SPPath.POSITION, time.inWholeMilliseconds, newTime.inWholeMilliseconds) + ) + state.mod.setSelected(SPTimeline.SPPath.POSITION, newTime.inWholeMilliseconds) + } + timeline.timeline.pushChange(change) + } + + inner class FieldComponent(axis: Axis) : UIContainer() { + init { + constrain { + y = SiblingConstraint(4f) + width = 100.percent + height = 20.pixels + } + } + + val label by UIContainer().constrain { + width = 16.pixels + height = 100.percent + }.addChild(UIText(axis.toString()).constrain { + x = CenterConstraint() + y = CenterConstraint() + color = axis.color.toConstraint() + }) childOf this + + val input by DecimalInputField().constrain { + x = SiblingConstraint() + width = FillConstraint() - 6.pixels + } childOf this + + init { + input.onActivate { apply() } + } + + var value by input::value + } + + class DecimalInputField( + fractionalDigits: Int = 5, + ) : UIContainer() { + private val fractionMultiplier = (10.0).pow(fractionalDigits) + + init { + constrain { + width = 100.percent + height = 100.percent + } + } + + val expression by UIInputField(UIExpressionInput()).constrain { + width = 100.percent + height = 100.percent + } hiddenChildOf this + + val decimal by UIContainer().constrain { + width = 100.percent + height = 100.percent + } childOf this + + val integer by UIInputField(UIIntegerInput()).constrain { + width = FillConstraint() + } childOf decimal + + val dot by UIContainer().constrain { + x = SiblingConstraint() + width = 5.pixels + height = 100.percent + }.addChild(UIText(".", shadow = false).constrain { + x = CenterConstraint() + y = CenterConstraint() + }) childOf decimal + + val fraction by UIInputField(UIIntegerInput(fixedDigits = fractionalDigits)).constrain { + x = SiblingConstraint() + width = 37.pixels + } childOf decimal + + init { + val maybeSwitchToExpression: UIComponent.(typedChar: Char, keyCode: Int) -> Unit = { typedChar, keyCode -> + this as UIIntegerInput + if (typedChar in UIExpressionInput.expressionChars && (typedChar != '-' || !isCursorAtAbsoluteStart)) { + decimal.hide(instantly = true) + expression.unhide() + + with(expression.input) { + setText(integer.input.getText() + "." + fraction.input.getText()) + grabWindowFocus() + setActive(true) + keyTypedListeners.forEach { it(typedChar, keyCode) } + } + } + } + integer.input.keyTypedListeners.add(0, maybeSwitchToExpression) + fraction.input.keyTypedListeners.add(0, maybeSwitchToExpression) + + expression.input.onActivate { + // Re-assigning the value will switch back to integer+fraction fields + value = expression.input.value ?: return@onActivate + + // Forward the activation + integer.input.activateAction("") + } + expression.input.onFocusLost { + // Re-assigning the value will switch back to integer+fraction fields + value = value + } + } + + var value: Double + get() { + val int = integer.input.value + return int + (int.sign * fraction.input.value / fractionMultiplier) + } + set(value) { + integer.input.value = value.toInt() + fraction.input.value = (abs(value - value.toInt()) * fractionMultiplier).roundToInt() + + expression.hide(instantly = true) + decimal.unhide() + if (Window.ofOrNull(expression) != null && expression.input.hasFocus()) { + integer.input.grabWindowFocus() + } + } + + fun onActivate(listener: () -> Unit) { + integer.input.onActivate { listener() } + fraction.input.onActivate { listener() } + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/icons/pos_keyframe_panel.png b/src/main/resources/assets/replaymod/icons/pos_keyframe_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd8eeb0404e83df6add1ad9b9705eaa40e5ddbd GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=v!{z=h(&L5f&^>C!FmlD(}F7p z7>z&tuV-ufFVNJe Date: Sat, 4 Sep 2021 13:07:13 +0200 Subject: [PATCH 008/132] Move zoom buttons below timeline, next to scroll bar Cause we'll be adding more buttons there. --- .../simplepathing/gui/GuiPathingKt.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index bcd86e76..f1eb76a6 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -186,7 +186,7 @@ class GuiPathingKt( val timeline by UITimeline().constrain { x = SiblingConstraint(5f) - width = FillConstraint(false) - basicWidthConstraint { zoomButtonPanel.getWidth() } - 2.pixels + width = FillConstraint(false) height = 20.pixels }.apply { enableIndicators() @@ -209,14 +209,25 @@ class GuiPathingKt( } } childOf secondRow - private val scrollbar by UITexturedScrollBar().constrain { + private val belowTimeline by UIContainer().constrain { x = 0.pixels boundTo timeline y = SiblingConstraint(1f) boundTo timeline width = CopyConstraintFloat() boundTo timeline height = 9.pixels + } childOf window + + private val belowTimelineButtons by UIContainer().constrain { + width = ChildBasedSizeConstraint() + height = 100.percent + } childOf belowTimeline + + private val scrollbar by UITexturedScrollBar().constrain { + x = SiblingConstraint(2f) + width = FillConstraint(useSiblings = false) + height = 100.percent }.apply { timeline.content.setHorizontalScrollBarComponent(grip) - } childOf window + } childOf belowTimeline private val timelineTime by UITimelineTime(timeline).constrain { x = 0.pixels boundTo timeline @@ -225,13 +236,8 @@ class GuiPathingKt( height = 8.pixels } childOf window - private val zoomButtonPanel by UIContainer().constrain { - x = 0.pixels(alignOpposite = true) - width = ChildBasedMaxSizeConstraint() - height = ChildBasedSizeConstraint() - } childOf secondRow - private val zoomInButton by UIButton().constrain { + x = SiblingConstraint(2f) width = 9.pixels height = 9.pixels }.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(40, 20, 9, 9)) { @@ -239,10 +245,10 @@ class GuiPathingKt( addLine("replaymod.gui.ingame.menu.zoomin".i18n()) }.onMouseClick { timeline.zoom.set { it * 2 / 3 } - } childOf zoomButtonPanel + } childOf belowTimelineButtons private val zoomOutButton by UIButton().constrain { - y = SiblingConstraint(2f) + x = SiblingConstraint(2f) width = 9.pixels height = 9.pixels }.texture(ReplayMod.TEXTURE, UITexture.TextureData.ofSize(40, 30, 9, 9)) { @@ -250,7 +256,7 @@ class GuiPathingKt( addLine("replaymod.gui.ingame.menu.zoomout".i18n()) }.onMouseClick { timeline.zoom.set { it * 3 / 2 } - } childOf zoomButtonPanel + } childOf belowTimelineButtons private val positionKeyframePanel by UIPositionKeyframePanel(state).apply { overlay.kt.bottomRightPanel.insertChildAt(toggleButton, 0) From e94003e9f4eec5d45c0082192d05c7d146b134e6 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 4 Sep 2021 14:09:05 +0200 Subject: [PATCH 009/132] Show replay speed of segments via colored bars (closes #482) --- .../kotlin/com/replaymod/core/gui/utils/state.kt | 4 ++++ .../simplepathing/gui/UITimelineKeyframes.kt | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/state.kt b/src/main/kotlin/com/replaymod/core/gui/utils/state.kt index 1a61b0fe..25e7b5b2 100644 --- a/src/main/kotlin/com/replaymod/core/gui/utils/state.kt +++ b/src/main/kotlin/com/replaymod/core/gui/utils/state.kt @@ -50,3 +50,7 @@ fun State.bindTransition(update: (done: () -> Unit, oldState: S, newState } onSetValue(::onStateChanged) } + +fun State.zip(b: State, c: State): State> = zip(b).zip(c).map { values -> + Triple(values.first.first, values.first.second, values.second) +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt index e65e8b3e..78d62b16 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt @@ -46,7 +46,9 @@ class UITimelineKeyframes( height = ChildBasedSizeConstraint() } - state.timeKeyframes.zip(state.selectionTimeKeyframes).onSetValueAndNow { (keyframes, selection) -> + val speed = pollingState { state.gui.java.overlay.speedSliderValue } + + state.timeKeyframes.zip(state.selectionTimeKeyframes, speed).onSetValueAndNow { (keyframes, selection, speed) -> val row = rows[1] row.clearChildren() @@ -57,9 +59,20 @@ class UITimelineKeyframes( val lineEffect = LineToReplayTimelineEffect(replayTimeline, replayTime) UIKeyframe(time, KeyframeType.TIME, time in selection) effect lineEffect childOf row + val segmentSpeed = if (prevTime != time) (replayTime - prevReplayTime) / (time - prevTime) else speed + // Draw red quads on time path segments that would require time going backwards if (prevReplayTime > replayTime) { UISegment(prevTime, time, Color.RED) childOf row + } else if ((segmentSpeed / speed - 1).absoluteValue > 0.01) { + // Draw white/green/yellow quads on paused/slower/faster path segments + UISegment(prevTime, time, when { + segmentSpeed == 0.0 -> Color.WHITE + segmentSpeed < speed -> Color.GREEN + else -> Color.YELLOW + }).addTooltip { + addLine("Replay Speed: %.2fx".format(segmentSpeed)) + } childOf row } prevTime = time prevReplayTime = replayTime From 9e7020dd49cf53116f5299889b77863244fab40c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 4 Sep 2021 17:05:38 +0200 Subject: [PATCH 010/132] Add new time keyframe edit panel --- .../simplepathing/gui/GuiPathingKt.kt | 10 + .../simplepathing/gui/panels/UITimePanel.kt | 177 ++++++++++++++++++ .../assets/replaymod/icons/time_panel.png | Bin 0 -> 84 bytes 3 files changed, 187 insertions(+) create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt create mode 100644 src/main/resources/assets/replaymod/icons/time_panel.png diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index f1eb76a6..3fe7dee3 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -20,6 +20,7 @@ import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.SPTimeline.SPPath import com.replaymod.simplepathing.Setting import com.replaymod.simplepathing.gui.panels.UIPositionKeyframePanel +import com.replaymod.simplepathing.gui.panels.UITimePanel import de.johni0702.minecraft.gui.popup.GuiInfoPopup import gg.essential.elementa.components.UIContainer import gg.essential.elementa.constraints.* @@ -262,6 +263,15 @@ class GuiPathingKt( overlay.kt.bottomRightPanel.insertChildAt(toggleButton, 0) } hiddenChildOf window + private val timePanel by UITimePanel(state).constrain { + x = 0.pixels boundTo belowTimeline + y = SiblingConstraint(1f) boundTo belowTimeline + }.apply { + belowTimelineButtons.insertChildAt(toggleButton.constrain { + x = SiblingConstraint(2f) + }, 0) + } hiddenChildOf window + init { val speedValue = window.pollingState { overlay.speedSlider.value } val replayTime = window.pollingState { replayHandler.replaySender.currentTimeStamp() } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt new file mode 100644 index 00000000..1e6d5426 --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt @@ -0,0 +1,177 @@ +package com.replaymod.simplepathing.gui.panels + +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.common.input.* +import com.replaymod.core.gui.utils.* +import com.replaymod.core.utils.i18n +import com.replaymod.simplepathing.SPTimeline +import com.replaymod.simplepathing.gui.KeyframeState +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.UIWrappedText +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.getStringSplitToWidth +import gg.essential.elementa.utils.withAlpha +import java.awt.Color +import kotlin.time.Duration + +class UITimePanel( + val state: KeyframeState, +) : UIBlock(Color(32, 32, 32).withAlpha(0.6f)) { + + val open = BasicState(false) + + val toggleButton by UIButton().constrain { + width = 9.pixels + height = 9.pixels + }.texture(Resources.icon("time_panel")).onMouseClick { + open.set { !it } + } as UIButton + + init { + open.onSetValue { if (it) unhide() else hide(true) } + + constrain { + width = ChildBasedSizeConstraint() + 10.pixels + height = ChildBasedSizeConstraint() + 4.pixels + } + } + + val container by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + } childOf this + + val infoContainer by UIContainer().constrain { + x = SiblingConstraint(5f) + y = CenterConstraint() + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf container + + private val hasSelectedKeyframe = state.selectionTimeKeyframes.map { it.isNotEmpty() } + + val infoCurrentTime by UIText("Current Time".i18n(), shadow = false).constrain { + color = hasSelectedKeyframe.map { if (it) Color.BLACK else Color.WHITE }.toConstraint() + } childOf infoContainer + + val infoSelectedKeyframe by UIWrappedText("Selected Time Keyframe".i18n(), shadow = false).constrain { + y = SiblingConstraint(5f) + width = basicWidthConstraint { component -> + component as UIWrappedText + getStringSplitToWidth( + component.getText(), + 80f, + component.getTextScale(), + ensureSpaceAtEndOfLines = false, + fontProvider = component.getFontProvider() + ).maxOf { it.width(component.getTextScale(), component.getFontProvider()) } + } + color = hasSelectedKeyframe.map { if (it) Color.WHITE else Color.BLACK }.toConstraint() + } childOf infoContainer + + val divider by UIBlock(Color.WHITE).constrain { + x = SiblingConstraint(5f) + width = 1.pixel + height = basicHeightConstraint { inputContainer.getHeight() } + } childOf container + + val inputContainer by UIContainer().constrain { + x = SiblingConstraint(5f) + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf container + + val replayTimeContainer by UIContainer().constrain { + x = 0.pixels(alignOpposite = true) + y = SiblingConstraint(4f) + width = ChildBasedSizeConstraint() + 4.pixels // FIXME that constraint is broken and ignores the last padding + height = ChildBasedMaxSizeConstraint() + } childOf inputContainer + + val videoTimeContainer by UIContainer().constrain { + x = 0.pixels(alignOpposite = true) + y = SiblingConstraint(4f) + width = ChildBasedSizeConstraint() + 4.pixels // FIXME that constraint is broken and ignores the last padding + height = ChildBasedMaxSizeConstraint() + } childOf inputContainer + + val replayTimeField by UIInputOrExpressionField.forTimeInput().constrain { + x = 0.pixels(alignOpposite = true) + width = 60.pixels + height = 20.pixels + }.apply { + input.onActivate { + val newReplayTime = input.value + val selection = state.selectedTimeKeyframes.get().entries.firstOrNull() + if (selection != null) { + val (videoTime, keyframe) = (selection) + if (keyframe.replayTime != newReplayTime) { + val timeline = state.mod.currentTimeline + timeline.timeline.pushChange(timeline.updateTimeKeyframe( + videoTime.inWholeMilliseconds, + newReplayTime.inWholeMilliseconds.toInt(), + )) + } + } else { + state.gui.replayHandler.doJump(newReplayTime.inWholeMilliseconds.toInt(), true) + } + } + } childOf replayTimeContainer + + val replayTimeLabel by UIText("Replay Time (min:sec:ms)".i18n()).constrain { + x = SiblingConstraint(5f, alignOpposite = true) + y = CenterConstraint() + } childOf replayTimeContainer + + val videoTimeField by UIInputOrExpressionField.forTimeInput().constrain { + x = 0.pixels(alignOpposite = true) + width = 60.pixels + height = 20.pixels + }.apply { + input.onActivate { + val newVideoTime = input.value + val selection = state.selectedTimeKeyframes.get().entries.firstOrNull() + if (selection != null) { + val (oldVideoTime, _) = selection + if (newVideoTime != oldVideoTime) { + val timeline = state.mod.currentTimeline + timeline.timeline.pushChange(timeline.moveKeyframe( + SPTimeline.SPPath.TIME, + oldVideoTime.inWholeMilliseconds, + newVideoTime.inWholeMilliseconds, + )) + state.mod.setSelected(SPTimeline.SPPath.TIME, newVideoTime.inWholeMilliseconds) + } + } else { + state.gui.timeline.cursor.position.set(newVideoTime) + } + } + } childOf videoTimeContainer + + val videoTimeLabel by UIText("Video Time (min:sec:ms)".i18n()).constrain { + x = SiblingConstraint(5f, alignOpposite = true) + y = CenterConstraint() + } childOf videoTimeContainer + + init { + val currentReplayTime = + pollingState { Duration.milliseconds(state.gui.replayHandler.replaySender.currentTimeStamp()) } + val currentVideoTime = state.gui.timeline.cursor.position + + state.selectedTimeKeyframes.zip(currentVideoTime.zip(currentReplayTime)).map { (selected, current) -> + selected.entries.firstOrNull()?.let { (videoTime, keyframe) -> + videoTime to keyframe.replayTime + } ?: current + }.onSetValueAndNow { (videoTime, replayTime) -> + replayTimeField.input.value = replayTime + videoTimeField.input.value = videoTime + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/icons/time_panel.png b/src/main/resources/assets/replaymod/icons/time_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..037d06dfdd609efd6691bb847972aa9027537bf2 GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VQl2i3Are!Q6Bbzg=w}rO@wni! hXwA|-rA>?sZcnwZp1b#77O0BB)78&qol`;+0|5IG7Lot} literal 0 HcmV?d00001 From 618fec25d8ee7c5b483d5af83fabf4792190c4df Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 25 Sep 2021 10:59:37 +0200 Subject: [PATCH 011/132] Convert custom State types into adapters --- .../com/replaymod/core/gui/common/state.kt | 36 +++++++++++++------ .../gui/common/timeline/UITimelineCursor.kt | 5 +-- .../simplepathing/gui/KeyframeState.kt | 9 ++--- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/com/replaymod/core/gui/common/state.kt b/src/main/kotlin/com/replaymod/core/gui/common/state.kt index 29f9cf2d..af249189 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/state.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/state.kt @@ -1,25 +1,39 @@ package com.replaymod.core.gui.common -import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State -class LazyState(value: T) : BasicState(value) { +fun , T> S.lazy() = LazyState(this) +fun , T> S.bounded(constrain: (value: T) -> T) = BoundedState(this, constrain) + +open class DelegatingState, T>(val inner: S) : State() { + init { + @Suppress("LeakingThis") + inner.onSetValue(this::notifyListeners) + } + + protected open fun notifyListeners(value: T) { + super.set(value) + } + + override fun get(): T = inner.get() + override fun set(value: T) = inner.set(value) +} + +class LazyState, T>(inner: S) : DelegatingState(inner) { private var dirty = false - override fun set(value: T) { - if (value == valueBacker) return - valueBacker = value + override fun notifyListeners(value: T) { dirty = true } fun flush() { if (!dirty) return - val value = get() - listeners.forEach { it(value) } + super.notifyListeners(get()) } } -class BoundedState(value: T, private val constrain: (value: T) -> T) : BasicState(value) { - override fun set(value: T) { - super.set(constrain(value)) - } +class BoundedState, T>(inner: S, private val constrain: (value: T) -> T) : DelegatingState(inner) { + override fun notifyListeners(value: T) = super.notifyListeners(constrain(value)) + override fun get(): T = constrain(super.get()) + override fun set(value: T) = super.set(constrain(value)) } diff --git a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt index 3c56b5c7..fc5da8ab 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/timeline/UITimelineCursor.kt @@ -1,13 +1,14 @@ package com.replaymod.core.gui.common.timeline -import com.replaymod.core.gui.common.BoundedState import com.replaymod.core.gui.common.UITexture +import com.replaymod.core.gui.common.bounded import gg.essential.elementa.components.UIContainer import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState import kotlin.time.Duration class UITimelineCursor(val timeline: UITimeline) : UIContainer() { - val position = BoundedState(Duration.ZERO) { it.coerceIn(Duration.ZERO..timeline.length.get()) } + val position = BasicState(Duration.ZERO).bounded { it.coerceIn(Duration.ZERO..timeline.length.get()) } val positionMillis get() = position.get().inWholeMilliseconds diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt index 8874c357..e576f3d6 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt @@ -1,6 +1,6 @@ package com.replaymod.simplepathing.gui -import com.replaymod.core.gui.common.LazyState +import com.replaymod.core.gui.common.lazy import com.replaymod.core.utils.associateNotNull import com.replaymod.core.utils.kt import com.replaymod.core.utils.orNull @@ -11,6 +11,7 @@ import com.replaymod.pathing.properties.TimestampProperty import com.replaymod.replaystudio.pathing.change.Change import com.replaymod.simplepathing.ReplayModSimplePathing import com.replaymod.simplepathing.SPTimeline +import gg.essential.elementa.state.BasicState import kotlin.time.Duration class KeyframeState( @@ -20,9 +21,9 @@ class KeyframeState( private var lastTimeline: SPTimeline? = null private var lastChange: Change? = null - val selection = LazyState(Selection(emptySet(), emptySet())) - val timeKeyframes = LazyState(emptyMap()) - val positionKeyframes = LazyState(emptyMap()) + val selection = BasicState(Selection(emptySet(), emptySet())).lazy() + val timeKeyframes = BasicState(emptyMap()).lazy() + val positionKeyframes = BasicState(emptyMap()).lazy() val selectionPositionKeyframes = selection.map { it.positionKeyframes } val selectionTimeKeyframes = selection.map { it.timeKeyframes } From 3ff5555a0466c0b71e8c4769de45cfbb0fe68d8b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 25 Sep 2021 15:24:11 +0200 Subject: [PATCH 012/132] Add multi-select for keyframes --- .../simplepathing/ReplayModSimplePathing.java | 48 ++++--- .../replaymod/simplepathing/SPTimeline.java | 17 +++ .../simplepathing/gui/GuiPathing.java | 70 ----------- .../preview/PathPreviewRenderer.java | 2 +- .../com/replaymod/core/utils/extensions.kt | 5 + .../simplepathing/gui/GuiPathingKt.kt | 85 +++++++++++-- .../simplepathing/gui/KeyframeState.kt | 118 ++++++++++++++++-- .../simplepathing/gui/UITimelineKeyframes.kt | 56 ++++++--- .../replaymod/simplepathing/gui/operations.kt | 71 +++++++++++ .../gui/panels/UIPositionKeyframePanel.kt | 21 ++-- .../simplepathing/gui/panels/UITimePanel.kt | 33 +++-- 11 files changed, 365 insertions(+), 161 deletions(-) create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/operations.kt diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 00377aea..03217ae4 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -5,6 +5,8 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; import com.replaymod.core.events.SettingsChangedCallback; +import com.replaymod.simplepathing.gui.KeyframeState; +import com.replaymod.simplepathing.gui.KeyframeType; import de.johni0702.minecraft.gui.utils.EventRegistrations; import com.replaymod.core.versions.MCVer.Keyboard; import com.replaymod.replay.ReplayHandler; @@ -15,7 +17,6 @@ import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.change.Change; -import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; import com.replaymod.replaystudio.replay.ReplayFile; @@ -99,20 +100,20 @@ public void registerKeyBindings(KeyBindingRegistry registry) { settingsRegistry.save(); }); core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> - guiPathing != null && guiPathing.deleteButtonPressed()); + guiPathing != null && guiPathing.kt.deleteButtonPressed()); keyPositionKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positionkeyframe", Keyboard.KEY_I, () -> { - if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, false); + if (guiPathing != null) guiPathing.kt.toggleKeyframe(KeyframeType.POSITION); }, true); core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positiononlykeyframe", 0, () -> { - if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, true); + if (guiPathing != null) guiPathing.kt.toggleKeyframe(KeyframeType.POSITION, true); }, true); keyTimeKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.timekeyframe", Keyboard.KEY_O, () -> { - if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.TIME, false); + if (guiPathing != null) guiPathing.kt.toggleKeyframe(KeyframeType.TIME); }, true); core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.bothkeyframes", 0, () -> { if (guiPathing != null) { - guiPathing.toggleKeyframe(SPPath.TIME, false); - guiPathing.toggleKeyframe(SPPath.POSITION, false); + guiPathing.kt.toggleKeyframe(KeyframeType.TIME); + guiPathing.kt.toggleKeyframe(KeyframeType.POSITION); } }, true); core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Z, () -> { @@ -188,7 +189,6 @@ private void onReplayClosing() { private void onReplayClosed() { currentTimeline = null; guiPathing = null; - selectedPath = null; } private GuiReplayOverlay getReplayOverlay() { @@ -197,31 +197,25 @@ private GuiReplayOverlay getReplayOverlay() { private SPTimeline currentTimeline; - private SPPath selectedPath; - private long selectedTime; - + @Deprecated public SPPath getSelectedPath() { - if (getReplayOverlay().timeline.getSelectedMarker() != null) { - selectedPath = null; - selectedTime = 0; - } - return selectedPath; + return this.guiPathing == null ? null : this.guiPathing.kt.getState().getSelectedPath(); } + @Deprecated public long getSelectedTime() { - return selectedTime; + return this.guiPathing == null ? 0 : this.guiPathing.kt.getState().getSelectedTime(); } - public boolean isSelected(Keyframe keyframe) { - return getSelectedPath() != null && currentTimeline.getKeyframe(selectedPath, selectedTime) == keyframe; + public boolean isSelected(SPPath path, long time) { + if (this.guiPathing == null) return false; + return this.guiPathing.kt.getState().isSelected(path, time); } + @Deprecated public void setSelected(SPPath path, long time) { - selectedPath = path; - selectedTime = time; - if (selectedPath != null) { - getReplayOverlay().timeline.setSelectedMarker(null); - } + if (this.guiPathing == null) return; + this.guiPathing.kt.getState().setSelected(path, time); } public void setCurrentTimeline(SPTimeline newTimeline) { @@ -229,13 +223,17 @@ public void setCurrentTimeline(SPTimeline newTimeline) { } private void setCurrentTimeline(SPTimeline newTimeline, boolean save) { - selectedPath = null; currentTimeline = newTimeline; if (!save) { lastTimeline = newTimeline; lastChange = newTimeline.getTimeline().peekUndoStack(); } updateDefaultInterpolatorType(); + if (this.guiPathing != null) { + KeyframeState state = this.guiPathing.kt.getState(); + state.getSelection().set(KeyframeState.Selection.EMPTY); + state.update(); + } } public void clearCurrentTimeline() { diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java index 561122d4..96a83120 100644 --- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -307,6 +307,23 @@ public void removeTimeKeyframe(long time) { timeline.pushChange(change); } + public Change removeKeyframe(SPPath spPath, long time) { + LOGGER.debug("Removing {} keyframe at {}", spPath, time); + + Path path = getPath(spPath); + Keyframe keyframe = path.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "No keyframe at that time"); + + Change change = create(path, keyframe); + change.apply(timeline); + + Change specPosUpdate = updateSpectatorPositions(); + specPosUpdate.apply(timeline); + + return CombinedChange.createFromApplied(change, specPosUpdate); + } + public Change setInterpolatorToDefault(long time) { LOGGER.debug("Setting interpolator of position keyframe at {} to the default", time); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index ee9ad830..f92691ea 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -12,7 +12,6 @@ import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayHandler; -import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; @@ -26,10 +25,8 @@ import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; -import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; -import net.minecraft.client.resource.language.I18n; import net.minecraft.util.crash.CrashReport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -110,14 +107,6 @@ public void clearKeyframesButtonPressed() { }); } - public boolean deleteButtonPressed() { - if (mod.getSelectedPath() != null) { - toggleKeyframe(mod.getSelectedPath(), false); - return true; - } - return false; - } - private void startLoadingEntityTracker() { Preconditions.checkState(entityTrackerFuture == null); // Start loading entity tracker @@ -240,65 +229,6 @@ public void onFailure(@Nonnull Throwable t) { return true; } - /** - * Called when either one of the property buttons is pressed. - * @param path {@code TIME} for the time property button, {@code POSITION} for the place property button - * @param neverSpectator when true, will insert a position keyframe even when currently spectating an entity - */ - public void toggleKeyframe(SPPath path, boolean neverSpectator) { - LOGGER.debug("Updating keyframe on path {}" + path); - if (!loadEntityTracker(() -> toggleKeyframe(path, neverSpectator))) return; - - int time = (int) kt.getTimeline().getCursor().getPositionMillis(); - SPTimeline timeline = mod.getCurrentTimeline(); - - if (timeline.getPositionPath().getKeyframes().isEmpty() && - timeline.getTimePath().getKeyframes().isEmpty() && - time > 1000) { - String text = I18n.translate("replaymod.gui.ingame.first_keyframe_not_at_start_warning"); - GuiInfoPopup.open(overlay, text.split("\\\\n")); - } - - switch (path) { - case TIME: - if (mod.getSelectedPath() == path) { - LOGGER.debug("Selected keyframe is time keyframe -> removing keyframe"); - timeline.removeTimeKeyframe(mod.getSelectedTime()); - mod.setSelected(null, 0); - } else if (timeline.isTimeKeyframe(time)) { - LOGGER.debug("Keyframe at cursor position is time keyframe -> removing keyframe"); - timeline.removeTimeKeyframe(time); - mod.setSelected(null, 0); - } else { - LOGGER.debug("No time keyframe found -> adding new keyframe"); - timeline.addTimeKeyframe(time, replayHandler.getReplaySender().currentTimeStamp()); - mod.setSelected(path, time); - } - break; - case POSITION: - if (mod.getSelectedPath() == path) { - LOGGER.debug("Selected keyframe is position keyframe -> removing keyframe"); - timeline.removePositionKeyframe(mod.getSelectedTime()); - mod.setSelected(null, 0); - } else if (timeline.isPositionKeyframe(time)) { - LOGGER.debug("Keyframe at cursor position is position keyframe -> removing keyframe"); - timeline.removePositionKeyframe(time); - mod.setSelected(null, 0); - } else { - LOGGER.debug("No position keyframe found -> adding new keyframe"); - CameraEntity camera = replayHandler.getCameraEntity(); - int spectatedId = -1; - if (!replayHandler.isCameraView() && !neverSpectator) { - spectatedId = replayHandler.getOverlay().getMinecraft().getCameraEntity().getEntityId(); - } - timeline.addPositionKeyframe(time, camera.getX(), camera.getY(), camera.getZ(), - camera.yaw, camera.pitch, camera.roll, spectatedId); - mod.setSelected(path, time); - } - break; - } - } - public ReplayModSimplePathing getMod() { return mod; } diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java index e43fe0ca..bc2b7110 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java @@ -266,7 +266,7 @@ private void drawPoint(Vector3f view, Vector3f pos, Keyframe keyframe) { float posY = 0f; float size = 10f / ReplayMod.TEXTURE_SIZE; - if (mod.isSelected(keyframe)) { + if (mod.isSelected(SPTimeline.SPPath.POSITION, keyframe.getTime())) { posY += size; } diff --git a/src/main/kotlin/com/replaymod/core/utils/extensions.kt b/src/main/kotlin/com/replaymod/core/utils/extensions.kt index f542d659..61c5c246 100644 --- a/src/main/kotlin/com/replaymod/core/utils/extensions.kt +++ b/src/main/kotlin/com/replaymod/core/utils/extensions.kt @@ -1,5 +1,6 @@ package com.replaymod.core.utils +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f import net.minecraft.client.resource.language.I18n import java.util.* @@ -14,6 +15,8 @@ val org.apache.commons.lang3.tuple.Triple.kt: Triple fun Triple.toDouble() = Triple(first.toDouble(), second.toDouble(), third.toDouble()) +fun Triple.toVector3f() = Vector3f(first.toFloat(), second.toFloat(), third.toFloat()) + inline fun Iterable.associateNotNull(transform: (T) -> Pair?): Map { val destination = LinkedHashMap((this as? Collection)?.size ?: 16) for (element in this) { @@ -21,3 +24,5 @@ inline fun Iterable.associateNotNull(transform: (T) -> Pair?) } return destination } + +fun MutableSet.toggle(element: T) = if (element in this) remove(element) else add(element) diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 3fe7dee3..713eea1f 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -10,12 +10,13 @@ import com.replaymod.core.gui.common.scrollbar.UITexturedScrollBar import com.replaymod.core.gui.common.timeline.UITimeline import com.replaymod.core.gui.common.timeline.UITimelineTime import com.replaymod.core.gui.utils.* -import com.replaymod.core.utils.* +import com.replaymod.core.utils.i18n import com.replaymod.core.versions.MCVer.Keyboard import com.replaymod.pathing.player.RealtimeTimelinePlayer import com.replaymod.render.gui.GuiRenderQueue import com.replaymod.render.gui.GuiRenderSettings import com.replaymod.replay.ReplayHandler +import com.replaymod.replaystudio.pathing.change.CombinedChange import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.SPTimeline.SPPath import com.replaymod.simplepathing.Setting @@ -26,6 +27,7 @@ import gg.essential.elementa.components.UIContainer import gg.essential.elementa.constraints.* import gg.essential.elementa.dsl.* import gg.essential.elementa.state.State +import net.minecraft.client.resource.language.I18n import java.util.concurrent.CancellationException import kotlin.time.Duration @@ -33,7 +35,7 @@ class GuiPathingKt( val java: GuiPathing, val replayHandler: ReplayHandler, ) { - private val state = KeyframeState(java.mod, this) + val state = KeyframeState(java.mod, this) private val overlay = replayHandler.overlay private val mod = java.mod private val core = mod.core @@ -136,9 +138,7 @@ class GuiPathingKt( ) private val positionKeyframeButtonType = window.pollingState(PositionButtonType()) { - val time = state.selectedPositionKeyframes.get().keys.firstOrNull() - ?: timeline.cursor.position.get() - val keyframe = state.positionKeyframes.get()[time] + val keyframe = state.selectedPositionKeyframes.get().values.firstOrNull() PositionButtonType(when { keyframe?.entityId != null -> KeyframeType.SPECTATOR keyframe == null && !replayHandler.isCameraView -> KeyframeType.SPECTATOR @@ -159,13 +159,11 @@ class GuiPathingKt( }) } }.onMouseClick { - java.toggleKeyframe(SPPath.POSITION, false) + toggleKeyframe(positionKeyframeButtonType.get().type) } childOf secondRow private val timeKeyframePresent: State = window.pollingState(false) { - val time = state.selectedTimeKeyframes.get().keys.firstOrNull() - ?: timeline.cursor.position.get() - val keyframe = state.timeKeyframes.get()[time] + val keyframe = state.selectedTimeKeyframes.get().values.firstOrNull() keyframe != null } @@ -182,7 +180,7 @@ class GuiPathingKt( }) } }.onMouseClick { - java.toggleKeyframe(SPPath.TIME, false) + toggleKeyframe(KeyframeType.TIME) } childOf secondRow val timeline by UITimeline().constrain { @@ -200,7 +198,7 @@ class GuiPathingKt( }.onLeftMouse { mouseX, _ -> val time = getTimeAt(mouseX) cursor.position.set(time) - mod.setSelected(null, 0) + state.selection.set(KeyframeState.Selection.EMPTY) }.onLeftClick { it.stopImmediatePropagation() }.onAnimationFrame { @@ -313,6 +311,69 @@ class GuiPathingKt( timeline.cursor.position.set(keyframeCursor + cursorPassed) timeline.cursor.ensureVisibleWithPadding() // Deselect keyframe to allow the user to add a new one right away - mod.setSelected(null, 0) + state.selection.set(KeyframeState.Selection.EMPTY) + } + + @JvmOverloads + fun toggleKeyframe(type: KeyframeType, neverSpectator: Boolean = false) { + val path = type.path + val time = timeline.cursor.position.get() + val timeline = mod.currentTimeline + + if (state.keyframes.values.all { it.get().isEmpty() } && time > Duration.seconds(1)) { + val text = I18n.translate("replaymod.gui.ingame.first_keyframe_not_at_start_warning") + GuiInfoPopup.open(overlay, *text.split("\\n").toTypedArray()) + } + + val selection = state.selection.get()[path] + if (selection.isEmpty()) { + // Nothing selected, create new keyframe + + // If a keyframe is already present at this time, cannot add another one + if (time in state[path].get()) { + return + } + + when (path) { + SPPath.TIME -> { + timeline.addTimeKeyframe(time.inWholeMilliseconds, replayHandler.replaySender.currentTimeStamp()) + } + SPPath.POSITION -> { + val camera = replayHandler.cameraEntity + var spectatedId = -1 + if (!replayHandler.isCameraView && !neverSpectator) { + spectatedId = replayHandler.overlay.minecraft.getCameraEntity()!!.entityId + } + timeline.addPositionKeyframe(time.inWholeMilliseconds, + camera.x, camera.y, camera.z, + camera.yaw, camera.pitch, camera.roll, + spectatedId) + } + } + } else { + // Keyframe(s) selected, remove them + state.selection.set { it.mutate { this[path].clear() } } + val changes = selection.map { timeline.removeKeyframe(path, it.inWholeMilliseconds) } + timeline.timeline.pushChange(CombinedChange.createFromApplied(*changes.toTypedArray())) + } + + state.update() + } + + fun deleteButtonPressed(): Boolean { + val timeline = mod.currentTimeline ?: return false + + val selection = state.selection.get().toMap() + if (selection.all { it.value.isEmpty() }) { + return false + } + + state.selection.set(KeyframeState.Selection.EMPTY) + val changes = selection.flatMap { (path, keyframes) -> + keyframes.map { timeline.removeKeyframe(path, it.inWholeMilliseconds) } + } + timeline.timeline.pushChange(CombinedChange.createFromApplied(*changes.toTypedArray())) + + return true } } \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt index e576f3d6..340ddc63 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/KeyframeState.kt @@ -21,10 +21,16 @@ class KeyframeState( private var lastTimeline: SPTimeline? = null private var lastChange: Change? = null - val selection = BasicState(Selection(emptySet(), emptySet())).lazy() + val selection = BasicState(Selection.EMPTY).lazy() val timeKeyframes = BasicState(emptyMap()).lazy() val positionKeyframes = BasicState(emptyMap()).lazy() + operator fun get(path: SPTimeline.SPPath) = when (path) { + SPTimeline.SPPath.TIME -> timeKeyframes + SPTimeline.SPPath.POSITION -> positionKeyframes + } + val keyframes = SPTimeline.SPPath.values().associateWith { get(it) } + val selectionPositionKeyframes = selection.map { it.positionKeyframes } val selectionTimeKeyframes = selection.map { it.timeKeyframes } @@ -33,13 +39,18 @@ class KeyframeState( val selectedPositionKeyframes = positionKeyframes.zip(selectionPositionKeyframes) .map { (keyframes, selection) -> keyframes.filterKeys { it in selection } } - fun update() { - val selectedTime = setOf(Duration.milliseconds(mod.selectedTime)) + init { + selection.inner.onSetValue { + if (it != Selection.EMPTY) { + gui.java.overlay.timeline.selectedMarker = null + } + } + } - selection.set(Selection( - if (mod.selectedPath == SPTimeline.SPPath.TIME) selectedTime else emptySet(), - if (mod.selectedPath == SPTimeline.SPPath.POSITION) selectedTime else emptySet(), - )) + fun update() { + if (gui.java.overlay.timeline.selectedMarker != null) { + selection.set(Selection.EMPTY) + } val timeline = mod.currentTimeline val change = timeline.timeline.peekUndoStack() @@ -69,6 +80,36 @@ class KeyframeState( lastChange = null } + @Deprecated("does not support multi-selection") + fun getSelectedPath(): SPTimeline.SPPath? { + val selection = selection.get() + return when { + selection.timeKeyframes.isNotEmpty() -> SPTimeline.SPPath.TIME + selection.positionKeyframes.isNotEmpty() -> SPTimeline.SPPath.POSITION + else -> null + } + } + + @Deprecated("does not support multi-selection") + fun getSelectedTime(): Long { + val selection = selection.get() + val time = selection.timeKeyframes.firstOrNull() ?: selection.positionKeyframes.firstOrNull() ?: Duration.ZERO + return time.inWholeMilliseconds + } + + @Deprecated("does not support multi-selection") + fun setSelected(path: SPTimeline.SPPath?, time: Long) { + val keyframeSet = mutableSetOf(Duration.milliseconds(time)) + selection.set(when (path) { + SPTimeline.SPPath.TIME -> Selection.EMPTY.mutate { timeKeyframes = keyframeSet } + SPTimeline.SPPath.POSITION -> Selection.EMPTY.mutate { positionKeyframes = keyframeSet } + null -> Selection.EMPTY + }) + } + + fun isSelected(path: SPTimeline.SPPath, time: Long): Boolean = + Duration.milliseconds(time) in selection.get()[path] + data class TimeKeyframe( val replayTime: Duration, ) @@ -79,10 +120,67 @@ class KeyframeState( val entityId: Int?, ) - data class Selection( - val timeKeyframes: Set, - val positionKeyframes: Set, + class Selection( + timeKeyframes: Set, + positionKeyframes: Set, + ) { + val timeKeyframes: Set = timeKeyframes.toSortedSet() + val positionKeyframes: Set = positionKeyframes.toSortedSet() + + constructor(paths: Map>) : this( + paths[SPTimeline.SPPath.TIME] ?: emptySet(), + paths[SPTimeline.SPPath.POSITION] ?: emptySet(), + ) + + fun toMap() = SPTimeline.SPPath.values().associateWith { get(it) } + + val size: Int + get() = toMap().values.sumOf { it.size } + + operator fun get(path: SPTimeline.SPPath) = when (path) { + SPTimeline.SPPath.TIME -> timeKeyframes + SPTimeline.SPPath.POSITION -> positionKeyframes + } + + inline fun mutate(block: MutableSelection.() -> Unit): Selection = + MutableSelection(this).apply(block).toSelection() + + inline fun map(func: (Duration) -> Duration): Selection = + mapWithPath { _, duration -> func(duration) } + + inline fun mapWithPath( + reverse: Boolean = false, + func: (SPTimeline.SPPath, Duration) -> Duration, + ): Selection = Selection(toMap().mapValues { (path, keyframes) -> + if (!reverse) { + keyframes.mapTo(mutableSetOf()) { func(path, it) } + } else { + keyframes.reversed().map { func(path, it) }.asReversed().toSet() + } + }) + + companion object { + fun single(path: SPTimeline.SPPath, time: Duration): Selection = Selection(mapOf(path to setOf(time))) + + @JvmField + val EMPTY = Selection(emptySet(), emptySet()) + } + } + + class MutableSelection( + var timeKeyframes: MutableSet, + var positionKeyframes: MutableSet, ) { + constructor(selection: Selection) : this( + selection.timeKeyframes.toMutableSet(), + selection.positionKeyframes.toMutableSet(), + ) + + fun toSelection() = Selection( + timeKeyframes.toSet(), + positionKeyframes.toSet(), + ) + operator fun get(path: SPTimeline.SPPath) = when (path) { SPTimeline.SPPath.TIME -> timeKeyframes SPTimeline.SPPath.POSITION -> positionKeyframes diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt index 78d62b16..bd29b7b7 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt @@ -19,6 +19,7 @@ import gg.essential.elementa.constraints.* import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.Effect import gg.essential.elementa.effects.ScissorEffect +import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack import net.minecraft.client.render.Tessellator import net.minecraft.client.render.VertexFormats @@ -132,23 +133,13 @@ class UITimelineKeyframes( dragging.change?.undo(spTimeline.timeline) // Compute new time - val oldTime = dragging.keyframe - var newTime = (oldTime + parentOfType()!!.unit.get() * diff.toDouble()) - .coerceAtLeast(Duration.ZERO) + val deltaTime = parentOfType()!!.unit.get() * diff.toDouble() - // If there already is a keyframe at the target time, then increase the time by one until there is none - while (spTimeline.getKeyframe(path, newTime.inWholeMilliseconds) != null) { - newTime += Duration.Companion.milliseconds(1) - } + // Move keyframe to new position and store change for later undoing / pushing to history + dragging.change = state.moveKeyframes(dragging.selection, deltaTime) - // Move keyframe to new position and - // store change for later undoing / pushing to history - dragging.change = - spTimeline.moveKeyframe(path, oldTime.inWholeMilliseconds, newTime.inWholeMilliseconds) + // Refresh keyframe state (required because we do not yet commit the Change) state.refreshKeyframes() - - // Selected keyframe has been moved - state.mod.setSelected(path, newTime.inWholeMilliseconds) } onMouseRelease { @@ -159,7 +150,7 @@ class UITimelineKeyframes( } data class Dragging( - val keyframe: Duration, + val selection: KeyframeState.Selection, val mouseX: Float, var passedThreshold: Boolean = false, /** @@ -187,10 +178,41 @@ class UITimelineKeyframes( height = 5.pixels } + // Will be selected on mouse release except if we ended up dragging instead + var delayedSelect: Pair? = null + onMouseRelease { + val (selection, dragging) = delayedSelect.also { delayedSelect = null } ?: return@onMouseRelease + if (!dragging.passedThreshold) { + state.selection.set(selection) + } + } + onLeftClick(1) { event -> event.stopImmediatePropagation() - state.mod.setSelected(type.path, time.inWholeMilliseconds) - parentOfType()?.dragging = Row.Dragging(time, event.absoluteX) + val row = parentOfType() + val selection = state.selection.get() + when { + // When Shift or Ctrl (cause Shift moves the camera) is held + UKeyboard.isShiftKeyDown() || UKeyboard.isCtrlKeyDown() -> { + // flip the selection state of this keyframe + state.selection.set(selection.mutate { this[type.path].toggle(time) }) + row?.dragging = null + } + // If multiple keyframes are selected and this is one of them + selection.size > 1 && time in selection[type.path] -> { + // We need delay the selection in case the user wants to drag all of them around + delayedSelect = Pair( + KeyframeState.Selection.single(type.path, time), + Row.Dragging(selection, event.absoluteX).also { row?.dragging = it }, + ) + } + else -> { + // Otherwise, select this keyframe and get ready for dragging + val newSelection = KeyframeState.Selection.single(type.path, time) + state.selection.set(newSelection) + row?.dragging = Row.Dragging(newSelection, event.absoluteX) + } + } } onLeftClick(2) { event -> event.stopImmediatePropagation() diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/operations.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/operations.kt new file mode 100644 index 00000000..6eb4e267 --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/operations.kt @@ -0,0 +1,71 @@ +package com.replaymod.simplepathing.gui + +import com.replaymod.core.utils.kt +import com.replaymod.core.utils.orNull +import com.replaymod.pathing.properties.CameraProperties +import com.replaymod.pathing.properties.SpectatorProperty +import com.replaymod.pathing.properties.TimestampProperty +import com.replaymod.replaystudio.pathing.change.Change +import com.replaymod.replaystudio.pathing.change.CombinedChange +import com.replaymod.simplepathing.SPTimeline +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f +import kotlin.time.Duration + +fun KeyframeState.moveKeyframes(keyframes: KeyframeState.Selection, deltaTime: Duration): Change { + if (deltaTime == Duration.ZERO) { + return CombinedChange.createFromApplied() + } + + val spTimeline = mod.currentTimeline ?: throw IllegalStateException() + + val changes = mutableListOf() + val newSelection = keyframes.mapWithPath(reverse = deltaTime.isPositive()) { path, oldTime -> + var newTime = (oldTime + deltaTime).coerceAtLeast(Duration.ZERO) + + // If there already is a keyframe at the target time, then increase the time by one until there is none + while (spTimeline.getKeyframe(path, newTime.inWholeMilliseconds) != null) { + newTime += Duration.milliseconds(1) + } + + changes += spTimeline.moveKeyframe(path, oldTime.inWholeMilliseconds, newTime.inWholeMilliseconds) + + newTime + } + + this.selection.set(newSelection) + + return CombinedChange.createFromApplied(*changes.toTypedArray()) +} + +fun KeyframeState.movePosKeyframes(keyframes: KeyframeState.Selection, deltaPos: Vector3f, deltaRot: Vector3f): Change { + val spTimeline = mod.currentTimeline ?: throw IllegalStateException() + + val changes = mutableListOf() + for (time in keyframes[SPTimeline.SPPath.POSITION]) { + val keyframe = spTimeline.getKeyframe(SPTimeline.SPPath.POSITION, time.inWholeMilliseconds) ?: continue + if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent) continue + val (x, y, z) = keyframe.getValue(CameraProperties.POSITION).orNull?.kt ?: continue + val (yaw, pitch, roll) = keyframe.getValue(CameraProperties.ROTATION).orNull?.kt ?: continue + changes += spTimeline.updatePositionKeyframe( + time.inWholeMilliseconds, + x + deltaPos.x, y + deltaPos.y, z + deltaPos.z, + yaw + deltaRot.x, pitch + deltaRot.y, roll + deltaRot.z + ) + } + + return CombinedChange.createFromApplied(*changes.toTypedArray()) +} + +fun KeyframeState.moveTimeKeyframes(keyframes: KeyframeState.Selection, deltaTime: Duration): Change { + val spTimeline = mod.currentTimeline ?: throw IllegalStateException() + + val changes = mutableListOf() + for (time in keyframes[SPTimeline.SPPath.TIME]) { + val keyframe = spTimeline.getKeyframe(SPTimeline.SPPath.TIME, time.inWholeMilliseconds) ?: continue + val replayTime = keyframe.getValue(TimestampProperty.PROPERTY).orNull ?: continue + val newReplayTime = (replayTime + deltaTime.inWholeMilliseconds).coerceAtLeast(0) + changes += spTimeline.updateTimeKeyframe(time.inWholeMilliseconds, newReplayTime.toInt()) + } + + return CombinedChange.createFromApplied(*changes.toTypedArray()) +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt index 7f4af096..2839276e 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionKeyframePanel.kt @@ -6,12 +6,16 @@ import com.replaymod.core.gui.utils.Resources import com.replaymod.core.gui.utils.hiddenChildOf import com.replaymod.core.gui.utils.onSetValueAndNow import com.replaymod.core.utils.i18n +import com.replaymod.core.utils.toVector3f import com.replaymod.core.utils.transpose import com.replaymod.replay.gui.overlay.panels.UIToggleablePanel import com.replaymod.replaystudio.pathing.change.Change import com.replaymod.replaystudio.pathing.change.CombinedChange import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.gui.KeyframeState +import com.replaymod.simplepathing.gui.moveKeyframes +import com.replaymod.simplepathing.gui.movePosKeyframes +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f import gg.essential.elementa.UIComponent import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIText @@ -118,23 +122,26 @@ class UIPositionKeyframePanel( } private fun apply() { - val time = state.selectedPositionKeyframes.get().keys.firstOrNull() ?: return + val selection = state.selection.get() + val (time, keyframe) = state.selectedPositionKeyframes.get().entries.firstOrNull() ?: return + val (pos, rot) = keyframe val timeline = state.mod.currentTimeline - var change: Change = timeline.updatePositionKeyframe( - time.inWholeMilliseconds, - translateX.value, translateY.value, translateZ.value, - rotateX.value.toFloat(), rotateY.value.toFloat(), rotateZ.value.toFloat(), + val newPos = Triple(translateX.value, translateY.value, translateZ.value) + val newRot = Triple(rotateX.value, rotateY.value, rotateZ.value) + var change = state.movePosKeyframes(selection, + Vector3f.sub(newPos.toVector3f(), pos.toVector3f(), null), + Vector3f.sub(newRot.toVector3f(), rot.toVector3f(), null), ) val newTime = timeField.input.value if (newTime != time) { change = CombinedChange.createFromApplied( change, - timeline.moveKeyframe(SPTimeline.SPPath.POSITION, time.inWholeMilliseconds, newTime.inWholeMilliseconds) + state.moveKeyframes(selection, newTime - time) ) - state.mod.setSelected(SPTimeline.SPPath.POSITION, newTime.inWholeMilliseconds) } + timeline.timeline.pushChange(change) } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt index 1e6d5426..c50fb217 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimePanel.kt @@ -4,8 +4,9 @@ import com.replaymod.core.gui.common.UIButton import com.replaymod.core.gui.common.input.* import com.replaymod.core.gui.utils.* import com.replaymod.core.utils.i18n -import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.gui.KeyframeState +import com.replaymod.simplepathing.gui.moveKeyframes +import com.replaymod.simplepathing.gui.moveTimeKeyframes import gg.essential.elementa.components.UIBlock import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIText @@ -109,15 +110,11 @@ class UITimePanel( }.apply { input.onActivate { val newReplayTime = input.value - val selection = state.selectedTimeKeyframes.get().entries.firstOrNull() - if (selection != null) { - val (videoTime, keyframe) = (selection) - if (keyframe.replayTime != newReplayTime) { - val timeline = state.mod.currentTimeline - timeline.timeline.pushChange(timeline.updateTimeKeyframe( - videoTime.inWholeMilliseconds, - newReplayTime.inWholeMilliseconds.toInt(), - )) + val oldReplayTime = state.selectedTimeKeyframes.get().values.firstOrNull()?.replayTime + if (oldReplayTime != null) { + if (oldReplayTime != newReplayTime) { + val change = state.moveTimeKeyframes(state.selection.get(), newReplayTime - oldReplayTime) + state.mod.currentTimeline.timeline.pushChange(change) } } else { state.gui.replayHandler.doJump(newReplayTime.inWholeMilliseconds.toInt(), true) @@ -137,17 +134,15 @@ class UITimePanel( }.apply { input.onActivate { val newVideoTime = input.value - val selection = state.selectedTimeKeyframes.get().entries.firstOrNull() - if (selection != null) { - val (oldVideoTime, _) = selection + val oldVideoTime = state.selectedTimeKeyframes.get().keys.firstOrNull() + if (oldVideoTime != null) { if (newVideoTime != oldVideoTime) { + val timeKeyframes = KeyframeState.Selection.EMPTY.mutate { + timeKeyframes += state.selectionTimeKeyframes.get() + } val timeline = state.mod.currentTimeline - timeline.timeline.pushChange(timeline.moveKeyframe( - SPTimeline.SPPath.TIME, - oldVideoTime.inWholeMilliseconds, - newVideoTime.inWholeMilliseconds, - )) - state.mod.setSelected(SPTimeline.SPPath.TIME, newVideoTime.inWholeMilliseconds) + val change = state.moveKeyframes(timeKeyframes, newVideoTime - oldVideoTime) + timeline.timeline.pushChange(change) } } else { state.gui.timeline.cursor.position.set(newVideoTime) From 898fd85ff05e9d6e36d24f9bd39fe6920c937977 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 25 Sep 2021 23:28:05 +0200 Subject: [PATCH 013/132] Add position offset panel --- .../replaymod/core/KeyBindingRegistry.java | 4 + .../com/replaymod/core/versions/MCVer.java | 6 + .../simplepathing/ReplayModSimplePathing.java | 29 +++ .../com/replaymod/core/gui/common/UIButton.kt | 11 +- .../core/gui/common/input/UIDecimalInput.kt | 70 +++++ .../common/input/UIInputOrExpressionField.kt | 7 + .../com/replaymod/core/gui/utils/Axis.kt | 8 + .../simplepathing/gui/GuiPathingKt.kt | 9 + .../gui/panels/UIPositionOffsetPanel.kt | 239 ++++++++++++++++++ .../replaymod/icons/pos_offset_panel.png | Bin 0 -> 120 bytes .../assets/replaymod/icons/settings.license | 22 ++ .../assets/replaymod/icons/settings.png | Bin 0 -> 11440 bytes 12 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/input/UIDecimalInput.kt create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionOffsetPanel.kt create mode 100644 src/main/resources/assets/replaymod/icons/pos_offset_panel.png create mode 100644 src/main/resources/assets/replaymod/icons/settings.license create mode 100644 src/main/resources/assets/replaymod/icons/settings.png diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java index 6223717c..d92e49cc 100644 --- a/src/main/java/com/replaymod/core/KeyBindingRegistry.java +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -176,6 +176,10 @@ public boolean isBound() { //#endif } + public boolean isPressed() { + return keyBinding.isPressed(); + } + public void trigger() { KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding; acc.setPressTime(acc.getPressTime() + 1); diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index ac4dd754..16e3cac6 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -430,6 +430,7 @@ public static abstract class Keyboard { //#if MC>=11400 public static final int KEY_LCONTROL = GLFW.GLFW_KEY_LEFT_CONTROL; public static final int KEY_LSHIFT = GLFW.GLFW_KEY_LEFT_SHIFT; + public static final int KEY_LALT = GLFW.GLFW_KEY_LEFT_ALT; public static final int KEY_ESCAPE = GLFW.GLFW_KEY_ESCAPE; public static final int KEY_HOME = GLFW.GLFW_KEY_HOME; public static final int KEY_END = GLFW.GLFW_KEY_END; @@ -437,6 +438,8 @@ public static abstract class Keyboard { public static final int KEY_DOWN = GLFW.GLFW_KEY_DOWN; public static final int KEY_LEFT = GLFW.GLFW_KEY_LEFT; public static final int KEY_RIGHT = GLFW.GLFW_KEY_RIGHT; + public static final int KEY_PAGE_UP = GLFW.GLFW_KEY_PAGE_UP; + public static final int KEY_PAGE_DOWN = GLFW.GLFW_KEY_PAGE_DOWN; public static final int KEY_BACK = GLFW.GLFW_KEY_BACKSPACE; public static final int KEY_DELETE = GLFW.GLFW_KEY_DELETE; public static final int KEY_RETURN = GLFW.GLFW_KEY_ENTER; @@ -471,6 +474,7 @@ public static abstract class Keyboard { //#else //$$ public static final int KEY_LCONTROL = org.lwjgl.input.Keyboard.KEY_LCONTROL; //$$ public static final int KEY_LSHIFT = org.lwjgl.input.Keyboard.KEY_LSHIFT; + //$$ public static final int KEY_LALT = org.lwjgl.input.Keyboard.KEY_LALT; //$$ public static final int KEY_ESCAPE = org.lwjgl.input.Keyboard.KEY_ESCAPE; //$$ public static final int KEY_HOME = org.lwjgl.input.Keyboard.KEY_HOME; //$$ public static final int KEY_END = org.lwjgl.input.Keyboard.KEY_END; @@ -478,6 +482,8 @@ public static abstract class Keyboard { //$$ public static final int KEY_DOWN = org.lwjgl.input.Keyboard.KEY_DOWN; //$$ public static final int KEY_LEFT = org.lwjgl.input.Keyboard.KEY_LEFT; //$$ public static final int KEY_RIGHT = org.lwjgl.input.Keyboard.KEY_RIGHT; + //$$ public static final int KEY_PAGE_UP = org.lwjgl.input.Keyboard.KEY_PRIOR; + //$$ public static final int KEY_PAGE_DOWN = org.lwjgl.input.Keyboard.KEY_NEXT; //$$ public static final int KEY_BACK = org.lwjgl.input.Keyboard.KEY_BACK; //$$ public static final int KEY_DELETE = org.lwjgl.input.Keyboard.KEY_DELETE; //$$ public static final int KEY_RETURN = org.lwjgl.input.Keyboard.KEY_RETURN; diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 03217ae4..d5276558 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -46,6 +46,14 @@ public class ReplayModSimplePathing extends EventRegistrations implements Module public KeyBindingRegistry.Binding keyTimeKeyframe; public KeyBindingRegistry.Binding keySyncTime; + public KeyBindingRegistry.Binding keyRotationMode; + public KeyBindingRegistry.Binding keyPlusX; + public KeyBindingRegistry.Binding keyPlusY; + public KeyBindingRegistry.Binding keyPlusZ; + public KeyBindingRegistry.Binding keyMinusX; + public KeyBindingRegistry.Binding keyMinusY; + public KeyBindingRegistry.Binding keyMinusZ; + public static Logger LOGGER = LogManager.getLogger(); private GuiPathing guiPathing; @@ -116,6 +124,27 @@ public void registerKeyBindings(KeyBindingRegistry registry) { guiPathing.kt.toggleKeyframe(KeyframeType.POSITION); } }, true); + + keyRotationMode = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.rotation", Keyboard.KEY_LALT, () -> {}, true); + keyPlusX = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.px", Keyboard.KEY_RIGHT, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getPlusXButton().activate(); + }, true); + keyPlusY = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.py", Keyboard.KEY_PAGE_UP, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getPlusYButton().activate(); + }, true); + keyPlusZ = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.pz", Keyboard.KEY_UP, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getPlusZButton().activate(); + }, true); + keyMinusX = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.mx", Keyboard.KEY_LEFT, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getMinusXButton().activate(); + }, true); + keyMinusY = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.my", Keyboard.KEY_PAGE_DOWN, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getMinusYButton().activate(); + }, true); + keyMinusZ = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.offset.mz", Keyboard.KEY_DOWN, () -> { + if (guiPathing != null) guiPathing.kt.getPositionOffsetPanel().getMinusZButton().activate(); + }, true); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Z, () -> { if (Screen.hasControlDown() && currentTimeline != null) { Timeline timeline = currentTimeline.getTimeline(); diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt index 251c1733..a9f0139d 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt @@ -1,6 +1,7 @@ package com.replaymod.core.gui.common import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock import gg.essential.elementa.components.UIImage import gg.essential.elementa.components.UIText import gg.essential.elementa.constraints.CenterConstraint @@ -8,6 +9,7 @@ import gg.essential.elementa.dsl.* import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.invisible import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import net.minecraft.client.MinecraftClient @@ -43,9 +45,14 @@ class UIButton : UIComponent() { constrain { width = 200.pixels height = 20.pixels + color = Color.WHITE.invisible().toConstraint() } } + fun label(text: String) = label { + bindText(BasicState(text)) + } + fun label(configure: UIText.() -> Unit) = apply { val component = UIText() childOf this component.constrain { @@ -97,7 +104,9 @@ class UIButton : UIComponent() { val bottom = getBottom().toDouble() UGraphics.bindTexture(0, WIDGETS_TEXTURE) - UI4Slice.draw(matrixStack, left, right, top, bottom, getColor(), background.get()) + UI4Slice.draw(matrixStack, left, right, top, bottom, Color.WHITE, background.get()) + + UIBlock.drawBlock(matrixStack, getColor(), left + 1, top + 1, right - 1, bottom - 1) super.draw(matrixStack) } diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIDecimalInput.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIDecimalInput.kt new file mode 100644 index 00000000..76ed4c92 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIDecimalInput.kt @@ -0,0 +1,70 @@ +package com.replaymod.core.gui.common.input + +import java.math.BigDecimal +import java.math.MathContext +import java.text.DecimalFormat + +class UIDecimalInput( + precision: Int = 5, +) : UIAdvancedTextInput() { + private val mathContext = MathContext(precision) + private val format = DecimalFormat("0." + "0".repeat(precision)).apply { + minimumFractionDigits = precision + maximumFractionDigits = precision + } + + private fun BigDecimal.formatString() = format.format(this) + + private fun String.parseDecimal(): BigDecimal? = try { + BigDecimal(this, mathContext) + } catch (e: NumberFormatException) { + null + } + + var value: BigDecimal + get() = getText().parseDecimal() ?: BigDecimal.ZERO + set(value) { + val str = value.formatString() + if (getText() != str) { + setText(str) + } + } + + override fun shouldReplaceExistingText(add: AddTextOperation): Boolean = + super.shouldReplaceExistingText(add) && + (add.newText.length != 1 || getText()[add.startPos.column] != '.' || add.newText == ".") + + override fun fixText(): TextOperation? { + val unformatted = getText() + val duration = getText().parseDecimal() ?: return null + val formatted = duration.formatString() + + if (formatted != unformatted) { + val start = LinePosition(0, 0, false) + return ReplaceTextOperation( + AddTextOperation(formatted, start), + RemoveTextOperation(start, LinePosition(0, unformatted.length, false), false) + ) + } + + return NoOperation() + } + + override fun commitTextOperation(operation: TextOperation) { + var op = operation + + // If they try to delete the separator, move the cursor before it instead + if (op is RemoveTextOperation && op.text == ".") { + cursor = op.startPos + otherSelectionEnd = cursor + return + } + + // If they add the final digit before a separator, move the cursor past the separator as well + if (op is AddTextOperation && getText().drop(op.endPos.column).startsWith(".")) { + op = AddTextOperation(op.newText + ".", op.startPos) + } + + super.commitTextOperation(op) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt index cb14f5ee..589d2374 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/input/UIInputOrExpressionField.kt @@ -4,6 +4,7 @@ import com.replaymod.core.gui.common.elementa.UITextInput import com.replaymod.core.gui.utils.hiddenChildOf import gg.essential.elementa.components.UIContainer import gg.essential.elementa.dsl.* +import java.math.BigDecimal import java.util.* import java.util.concurrent.TimeUnit import kotlin.time.Duration @@ -72,5 +73,11 @@ class UIInputOrExpressionField( { "%.3f".format(Locale.ROOT, value.toDouble(TimeUnit.SECONDS)) }, { value = Duration.seconds(it) }, ) + + fun forDecimalInput(input: UIDecimalInput = UIDecimalInput()) = UIInputOrExpressionField( + UIInputField(input), + { getText() }, + { value = BigDecimal(it) }, + ) } } \ No newline at end of file diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt b/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt index c7abccf6..31aa524d 100644 --- a/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt +++ b/src/main/kotlin/com/replaymod/core/gui/utils/Axis.kt @@ -1,5 +1,6 @@ package com.replaymod.core.gui.utils +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f import java.awt.Color enum class Axis( @@ -8,4 +9,11 @@ enum class Axis( X(Color.RED), Y(Color.GREEN), Z(Color.BLUE), + ; + + fun toVector3f() = when (this) { + X -> Vector3f(1f, 0f, 0f) + Y -> Vector3f(0f, 1f, 0f) + Z -> Vector3f(0f, 0f, 1f) + } } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 713eea1f..8eb416a7 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -21,6 +21,7 @@ import com.replaymod.simplepathing.SPTimeline import com.replaymod.simplepathing.SPTimeline.SPPath import com.replaymod.simplepathing.Setting import com.replaymod.simplepathing.gui.panels.UIPositionKeyframePanel +import com.replaymod.simplepathing.gui.panels.UIPositionOffsetPanel import com.replaymod.simplepathing.gui.panels.UITimePanel import de.johni0702.minecraft.gui.popup.GuiInfoPopup import gg.essential.elementa.components.UIContainer @@ -270,6 +271,14 @@ class GuiPathingKt( }, 0) } hiddenChildOf window + val positionOffsetPanel by UIPositionOffsetPanel(window, state).apply { + toggleButton.constrain { + x = SiblingConstraint(4f) + y = 0.pixels(alignOpposite = true) + } + overlay.kt.bottomLeftPanel.insertChildAt(toggleButton, 0) + } hiddenChildOf window + init { val speedValue = window.pollingState { overlay.speedSlider.value } val replayTime = window.pollingState { replayHandler.replaySender.currentTimeStamp() } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionOffsetPanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionOffsetPanel.kt new file mode 100644 index 00000000..9607f7ef --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UIPositionOffsetPanel.kt @@ -0,0 +1,239 @@ +package com.replaymod.simplepathing.gui.panels + +import com.replaymod.core.KeyBindingRegistry +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.common.input.UIInputOrExpressionField +import com.replaymod.core.gui.utils.* +import com.replaymod.core.utils.i18n +import com.replaymod.replay.gui.overlay.panels.UIToggleablePanel +import com.replaymod.simplepathing.gui.KeyframeState +import com.replaymod.simplepathing.gui.movePosKeyframes +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.withAlpha +import java.awt.Color +import java.math.BigDecimal + +class UIPositionOffsetPanel( + val window: Window, + val state: KeyframeState, +) : UIToggleablePanel() { + + init { + toggleButton.texture(Resources.icon("pos_offset_panel")) + + constrain { + x = 0.pixels() boundTo toggleButton + y = 2.pixels(alignOutside = true) boundTo toggleButton + width = ChildBasedSizeConstraint() + 10.pixels + height = ChildBasedSizeConstraint() + 10.pixels + } + + open.onSetValue { if (!it) keyBindsPanel.open.set(false) } + } + + private val container: UIContainer by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = basicWidthConstraint { topButtons.getWidth() } + height = ChildBasedSizeConstraint() + } childOf this + + private val translateContainer by UIContainer().constrain { + y = SiblingConstraint(3f) + width = 100.percent + height = 20.pixels + } childOf container + + private val rotateContainer by UIContainer().constrain { + y = SiblingConstraint(3f) + width = 100.percent + height = 20.pixels + } childOf container + + private val topButtons by UIContainer().constrain { + y = SiblingConstraint(5f) + width = ChildBasedSizeConstraint() + height = 20.pixels + } childOf container + + private val bottomButtons by UIContainer().constrain { + y = SiblingConstraint(3f) + width = 100.percent + height = 20.pixels + } childOf container + + private val translateField by UIInputOrExpressionField.forDecimalInput().constrain { + x = 0.pixels(alignOpposite = true) + width = 60.pixels + height = 20.pixels + }.apply { + input.value = BigDecimal(1) + input.onActivate { releaseWindowFocus() } + } childOf translateContainer + + private val translateLabel by UIText("Translate:").constrain { + x = 1.pixel(alignOutside = true) boundTo translateField + y = CenterConstraint() + } childOf translateContainer + + private val rotateField by UIInputOrExpressionField.forDecimalInput().constrain { + x = 0.pixels(alignOpposite = true) + width = 60.pixels + height = 20.pixels + }.apply { + input.value = BigDecimal(15) + input.onActivate { releaseWindowFocus() } + } childOf rotateContainer + + private val rotateLabel by UIText("Rotate:").constrain { + x = 1.pixel(alignOutside = true) boundTo rotateField + y = CenterConstraint() + } childOf rotateContainer + + private val rotateButton by UIButton().constrain { + x = SiblingConstraint(padding = 3f) + width = ChildBasedSizeConstraint() + 10.pixels + }.label("R. mode").highlightWhenActive(Color.ORANGE, state.mod.keyRotationMode) childOf topButtons + + val plusXButton by OffsetButton(Axis.X, state.mod.keyPlusX) childOf topButtons + val plusYButton by OffsetButton(Axis.Y, state.mod.keyPlusY) childOf topButtons + val plusZButton by OffsetButton(Axis.Z, state.mod.keyPlusZ) childOf topButtons + + val minusXButton by OffsetButton(Axis.X, state.mod.keyMinusX, plusXButton) childOf bottomButtons + val minusYButton by OffsetButton(Axis.Y, state.mod.keyMinusY, plusYButton) childOf bottomButtons + val minusZButton by OffsetButton(Axis.Z, state.mod.keyMinusZ, plusZButton) childOf bottomButtons + + inner class OffsetButton( + private val axis: Axis, + keyBinding: KeyBindingRegistry.Binding, + private val inverse: OffsetButton? = null, + ) : UIContainer() { + init { + constrain { + x = if (inverse != null) { + 0.pixels boundTo inverse.button + } else { + SiblingConstraint(padding = 3f) + } + width = 20.pixels + height = 20.pixels + } + } + + val button by UIButton().constrain { + width = 100.percent + height = 100.percent + }.label((if (inverse == null) "+" else "-") + axis).onLeftClick { + activate() + }.highlightWhenActive(axis.color, keyBinding) childOf this + + fun activate() { + val timeline = state.mod.currentTimeline ?: return + val vector = axis.toVector3f() + if (inverse != null) vector.scale(-1f) + val (pos, rot) = if (state.mod.keyRotationMode.isPressed) { + Vector3f() to vector + } else { + vector to Vector3f() + } + pos.scale(translateField.input.value.toFloat()) + rot.scale(rotateField.input.value.toFloat()) + val change = state.movePosKeyframes(state.selection.get(), pos, rot) + timeline.timeline.pushChange(change) + } + } + + private val keyBindsPanel by KeyBindsPanel().constrain { + x = SiblingConstraint(padding = 2f) boundTo this@UIPositionOffsetPanel + y = 0.pixels(alignOpposite = true) boundTo this@UIPositionOffsetPanel + }.apply { + toggleButton.constrain { + x = 0.pixels + y = 0.pixels + } childOf bottomButtons + } hiddenChildOf window + + private inner class KeyBindsPanel : UIToggleablePanel() { + init { + toggleButton.texture(Resources.icon("settings")) + + constrain { + width = ChildBasedSizeConstraint() + 10.pixels + height = ChildBasedSizeConstraint() + 10.pixels + } + } + + private val container by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = basicWidthConstraint { it.children.first().getWidth() } + height = ChildBasedSizeConstraint() + } childOf this + + init { + Row(state.mod.keyRotationMode).constrain { width = ChildBasedSizeConstraint(5f) } childOf container + Row(state.mod.keyPlusX) childOf container + Row(state.mod.keyMinusX) childOf container + Row(state.mod.keyPlusY) childOf container + Row(state.mod.keyMinusY) childOf container + Row(state.mod.keyPlusZ) childOf container + Row(state.mod.keyMinusZ) childOf container + } + + inner class Row(keyBinding: KeyBindingRegistry.Binding) : UIContainer() { + init { + constrain { + y = SiblingConstraint() + width = 100.percent + height = 20.pixels + } + } + + private val label by UIText().constrain { + y = CenterConstraint() + }.setText(keyBinding.name.i18n()) childOf this + + private val button by UIButton().constrain { + x = 0.pixels(alignOpposite = true) + width = 80.pixels + }.label { + bindText(pollingState { if (keyBinding.isBound) keyBinding.boundKey else "" }) + }.onLeftClick { + // TODO need to somehow get our hands on the scan code + /* + val inputCatcher by UIContainer().constrain { + width = 100.percentOfWindow + height = 100.percentOfWindow + } childOf window + + inputCatcher.setFloating(true) + */ + } childOf this + } + } + + private fun UIButton.highlightWhenActive(highlightColor: Color, keyBinding: KeyBindingRegistry.Binding) = apply { + val keyPressed = pollingState { keyBinding.isPressed } + val buttonPressed = BasicState(false) + val highlighted = keyPressed.zip(buttonPressed).map { (a, b) -> a || b } + + constrain { + color = highlighted.map { highlightColor.withAlpha(if (it) 0.5f else 0f) }.toConstraint() + } + onLeftClick { + buttonPressed.set(true) + } + onMouseRelease { + buttonPressed.set(false) + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/icons/pos_offset_panel.png b/src/main/resources/assets/replaymod/icons/pos_offset_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..328ae255cd68209d63b7e4200791db5635bba11d GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=ou`Xqh(&L5f&^;_>wiy%6ICCi zP8>MkAaIz+(R_{)b7I!Ymrk!FJI?-I%r+@a;i__L*vYmXExuRRm~)+AVsMahI%8P7 SGYV)DgQu&X%Q~loCI$drQ6%dC literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/replaymod/icons/settings.license b/src/main/resources/assets/replaymod/icons/settings.license new file mode 100644 index 00000000..11273218 --- /dev/null +++ b/src/main/resources/assets/replaymod/icons/settings.license @@ -0,0 +1,22 @@ +The "settings" icon (three horizontal sliders) is based on ModMenu's settings icon. +Original: https://github.com/Prospector/ModMenu/blob/74200fb7baa697b4e865b15a707a560757a7ee8f/src/main/resources/assets/modmenu/textures/gui/configure_button.png + +Copyright (c) 2018-2020 Prospector + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/main/resources/assets/replaymod/icons/settings.png b/src/main/resources/assets/replaymod/icons/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..f8e9c6710955e65a3f62dcb6f3821e4bff799fc5 GIT binary patch literal 11440 zcmeHs2UJtrx^9pr2qINFF^GT|NJ2vIMF?FwNHG;cC-g2&1d-keO^Q@$0u~gc2m*o| z!A22LK#<-MloxdGefB=*-1qJo_uMna8)JvD0_&gu`sV-rZBFKjH8Rj*r01pw004|g zZ4G1cZ?>cBI1TyxW-K2g0C2h}(A3h~8081_AiCoTE;yiffCmnU^C#c|0RMM2xmH9$ z`FPF4`*C(^#B-UQ&|7{l`d26+Lb?rKMtLfEg`V~4v2=NOQViq2y+3r@P(J8~+MMgM zG1i}-wl6Pw@HB>)7p8B&#J;(8z_RST{Viytb!xY7V|l1nXkthCamx_J;N_1d!Cd9> z`)XlrMk{nSdz_#1dv9nYFm3RcPf`Z>i7T9Saj>$e6lxPMs@(`x4jX9?6O>T;@@7a? zDU6A0>3MJFYSui2fI8oq-5fP?oz9F042RnBLMhP@>g z>e;?^-Q-IT3c0YBx+kzl+Pb}N!Z0acH`w}gbdRgqI>xG*(f1v`#rCVxqDt3PMt`fk zG$bw9BG`A7F^p@G?Ng6iX8q(QwKL?q*mf-n)>|@T9?=lcJ~g1eYY$jl{qI)xL7EwZ#ufaXHi~v zavZvuyu->NH53Fe?HXrBRregTXi$2GtzEQf=1}BVe92jU3VsK3jHftPdp=@^?jxJ9 z*M?qmVC{|NU`9OZI9+i(;kbkgJ3#6{I3|Lr=`-d!v@8n~i)j92q75xgOTB8}uN1@U z)hC;2<0bYuibePQeKr#(!l)UO%+nmFJEgDQai>aMbW@nPSJbrpV5O)hKXAG9^(4)* zWXE;En+kDUK6mHK3Z}aBB!^&MT>8ar2+`~o)&c2e7Gw9`ZObp-dpY=U@xjc?wlIbWVJ6=W`j01RSn+O3nbNz>L>o+Pk8jO@(|f6Cv^+PS)V@+vBpLU@`I zNlcvoW*2nxd|TqFnpXSWO~8|++UB&K@kZ{UV(PVB7SVGs@G4*T+8K9&r;{OmnK%F% zzCt1Y#=o6EsI-#xQ(ii=aPhgbXzj8A(-ydtKp&UKXUo&{`o}PVs zaL}<};p>)Q@Hp#P~a5{M|ihIeJ}{rJaE+PZDDULD0* ziOo*D{H3?A;q&QBX7;_rfz%p*=C6(u2B{12#rY2bIXT-muo*sG?qBX*sCfGH(t_US zh27QSy*5%R`X&^Vo@%qJH{w zV%8K7icesB8GT}QfO;+_`0M&1Hs<=*t3g*k)5{>uS_Ku{SQb@vLhWPsU+sl-fH743 zDC>}c<7?3=@6;x%p>`7^GBMA!(O>9s7*gGXV>ZuNlD_AtQ@Qc26^6yv*0t8NYm3|G zm%1J=%N!4X(87tMvh91~bScT~k~`pc@AuDZgPV@lr{*b>4g-4t&)f^AtXzn<7QfvH_3 z#l|wAKub(CbV-&Ki5gWLcc4wDFXbAt%g`B{+ zL>50|oC(ud9dlrhMLmMcSx3W;D}2H_zf}$3Q~#~ClECmr$bx^Q6XEfo`FbgzjZ;T_ zcJznnvUxt`2XnVLWJ@Zk5ZgEW;$jpJ8g;8{m+9UvmyVATMwJ5q7CaaE;H6qd{O|Qf z76@;eD3^tw7>1kzWMpaItGLw$?YR=wG1U}*g6{*0GIQ&mkb&Y{c(mSHp(U*4?0QK+ z=(Kznr$O>1<^`*JR3A9F#a)tG&j;*%usS22x@hwiBx}EMYxi;$v+E-rE6I!WC374s zeUFK4i0qN_o#T6VX?UcR(4^rnv(!Q3X?m#s?3@7VoEbrp1w&hXk=wl z^Eb*_ThhzjLww8#+~_ANLf0CyI<{xPXU}(P??#nKE#a{_$1ql0OEs}=vI^00=Pu35 z!srMu`!9&k{*ru&)55VjQ#HEw`j%SxxeoT#51-0!Wq>EbXPxDo#Ne08*Xum%Gc-^q z?&4~kr70?F6htb`1#JckUxij7Sd-~%*ER1Ok%$q&Z^jiA-UpTjxkvVL&Kfe@^50~l znWuGhsa!MACf=9?w$p%&B6QZdAGvr^BfIK{=DY`lfZFCAq_Lr<=$qRjG8us;yQ^g- zS|!oB{Ccs^6cAg_;6{KqVgc^$3QJ|;lT?B9i0DZW!gpe-iSWzGb>ZE`D27zD!oh4* zTx2^vSm-0~t`ISq*V89Ct8O&1Y{dC^u4sfPEt=FNmmZK+BW})R!O0kNoPD`czdEIT zNauXCV1Kn7DJrC8HtD0T1L<|t^lo_xm2`>ec;qgPMov0Ot>iiFGF>JxuK0Wx^ablI z7Wk}DX55_g@REVYlHOTu={o{1J>W5hiw+!0mX@?{W-|snhbml|HX}vZrW1H+1+n~* z*H^2WH-t{ajdqH@lw=N@`7mFm`^G+J=*`1usB8K_)F}-vUSp$|dtB7faej^t@7z$F zpZw3YYTJ3HP$Pt1x$vMa%=>wZmuirnCg}1g5sNj=S4zt~R`oLKFlU2m{fEza2OiJ8 zC=nz1+?M)P|L}sgIFH|)*`tq-#l3aWWutp7w33Nm+PE1st>|I_H>biwi#AiESnhnH z$x_eJ{j9A6q3ffWA<|bperu=OSb8%Xg79^maxN;<$!-`3xSWzFqkq~W$u&S%BUsX(m7`85d7drtu(ek#$h~l%XLimuf=d4x;){{MZb2Pz zqfq^Do-Z-dLAPEtS=3Xcm8C&yt%KhpHG%a#a6sC*XkhheUVM`93*M87C;5YQK-#-$ znQC*O?6686bNu_-oRg%CCtuwy7_01PrZ{wnlK6_$!EEJGa9U$+pu-q?OFOteX!HoB_ z=24s7f2dxzI}>8;RUY&0bBMCN;-EwRq_koBt7Yz~hqN$;wADGA+dj%lSSHG^*Y|{M zq2*T&?UJ1KEUbC8oMo*dC6}t6rpWQ)l8Rt<(2_Cbl$gXik6}}WsWTiaBFAzyB$i5oheteGL7=Y{O9>i82QaOPkg|$3{QKU z-!66jz#7O|jy=an!#6LRpt8<|WXX@G-U<>xI_39Rs@0fE3Vi%_Hap<@w!M%f(=XqP zuy-;+_~L6>RN?GZ=?VMWkhN)zI?dK7Qxysg?GAPAM|lx-CYq{Lr|sArdOEXw;*17u z8_P9JZ6|R%; zN-0-ADezSoV_5VtGnY8QOg5QrfBzIVj{Vu_0D)r< zP(Vn$K17!{um#1g1~8?cW&f1ddXhaxba;XW3t|V)aJ>Kp+0j|+vonkAl<}PGV2bG?ziirxUkLyJGnEple^) z!|cqmtf+LQm{s4kfQg!Gd<9%qP|yC1{CUNvYT=Dv8gEkEzn#`Fzsd2~D{P;2M_N9V zc<4a8oXMEL)M|29(l17xNDSWOYogtGH{CvtY65_n8|>+*%#kx~+>f4$d{$lKto;nR)f#pfN{p|{V(+Q zSF(zhhT1#9jlN3jK;mONPQvMhRBP_yNl7|B&7D}2R(h+)NnX+lsqNB zinm?Bc@?VG#9;$#2`g?lmO?duJO`g+ZAOH3o$GWm;JT=T{ca0nO+F3Mc9l-K)Rzy{ z%oJ##aF==`Ii&1{^%hFR>q7KC?x0e0ezvBcrA^jckc zRViO%ftZ`rinXR%viqfKtyf&ujkb~p9*OXyA)?jOK0#B^_^`z_enMX_1^ z?^fnW1+q5p?8}^Gp*E~iWgt9vaU$Wo^-fz$_xGtmD2eH0iQBh#9V?tjb6W%b;fUR9 zW_JBf#T34^^BGdRhWw`6>YB~KFLj&(B0i)wWm3QUSmON}e%rzO*GIn)s&+QdS0;#+8Gc; z3=Q~mF5HA>#|3NRETelPcCy<0Ij9h4u~FG-8QI4qOUHDvJ=8H#Pu{niSF-Yd``lqb zP*a1rM&5bbRcTakCFOLax*uME8X5>lX1JOW!8@pT)#CL_zFjV-$=g1jU*Yy!eF@)z zc42L!j)7kU5W{VpI$z(0VMU5}zK0hLgsij)oc~f2t(Utw$)B2^b6?sdh_%b_0kf3Q z+t4MR&3C62Y95%S`nbKN)65vrX_Q_m#Vn-$cZ zSJazzmcfqEniW@;%@g5u7W$Ri>Sj8tyC?VRH(e3Z9SWUY4ZT57BQR%N-}GZ>Sf$8y%My57$bOVEILR_Nw~E zXMT*$GkdU~YKLg9t)HyUmVe*#Tk9;ht>Yt}%fvD86w=FmdHEm73GvMg8D)p!TN8h#7bD+_+@-7 zl+K1N->&=+#Gbs9-o;ttBtPx8;8&A;20a2Y6_E<{-P?|=&(O-OmAe1lqPMzKJfkGP zzj#K%4Qd2E6xF>_Kd zpLdWh3J4;p+q+x1pBc=TTc7hSdjmcZlNKUgFYo8y)ZDL<5FGC@kIG*W zgFJ{5Y;O@7e_6F&sV8poUb6q9b+)mMN^|PNfq6In=lN{`6fY^Hd!r_?tiF5qZdY!8 z)DPScdTc>$SE(XgdMVT(Orn(!zjJ@}JfFH(wO_75CfMVQV}M*2)5~v#{^45=v(Wd( zhH;gt{wKp)=dlqM9K6ey1`B!=!!xZZ!n12C9tW~eL|=8e(4D9*O9;H~tOL>B4Wsru zp;7f!>)4kemeJPH@<5W_c0$iq>=V3L$Y5>5JS^X{nCP++X-AIh%O;&0D$+EjXva^m z&$yp#HH|!VYOI3s*qg;tTfyp=sKs*+d7c=fwZSdgbrrozK_Q#cg@_$dnO>O+4a0at z*&e9*zJc(AySyv6@U5i)y-Pt{b)HGu3nuvm#Q zOHv~S~{er0C2q{NWe@U`kri4KRMOi=i%r5RzWRi|bPke>1Dl{aH94Y;P;S9}UNGgF_GZU3r>C9A)sQQ(GZTdPB8K;pT3 zK|-~C!DiH*Iq$Q#R;))4(yi>(frr@9iy3UV=>^jyW$@X_Fk>{%;j?JZDVYC#pX1vu z?5mk3#jDB=Id>m#8g5VV45a1Op1L{o@HO)6HOjkiJxmi*(2pb^tEp^NN#+9KlPovh(wK8+&FJ0qPmw8clAAe!Of(z3Olne# z9bJBA&#C=Itl#FgY?KH`Jrx~@EiFllLaO8LLW$+|m!T!GVK26HKNvKtb8x z1BG$Mc>~F@Cb%hrRvxy2fCQ`}$O54c(f3ftITEx3i8zx$15-?(Ge#B*QdXi@@RuV4 zxZ=D~Kz~;kH!nGVMbHmiIr85}VhIrNhl;neBFIwT2&nE(!~x;raB&D&)1TlA11Zr1 z6^K~8oUz8mUm?hMiXcaCZx1;M2|qtSaX(3MccOy?R90420s@nO!N6n=Y=5>JiH0+Zonf<6x!X#TM+~zj{|?R&(%X;|95yduV1}D_J@Q&%0mJw z4v}zmmH6WmUf!C%WRPDC`lnBLnUY^ZNf_h2+AO1n? zk0yB&@PG62pPo4y`3H5fo&S7>-<jggha zN`disDKr==2}gis5f~XT29AS5P&hbVS_bw92&9{rH_8oz`wfCXhLA>}aI!KeEEp># zDGi205K>^2v@{+pEiH+|V-Q$L2vqhD5QanoIgL>+|08RT0wl+1BNFLH(+VL~C| zF!+xVEJp4k84*R!G=eM20Vm<%=I~?S_YlgdySum(_1&>JMUdp*4Uv1S(a-mxiXbmz zEj1ugQ(X!wDq2ZhMNCB+~xIml1;ziYb_P2JsH6hTMs03Px99Za8G^jMTP>d$acgeeRv2Zzc* z;eQ85CPcso{J*r1N*t*0GrzP6Uf%A+fFGitb#H?6{CW5D)`jq+;DNv&r7MTR{3ML| zK@b5NH{!G!80*MN9wL=zkwy*`qXrOUogCOSAs~UuhUjRuYYdgQ0kc3>c4w zOM+!k2pO<642r>HVbUmR^v`_$_s91i>)=1Y7mG*Xq%jCQSVk6y1;ep$7@4m$8jO~b zl0lGTj*&wDY`*^$UoG--^`pvxB0n15KTY9Z>-s;y2Zg|qn?6bgEF}ZSgW)n#2(S!V z1_73of|6GRX(<>E^|Pt|f5HbRiABIA(HJm!tsut-hev}^Ff0m;#b9AjX@o2k_v^~` zKjK4fF;Fl>77Rm}LZ##oQsg!H&mmKgINCG))=L#6{-N9cRQSUti`<%i%E+5H@?KEl z_s!t1J&SDb|K{h{F8{wd1rYexB>$3s|8mz~?)sNJ@GmLtFJ~zoh(EcKzGj zMgOYlTrjn(4BmjXxp*zbHV^O*r8*@Wk)ipI-v7SW|4XQdiI-yj&&Dn*SC>qR54GR_T zE37<7e?e-#LM;|#a)h`FFhweLafaeX$uA_>aj(89T7YPqjujS{c8P2T0I6x9aYyY+ G#Qy=2f8TKc literal 0 HcmV?d00001 From 322e82462e110340a4a7c1eb0c33b127e0ab902b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Sep 2021 16:03:14 +0200 Subject: [PATCH 014/132] Implement box selection --- .../com/replaymod/core/gui/utils/boxSelect.kt | 155 ++++++++++++++++++ .../simplepathing/gui/GuiPathingKt.kt | 8 + .../simplepathing/gui/UITimelineKeyframes.kt | 2 +- 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/replaymod/core/gui/utils/boxSelect.kt diff --git a/src/main/kotlin/com/replaymod/core/gui/utils/boxSelect.kt b/src/main/kotlin/com/replaymod/core/gui/utils/boxSelect.kt new file mode 100644 index 00000000..5f127d53 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/utils/boxSelect.kt @@ -0,0 +1,155 @@ +package com.replaymod.core.gui.utils + +import com.replaymod.core.gui.utils.Box.Companion.toBox +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.Window +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.effects.ScissorEffect +import gg.essential.elementa.utils.withAlpha +import gg.essential.universal.UMatrixStack +import gg.essential.universal.UResolution +import java.awt.Color +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +interface BoxSelectable + +/** + * Enables box selections to be draw anywhere within this component (usually the window). + * This is safe to call multiple times on the same component (e.g. for the window by independent code). + */ +fun T.onBoxSelection(onChange: (selected: List, final: Boolean) -> Unit) = apply { + var currentBoxSelect: BoxSelect? = null + + onLeftClick { event -> + event.stopPropagation() + currentBoxSelect = BoxSelect(event.absoluteX to event.absoluteY) + } + onMouseDrag { mouseX, mouseY, _ -> + val boxSelect = currentBoxSelect ?: return@onMouseDrag + + boxSelect.second = (mouseX + getLeft()) to (mouseY + getTop()) + val box = boxSelect.box + + if (!boxSelect.passedThreshold) { + if (box.width < 3 && box.height < 3) { + return@onMouseDrag + } else { + boxSelect.passedThreshold = true + + if (effects.find { it is BoxSelectionEffect } == null) { + enableEffect(BoxSelectionEffect(boxSelect)) + } + } + } + onChange(findAllInBox(box), false) + } + onMouseRelease { + val boxSelect = currentBoxSelect?.also { currentBoxSelect = null } ?: return@onMouseRelease + if (!boxSelect.passedThreshold) { + return@onMouseRelease + } + removeEffect() + onChange(findAllInBox(boxSelect.box), true) + } +} + +private data class BoxSelect( + var first: Pair, + var second: Pair = first, + var passedThreshold: Boolean = false, +) { + val box: Box + get() { + val (x1, y1) = first + val (x2, y2) = second + return Box.fromBounds(min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)) + } +} + +/** Box around some center point (x, y). Note that (x, y) is **not** the top left point here, it is the center. */ +private data class Box(val x: Float, val y: Float, val width: Float, val height: Float) { + val left get() = x - width / 2 + val top get() = y - height / 2 + val right get() = x + width / 2 + val bottom get() = y + height / 2 + + private fun intersectsX(other: Box) = abs(this.x - other.x) * 2 < this.width + other.width + private fun intersectsY(other: Box) = abs(this.y - other.y) * 2 < this.height + other.height + fun intersects(other: Box) = intersectsX(other) && intersectsY(other) + + companion object { + fun fromBounds(left: Float, top: Float, right: Float, bottom: Float): Box { + val width = right - left + val height = bottom - top + return Box(left + width / 2, top + height / 2, width, height) + } + + fun UIComponent.toBox() = fromBounds(getLeft(), getTop(), getRight(), getBottom()) + } +} + +private val dummyMatrixStack = UMatrixStack() + +private fun UIComponent.findAllInBox(box: Box): List { + val parentWindow = Window.of(this) + val result = mutableListOf() + + fun UIComponent.collect() { + if (!box.intersects(this.toBox())) { + return + } + + if (this is BoxSelectable) { + result.add(this) + } + + // Window.isAreaVisible uses the scissor effect to determine whether an area is visible + val scissors = effects.filterIsInstance() + scissors.forEach { it.beforeDraw(dummyMatrixStack) } + + for (child in children) { + if (alwaysDrawChildren() || parentWindow.isAreaVisible( + child.getLeft().toDouble(), + child.getTop().toDouble(), + child.getRight().toDouble(), + child.getBottom().toDouble() + ) + ) { + child.collect() + } + } + + scissors.forEach { it.afterDraw(dummyMatrixStack) } + } + collect() + + return result +} + +/** + * Draws a highlight and border for the given box selection on top of a component. + */ +private class BoxSelectionEffect(val boxSelect: BoxSelect) : Effect() { + override fun afterDraw(matrixStack: UMatrixStack) { + val box = boxSelect.box + val l = box.left.toDouble() + val t = box.top.toDouble() + val r = box.right.toDouble() + val b = box.bottom.toDouble() + val w = box.width.toDouble() + val h = box.height.toDouble() + val d = 1.0 / UResolution.scaleFactor // a single (real) pixel + + // Background + UIBlock.drawBlock(matrixStack, Color.YELLOW.withAlpha(0.3f), l, t, r, b) + + // Outline + UIBlock.drawBlockSized(matrixStack, Color.YELLOW, l, t, d, h) // left + UIBlock.drawBlockSized(matrixStack, Color.YELLOW, l, t, w, d) // top + UIBlock.drawBlockSized(matrixStack, Color.YELLOW, r, t, d, h) // right + UIBlock.drawBlockSized(matrixStack, Color.YELLOW, l, b, w, d) // bottom + } +} diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 8eb416a7..05fac67d 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -50,6 +50,14 @@ class GuiPathingKt( init { window.onAnimationFrame { state.update() } + + window.onBoxSelection { components, _ -> + state.selection.set(KeyframeState.Selection.EMPTY.mutate { + for (keyframe in components.filterIsInstance()) { + this[keyframe.type.path] += keyframe.time + } + }) + } } private val secondRow by UIContainer().constrain { diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt index bd29b7b7..c8df07ab 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt @@ -165,7 +165,7 @@ class UITimelineKeyframes( val time: Duration, val type: KeyframeType, val selected: Boolean, - ) : UIContainer() { + ) : UIContainer(), BoxSelectable { val icon by UITexture(ReplayMod.TEXTURE, type.timelineIcon.offset(if (selected) 5 else 0, 0)).constrain { width = 100.percent height = 100.percent From 8b50e4d57d780c0251752005745abc64ab19f030 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 24 Oct 2021 20:32:57 +0200 Subject: [PATCH 015/132] Add checkbox Elementa component --- .../replaymod/core/gui/common/UICheckbox.kt | 75 ++++++++++++++++++ .../assets/replaymod/icons/checkmark.png | Bin 0 -> 87 bytes 2 files changed, 75 insertions(+) create mode 100644 src/main/kotlin/com/replaymod/core/gui/common/UICheckbox.kt create mode 100644 src/main/resources/assets/replaymod/icons/checkmark.png diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UICheckbox.kt b/src/main/kotlin/com/replaymod/core/gui/common/UICheckbox.kt new file mode 100644 index 00000000..ef78dd35 --- /dev/null +++ b/src/main/kotlin/com/replaymod/core/gui/common/UICheckbox.kt @@ -0,0 +1,75 @@ +package com.replaymod.core.gui.common + +import com.replaymod.core.gui.utils.Resources +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.invisible +import gg.essential.elementa.utils.withAlpha +import gg.essential.universal.UMatrixStack +import java.awt.Color + +class UICheckbox : UIComponent() { + + private val hovered = BasicState(false) + private val enabled = BasicState(true) + val checked = BasicState(false) + + var isChecked: Boolean + get() = checked.get() + set(value) = checked.set(value) + + private val checkmark by UITexture(Resources.icon("checkmark"), UITexture.TextureData.full()).constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 9.pixels + height = 9.pixels + color = checked.zip(enabled).map { (checked, enabled) -> + if (enabled) { + Color.WHITE.withAlpha(if (checked) 1f else 0f) + } else { + if (checked) Color.GRAY else Color.DARK_GRAY + } + }.toConstraint() + } childOf this + + init { + onMouseEnter { hovered.set(true) } + onMouseLeave { hovered.set(false) } + + onMouseClick { + if (!enabled.get()) return@onMouseClick + UIButton.playClickSound() + checked.set { !it } + } + + constrain { + width = 9.pixels + height = 9.pixels + color = Color.WHITE.invisible().toConstraint() + } + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val left = getLeft().toDouble() + val right = getRight().toDouble() + val top = getTop().toDouble() + val bottom = getBottom().toDouble() + + // Outline + val outlineColor = if (hovered.get() && enabled.get()) Color.WHITE else Color.BLACK + UIBlock.drawBlock(matrixStack, outlineColor, left, top, right, bottom) + + // Background + val backgroundColor = Color.DARK_GRAY.darker() + UIBlock.drawBlock(matrixStack, backgroundColor, left + 1, top + 1, right - 1, bottom - 1) + + // Checkmark + super.draw(matrixStack) + } +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/icons/checkmark.png b/src/main/resources/assets/replaymod/icons/checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..1482fad5c9b377082f6022ab63e6f75098061404 GIT binary patch literal 87 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VvYsxEAre!Q6Bcm)`2YWZ{UwnF k+z(6`)f{(;hOjeCS|$BdbNPf~ph5;uS3j3^P6 Date: Sun, 24 Oct 2021 20:33:19 +0200 Subject: [PATCH 016/132] Add time shifting panel --- .../simplepathing/gui/GuiPathingKt.kt | 13 ++ .../gui/panels/UITimeOffsetPanel.kt | 123 ++++++++++++++++++ .../assets/replaymod/icons/minus.png | Bin 0 -> 84 bytes .../resources/assets/replaymod/icons/plus.png | Bin 0 -> 93 bytes 4 files changed, 136 insertions(+) create mode 100644 src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimeOffsetPanel.kt create mode 100644 src/main/resources/assets/replaymod/icons/minus.png create mode 100644 src/main/resources/assets/replaymod/icons/plus.png diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 05fac67d..2d5d9d81 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -22,6 +22,7 @@ import com.replaymod.simplepathing.SPTimeline.SPPath import com.replaymod.simplepathing.Setting import com.replaymod.simplepathing.gui.panels.UIPositionKeyframePanel import com.replaymod.simplepathing.gui.panels.UIPositionOffsetPanel +import com.replaymod.simplepathing.gui.panels.UITimeOffsetPanel import com.replaymod.simplepathing.gui.panels.UITimePanel import de.johni0702.minecraft.gui.popup.GuiInfoPopup import gg.essential.elementa.components.UIContainer @@ -192,6 +193,12 @@ class GuiPathingKt( toggleKeyframe(KeyframeType.TIME) } childOf secondRow + private val unusedButton by UIContainer().constrain { + x = SiblingConstraint(5f) + width = 25.pixels + height = 20.pixels + } childOf secondRow + val timeline by UITimeline().constrain { x = SiblingConstraint(5f) width = FillConstraint(false) @@ -279,6 +286,12 @@ class GuiPathingKt( }, 0) } hiddenChildOf window + private val timeOffsetPanel by UITimeOffsetPanel(state, timePanel).constrain { + x = 0.pixels boundTo secondRow + y = SiblingConstraint(1f) boundTo belowTimeline + width = basicWidthConstraint { timePanel.getLeft() - 5f - it.getLeft() } + } hiddenChildOf window + val positionOffsetPanel by UIPositionOffsetPanel(window, state).apply { toggleButton.constrain { x = SiblingConstraint(4f) diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimeOffsetPanel.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimeOffsetPanel.kt new file mode 100644 index 00000000..f2f1d6cb --- /dev/null +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/panels/UITimeOffsetPanel.kt @@ -0,0 +1,123 @@ +package com.replaymod.simplepathing.gui.panels + +import com.replaymod.core.gui.common.UIButton +import com.replaymod.core.gui.common.UICheckbox +import com.replaymod.core.gui.common.input.* +import com.replaymod.core.gui.utils.* +import com.replaymod.core.utils.i18n +import com.replaymod.simplepathing.gui.KeyframeState +import com.replaymod.simplepathing.gui.moveKeyframes +import com.replaymod.simplepathing.gui.moveTimeKeyframes +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.constraints.* +import gg.essential.elementa.dsl.* +import gg.essential.elementa.utils.withAlpha +import java.awt.Color +import kotlin.time.Duration + +class UITimeOffsetPanel( + val state: KeyframeState, + timePanel: UITimePanel, +) : UIBlock(Color(32, 32, 32).withAlpha(0.6f)) { + + init { + timePanel.open.onSetValue { if (it) unhide() else hide(true) } + + constrain { + height = ChildBasedSizeConstraint() + 10.pixels + } + } + + val container by UIContainer().constrain { + x = CenterConstraint() + y = CenterConstraint() + width = 100.percent - 10.pixels + height = ChildBasedSizeConstraint() + } childOf this + + val label by UIContainer().constrain { + width = 100.percent + height = 14.pixels + }.addChild(UIText("Shift time (min:sec:ms)".i18n()).constrain { + x = CenterConstraint() + y = CenterConstraint() + }) childOf container + + val row by UIContainer().constrain { + x = CenterConstraint() + y = SiblingConstraint(2f) + width = ChildBasedSizeConstraint() + height = 20.pixels + } childOf container + + val minusButton by UIButton().texture(Resources.icon("minus")).constrain { + width = 20.pixels + height = 20.pixels + }.onLeftClick { + shiftTime(-1) + } childOf row + + val plusButton by UIButton().texture(Resources.icon("plus")).constrain { + x = SiblingConstraint(3f) + width = 20.pixels + height = 20.pixels + }.onLeftClick { + shiftTime(1) + } childOf row + + val timeField by UIInputOrExpressionField.forTimeInput(UITimeInput(minuteDigits = 2)).constrain { + x = SiblingConstraint(5f) + width = 54.pixels + height = 20.pixels + }.apply { + input.value = Duration.milliseconds(100) + } childOf row + + val checkboxes by UIContainer().constrain { + x = SiblingConstraint(5f) + width = ChildBasedMaxSizeConstraint() + height = 100.percent + } childOf row + + val replayTimeCheckbox by UICheckbox().constrain { + }.apply { + isChecked = true + }.addTooltip { + addLine("Replay Time") + } childOf checkboxes + + val videoTimeCheckbox by UICheckbox().constrain { + y = 0.pixels(alignOpposite = true) + }.apply { + isChecked = true + }.addTooltip { + addLine("Video Time") + } childOf checkboxes + + private fun shiftTime(direction: Int) { + val deltaTime = timeField.input.value * direction + + if (replayTimeCheckbox.isChecked) { + if (state.selection.get().size > 0) { + val change = state.moveTimeKeyframes(state.selection.get(), deltaTime) + state.mod.currentTimeline.timeline.pushChange(change) + } else { + with(state.gui.replayHandler) { + val newTime = replaySender.currentTimeStamp() + deltaTime.inWholeMilliseconds + doJump(newTime.toInt(), true) + } + } + } + + if (videoTimeCheckbox.isChecked) { + if (state.selection.get().size > 0) { + val change = state.moveKeyframes(state.selection.get(), deltaTime) + state.mod.currentTimeline.timeline.pushChange(change) + } else { + state.gui.timeline.cursor.position.set { it + deltaTime } + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/icons/minus.png b/src/main/resources/assets/replaymod/icons/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..3b5f126b952ed77dd113a420e8cca086480aaaad GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=l&6bhh(&L5f&}a0ge49CU78w= g3q*RF7VWXh(&L5f&{Cx@SpakKxR+# p1_22R=E6vaZH$ZA`k1Vk8GhRL7xFCpeG#ah!PC{xWt~$(697mB7ApV% literal 0 HcmV?d00001 From d2f27e391893d4357266a982323194fec54840f7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 14 Nov 2021 16:08:35 +0100 Subject: [PATCH 017/132] Update Loom to 0.10 --- build.gradle | 8 ++++---- jGui | 2 +- root.gradle.kts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 378e8923..6b151350 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ buildscript { dependencies { classpath 'gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0' if (fabric) { - classpath 'fabric-loom:fabric-loom.gradle.plugin:0.8-SNAPSHOT' + classpath 'fabric-loom:fabric-loom.gradle.plugin:0.10-SNAPSHOT' } else if (mcVersion >= 11400) { classpath('net.minecraftforge.gradle:ForgeGradle:5.0.5') { // the FG people still haven't learned to not do breaking changes exclude group: 'trove', module: 'trove' // preprocessor/idea requires more recent one @@ -124,8 +124,8 @@ group= "com.replaymod" archivesBaseName = "replaymod" if (FABRIC) { - minecraft { - refmapName = 'mixins.replaymod.refmap.json' + loom { + mixin.defaultRefmapName.set('mixins.replaymod.refmap.json') runConfigs.all { ideConfigGenerated = true } @@ -334,7 +334,7 @@ dependencies { shadow "com.github.ReplayMod:ReplayStudio:c9de2f5", shadeExclusions - implementation(jGui){ + implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those } shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' diff --git a/jGui b/jGui index 31bcfabe..8b28d1ce 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 31bcfabe67a5b2de490e0f5c53c1a59e636497be +Subproject commit 8b28d1ce0c8152a1ba8a4db8f5f3ea8f80e7cde8 diff --git a/root.gradle.kts b/root.gradle.kts index 870e71bc..8847c31c 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -2,8 +2,8 @@ import groovy.json.JsonOutput import java.io.ByteArrayOutputStream plugins { - id("fabric-loom") version "0.8-SNAPSHOT" apply false - id("com.replaymod.preprocess") version "123fb7a" + id("fabric-loom") version "0.10-SNAPSHOT" apply false + id("com.replaymod.preprocess") version "7746c47" id("com.github.hierynomus.license") version "0.15.0" } From 6e1febdab84159938f5fa5ce67c7a4dbeab406a0 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 14 Nov 2021 16:15:35 +0100 Subject: [PATCH 018/132] Port to MC 1.18-pre1 --- .gitignore | Bin 2588 -> 2605 bytes build.gradle | 11 +- jGui | 2 +- root.gradle.kts | 2 + settings.gradle.kts | 2 + .../com/replaymod/core/versions/Patterns.java | 31 ++++++ .../replaymod/editor/gui/MarkerProcessor.java | 7 +- .../java/com/replaymod/render/blend/Util.java | 4 + .../render/mixin/ChunkInfoAccessor.java | 1 + .../mixin/Mixin_BlockOnChunkRebuilds.java | 13 +++ .../render/mixin/Mixin_ChromaKeyColorSky.java | 6 +- .../replaymod/replay/FullReplaySender.java | 11 ++ .../resources/mixins.render.replaymod.json | 3 + versions/1.18/.gitkeep | 0 .../render/mixin/ChunkInfoAccessor.java | 12 ++ .../render/mixin/Mixin_ForceChunkLoading.java | 105 ++++++++++++++++++ versions/mapping-fabric-1.18-1.17.1.txt | 16 +++ 17 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java create mode 100644 versions/1.18/.gitkeep create mode 100644 versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java create mode 100644 versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java create mode 100644 versions/mapping-fabric-1.18-1.17.1.txt diff --git a/.gitignore b/.gitignore index 19586e3e631dc120c22d76ce9f3be3c1866fc166..37399e16c7aac5fd9c47c4cd6b9f1b1f90b5b957 100644 GIT binary patch delta 25 gcmbOuvQ|V;zo;}%pG&_iwWv5VKd)F{Yl93I0BU~-7XSbN delta 8 PcmZ20GDl>ipbQrP4M750 diff --git a/build.gradle b/build.gradle index 6b151350..40c1e3a2 100644 --- a/build.gradle +++ b/build.gradle @@ -243,6 +243,7 @@ dependencies { 11604: '1.16.4', 11700: '1.17', 11701: '1.17.1', + 11800: '1.18-pre1', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -252,8 +253,9 @@ dependencies { 11604: '1.16.4+build.6:v2', 11700: '1.17+build.13:v2', 11701: '1.17.1+build.29:v2', + 11800: '1.18-pre1+build.5:v2', ][mcVersion] - modImplementation 'net.fabricmc:fabric-loader:0.11.6' + modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ 11404: '0.4.3+build.247-1.14', 11502: '0.5.1+build.294-1.15', @@ -262,6 +264,7 @@ dependencies { 11604: '0.25.1+build.416-1.16', 11700: '0.36.0+1.17', 11701: '0.37.1+1.17', + 11800: '0.42.2+1.18', ][mcVersion] def fabricApiModules = [ "api-base", @@ -332,7 +335,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:c9de2f5", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:a5a92b6", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those @@ -340,7 +343,9 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11700) { + if (mcVersion >= 11800) { + modImplementation 'com.terraformersmc:modmenu:3.0.0' + } else if (mcVersion >= 11700) { modImplementation 'com.terraformersmc:modmenu:2.0.0-beta.7' } else if (mcVersion >= 11602) { modImplementation 'com.terraformersmc:modmenu:1.16.8' diff --git a/jGui b/jGui index 8b28d1ce..de380210 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 8b28d1ce0c8152a1ba8a4db8f5f3ea8f80e7cde8 +Subproject commit de380210d7499a8619a92c92b4bcabed7edaa14c diff --git a/root.gradle.kts b/root.gradle.kts index 8847c31c..f105140d 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -189,6 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11800 = createNode("1.18", 11800, "yarn") val mc11701 = createNode("1.17.1", 11701, "yarn") val mc11700 = createNode("1.17", 11700, "yarn") val mc11604 = createNode("1.16.4", 11604, "yarn") @@ -207,6 +208,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11800.link(mc11701, file("versions/mapping-fabric-1.18-1.17.1.txt")) mc11701.link(mc11700) mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt")) mc11604.link(mc11601) diff --git a/settings.gradle.kts b/settings.gradle.kts index 02c25403..e916eed4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ val jGuiVersions = listOf( "1.16.4", "1.17", "1.17.1", + "1.18", ) val replayModVersions = listOf( // "1.7.10", @@ -50,6 +51,7 @@ val replayModVersions = listOf( "1.16.4", "1.17", "1.17.1", + "1.18", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index f7dee207..416d89c0 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -1,5 +1,8 @@ package com.replaymod.core.versions; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.replaymod.gradle.remap.Pattern; import net.minecraft.client.MinecraftClient; import net.minecraft.client.options.KeyBinding; @@ -23,6 +26,12 @@ import org.lwjgl.opengl.GL11; //#endif +//#if MC>=11600 +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Matrix4f; +//#else +//#endif + //#if MC>=11400 import net.minecraft.client.gui.widget.AbstractButtonWidget; import net.minecraft.client.util.Window; @@ -509,4 +518,26 @@ private static void GL11_glRotatef(float angle, float x, float y, float z) { GL11.glRotatef(angle, x, y, z); //#endif } + + // FIXME preprocessor bug: there are mappings for this, not sure why it doesn't remap by itself + //#if MC>=11600 + @Pattern + private static Matrix4f getPositionMatrix(MatrixStack.Entry stack) { + //#if MC>=11800 + //$$ return stack.getPositionMatrix(); + //#else + return stack.getModel(); + //#endif + } + //#endif + + @SuppressWarnings("rawtypes") // preprocessor bug: doesn't work with generics + @Pattern + private static void Futures_addCallback(ListenableFuture future, FutureCallback callback) { + //#if MC>=11800 + //$$ Futures.addCallback(future, callback, Runnable::run); + //#else + Futures.addCallback(future, callback); + //#endif + } } diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index e3c40d5a..d39c1f6b 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -4,6 +4,7 @@ import com.replaymod.core.versions.MCVer; import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.filter.DimensionTracker; import com.replaymod.replaystudio.filter.SquashFilter; import com.replaymod.replaystudio.filter.StreamFilter; import com.replaymod.replaystudio.io.ReplayInputStream; @@ -122,7 +123,8 @@ public static List> apply(Path path, Consumer int splitCounter = 0; PacketTypeRegistry registry = MCVer.getPacketTypeRegistry(true); - SquashFilter squashFilter = new SquashFilter(); + DimensionTracker dimensionTracker = new DimensionTracker(); + SquashFilter squashFilter = new SquashFilter(null, null); List> outputPaths = new ArrayList<>(); @@ -180,7 +182,7 @@ public static List> apply(Path path, Consumer cutFilter.release(); } startCutOffset = nextMarker.getTime(); - cutFilter = new SquashFilter(); + cutFilter = new SquashFilter(dimensionTracker); } else if (MARKER_NAME_END_CUT.equals(nextMarker.getName())) { timeOffset += nextMarker.getTime() - startCutOffset; if (cutFilter != null) { @@ -208,6 +210,7 @@ public static List> apply(Path path, Consumer continue; } + dimensionTracker.onPacket(null, nextPacket); if (hasFurtherOutputs) { squashFilter.onPacket(null, nextPacket); } diff --git a/src/main/java/com/replaymod/render/blend/Util.java b/src/main/java/com/replaymod/render/blend/Util.java index c43e695a..78138995 100644 --- a/src/main/java/com/replaymod/render/blend/Util.java +++ b/src/main/java/com/replaymod/render/blend/Util.java @@ -183,12 +183,16 @@ public static void insert(ListBase list, CPointer element) throws IOExcept } public static String getTileEntityId(BlockEntity tileEntity) { + //#if MC>=11800 + //$$ NbtCompound nbt = tileEntity.createNbt(); + //#else CompoundTag nbt = new CompoundTag(); //#if MC>=11400 tileEntity.toTag(nbt); //#else //$$ tileEntity.writeToNBT(nbt); //#endif + //#endif return nbt.getString("id"); } diff --git a/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java b/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java new file mode 100644 index 00000000..03235945 --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java @@ -0,0 +1 @@ +// 1.18+ diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_BlockOnChunkRebuilds.java b/src/main/java/com/replaymod/render/mixin/Mixin_BlockOnChunkRebuilds.java index a11818c4..fdbdd826 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_BlockOnChunkRebuilds.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_BlockOnChunkRebuilds.java @@ -24,7 +24,20 @@ public abstract class Mixin_BlockOnChunkRebuilds implements ForceChunkLoadingHook.IBlockOnChunkRebuilds { @Shadow @Final private Queue threadBuffers; + //#if MC>=11800 + //$$ @org.spongepowered.asm.mixin.Unique + //$$ private boolean upload() { + //$$ boolean anything = false; + //$$ Runnable runnable; + //$$ while ((runnable = this.uploadQueue.poll()) != null) { + //$$ runnable.run(); + //$$ anything = true; + //$$ } + //$$ return anything; + //$$ } + //#else @Shadow public abstract boolean upload(); + //#endif @Shadow @Final private TaskExecutor mailbox; diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java index b43d7bd7..bb60ab4e 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java @@ -20,7 +20,11 @@ public abstract class Mixin_ChromaKeyColorSky { @Shadow @Final private MinecraftClient client; - //#if MC>=11400 || 10710>=MC + //#if MC>=11800 + //$$ @Inject(method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V", + //$$ at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", remap = false, shift = At.Shift.AFTER), + //$$ cancellable = true) + //#elseif MC>=11400 || 10710>=MC @Inject(method = "renderSky", at = @At("HEAD"), cancellable = true) //#else //$$ @Inject(method = "renderSky(FI)V", at = @At("HEAD"), cancellable = true) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index c4cc978e..bb22f445 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -604,14 +604,19 @@ protected Packet processPacket(Packet p) throws Exception { //#if MC>=11400 p = new GameJoinS2CPacket( entId, + //#if MC>=11800 + //$$ packet.hardcore(), + //#endif GameMode.SPECTATOR, //#if MC>=11600 GameMode.SPECTATOR, //#endif + //#if MC<11800 //#if MC>=11500 packet.getSha256Seed(), //#endif false, + //#endif //#if MC>=11600 //#if MC>=11603 packet.getDimensionIds(), @@ -626,11 +631,17 @@ protected Packet processPacket(Packet p) throws Exception { //#else //$$ packet.getDimension(), //#endif + //#if MC>=11800 + //$$ packet.sha256Seed(), + //#endif 0, // max players (has no getter -> never actually used) //#if MC<11600 //$$ packet.getGeneratorType(), //#endif packet.getViewDistance(), + //#if MC>=11800 + //$$ packet.simulationDistance(), + //#endif packet.hasReducedDebugInfo() //#if MC>=11500 , packet.showsDeathScreen() diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 7bddb362..108c1484 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -4,6 +4,9 @@ "mixins": [], "server": [], "client": [ + //#if MC>=11800 + //$$ "ChunkInfoAccessor", + //#endif "Mixin_ChromaKeyColorSky", "Mixin_ChromaKeyDisableFog", "Mixin_ChromaKeyForceSky", diff --git a/versions/1.18/.gitkeep b/versions/1.18/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java b/versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java new file mode 100644 index 00000000..c4fbdd96 --- /dev/null +++ b/versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java @@ -0,0 +1,12 @@ +// 1.18+ +package com.replaymod.render.mixin; + +import net.minecraft.client.render.chunk.ChunkBuilder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(targets = "net.minecraft.client.render.WorldRenderer$ChunkInfo") +public interface ChunkInfoAccessor { + @Accessor + ChunkBuilder.BuiltChunk getChunk(); +} diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java new file mode 100644 index 00000000..118bb3d3 --- /dev/null +++ b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java @@ -0,0 +1,105 @@ +package com.replaymod.render.mixin; + +import com.replaymod.render.hooks.ForceChunkLoadingHook; +import com.replaymod.render.hooks.IForceChunkLoading; +import com.replaymod.render.utils.FlawlessFrames; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.Frustum; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.render.chunk.ChunkBuilder; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@Mixin(WorldRenderer.class) +public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { + private ForceChunkLoadingHook replayModRender_hook; + + @Override + public void replayModRender_setHook(ForceChunkLoadingHook hook) { + this.replayModRender_hook = hook; + } + + @Shadow private ChunkBuilder chunkBuilder; + + @Shadow protected abstract void setupTerrain(Camera par1, Frustum par2, boolean par3, boolean par4); + + @Shadow private Frustum frustum; + + @Shadow private Frustum capturedFrustum; + + @Shadow @Final private MinecraftClient client; + + @Shadow @Final private ObjectArrayList field_34807; + + @Shadow private boolean field_34810; + + @Shadow @Final private BlockingQueue field_34816; + + @Shadow private Future field_34808; + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V")) + private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f matrix4f, CallbackInfo ci) { + if (replayModRender_hook == null) { + return; + } + if (FlawlessFrames.hasSodium()) { + return; + } + + assert this.client.player != null; + + do { + // Determine which chunks shall be visible + setupTerrain(camera, this.frustum, this.capturedFrustum != null, this.client.player.isSpectator()); + + // Wait for async processing to be complete + if (this.field_34808 != null) { + try { + this.field_34808.get(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (TimeoutException e) { + e.printStackTrace(); + } + } + + // Schedule all chunks which need rebuilding (we schedule even important rebuilds because we wait for + // all of them anyway and this way we can take advantage of threading) + for (ChunkInfoAccessor chunkInfo : this.field_34807) { + ChunkBuilder.BuiltChunk builtChunk = chunkInfo.getChunk(); + if (!builtChunk.needsRebuild()) { + continue; + } + // MC sometimes schedules invalid chunks when you're outside of loaded chunks (e.g. y > 256) + if (builtChunk.shouldBuild()) { + builtChunk.scheduleRebuild(this.chunkBuilder); + } + builtChunk.cancelRebuild(); + } + + // Upload all chunks + this.field_34810 |= ((ForceChunkLoadingHook.IBlockOnChunkRebuilds) this.chunkBuilder).uploadEverythingBlocking(); + + // Repeat until no more updates are needed + } while (this.field_34810 || !this.field_34816.isEmpty()); + } +} diff --git a/versions/mapping-fabric-1.18-1.17.1.txt b/versions/mapping-fabric-1.18-1.17.1.txt new file mode 100644 index 00000000..1384942e --- /dev/null +++ b/versions/mapping-fabric-1.18-1.17.1.txt @@ -0,0 +1,16 @@ +net.minecraft.client.util.math.MatrixStack multiplyPositionMatrix() method_34425() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket playerEntityId() getEntityId() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket hardcore() isHardcore() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket gameMode() getGameMode() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket previousGameMode() getPreviousGameMode() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionIds() getDimensionIds() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket registryManager() getRegistryManager() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionType() getDimensionType() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionId() getDimensionId() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket sha256Seed() getSha256Seed() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket maxPlayers() getMaxPlayers() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket viewDistance() getViewDistance() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket reducedDebugInfo() hasReducedDebugInfo() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket showDeathScreen() showsDeathScreen() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket debugWorld() isDebugWorld() +net.minecraft.network.packet.s2c.play.GameJoinS2CPacket flatWorld() isFlatWorld() From dd055e5c5baa7bac234a364ede520b8423d3cfd5 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 16:21:00 +0100 Subject: [PATCH 019/132] Fix build for 1.8 --- .../replaymod/render/mixin/Mixin_ChromaKeyForceSky.java | 9 +++++---- .../mixin/Mixin_PreserveDepthDuringHandRendering.java | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java index 1d22d82a..f5b63a21 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java @@ -27,14 +27,15 @@ public abstract class Mixin_ChromaKeyForceSky { @Shadow @Final private MinecraftClient client; + // FIXME preprocessor bug: should be able to remap these //#if MC>=11500 @ModifyConstant(method = "render", constant = @Constant(intValue = 4)) - //#else - //#if MC>=11400 + //#elseif MC>=11400 //$$ @ModifyConstant(method = "renderCenter", constant = @Constant(intValue = 4)) - //#else + //#elseif MC>=10809 //$$ @ModifyConstant(method = "updateCameraAndRender(FJ)V", constant = @Constant(intValue = 4)) - //#endif + //#else + //$$ @ModifyConstant(method = "updateCameraAndRender(F)V", constant = @Constant(intValue = 4)) //#endif private int forceSkyWhenChromaKeying(int value) { EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this.client.gameRenderer).replayModRender_getHandler(); diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java index 5edd5e29..fb7e3877 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java @@ -10,7 +10,12 @@ @Mixin(GameRenderer.class) public abstract class Mixin_PreserveDepthDuringHandRendering { @ModifyArg( + // FIXME preprocessor bug: 1.8.9 uses method with `(FJ)V` when just name would be enough + //#if MC>=10809 method = "renderWorld", + //#else + //$$ method = "updateCameraAndRender(F)V", + //#endif at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"), index = 0 ) From 090778ca63360008a142ba7b289e5326595ec62f Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 15:44:10 +0100 Subject: [PATCH 020/132] Fix build for 1.15.x and below --- src/main/java/com/replaymod/core/versions/Patterns.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index 416d89c0..a55641e8 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -529,6 +529,8 @@ private static Matrix4f getPositionMatrix(MatrixStack.Entry stack) { return stack.getModel(); //#endif } + //#else + //$$ private static void getPositionMatrix() {} //#endif @SuppressWarnings("rawtypes") // preprocessor bug: doesn't work with generics From 2ee8fe49c50b3dd4ea6b14dcfd52936f8f51f52b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 13:14:26 +0100 Subject: [PATCH 021/132] Update FG 2.1 to download MCP from Forge maven Cause the MCP site is now offline. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 40c1e3a2..c0e3bf6c 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { classpath('com.github.ReplayMod:ForgeGradle:' + ( mcVersion >= 11200 ? '34ab703' : // FG 2.3 mcVersion >= 10904 ? '5d1e8d8' : // FG 2.2 - 'd1a7165' // FG 2.1 + 'ceb83c0' // FG 2.1 ) + ':all') } else { classpath 'com.github.ReplayMod:ForgeGradle:a8a9e0ca:all' // FG 1.2 From b6f623efb81da38682a4f970ac28afb4d80fd694 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 13:56:52 +0100 Subject: [PATCH 022/132] Update Gradle to 7.3 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f80bbf5..e750102e 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From ce5886a4279d16d9e7f66cb06eb13b4a984441e4 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 13:57:51 +0100 Subject: [PATCH 023/132] Port to MC 1.18-rc4 --- build.gradle | 12 +++++------ jGui | 2 +- .../core/mixin/MinecraftAccessor.java | 8 +++++++ .../com/replaymod/core/versions/Patterns.java | 21 +++++++++++++++++++ .../extras/playeroverview/PlayerOverview.java | 2 +- .../com/replaymod/render/utils/RenderJob.java | 2 +- .../render/mixin/Mixin_ForceChunkLoading.java | 13 +++++++----- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index c0e3bf6c..838af31c 100644 --- a/build.gradle +++ b/build.gradle @@ -108,9 +108,9 @@ preprocess { def mcVersionStr = "${(int)(mcVersion/10000)}.${(int)(mcVersion/100)%100}" + (mcVersion%100==0 ? '' : ".${mcVersion%100}") -sourceCompatibility = targetCompatibility = mcVersion >= 11700 ? 16 : 1.8 +sourceCompatibility = targetCompatibility = mcVersion >= 11800 ? 17 : mcVersion >= 11700 ? 16 : 1.8 tasks.withType(JavaCompile).configureEach { - options.release = mcVersion >= 11700 ? 16 : 8 + options.release = mcVersion >= 11800 ? 17 : mcVersion >= 11700 ? 16 : 8 } if (mcVersion >= 11400) { @@ -243,7 +243,7 @@ dependencies { 11604: '1.16.4', 11700: '1.17', 11701: '1.17.1', - 11800: '1.18-pre1', + 11800: '1.18-rc4', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -253,7 +253,7 @@ dependencies { 11604: '1.16.4+build.6:v2', 11700: '1.17+build.13:v2', 11701: '1.17.1+build.29:v2', - 11800: '1.18-pre1+build.5:v2', + 11800: '1.18-rc4+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ @@ -264,7 +264,7 @@ dependencies { 11604: '0.25.1+build.416-1.16', 11700: '0.36.0+1.17', 11701: '0.37.1+1.17', - 11800: '0.42.2+1.18', + 11800: '0.43.1+1.18', ][mcVersion] def fabricApiModules = [ "api-base", @@ -335,7 +335,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:a5a92b6", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:69b1296", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those diff --git a/jGui b/jGui index de380210..37b1273b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit de380210d7499a8619a92c92b4bcabed7edaa14c +Subproject commit 37b1273be49e7780438088f220111b44c19e4e9b diff --git a/src/main/java/com/replaymod/core/mixin/MinecraftAccessor.java b/src/main/java/com/replaymod/core/mixin/MinecraftAccessor.java index 8f26e4f5..5e09f787 100644 --- a/src/main/java/com/replaymod/core/mixin/MinecraftAccessor.java +++ b/src/main/java/com/replaymod/core/mixin/MinecraftAccessor.java @@ -10,6 +10,10 @@ import java.util.Queue; +//#if MC>=11800 +//$$ import java.util.function.Supplier; +//#endif + //#if MC>=11400 import java.util.concurrent.CompletableFuture; //#endif @@ -49,7 +53,11 @@ public interface MinecraftAccessor { //#endif @Accessor("crashReport") + //#if MC>=11800 + //$$ Supplier getCrashReporter(); + //#else CrashReport getCrashReporter(); + //#endif //#if MC<11400 //$$ @Accessor diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index a55641e8..811ccb51 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -3,6 +3,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.replaymod.core.mixin.MinecraftAccessor; import com.replaymod.gradle.remap.Pattern; import net.minecraft.client.MinecraftClient; import net.minecraft.client.options.KeyBinding; @@ -13,6 +14,8 @@ import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.crash.CrashReport; import net.minecraft.util.crash.CrashReportSection; import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; @@ -542,4 +545,22 @@ private static void Futures_addCallback(ListenableFuture future, FutureCallback Futures.addCallback(future, callback); //#endif } + + @Pattern + private static void setCrashReport(MinecraftClient mc, CrashReport report) { + //#if MC>=11800 + //$$ mc.setCrashReportSupplier(() -> report); + //#else + mc.setCrashReport(report); + //#endif + } + + @Pattern + private static CrashException crashReportToException(MinecraftClient mc) { + //#if MC>=11800 + //$$ return new CrashException(((MinecraftAccessor) mc).getCrashReporter().get()); + //#else + return new CrashException(((MinecraftAccessor) mc).getCrashReporter()); + //#endif + } } diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java index d5ecb0be..14045dff 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java @@ -1,6 +1,5 @@ package com.replaymod.extras.playeroverview; -import com.google.common.base.Optional; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.PreRenderHandCallback; import com.replaymod.core.utils.Utils; @@ -10,6 +9,7 @@ import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.events.ReplayClosedCallback; import com.replaymod.replay.events.ReplayOpenedCallback; +import com.replaymod.replaystudio.lib.guava.base.Optional; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; diff --git a/src/main/java/com/replaymod/render/utils/RenderJob.java b/src/main/java/com/replaymod/render/utils/RenderJob.java index 5203d254..0510087d 100644 --- a/src/main/java/com/replaymod/render/utils/RenderJob.java +++ b/src/main/java/com/replaymod/render/utils/RenderJob.java @@ -1,12 +1,12 @@ package com.replaymod.render.utils; -import com.google.common.base.Optional; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.replaymod.render.RenderSettings; +import com.replaymod.replaystudio.lib.guava.base.Optional; import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java index 118bb3d3..e7cfc626 100644 --- a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java +++ b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java @@ -11,6 +11,7 @@ import net.minecraft.client.render.LightmapTextureManager; import net.minecraft.client.render.WorldRenderer; import net.minecraft.client.render.chunk.ChunkBuilder; +import net.minecraft.client.render.chunk.ChunkRendererRegionBuilder; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.math.Matrix4f; import org.spongepowered.asm.mixin.Final; @@ -45,11 +46,11 @@ public void replayModRender_setHook(ForceChunkLoadingHook hook) { @Shadow @Final private MinecraftClient client; - @Shadow @Final private ObjectArrayList field_34807; + @Shadow @Final private ObjectArrayList chunkInfos; @Shadow private boolean field_34810; - @Shadow @Final private BlockingQueue field_34816; + @Shadow @Final private BlockingQueue builtChunks; @Shadow private Future field_34808; @@ -64,6 +65,8 @@ private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTim assert this.client.player != null; + ChunkRendererRegionBuilder chunkRendererRegionBuilder = new ChunkRendererRegionBuilder(); + do { // Determine which chunks shall be visible setupTerrain(camera, this.frustum, this.capturedFrustum != null, this.client.player.isSpectator()); @@ -84,14 +87,14 @@ private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTim // Schedule all chunks which need rebuilding (we schedule even important rebuilds because we wait for // all of them anyway and this way we can take advantage of threading) - for (ChunkInfoAccessor chunkInfo : this.field_34807) { + for (ChunkInfoAccessor chunkInfo : this.chunkInfos) { ChunkBuilder.BuiltChunk builtChunk = chunkInfo.getChunk(); if (!builtChunk.needsRebuild()) { continue; } // MC sometimes schedules invalid chunks when you're outside of loaded chunks (e.g. y > 256) if (builtChunk.shouldBuild()) { - builtChunk.scheduleRebuild(this.chunkBuilder); + builtChunk.scheduleRebuild(this.chunkBuilder, chunkRendererRegionBuilder); } builtChunk.cancelRebuild(); } @@ -100,6 +103,6 @@ private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTim this.field_34810 |= ((ForceChunkLoadingHook.IBlockOnChunkRebuilds) this.chunkBuilder).uploadEverythingBlocking(); // Repeat until no more updates are needed - } while (this.field_34810 || !this.field_34816.isEmpty()); + } while (this.field_34810 || !this.builtChunks.isEmpty()); } } From 31817bcf11ed0e173ee6500a19e3a36a66f44940 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 14:50:39 +0100 Subject: [PATCH 024/132] Split replay folder and file management into their own classes --- .../java/com/replaymod/core/ReplayMod.java | 192 +----------------- .../core/files/ReplayFilesService.java | 154 ++++++++++++++ .../core/files/ReplayFoldersService.java | 69 +++++++ .../replaymod/core/gui/RestoreReplayGui.java | 2 +- .../replaymod/editor/gui/GuiEditReplay.java | 4 +- .../replaymod/editor/gui/MarkerProcessor.java | 10 +- .../recording/gui/GuiSavingReplay.java | 2 +- .../handler/ConnectionEventHandler.java | 4 +- .../recording/packet/PacketListener.java | 2 +- .../replaymod/render/gui/GuiRenderQueue.java | 2 +- .../com/replaymod/replay/ReplayModReplay.java | 2 +- .../replay/gui/screen/GuiReplayViewer.java | 6 +- 12 files changed, 245 insertions(+), 204 deletions(-) create mode 100644 src/main/java/com/replaymod/core/files/ReplayFilesService.java create mode 100644 src/main/java/com/replaymod/core/files/ReplayFoldersService.java diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index c21ed0be..39e55c12 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -1,10 +1,10 @@ package com.replaymod.core; -import com.google.common.net.PercentEscaper; import com.replaymod.compat.ReplayModCompat; +import com.replaymod.core.files.ReplayFilesService; +import com.replaymod.core.files.ReplayFoldersService; import com.replaymod.core.gui.GuiBackgroundProcesses; import com.replaymod.core.gui.GuiReplaySettings; -import com.replaymod.core.gui.RestoreReplayGui; import com.replaymod.core.versions.MCVer; import com.replaymod.core.versions.scheduler.Scheduler; import com.replaymod.core.versions.scheduler.SchedulerImpl; @@ -14,12 +14,9 @@ import com.replaymod.render.ReplayModRender; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion; -import com.replaymod.replaystudio.replay.ReplayFile; -import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.studio.ReplayStudio; import com.replaymod.replaystudio.util.I18n; import com.replaymod.simplepathing.ReplayModSimplePathing; -import de.johni0702.minecraft.gui.container.GuiScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.client.options.Option; import net.minecraft.resource.DirectoryResourcePack; @@ -29,21 +26,12 @@ import net.minecraft.text.TranslatableText; import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryStream; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -73,6 +61,8 @@ public class ReplayMod implements Module, Scheduler { private final List modules = new ArrayList<>(); private final GuiBackgroundProcesses backgroundProcesses = new GuiBackgroundProcesses(); + public final ReplayFoldersService folders = new ReplayFoldersService(settingsRegistry); + public final ReplayFilesService files = new ReplayFilesService(folders); /** * Whether the current MC version is supported by the embedded ReplayStudio version. @@ -116,58 +106,6 @@ public SettingsRegistry getSettingsRegistry() { return settingsRegistry; } - public Path getReplayFolder() throws IOException { - String str = getSettingsRegistry().get(Setting.RECORDING_PATH); - return Files.createDirectories(getMinecraft().runDirectory.toPath().resolve(str)); - } - - /** - * Folder into which replay backups are saved before the MarkerProcessor is unleashed. - */ - public Path getRawReplayFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("raw")); - } - - /** - * Folder into which replays are recorded. - * Distinct from the main folder, so they cannot be opened while they are still saving. - */ - public Path getRecordingFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("recording")); - } - - /** - * Folder in which replay cache files are stored. - * Distinct from the recording folder cause people kept confusing them with recordings. - */ - public Path getCacheFolder() throws IOException { - String str = getSettingsRegistry().get(Setting.CACHE_PATH); - Path path = getMinecraft().runDirectory.toPath().resolve(str); - Files.createDirectories(path); - try { - Files.setAttribute(path, "dos:hidden", true); - } catch (UnsupportedOperationException ignored) { - } catch (Exception e) { - e.printStackTrace(); - } - return path; - } - - private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false); - - public Path getCachePathForReplay(Path replay) throws IOException { - Path replayFolder = getReplayFolder(); - Path cacheFolder = getCacheFolder(); - Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath()); - return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString())); - } - - public Path getReplayPathForCache(Path cache) throws IOException { - String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8"); - Path replayFolder = getReplayFolder(); - return replayFolder.resolve(relative); - } - public static final DirectoryResourcePack jGuiResourcePack = createJGuiResourcePack(); public static final String JGUI_RESOURCE_PACK_NAME = "replaymod_jgui"; private static DirectoryResourcePack createJGuiResourcePack() { @@ -230,114 +168,7 @@ public void initClient() { } //#endif - runPostStartup(() -> { - final long DAYS = 24 * 60 * 60 * 1000; - - // Cleanup any cache folders still remaining in the recording folder (we once used to put them there) - try { - Files.walkFileTree(getReplayFolder(), new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - String name = dir.getFileName().toString(); - if (name.endsWith(".mcpr.cache")) { - FileUtils.deleteDirectory(dir.toFile()); - return FileVisitResult.SKIP_SUBTREE; - } - return super.preVisitDirectory(dir, attrs); - } - }); - } catch (IOException e) { - e.printStackTrace(); - } - - // Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging) - try (DirectoryStream paths = Files.newDirectoryStream(getRawReplayFolder())) { - for (Path path : paths) { - if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) { - Files.delete(path); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - - // Move anything which is still in the recording folder into the regular replay folder - // so it can be opened and/or recovered - try (DirectoryStream paths = Files.newDirectoryStream(getRecordingFolder())) { - for (Path path : paths) { - Path destination = getReplayFolder().resolve(path.getFileName()); - if (Files.exists(destination)) { - continue; // better play it save - } - Files.move(path, destination); - } - } catch (IOException e) { - e.printStackTrace(); - } - - // Cleanup cache folders 7 days after last modification or when its replay is gone - try (DirectoryStream paths = Files.newDirectoryStream(getCacheFolder())) { - for (Path path : paths) { - if (Files.isDirectory(path)) { - Path replay = getReplayPathForCache(path); - long lastModified = Files.getLastModifiedTime(path).toMillis(); - if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) { - FileUtils.deleteDirectory(path.toFile()); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - - // Cleanup deleted corrupted replays - try (DirectoryStream paths = Files.newDirectoryStream(getReplayFolder())) { - for (Path path : paths) { - String name = path.getFileName().toString(); - if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) { - long lastModified = Files.getLastModifiedTime(path).toMillis(); - if (lastModified + 2 * DAYS < System.currentTimeMillis()) { - FileUtils.deleteDirectory(path.toFile()); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - - // Restore corrupted replays - try (DirectoryStream paths = Files.newDirectoryStream(getReplayFolder())) { - for (Path path : paths) { - String name = path.getFileName().toString(); - if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) { - Path original = path.resolveSibling(FilenameUtils.getBaseName(name)); - Path noRecoverMarker = original.resolveSibling(original.getFileName() + ".no_recover"); - if (Files.exists(noRecoverMarker)) { - // This file, when its markers are processed, doesn't actually result in any replays. - // So we don't really need to recover it either, let's just get rid of it. - FileUtils.deleteDirectory(path.toFile()); - Files.delete(noRecoverMarker); - continue; - } - new RestoreReplayGui(this, GuiScreen.wrap(mc.currentScreen), original.toFile()).display(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - - // Cleanup leftover no_recover files - try (DirectoryStream paths = Files.newDirectoryStream(getReplayFolder())) { - for (Path path : paths) { - String name = path.getFileName().toString(); - if (name.endsWith(".no_recover")) { - Files.delete(path); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - }); + runPostStartup(() -> files.initialScan(this)); } @Override @@ -437,17 +268,4 @@ public static boolean isCompatible(int fileFormatVersion, int protocolVersion) { return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion, MCVer.getProtocolVersion()); } } - - public ReplayFile openReplay(Path path) throws IOException { - return openReplay(path, path); - } - - public ReplayFile openReplay(Path input, Path output) throws IOException { - return new ZipReplayFile( - new ReplayStudio(), - input != null ? input.toFile() : null, - output.toFile(), - getCachePathForReplay(output).toFile() - ); - } } diff --git a/src/main/java/com/replaymod/core/files/ReplayFilesService.java b/src/main/java/com/replaymod/core/files/ReplayFilesService.java new file mode 100644 index 00000000..6439b344 --- /dev/null +++ b/src/main/java/com/replaymod/core/files/ReplayFilesService.java @@ -0,0 +1,154 @@ +package com.replaymod.core.files; + +import com.replaymod.core.ReplayMod; +import com.replaymod.core.gui.RestoreReplayGui; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ZipReplayFile; +import com.replaymod.replaystudio.studio.ReplayStudio; +import de.johni0702.minecraft.gui.container.GuiScreen; +import net.minecraft.util.Util; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +public class ReplayFilesService { + private final ReplayFoldersService folders; + + public ReplayFilesService(ReplayFoldersService folders) { + this.folders = folders; + } + + public ReplayFile open(Path path) throws IOException { + return open(path, path); + } + + public ReplayFile open(Path input, Path output) throws IOException { + return new ZipReplayFile( + new ReplayStudio(), + input != null ? input.toFile() : null, + output.toFile(), + folders.getCachePathForReplay(output).toFile() + ); + } + + public void initialScan(ReplayMod core) { + // Move anything which is still in the recording folder into the regular replay folder + // so it can be opened and/or recovered + try (DirectoryStream paths = Files.newDirectoryStream(folders.getRecordingFolder())) { + for (Path path : paths) { + Path destination = folders.getReplayFolder().resolve(path.getFileName()); + if (Files.exists(destination)) { + continue; // better play it save + } + Files.move(path, destination); + } + } catch (IOException e) { + e.printStackTrace(); + } + + // Restore corrupted replays + try (DirectoryStream paths = Files.newDirectoryStream(folders.getReplayFolder())) { + for (Path path : paths) { + String name = path.getFileName().toString(); + if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) { + Path original = path.resolveSibling(FilenameUtils.getBaseName(name)); + Path noRecoverMarker = original.resolveSibling(original.getFileName() + ".no_recover"); + if (Files.exists(noRecoverMarker)) { + // This file, when its markers are processed, doesn't actually result in any replays. + // So we don't really need to recover it either, let's just get rid of it. + FileUtils.deleteDirectory(path.toFile()); + Files.delete(noRecoverMarker); + continue; + } + new RestoreReplayGui(core, GuiScreen.wrap(core.getMinecraft().currentScreen), original.toFile()).display(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + // Run general purpose, non-essential cleanup in a background thread + new Thread(this::cleanup, "replaymod-cleanup").start(); + } + + private void cleanup() { + final long DAYS = 24 * 60 * 60 * 1000; + + // Cleanup any cache folders still remaining in the recording folder (we once used to put them there) + try { + Files.walkFileTree(folders.getReplayFolder(), new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String name = dir.getFileName().toString(); + if (name.endsWith(".mcpr.cache")) { + FileUtils.deleteDirectory(dir.toFile()); + return FileVisitResult.SKIP_SUBTREE; + } + return super.preVisitDirectory(dir, attrs); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + + // Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging) + try (DirectoryStream paths = Files.newDirectoryStream(folders.getRawReplayFolder())) { + for (Path path : paths) { + if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) { + Files.delete(path); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + // Cleanup cache folders 7 days after last modification or when its replay is gone + try (DirectoryStream paths = Files.newDirectoryStream(folders.getCacheFolder())) { + for (Path path : paths) { + if (Files.isDirectory(path)) { + Path replay = folders.getReplayPathForCache(path); + long lastModified = Files.getLastModifiedTime(path).toMillis(); + if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) { + FileUtils.deleteDirectory(path.toFile()); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + // Cleanup deleted corrupted replays + try (DirectoryStream paths = Files.newDirectoryStream(folders.getReplayFolder())) { + for (Path path : paths) { + String name = path.getFileName().toString(); + if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) { + long lastModified = Files.getLastModifiedTime(path).toMillis(); + if (lastModified + 2 * DAYS < System.currentTimeMillis()) { + FileUtils.deleteDirectory(path.toFile()); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + // Cleanup leftover no_recover files + try (DirectoryStream paths = Files.newDirectoryStream(folders.getReplayFolder())) { + for (Path path : paths) { + String name = path.getFileName().toString(); + if (name.endsWith(".no_recover")) { + Files.delete(path); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/replaymod/core/files/ReplayFoldersService.java b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java new file mode 100644 index 00000000..6587704d --- /dev/null +++ b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java @@ -0,0 +1,69 @@ +package com.replaymod.core.files; + +import com.google.common.net.PercentEscaper; +import com.replaymod.core.Setting; +import com.replaymod.core.SettingsRegistry; +import net.minecraft.client.MinecraftClient; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ReplayFoldersService { + private final Path mcDir = MinecraftClient.getInstance().runDirectory.toPath(); + private final SettingsRegistry settings; + + public ReplayFoldersService(SettingsRegistry settings) { + this.settings = settings; + } + + public Path getReplayFolder() throws IOException { + return Files.createDirectories(mcDir.resolve(settings.get(Setting.RECORDING_PATH))); + } + + /** + * Folder into which replay backups are saved before the MarkerProcessor is unleashed. + */ + public Path getRawReplayFolder() throws IOException { + return Files.createDirectories(getReplayFolder().resolve("raw")); + } + + /** + * Folder into which replays are recorded. + * Distinct from the main folder, so they cannot be opened while they are still saving. + */ + public Path getRecordingFolder() throws IOException { + return Files.createDirectories(getReplayFolder().resolve("recording")); + } + + /** + * Folder in which replay cache files are stored. + * Distinct from the recording folder cause people kept confusing them with recordings. + */ + public Path getCacheFolder() throws IOException { + Path path = Files.createDirectories(mcDir.resolve(settings.get(Setting.CACHE_PATH))); + try { + Files.setAttribute(path, "dos:hidden", true); + } catch (UnsupportedOperationException ignored) { + } catch (Exception e) { + e.printStackTrace(); + } + return path; + } + + private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false); + + public Path getCachePathForReplay(Path replay) throws IOException { + Path replayFolder = getReplayFolder(); + Path cacheFolder = getCacheFolder(); + Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath()); + return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString())); + } + + public Path getReplayPathForCache(Path cache) throws IOException { + String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8"); + Path replayFolder = getReplayFolder(); + return replayFolder.resolve(relative); + } +} diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index 4d9ba137..3a9f13c0 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -108,7 +108,7 @@ private void recoverInBackground() { } private void tryRecover(Consumer progress) throws IOException { - ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath()); + ReplayFile replayFile = ReplayMod.instance.files.open(file.toPath()); // Commit all not-yet-committed files into the main zip file. // If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data! replayFile.save(); diff --git a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java index 1ee86995..205d4059 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java +++ b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java @@ -60,7 +60,7 @@ protected GuiEditReplay(GuiContainer container, Path inputPath) throws IOExcepti super(container); this.inputPath = inputPath; - try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) { + try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) { markers = replayFile.getMarkers().or(HashSet::new); timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers); timeline.setSize(300, 20) @@ -147,7 +147,7 @@ private void apply() { ProgressPopup progressPopup = new ProgressPopup(this); new Thread(() -> { - try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) { + try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) { replayFile.writeMarkers(markers); replayFile.save(); } catch (IOException e) { diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index d39c1f6b..0bce65f3 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -49,7 +49,7 @@ public class MarkerProcessor { public static final String MARKER_NAME_SPLIT = "_RM_SPLIT"; private static boolean hasWork(Path path) throws IOException { - try (ReplayFile inputReplayFile = ReplayMod.instance.openReplay(path)) { + try (ReplayFile inputReplayFile = ReplayMod.instance.files.open(path)) { return inputReplayFile.getMarkers().or(HashSet::new).stream().anyMatch(m -> m.getName() != null && m.getName().startsWith("_RM_")); } } @@ -113,7 +113,7 @@ public static List> apply(Path path, Consumer ReplayMod mod = ReplayMod.instance; if (!hasWork(path)) { ReplayMetaData metaData; - try (ReplayFile inputReplayFile = mod.openReplay(path)) { + try (ReplayFile inputReplayFile = mod.files.open(path)) { metaData = inputReplayFile.getMetaData(); } return Collections.singletonList(Pair.of(path, metaData)); @@ -128,7 +128,7 @@ public static List> apply(Path path, Consumer List> outputPaths = new ArrayList<>(); - Path rawFolder = ReplayMod.instance.getRawReplayFolder(); + Path rawFolder = ReplayMod.instance.folders.getRawReplayFolder(); Path inputPath = rawFolder.resolve(path.getFileName()); for (int i = 1; Files.exists(inputPath); i++) { inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr"); @@ -136,7 +136,7 @@ public static List> apply(Path path, Consumer Files.createDirectories(inputPath.getParent()); Files.move(path, inputPath); - try (ReplayFile inputReplayFile = mod.openReplay(inputPath)) { + try (ReplayFile inputReplayFile = mod.files.open(inputPath)) { List markers = inputReplayFile.getMarkers().or(HashSet::new) .stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList()); Iterator markerIterator = markers.iterator(); @@ -153,7 +153,7 @@ public static List> apply(Path path, Consumer while (nextPacket != null && outputFileSuffixes.hasNext()) { Path outputPath = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr"); - try (ReplayFile outputReplayFile = mod.openReplay(null, outputPath)) { + try (ReplayFile outputReplayFile = mod.files.open(null, outputPath)) { long duration = 0; Set outputMarkers = new HashSet<>(); ReplayMetaData metaData = inputReplayFile.getMetaData(); diff --git a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java index 5ca75e7e..6fb66db9 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java +++ b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java @@ -154,7 +154,7 @@ private void applyOutput(Path path, String newName) { } try { - Path replaysFolder = core.getReplayFolder(); + Path replaysFolder = core.folders.getReplayFolder(); Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName)); for (int i = 1; Files.exists(newPath); i++) { newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")")); diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 0e90c7eb..60a43a50 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -127,8 +127,8 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { } String name = sdf.format(Calendar.getInstance().getTime()); - Path outputPath = core.getRecordingFolder().resolve(Utils.replayNameToFileName(name)); - ReplayFile replayFile = core.openReplay(outputPath); + Path outputPath = core.folders.getRecordingFolder().resolve(Utils.replayNameToFileName(name)); + ReplayFile replayFile = core.files.open(outputPath); replayFile.writeModInfo(ModCompat.getInstalledNetworkMods()); diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index 1771395a..e37fefad 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -280,7 +280,7 @@ public void channelInactive(ChannelHandlerContext ctx) { // We still have the replay, so we just save it (at least for a few weeks) in case they change their mind String replayName = FilenameUtils.getBaseName(outputPath.getFileName().toString()); - Path rawFolder = ReplayMod.instance.getRawReplayFolder(); + Path rawFolder = ReplayMod.instance.folders.getRawReplayFolder(); Path rawPath = rawFolder.resolve(outputPath.getFileName()); for (int i = 1; Files.exists(rawPath); i++) { rawPath = rawPath.resolveSibling(replayName + "." + i + ".mcpr"); diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index a019b565..db37e673 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -228,7 +228,7 @@ public static void processMultipleReplays( ReplayHandler replayHandler; ReplayFile replayFile = null; try { - replayFile = mod.getCore().openReplay(next.getKey().toPath()); + replayFile = mod.getCore().files.open(next.getKey().toPath()); replayHandler = mod.startReplay(replayFile, true, false); } catch (IOException e) { Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {}); diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java index 05113411..cb225447 100644 --- a/src/main/java/com/replaymod/replay/ReplayModReplay.java +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -159,7 +159,7 @@ public CameraController apply(@Nullable CameraEntity cameraEntity) { } public void startReplay(File file) throws IOException { - startReplay(core.openReplay(file.toPath())); + startReplay(core.files.open(file.toPath())); } public void startReplay(ReplayFile replayFile) throws IOException { 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 bd889eb0..de4279aa 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -118,7 +118,7 @@ public void run() { @Override public void run() { try { - File folder = mod.getCore().getReplayFolder().toFile(); + File folder = mod.getCore().folders.getReplayFolder().toFile(); MCVer.openFile(folder); } catch (IOException e) { @@ -221,7 +221,7 @@ public GuiReplayViewer(ReplayModReplay mod) { this.mod = mod; try { - list.setFolder(mod.getCore().getReplayFolder().toFile()); + list.setFolder(mod.getCore().folders.getReplayFolder().toFile()); } catch (IOException e) { throw new CrashException(CrashReport.create(e, "Getting replay folder")); } @@ -370,7 +370,7 @@ public GuiReplayList(GuiContainer container) { Arrays.sort(files, Comparator.comparingLong(f -> lastModified.computeIfAbsent(f, File::lastModified)).reversed()); for (final File file : files) { if (Thread.interrupted()) break; - try (ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath())) { + try (ReplayFile replayFile = ReplayMod.instance.files.open(file.toPath())) { final Image thumb = Optional.ofNullable(replayFile.getThumbBytes().orNull()).flatMap(stream -> { try (InputStream in = stream) { return Optional.of(Image.read(in)); From bd0dd10942991930024502fc2cadb349ecb732b8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 15:38:51 +0100 Subject: [PATCH 025/132] Ensure we cannot accidentally open a replay file twice E.g. from the Replay Viewer while it is still being recovered. Because that would potentially corrupt it. --- .../core/files/DelegatingReplayFile.java | 202 ++++++++++++++++++ .../core/files/ManagedReplayFile.java | 23 ++ .../core/files/ReplayFilesService.java | 50 ++++- 3 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/replaymod/core/files/DelegatingReplayFile.java create mode 100644 src/main/java/com/replaymod/core/files/ManagedReplayFile.java diff --git a/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java b/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java new file mode 100644 index 00000000..26950f93 --- /dev/null +++ b/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java @@ -0,0 +1,202 @@ +package com.replaymod.core.files; + +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.data.ModInfo; +import com.replaymod.replaystudio.data.ReplayAssetEntry; +import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.io.ReplayOutputStream; +import com.replaymod.replaystudio.lib.guava.base.Optional; +import com.replaymod.replaystudio.pathing.PathingRegistry; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.protocol.PacketTypeRegistry; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +public class DelegatingReplayFile implements ReplayFile { + private final ReplayFile delegate; + + public DelegatingReplayFile(ReplayFile delegate) { + this.delegate = delegate; + } + + @Override + public Optional get(String entry) throws IOException { + return this.delegate.get(entry); + } + + @Override + public Optional getCache(String entry) throws IOException { + return this.delegate.getCache(entry); + } + + @Override + public Map getAll(Pattern pattern) throws IOException { + return this.delegate.getAll(pattern); + } + + @Override + public OutputStream write(String entry) throws IOException { + return this.delegate.write(entry); + } + + @Override + public OutputStream writeCache(String entry) throws IOException { + return this.delegate.writeCache(entry); + } + + @Override + public void remove(String entry) throws IOException { + this.delegate.remove(entry); + } + + @Override + public void removeCache(String entry) throws IOException { + this.delegate.removeCache(entry); + } + + @Override + public void save() throws IOException { + this.delegate.save(); + } + + @Override + public void saveTo(File target) throws IOException { + this.delegate.saveTo(target); + } + + @Override + public ReplayMetaData getMetaData() throws IOException { + return this.delegate.getMetaData(); + } + + @Override + public void writeMetaData(PacketTypeRegistry registry, ReplayMetaData metaData) throws IOException { + this.delegate.writeMetaData(registry, metaData); + } + + @Override + public ReplayInputStream getPacketData(PacketTypeRegistry registry) throws IOException { + return this.delegate.getPacketData(registry); + } + + @Override + public ReplayOutputStream writePacketData() throws IOException { + return this.delegate.writePacketData(); + } + + @Override + public Map getResourcePackIndex() throws IOException { + return this.delegate.getResourcePackIndex(); + } + + @Override + public void writeResourcePackIndex(Map index) throws IOException { + this.delegate.writeResourcePackIndex(index); + } + + @Override + public Optional getResourcePack(String hash) throws IOException { + return this.delegate.getResourcePack(hash); + } + + @Override + public OutputStream writeResourcePack(String hash) throws IOException { + return this.delegate.writeResourcePack(hash); + } + + @Override + public Map getTimelines(PathingRegistry pathingRegistry) throws IOException { + return this.delegate.getTimelines(pathingRegistry); + } + + @Override + public void writeTimelines(PathingRegistry pathingRegistry, Map timelines) throws IOException { + this.delegate.writeTimelines(pathingRegistry, timelines); + } + + @Override + public Optional getThumb() throws IOException { + return this.delegate.getThumb(); + } + + @Override + public void writeThumb(BufferedImage image) throws IOException { + this.delegate.writeThumb(image); + } + + @Override + public Optional getThumbBytes() throws IOException { + return this.delegate.getThumbBytes(); + } + + @Override + public void writeThumbBytes(byte[] image) throws IOException { + this.delegate.writeThumbBytes(image); + } + + @Override + public Optional> getInvisiblePlayers() throws IOException { + return this.delegate.getInvisiblePlayers(); + } + + @Override + public void writeInvisiblePlayers(Set uuids) throws IOException { + this.delegate.writeInvisiblePlayers(uuids); + } + + @Override + public Optional> getMarkers() throws IOException { + return this.delegate.getMarkers(); + } + + @Override + public void writeMarkers(Set markers) throws IOException { + this.delegate.writeMarkers(markers); + } + + @Override + public Collection getAssets() throws IOException { + return this.delegate.getAssets(); + } + + @Override + public Optional getAsset(UUID uuid) throws IOException { + return this.delegate.getAsset(uuid); + } + + @Override + public OutputStream writeAsset(ReplayAssetEntry asset) throws IOException { + return this.delegate.writeAsset(asset); + } + + @Override + public void removeAsset(UUID uuid) throws IOException { + this.delegate.removeAsset(uuid); + } + + @Override + public Collection getModInfo() throws IOException { + return this.delegate.getModInfo(); + } + + @Override + public void writeModInfo(Collection modInfo) throws IOException { + this.delegate.writeModInfo(modInfo); + } + + @Override + public void close() throws IOException { + this.delegate.close(); + } +} diff --git a/src/main/java/com/replaymod/core/files/ManagedReplayFile.java b/src/main/java/com/replaymod/core/files/ManagedReplayFile.java new file mode 100644 index 00000000..e000e354 --- /dev/null +++ b/src/main/java/com/replaymod/core/files/ManagedReplayFile.java @@ -0,0 +1,23 @@ +package com.replaymod.core.files; + +import com.replaymod.replaystudio.replay.ReplayFile; + +import java.io.IOException; + +public class ManagedReplayFile extends DelegatingReplayFile { + private Runnable onClose; + + public ManagedReplayFile(ReplayFile delegate, Runnable onClose) { + super(delegate); + + this.onClose = onClose; + } + + @Override + public void close() throws IOException { + super.close(); + + onClose.run(); + onClose = () -> {}; + } +} diff --git a/src/main/java/com/replaymod/core/files/ReplayFilesService.java b/src/main/java/com/replaymod/core/files/ReplayFilesService.java index 6439b344..9090f4b5 100644 --- a/src/main/java/com/replaymod/core/files/ReplayFilesService.java +++ b/src/main/java/com/replaymod/core/files/ReplayFilesService.java @@ -17,9 +17,14 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class ReplayFilesService { private final ReplayFoldersService folders; + private final Set lockedPaths = Collections.newSetFromMap(new ConcurrentHashMap<>()); public ReplayFilesService(ReplayFoldersService folders) { this.folders = folders; @@ -30,12 +35,39 @@ public ReplayFile open(Path path) throws IOException { } public ReplayFile open(Path input, Path output) throws IOException { - return new ZipReplayFile( - new ReplayStudio(), - input != null ? input.toFile() : null, - output.toFile(), - folders.getCachePathForReplay(output).toFile() - ); + Path realInput = input != null ? input.toAbsolutePath().normalize() : null; + Path realOutput = output.toAbsolutePath().normalize(); + + if (realInput != null && !lockedPaths.add(realInput)) { + throw new FileLockedException(realInput); + } + if (!Objects.equals(realInput, realOutput) && !lockedPaths.add(realOutput)) { + if (realInput != null) { + lockedPaths.remove(realInput); + } + throw new FileLockedException(realOutput); + } + + Runnable onClose = () -> { + if (realInput != null) { + lockedPaths.remove(realInput); + } + lockedPaths.remove(realOutput); + }; + + ReplayFile replayFile; + try { + replayFile = new ZipReplayFile( + new ReplayStudio(), + realInput != null ? realInput.toFile() : null, + realOutput.toFile(), + folders.getCachePathForReplay(realOutput).toFile() + ); + } catch (IOException e) { + onClose.run(); + throw e; + } + return new ManagedReplayFile(replayFile, onClose); } public void initialScan(ReplayMod core) { @@ -151,4 +183,10 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th e.printStackTrace(); } } + + public static class FileLockedException extends IOException { + public FileLockedException(Path path) { + super(path.toString()); + } + } } From 549996b2709ade8f78d8d973208514cb80a8ff9f Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 17:37:02 +0100 Subject: [PATCH 026/132] Fix underwater visibility (fixes #572) Used to always be super short because that method returns 0 when `isSubmergedIn` returns false. --- root.gradle.kts | 2 +- src/main/java/com/replaymod/replay/camera/CameraEntity.java | 5 +++++ versions/mapping-fabric-1.16.1-1.15.2.txt | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 versions/mapping-fabric-1.16.1-1.15.2.txt diff --git a/root.gradle.kts b/root.gradle.kts index f105140d..fe2af4b6 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -212,7 +212,7 @@ preprocess { mc11701.link(mc11700) mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt")) mc11604.link(mc11601) - mc11601.link(mc11502) + mc11601.link(mc11502, file("versions/mapping-fabric-1.16.1-1.15.2.txt")) mc11502.link(mc11404, file("versions/mapping-fabric-1.15.2-1.14.4.txt")) mc11404.link(mc11404Forge, file("versions/mapping-1.14.4-fabric-forge.txt")) mc11404Forge.link(mc11202, file("versions/1.14.4-forge/mapping.txt")) diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index ba1b59e3..09716e7e 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -365,6 +365,11 @@ public boolean isInsideWall() { public boolean isSubmergedIn(Tag fluid) { return falseUnlessSpectating(entity -> entity.isSubmergedIn(fluid)); } + + @Override + public float getUnderwaterVisibility() { + return falseUnlessSpectating(__ -> true) ? super.getUnderwaterVisibility() : 1f; + } //#else //#if MC>=10800 //$$ @Override diff --git a/versions/mapping-fabric-1.16.1-1.15.2.txt b/versions/mapping-fabric-1.16.1-1.15.2.txt new file mode 100644 index 00000000..1f8df9e4 --- /dev/null +++ b/versions/mapping-fabric-1.16.1-1.15.2.txt @@ -0,0 +1 @@ +net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140() From 58ba590c84abd1b103ebaf7ddb9ff07d7d7d8965 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 10:11:22 +0100 Subject: [PATCH 027/132] Fix replay being restarted when resuming path playback --- .../com/replaymod/pathing/player/RealtimeTimelinePlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java index af53c5b8..cde6c3e9 100644 --- a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java +++ b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java @@ -67,7 +67,7 @@ public void onTick() { @Override public long getTimePassed() { - if (firstFrame) return 0; + if (firstFrame) return startOffset; if (loadingResources) return timeBeforeResourceLoading; return System.currentTimeMillis() - startTime; } From ff5c0f594e3b676aad4eec63f5bcb2df7588e274 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 11:37:12 +0100 Subject: [PATCH 028/132] Fix chunks missing on first frame when rendering without Sodium --- .../render/mixin/Mixin_ForceChunkLoading.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java index e7cfc626..288c4317 100644 --- a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java +++ b/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java @@ -26,6 +26,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; @Mixin(WorldRenderer.class) public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { @@ -54,6 +55,10 @@ public void replayModRender_setHook(ForceChunkLoadingHook hook) { @Shadow private Future field_34808; + @Shadow @Final private AtomicBoolean field_34809; + + @Shadow protected abstract void applyFrustum(Frustum par1); + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V")) private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f matrix4f, CallbackInfo ci) { if (replayModRender_hook == null) { @@ -85,6 +90,12 @@ private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTim } } + // If that async processing did change the chunk graph, we need to re-apply the frustum (otherwise this is + // only done in the next setupTerrain call, which not happen this frame) + if (this.field_34809.compareAndSet(true, false)) { + this.applyFrustum((new Frustum(frustum)).method_38557(8)); // call based on the one in setupTerrain + } + // Schedule all chunks which need rebuilding (we schedule even important rebuilds because we wait for // all of them anyway and this way we can take advantage of threading) for (ChunkInfoAccessor chunkInfo : this.chunkInfos) { From 88a23222b63f7918d970137bf36bc0c8f6cf0de7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 14:50:41 +0100 Subject: [PATCH 029/132] Fix vanilla 1.18 bug causing entities to get stuck outside sim range MC uses the client-side position of an entity to determine whether it is inside the simulation range and therefore whether it will get ticked. For some (most) entities it interpolates the client-side position to match the server-side one inside of the tick method... no prices for guessing where this goes wrong. Somewhat similar to the issue we have with chunk unloads which we work around in FullReplaySender but worse cause they'll get stuck if they ever leave the sim range even for a single tick, rather then just when their chunk is unloaded. --- .../replay/mixin/ClientWorldAccessor.java | 13 +++++++ .../mixin/Mixin_FixEntityNotTracking.java | 1 + .../resources/mixins.replay.replaymod.json | 4 ++ .../mixin/Mixin_FixEntityNotTracking.java | 38 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/com/replaymod/replay/mixin/ClientWorldAccessor.java create mode 100644 src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java create mode 100644 versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java diff --git a/src/main/java/com/replaymod/replay/mixin/ClientWorldAccessor.java b/src/main/java/com/replaymod/replay/mixin/ClientWorldAccessor.java new file mode 100644 index 00000000..ba16ed59 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/ClientWorldAccessor.java @@ -0,0 +1,13 @@ +package com.replaymod.replay.mixin; + +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientWorld.class) +public interface ClientWorldAccessor { + //#if MC>=11800 + //$$ @Accessor + //$$ net.minecraft.world.EntityList getEntityList(); + //#endif +} diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java b/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java new file mode 100644 index 00000000..464bb200 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java @@ -0,0 +1 @@ +// 1.18+ only diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index feb30fa7..6a481a03 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -6,6 +6,9 @@ "server": [], "client": [ "Mixin_FixNPCSkinCaching", + //#if MC>=11800 + //$$ "Mixin_FixEntityNotTracking", + //#endif //#if MC>=11600 "Mixin_MoveRealmsButton", //#endif @@ -13,6 +16,7 @@ "MixinCamera", "MixinInGameHud", //#endif + "ClientWorldAccessor", "EntityLivingBaseAccessor", //#if MC>=11400 "Mixin_ShowSpectatedHand_NoOF", diff --git a/versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java b/versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java new file mode 100644 index 00000000..1d95dd1b --- /dev/null +++ b/versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java @@ -0,0 +1,38 @@ +package com.replaymod.replay.mixin; + +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(ClientPlayNetworkHandler.class) +public class Mixin_FixEntityNotTracking { + @ModifyVariable(method = { "onEntityPosition", "onEntity", "onEntityPassengersSet" }, at = @At("RETURN"), ordinal = 0) + private Entity updatePositionIfNotTracked$0(Entity entity) { + if (entity != null) { + entity.streamSelfAndPassengers().forEach(this::updatePositionIfNotTracked); + } + return entity; + } + + private void updatePositionIfNotTracked(Entity entity) { + if (entity != null && entity.world instanceof ClientWorldAccessor world) { + if (!world.getEntityList().has(entity)) { + // Skip interpolation of position updates coming from server + // (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP) + int ticks = 0; + Vec3d prevPos; + do { + prevPos = entity.getPos(); + if (entity.hasVehicle()) { + entity.tickRiding(); + } else { + entity.tick(); + } + } while (prevPos.squaredDistanceTo(entity.getPos()) > 0.0001 && ticks++ < 100); + } + } + } +} From d6cd917d9d13055c02e410f2d9a0a0086a603b3f Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 29 Nov 2021 15:10:50 +0100 Subject: [PATCH 030/132] Fix passengers getting stuck in unloaded chunks (fixes #606) We need to update the vehicle before its passengers. --- .../replaymod/replay/FullReplaySender.java | 31 +++++++++++++------ versions/1.14.4-forge/mapping.txt | 1 + versions/1.9.4/mapping.txt | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index bb22f445..36cab830 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -55,6 +55,7 @@ import net.minecraft.network.packet.s2c.play.StatisticsS2CPacket; import net.minecraft.text.Text; import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -1205,15 +1206,7 @@ protected Packet processPacketSync(Packet p) { for (Entity entity : entitiesInChunk) { // Skip interpolation of position updates coming from server // (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP) - // Needs to be called at least 4 times thanks to - // EntityOtherPlayerMP#otherPlayerMPPosRotationIncrements (max vanilla value is 3) - for (int i = 0; i < 4; i++) { - //#if MC>=11400 - entity.tick(); - //#else - //$$ entity.onUpdate(); - //#endif - } + forcePositionForVehicleAndSelf(entity); // Check whether the entity has left the chunk //#if MC>=11700 @@ -1284,6 +1277,26 @@ protected Packet processPacketSync(Packet p) { return p; // During synchronous playback everything is sent normally } + private void forcePositionForVehicleAndSelf(Entity entity) { + Entity vehicle = entity.getVehicle(); + if (vehicle != null) { + forcePositionForVehicleAndSelf(vehicle); + } + + // Skip interpolation of position updates coming from server + // (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP) + int ticks = 0; + Vec3d prevPos; + do { + prevPos = entity.getPos(); + if (vehicle != null) { + entity.tickRiding(); + } else { + entity.tick(); + } + } while (prevPos.squaredDistanceTo(entity.getPos()) > 0.0001 && ticks++ < 100); + } + private static final class PacketData { private static final com.github.steveice10.netty.buffer.ByteBuf byteBuf = com.github.steveice10.netty.buffer.Unpooled.buffer(); private static final NetOutput netOutput = new ByteBufNetOutput(byteBuf); diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index f858f3e5..2096663b 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -27,6 +27,7 @@ net.minecraft.client.gui.GuiYesNoCallback confirmResult() confirmClicked() net.minecraft.util.text.ITextComponent getString() getUnformattedText() net.minecraft.network.play.server.SPacketRespawn func_212643_b() getDimensionID() net.minecraft.client.Minecraft getPackFinder() getResourcePackRepository() +net.minecraft.entity.Entity getPositionVec() getPositionVector() net.minecraftforge.client.event.GuiScreenEvent.InitGuiEvent addWidget() addButton() net.minecraftforge.client.event.GuiScreenEvent.InitGuiEvent removeWidget() removeButton() diff --git a/versions/1.9.4/mapping.txt b/versions/1.9.4/mapping.txt index 9225b4c1..66b70775 100644 --- a/versions/1.9.4/mapping.txt +++ b/versions/1.9.4/mapping.txt @@ -1,5 +1,6 @@ net.minecraft.stats.StatisticsManager net.minecraft.stats.StatFileWriter net.minecraft.init.MobEffects net.minecraft.potion.Potion +net.minecraft.util.math.Vec3d net.minecraft.util.Vec3 net.minecraft.util.text.TextComponentString net.minecraft.util.ChatComponentText net.minecraft.util.text.TextComponentTranslation net.minecraft.util.ChatComponentTranslation net.minecraft.util.text.Style net.minecraft.util.ChatStyle From 9fa5adf3954e7c567ee0500d140e75fc25223a2c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 1 Dec 2021 13:01:12 +0100 Subject: [PATCH 031/132] Bump to MC 1.18 --- build.gradle | 6 +++--- jGui | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 838af31c..0f5e811b 100644 --- a/build.gradle +++ b/build.gradle @@ -243,7 +243,7 @@ dependencies { 11604: '1.16.4', 11700: '1.17', 11701: '1.17.1', - 11800: '1.18-rc4', + 11800: '1.18', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -253,7 +253,7 @@ dependencies { 11604: '1.16.4+build.6:v2', 11700: '1.17+build.13:v2', 11701: '1.17.1+build.29:v2', - 11800: '1.18-rc4+build.1:v2', + 11800: '1.18+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ @@ -335,7 +335,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:69b1296", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:8cfd22c", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those diff --git a/jGui b/jGui index 37b1273b..3ac431cf 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 37b1273be49e7780438088f220111b44c19e4e9b +Subproject commit 3ac431cfa983009d4785f8962fdc8e0cc49eb825 From b10b4fd25a1af5ade7d7237250dd21bbbe121b5f Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 11:37:32 +0100 Subject: [PATCH 032/132] Fix crash when render queue is not saved properly (fixes #617) --- src/main/java/com/replaymod/render/utils/RenderJob.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/utils/RenderJob.java b/src/main/java/com/replaymod/render/utils/RenderJob.java index 0510087d..3554930f 100644 --- a/src/main/java/com/replaymod/render/utils/RenderJob.java +++ b/src/main/java/com/replaymod/render/utils/RenderJob.java @@ -81,10 +81,14 @@ public static List readQueue(ReplayFile replayFile) throws IOExceptio } try (InputStream in = optIn.get(); InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - return new GsonBuilder() + List jobs = new GsonBuilder() .registerTypeAdapter(Timeline.class, new TimelineTypeAdapter()) .create() .fromJson(reader, new TypeToken>(){}.getType()); + if (jobs == null) { + jobs = new ArrayList<>(); + } + return jobs; } } } From 29ef38213f03c7ce808bc398e01d92bd7660683b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 13:50:53 +0100 Subject: [PATCH 033/132] Fix vanilla entity update packet handling (fixes #607) --- .../com/replaymod/core/versions/MCVer.java | 13 ++- .../com/replaymod/core/versions/Patterns.java | 10 ++ .../com/replaymod/replay/ext/EntityExt.java | 9 ++ .../entity_tracking/Mixin_EntityExt.java | 36 ++++++ .../Mixin_FixPartialUpdates.java | 109 ++++++++++++++++++ .../resources/mixins.replay.replaymod.json | 2 + 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/replaymod/replay/ext/EntityExt.java create mode 100644 src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java create mode 100644 src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index ac4dd754..cb7c7f00 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -13,6 +13,12 @@ import net.minecraft.client.render.BufferBuilder; import net.minecraft.util.Identifier; import net.minecraft.util.Util; +import net.minecraft.util.math.Vec3d; + +//#if MC>=11604 +//#else +//$$ import net.minecraft.entity.Entity; +//#endif //#if MC>=11600 import net.minecraft.resource.ResourcePackSource; @@ -55,7 +61,6 @@ //#if MC>=10904 import com.replaymod.render.blend.mixin.ParticleAccessor; import net.minecraft.client.particle.Particle; -import net.minecraft.util.math.Vec3d; //#endif //#if MC>=10800 @@ -297,6 +302,12 @@ public static Vec3d getPosition(Particle particle, float partialTicks) { } //#endif + //#if MC<=11601 + //$$ public static Vec3d getTrackedPosition(Entity entity) { + //$$ return new Vec3d(entity.trackedX, entity.trackedY, entity.trackedZ); + //$$ } + //#endif + public static void openFile(File file) { //#if MC>=11400 Util.getOperatingSystem().open(file); diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index 811ccb51..884eb955 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -21,6 +21,7 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.network.PacketByteBuf; import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import net.minecraft.world.chunk.WorldChunk; @@ -563,4 +564,13 @@ private static CrashException crashReportToException(MinecraftClient mc) { return new CrashException(((MinecraftAccessor) mc).getCrashReporter()); //#endif } + + @Pattern + private static Vec3d getTrackedPosition(Entity entity) { + //#if MC>=11604 + return entity.getTrackedPosition(); + //#else + //$$ return com.replaymod.core.versions.MCVer.getTrackedPosition(entity); + //#endif + } } diff --git a/src/main/java/com/replaymod/replay/ext/EntityExt.java b/src/main/java/com/replaymod/replay/ext/EntityExt.java new file mode 100644 index 00000000..5f6e4ce4 --- /dev/null +++ b/src/main/java/com/replaymod/replay/ext/EntityExt.java @@ -0,0 +1,9 @@ +package com.replaymod.replay.ext; + +public interface EntityExt { + float replaymod$getTrackedYaw(); + void replaymod$setTrackedYaw(float value); + + float replaymod$getTrackedPitch(); + void replaymod$setTrackedPitch(float value); +} diff --git a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java new file mode 100644 index 00000000..993e9c6e --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java @@ -0,0 +1,36 @@ +package com.replaymod.replay.mixin.entity_tracking; + +import com.replaymod.replay.ext.EntityExt; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Entity.class) +public abstract class Mixin_EntityExt implements EntityExt { + + @Unique + private float trackedYaw; + + @Unique + private float trackedPitch; + + @Override + public float replaymod$getTrackedYaw() { + return this.trackedYaw; + } + + @Override + public float replaymod$getTrackedPitch() { + return this.trackedPitch; + } + + @Override + public void replaymod$setTrackedYaw(float value) { + this.trackedYaw = value; + } + + @Override + public void replaymod$setTrackedPitch(float value) { + this.trackedPitch = value; + } +} diff --git a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java new file mode 100644 index 00000000..255102ae --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java @@ -0,0 +1,109 @@ +package com.replaymod.replay.mixin.entity_tracking; + +import com.replaymod.replay.ext.EntityExt; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.entity.Entity; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Every Entity on the client has at least two position/rotation states: The local one and the server ("tracked") one. + * When receiving an update from the server, the server one needs to be updated and the local one will then usually be + * interpolated to the server one within a few ticks. + * + * Minecraft however incorrectly implements the server rotation update for position-only update packets by + * interpolating to the local rotation, which might not yet match the server rotation, rather than to the previously + * received server rotation. + * Similarly, with 1.15 and later, it incorrectly implements the server position update for rotation-only update packets + * by interpolating to the local position rather than the server one. + * + * Each of these will cause the client position to be in an incorrect state until the next update packed for the + * respective rotation/position of that entity. + * + * This mixin fixes those two issues by redirecting to the server rotation/position respectively. + * Minecraft does not currently even track the server rotation, so we need to do that as well. + */ +@Mixin(ClientPlayNetworkHandler.class) +public class Mixin_FixPartialUpdates { + + // + // Use correct rotation for position-only updates + // + + //#if MC>=11700 + //$$ @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getYaw()F")) + //#else + @Redirect(method = "onEntityUpdate", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;yaw:F", opcode = Opcodes.GETFIELD)) + //#endif + private float getTrackedYaw(Entity instance) { + return ((EntityExt) instance).replaymod$getTrackedYaw(); + } + + //#if MC>=11700 + //$$ @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getPitch()F")) + //#else + @Redirect(method = "onEntityUpdate", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;pitch:F", opcode = Opcodes.GETFIELD)) + //#endif + private float getTrackedPitch(Entity instance) { + return ((EntityExt) instance).replaymod$getTrackedPitch(); + } + + //#if MC>=11500 + // + // Use correct position for rotation-only updates + // + + @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getX()D")) + private double getTrackedX(Entity instance) { + return instance.getTrackedPosition().getX(); + } + + @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getY()D")) + private double getTrackedY(Entity instance) { + return instance.getTrackedPosition().getY(); + } + + @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getZ()D")) + private double getTrackedZ(Entity instance) { + return instance.getTrackedPosition().getZ(); + } + //#endif + + // + // Track server rotation + // + + private static final String ENTITY_UPDATE = "Lnet/minecraft/entity/Entity;updateTrackedPositionAndAngles(DDDFFIZ)V"; + + @Unique + private Entity entity; + + @ModifyVariable(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), ordinal = 0) + private Entity captureEntity(Entity entity) { + return this.entity = entity; + } + + @Inject(method = "onEntityUpdate", at = @At("RETURN")) + private void resetEntityField(CallbackInfo ci) { + this.entity = null; + } + + @ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 3) + private float captureTrackedYaw(float value) { + ((EntityExt) this.entity).replaymod$setTrackedYaw(value); + return value; + } + + @ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 4) + private float captureTrackedPitch(float value) { + ((EntityExt) this.entity).replaymod$setTrackedPitch(value); + return value; + } +} diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index 6a481a03..f373ed49 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -5,6 +5,8 @@ "mixins": [], "server": [], "client": [ + "entity_tracking.Mixin_EntityExt", + "entity_tracking.Mixin_FixPartialUpdates", "Mixin_FixNPCSkinCaching", //#if MC>=11800 //$$ "Mixin_FixEntityNotTracking", From 51aa07e01e7c18ddf273723ec9aae08b3e979eed Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 15:06:58 +0100 Subject: [PATCH 034/132] Revert "Fix spectator hand jumping when rotating across 0 yaw boundary" (fixes #601) This reverts commit 411eaa4ca8bb5f4975d90e94ed79fc0573fd5e81. The given solution does not work in old versions because those did not use `getYaw` for hand rendering. Even worse, their headYaw does wrap around and it does it in a different way than their yaw value, giving a worse result than without this commit. --- .../com/replaymod/replay/camera/CameraEntity.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 09716e7e..079f9740 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -22,7 +22,6 @@ import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.entity.Entity; -import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.decoration.ItemFrameEntity; import net.minecraft.entity.player.PlayerEntity; @@ -249,14 +248,6 @@ public void setCameraPosRot(Entity to) { this.lastRenderX = to.lastRenderX; this.lastRenderY = to.lastRenderY + yOffset; this.lastRenderZ = to.lastRenderZ; - if (to instanceof LivingEntity) { - LivingEntity toLiving = (LivingEntity) to; - this.headYaw = toLiving.headYaw; - this.prevHeadYaw = toLiving.prevHeadYaw; - } else { - this.headYaw = to.yaw; - this.prevHeadYaw = to.prevYaw; - } updateBoundingBox(); } @@ -265,7 +256,7 @@ public void setCameraPosRot(Entity to) { public float getYaw(float tickDelta) { Entity view = this.client.getCameraEntity(); if (view != null && view != this) { - return this.prevHeadYaw + (this.headYaw - this.prevHeadYaw) * tickDelta; + return this.prevYaw + (this.yaw - this.prevYaw) * tickDelta; } return super.getYaw(tickDelta); } @@ -712,7 +703,7 @@ private void updateArmYawAndPitch() { this.lastRenderYaw = this.renderYaw; this.lastRenderPitch = this.renderPitch; this.renderPitch = this.renderPitch + (this.pitch - this.renderPitch) * 0.5f; - this.renderYaw = this.renderYaw + (this.headYaw - this.renderYaw) * 0.5f; + this.renderYaw = this.renderYaw + (this.yaw - this.renderYaw) * 0.5f; } public boolean canSpectate(Entity e) { From 1b18b5c952ff13d9af54d920147e9c4af29be69f Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 16:08:44 +0100 Subject: [PATCH 035/132] Fix spectator hand jumping when rotating across 0 yaw boundary, take 2 The `yaw` value of non-client-players is constraint to [0; 360), so when that boundary is crossed, the `renderYaw` starts interpolating to its goal the incorrect way round (instead of crossing the 360 boundary as well). To fix that, we now add the difference between the current `renderYaw` and the desired one, modulo 360 (but with range (-180; 180]), to the current `renderYaw`. That way we always take the short way round. Additionally, because for rendering MC looks at the difference without modulo, afterwards (and when a new `yaw` is set) we wrap the result around such that the actual difference is always less than 180 and therefore rendered as intended. --- .../replaymod/replay/camera/CameraEntity.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 079f9740..5ae3fcec 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -248,6 +248,7 @@ public void setCameraPosRot(Entity to) { this.lastRenderX = to.lastRenderX; this.lastRenderY = to.lastRenderY + yOffset; this.lastRenderZ = to.lastRenderZ; + this.wrapArmYaw(); updateBoundingBox(); } @@ -703,7 +704,35 @@ private void updateArmYawAndPitch() { this.lastRenderYaw = this.renderYaw; this.lastRenderPitch = this.renderPitch; this.renderPitch = this.renderPitch + (this.pitch - this.renderPitch) * 0.5f; - this.renderYaw = this.renderYaw + (this.yaw - this.renderYaw) * 0.5f; + this.renderYaw = this.renderYaw + wrapDegrees(this.yaw - this.renderYaw) * 0.5f; + this.wrapArmYaw(); + } + + /** + * Minecraft renders the arm offset based on the difference between {@link #yaw} and {@link #renderYaw}. It does not + * wrap around the difference though, so if {@link #yaw} just wrapped around from 350 to 10 but {@link #renderYaw} + * is still at 355, then the difference will be inappropriately large. To fix this, we always wrap the + * {@link #renderYaw} such that it is no more than 180 degrees away from {@link #yaw}, even if that requires going + * outside the normal range. + */ + private void wrapArmYaw() { + this.renderYaw = wrapDegreesTo(this.renderYaw, this.yaw); + this.lastRenderYaw = wrapDegreesTo(this.lastRenderYaw, this.renderYaw); + } + + private static float wrapDegreesTo(float value, float towardsValue) { + while (towardsValue - value < -180) { + value -= 360; + } + while (towardsValue - value >= 180) { + value += 360; + } + return value; + } + + private static float wrapDegrees(float value) { + value %= 360; + return wrapDegreesTo(value, 0); } public boolean canSpectate(Entity e) { From 96f7b78a388027f560aad3e388ed16440aad1c10 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 16:33:57 +0100 Subject: [PATCH 036/132] Fix spectator hand jittering in 1.12.2 and below --- .../mixin/Mixin_FixHandOffsetTickDelta.java | 1 + .../resources/mixins.replay.replaymod.json | 2 + .../mixin/Mixin_FixHandOffsetTickDelta.java | 46 +++++++++++++++++++ versions/1.9.4/mapping.txt | 1 + 4 files changed, 50 insertions(+) create mode 100644 src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java create mode 100644 versions/1.12.2/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java b/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java new file mode 100644 index 00000000..fca4cd6c --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java @@ -0,0 +1 @@ +// 1.12.2 and below diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index f373ed49..356d50cc 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -17,6 +17,8 @@ //#if MC>=11400 "MixinCamera", "MixinInGameHud", + //#else + //$$ "Mixin_FixHandOffsetTickDelta", //#endif "ClientWorldAccessor", "EntityLivingBaseAccessor", diff --git a/versions/1.12.2/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java b/versions/1.12.2/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java new file mode 100644 index 00000000..6f447aed --- /dev/null +++ b/versions/1.12.2/src/main/java/com/replaymod/replay/mixin/Mixin_FixHandOffsetTickDelta.java @@ -0,0 +1,46 @@ +package com.replaymod.replay.mixin; + +import com.replaymod.replay.camera.CameraEntity; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.renderer.ItemRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * In 1.12.2 and below, Vanilla does not respect the tickDelta value when getting the yaw/pitch of the player for + * computing the hand offset. + * This causes the hand movement to be jittery when spectating another player in a replay. + */ +@Mixin(ItemRenderer.class) +public abstract class Mixin_FixHandOffsetTickDelta { + @Redirect(method = "rotateArm", at = @At(value = "FIELD", target = "Lnet/minecraft/client/entity/EntityPlayerSP;rotationYaw:F")) + private float getYaw( + EntityPlayerSP instance, + //#if MC<10900 + //$$ EntityPlayerSP arg, + //#endif + float tickDelta + ) { + if (instance instanceof CameraEntity) { + return instance.prevRotationYaw + (instance.rotationYaw - instance.prevRotationYaw) * tickDelta; + } else { + return instance.rotationYaw; + } + } + + @Redirect(method = "rotateArm", at = @At(value = "FIELD", target = "Lnet/minecraft/client/entity/EntityPlayerSP;rotationPitch:F")) + private float getPitch( + EntityPlayerSP instance, + //#if MC<10900 + //$$ EntityPlayerSP arg, + //#endif + float tickDelta + ) { + if (instance instanceof CameraEntity) { + return instance.prevRotationPitch + (instance.rotationPitch - instance.prevRotationPitch) * tickDelta; + } else { + return instance.rotationPitch; + } + } +} diff --git a/versions/1.9.4/mapping.txt b/versions/1.9.4/mapping.txt index 66b70775..ea42a2f1 100644 --- a/versions/1.9.4/mapping.txt +++ b/versions/1.9.4/mapping.txt @@ -8,6 +8,7 @@ net.minecraft.util.text.TextFormatting net.minecraft.util.EnumChatFormatting net.minecraft.util.text.ITextComponent net.minecraft.util.IChatComponent net.minecraft.network.datasync.EntityDataManager net.minecraft.entity.DataWatcher net.minecraft.network.datasync.EntityDataManager.DataEntry net.minecraft.entity.DataWatcher.WatchableObject +net.minecraft.client.renderer.ItemRenderer rotateArm() rotateWithPlayerRotations() net.minecraft.client.renderer.VertexBuffer net.minecraft.client.renderer.WorldRenderer net.minecraft.client.particle.Particle net.minecraft.client.particle.EntityFX net.minecraft.util.math.MathHelper net.minecraft.util.MathHelper From bd68a80152fa7e8e5f0c297a3af65b694c3d3a81 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 16:48:01 +0100 Subject: [PATCH 037/132] Fix chroma key rendering for 1.12.2 and below (fixes #589) Forge naming strikes again, there's two different `updateCameraAndRender` in 1.14.4 forge, which lead us to use `updateCameraAndRender` for the older version as well even though our desired method had a different name back then. --- .../com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java index f5b63a21..d619f02c 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyForceSky.java @@ -27,15 +27,12 @@ public abstract class Mixin_ChromaKeyForceSky { @Shadow @Final private MinecraftClient client; - // FIXME preprocessor bug: should be able to remap these //#if MC>=11500 @ModifyConstant(method = "render", constant = @Constant(intValue = 4)) //#elseif MC>=11400 //$$ @ModifyConstant(method = "renderCenter", constant = @Constant(intValue = 4)) - //#elseif MC>=10809 - //$$ @ModifyConstant(method = "updateCameraAndRender(FJ)V", constant = @Constant(intValue = 4)) //#else - //$$ @ModifyConstant(method = "updateCameraAndRender(F)V", constant = @Constant(intValue = 4)) + //$$ @ModifyConstant(method = "renderWorldPass", constant = @Constant(intValue = 4)) //#endif private int forceSkyWhenChromaKeying(int value) { EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this.client.gameRenderer).replayModRender_getHandler(); From 3d9d40656ebd1fa56e489ff811408d99588bd867 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 17:46:15 +0100 Subject: [PATCH 038/132] Fix recording of riptide animation (fixes #581) This code supposedly meant for recording when a player was eating/blocking was never needed to begin with cause the server echos all data in DataTracker anyway, and now it broke the riptide animation because that's part of the living flags as well but wasn't considered here, i.e. unconditionally overwritten. --- .../handler/RecordingEventHandler.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 2a7cee0f..4ccc2d9e 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -41,12 +41,10 @@ //#endif //#if MC>=10904 -import com.replaymod.recording.mixin.EntityLivingBaseAccessor; import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; import net.minecraft.network.packet.s2c.play.WorldEventS2CPacket; import net.minecraft.entity.EquipmentSlot; -import net.minecraft.entity.data.DataTracker; import net.minecraft.util.Hand; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; @@ -73,10 +71,6 @@ public class RecordingEventHandler extends EventRegistrations { private boolean wasSleeping; private int lastRiding = -1; private Integer rotationYawHeadBefore; - //#if MC>=10904 - private boolean wasHandActive; - private Hand lastActiveHand; - //#endif public RecordingEventHandler(PacketListener packetListener) { this.packetListener = packetListener; @@ -335,18 +329,6 @@ private void onPlayerTick() { wasSleeping = false; } - //#if MC>=10904 - // Active hand (e.g. eating, drinking, blocking) - if (player.isUsingItem() ^ wasHandActive || player.getActiveHand() != lastActiveHand) { - wasHandActive = player.isUsingItem(); - lastActiveHand = player.getActiveHand(); - DataTracker dataManager = new DataTracker(null); - int state = (wasHandActive ? 1 : 0) | (lastActiveHand == Hand.OFF_HAND ? 2 : 0); - dataManager.startTracking(EntityLivingBaseAccessor.getLivingFlags(), (byte) state); - packetListener.save(new EntityTrackerUpdateS2CPacket(player.getEntityId(), dataManager, true)); - } - //#endif - } catch(Exception e1) { e1.printStackTrace(); } From f4196748f503cf6f9a24fa51870c1bb15809460d Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 7 Dec 2021 18:53:35 +0100 Subject: [PATCH 039/132] Fix recording controls on menu-less pause screen (fixes #575) --- .../java/com/replaymod/recording/gui/GuiRecordingControls.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java index e766f0a5..708daee9 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java @@ -87,6 +87,9 @@ private void injectIntoIngameMenu(Screen guiScreen, if (!(guiScreen instanceof GameMenuScreen)) { return; } + if (buttonList.isEmpty()) { + return; // menu-less pause (F3+Esc) + } Function yPos = MCVer.findButton(buttonList, "menu.returnToMenu", 1) .map(Optional::of) From 59ac665c81e5b8c1d909da620013d2419c2832c9 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 8 Dec 2021 14:04:17 +0100 Subject: [PATCH 040/132] Disable auto-sync if there is no stable position (closes #488) Cause not doing anything is better than flickering between different parts of the timeline every frame. --- .../simplepathing/gui/GuiPathing.java | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index d4031bbd..d2117cf8 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -451,14 +451,14 @@ private void checkForAutoSync() { prevTime = time; } - public void syncTimeButtonPressed() { + private Integer computeSyncTime(int cursor) { // Current replay time int time = replayHandler.getReplaySender().currentTimeStamp(); - // Position of the cursor - int cursor = timeline.getCursorPosition(); // Get the last time keyframe before the cursor - mod.getCurrentTimeline().getTimePath().getKeyframes().stream() - .filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> { + Keyframe keyframe = mod.getCurrentTimeline().getTimePath().getKeyframes().stream() + .filter(it -> it.getTime() <= cursor).reduce((__, last) -> last) + .orElse(null); + if (keyframe != null) { // Cursor position at the keyframe int keyframeCursor = (int) keyframe.getTime(); // Replay time at the keyframe @@ -470,11 +470,49 @@ public void syncTimeButtonPressed() { double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : replayHandler.getOverlay().getSpeedSliderValue(); // Cursor time passed int cursorPassed = (int) (timePassed / speed); - // Move cursor to new position - timeline.setCursorPosition(keyframeCursor + cursorPassed).ensureCursorVisibleWithPadding(); - // Deselect keyframe to allow the user to add a new one right away - mod.setSelected(null, 0); - }); + // Return new position + return keyframeCursor + cursorPassed; + } else { + // No keyframes before cursor + return null; + } + } + + public void syncTimeButtonPressed() { + // Position of the cursor + int cursor = timeline.getCursorPosition(); + + // Update cursor once + Integer updatedCursor = computeSyncTime(cursor); + if (updatedCursor == null) { + return; // no keyframes before cursor, nothing we can do + } + cursor = updatedCursor; + + // Repeatedly update until we find a fix point + while (true) { + updatedCursor = computeSyncTime(cursor); + if (updatedCursor == null) { + // Cursor has gotten stuck before in front of all keyframes. + // Let's just use the last value we got, this shouldn't happen with ordinary timelines anyway. + break; + } + if (updatedCursor == cursor) { + // Found the fix point, we can stop now + break; + } + if (updatedCursor < cursor) { + // We've gone backwards, we'll likely get stuck in a loop, so abort the whole thing + return; + } + // Found a new position, take it, repeat + cursor = updatedCursor; + } + + // Move cursor to new position + timeline.setCursorPosition(cursor).ensureCursorVisibleWithPadding(); + // Deselect keyframe to allow the user to add a new one right away + mod.setSelected(null, 0); } public boolean deleteButtonPressed() { From ad62e51ab3b583c7c29a65879d24178cd26cb6dc Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 8 Dec 2021 16:22:06 +0100 Subject: [PATCH 041/132] Fix lighting when block is placed in same frame as chunk load --- .../java/com/replaymod/replay/FullReplaySender.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 36cab830..95b944a5 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -454,8 +454,14 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) //#if MC>=11400 if (p instanceof ChunkDataS2CPacket) { Runnable doLightUpdates = () -> { - if (mc.world != null) { - LightingProvider provider = mc.world.getChunkManager().getLightingProvider(); + ClientWorld world = mc.world; + if (world != null) { + //#if MC>=11800 + //$$ while (!world.hasNoChunkUpdaters()) { + //$$ world.runQueuedChunkUpdates(); + //$$ } + //#endif + LightingProvider provider = world.getChunkManager().getLightingProvider(); while (provider.hasUpdates()) { provider.doLightUpdates(Integer.MAX_VALUE, true, true); } From 2df9049e6bf0076c28b265bbd49301093ac5617a Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 8 Dec 2021 17:30:36 +0100 Subject: [PATCH 042/132] Fix error when loading old replay on Java 9+ (fixes #578) By updating the shadow plugin used in ReplayStudio. java.lang.IncompatibleClassChangeError: Inconsistent constant pool data in classfile for class com/replaymod/replaystudio/lib/viaversion/libs/kyori/adventure/text/Component. Method 'boolean lambda$static$0(com.replaymod.replaystudio.lib.viaversion.libs.kyori.adventure.text.Component, com.replaymod.replaystudio.lib.viaversion.libs.kyori.adventure.text.Component)' at index 1064 is CONSTANT_MethodRef and should be CONSTANT_InterfaceMethodRef --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f5e811b..b2f11729 100644 --- a/build.gradle +++ b/build.gradle @@ -335,7 +335,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:8cfd22c", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:70f59ef", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those From f943d116614b13e035a640c0eefbbd9dda7dab2c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 10 Dec 2021 11:30:43 +0100 Subject: [PATCH 043/132] Bump to MC 1.18.1 --- build.gradle | 3 +++ jGui | 2 +- root.gradle.kts | 4 ++-- settings.gradle.kts | 4 ++-- versions/{1.18 => 1.18.1}/.gitkeep | 0 .../java/com/replaymod/render/mixin/ChunkInfoAccessor.java | 0 .../com/replaymod/render/mixin/Mixin_ForceChunkLoading.java | 0 .../replaymod/replay/mixin/Mixin_FixEntityNotTracking.java | 0 ...abric-1.18-1.17.1.txt => mapping-fabric-1.18.1-1.17.1.txt} | 0 9 files changed, 8 insertions(+), 5 deletions(-) rename versions/{1.18 => 1.18.1}/.gitkeep (100%) rename versions/{1.18 => 1.18.1}/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java (100%) rename versions/{1.18 => 1.18.1}/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java (100%) rename versions/{1.18 => 1.18.1}/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java (100%) rename versions/{mapping-fabric-1.18-1.17.1.txt => mapping-fabric-1.18.1-1.17.1.txt} (100%) diff --git a/build.gradle b/build.gradle index b2f11729..59fd9e7b 100644 --- a/build.gradle +++ b/build.gradle @@ -244,6 +244,7 @@ dependencies { 11700: '1.17', 11701: '1.17.1', 11800: '1.18', + 11801: '1.18.1', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -254,6 +255,7 @@ dependencies { 11700: '1.17+build.13:v2', 11701: '1.17.1+build.29:v2', 11800: '1.18+build.1:v2', + 11801: '1.18.1+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ @@ -265,6 +267,7 @@ dependencies { 11700: '0.36.0+1.17', 11701: '0.37.1+1.17', 11800: '0.43.1+1.18', + 11801: '0.43.1+1.18', ][mcVersion] def fabricApiModules = [ "api-base", diff --git a/jGui b/jGui index 3ac431cf..c79b62a7 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 3ac431cfa983009d4785f8962fdc8e0cc49eb825 +Subproject commit c79b62a73e649fd6d16a1ffbd9e320555834bc46 diff --git a/root.gradle.kts b/root.gradle.kts index fe2af4b6..f94b12fc 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -189,7 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { - val mc11800 = createNode("1.18", 11800, "yarn") + val mc11801 = createNode("1.18.1", 11801, "yarn") val mc11701 = createNode("1.17.1", 11701, "yarn") val mc11700 = createNode("1.17", 11700, "yarn") val mc11604 = createNode("1.16.4", 11604, "yarn") @@ -208,7 +208,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") - mc11800.link(mc11701, file("versions/mapping-fabric-1.18-1.17.1.txt")) + mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt")) mc11701.link(mc11700) mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt")) mc11604.link(mc11601) diff --git a/settings.gradle.kts b/settings.gradle.kts index e916eed4..2030c3cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,7 @@ val jGuiVersions = listOf( "1.16.4", "1.17", "1.17.1", - "1.18", + "1.18.1", ) val replayModVersions = listOf( // "1.7.10", @@ -51,7 +51,7 @@ val replayModVersions = listOf( "1.16.4", "1.17", "1.17.1", - "1.18", + "1.18.1", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/versions/1.18/.gitkeep b/versions/1.18.1/.gitkeep similarity index 100% rename from versions/1.18/.gitkeep rename to versions/1.18.1/.gitkeep diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java b/versions/1.18.1/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java similarity index 100% rename from versions/1.18/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java rename to versions/1.18.1/src/main/java/com/replaymod/render/mixin/ChunkInfoAccessor.java diff --git a/versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java b/versions/1.18.1/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java similarity index 100% rename from versions/1.18/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java rename to versions/1.18.1/src/main/java/com/replaymod/render/mixin/Mixin_ForceChunkLoading.java diff --git a/versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java b/versions/1.18.1/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java similarity index 100% rename from versions/1.18/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java rename to versions/1.18.1/src/main/java/com/replaymod/replay/mixin/Mixin_FixEntityNotTracking.java diff --git a/versions/mapping-fabric-1.18-1.17.1.txt b/versions/mapping-fabric-1.18.1-1.17.1.txt similarity index 100% rename from versions/mapping-fabric-1.18-1.17.1.txt rename to versions/mapping-fabric-1.18.1-1.17.1.txt From e472713d1ccf6af4ecedfe1bf20bf5ecb9fec125 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 11 Dec 2021 10:51:07 +0100 Subject: [PATCH 044/132] Fix rotation from entity teleport getting lost (related to #619) This fixes the rotation of newly placed entities (cause they seem to get spawned with 0/0 and then get teleported to their correct rotation, which we didn't capture). This does not fix the rotation of already existing entities that just got sent to the client, those already get spawned with their rotation, which we don't yet capture. The following commit fixes that. --- .../mixin/entity_tracking/Mixin_FixPartialUpdates.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java index 255102ae..2ffd980a 100644 --- a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java +++ b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_FixPartialUpdates.java @@ -85,23 +85,23 @@ private double getTrackedZ(Entity instance) { @Unique private Entity entity; - @ModifyVariable(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), ordinal = 0) + @ModifyVariable(method = { "onEntityUpdate", "onEntityPosition" }, at = @At(value = "INVOKE", target = ENTITY_UPDATE), ordinal = 0) private Entity captureEntity(Entity entity) { return this.entity = entity; } - @Inject(method = "onEntityUpdate", at = @At("RETURN")) + @Inject(method = { "onEntityUpdate", "onEntityPosition" }, at = @At("RETURN")) private void resetEntityField(CallbackInfo ci) { this.entity = null; } - @ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 3) + @ModifyArg(method = { "onEntityUpdate", "onEntityPosition" }, at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 3) private float captureTrackedYaw(float value) { ((EntityExt) this.entity).replaymod$setTrackedYaw(value); return value; } - @ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 4) + @ModifyArg(method = { "onEntityUpdate", "onEntityPosition" }, at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 4) private float captureTrackedPitch(float value) { ((EntityExt) this.entity).replaymod$setTrackedPitch(value); return value; From 841c99148ec8f998bd6496bc1e95f954c49b8ac9 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 11 Dec 2021 11:00:57 +0100 Subject: [PATCH 045/132] Fix initial entity rotation getting lost (fixes #619) This fixes the rotation of already existing entities that just got sent to the client. Those get spawned with their rotation, which we didn't capture. This commit fixes that by initializing our tracked rotation to NaN and returning the original rotation until the tracked one is initialized (we do it this way rather than initializing during the spawn packet handling because there are many spawn packets, especially with modded, and we cannot possibly inject into all of them). This does not fix the rotation of newly placed entities (cause they seem to get spawned with 0/0 and then get teleported to their correct rotation). The previous commit fixed that case by capturing the rotation from the teleport packet. --- .../mixin/entity_tracking/Mixin_EntityExt.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java index 993e9c6e..7b5642eb 100644 --- a/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java +++ b/src/main/java/com/replaymod/replay/mixin/entity_tracking/Mixin_EntityExt.java @@ -3,25 +3,32 @@ import com.replaymod.replay.ext.EntityExt; import net.minecraft.entity.Entity; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @Mixin(Entity.class) public abstract class Mixin_EntityExt implements EntityExt { + @Shadow + public float yaw; + + @Shadow + public float pitch; + @Unique - private float trackedYaw; + private float trackedYaw = Float.NaN; @Unique - private float trackedPitch; + private float trackedPitch = Float.NaN; @Override public float replaymod$getTrackedYaw() { - return this.trackedYaw; + return !Float.isNaN(this.trackedYaw) ? this.trackedYaw : this.yaw; } @Override public float replaymod$getTrackedPitch() { - return this.trackedPitch; + return !Float.isNaN(this.trackedPitch) ? this.trackedPitch : this.pitch; } @Override From 9528a5761ca54ed5cc65d417932ae754ab275f20 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 27 Feb 2022 09:10:35 +0100 Subject: [PATCH 046/132] Update old ModMenu versions To use the new maven group and for compatibility with newer fabric-loader. --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 59fd9e7b..b486fbaf 100644 --- a/build.gradle +++ b/build.gradle @@ -353,9 +353,11 @@ dependencies { } else if (mcVersion >= 11602) { modImplementation 'com.terraformersmc:modmenu:1.16.8' } else if (mcVersion >= 11600) { - modImplementation 'io.github.prospector:modmenu:1.14.0+build.24' + modImplementation('com.terraformersmc:modmenu:1.14.15') { + exclude module: 'fabric-resource-loader-v0' // inappropriate version for 1.16.1 + } } else { - modImplementation 'io.github.prospector.modmenu:ModMenu:1.6.2-92' + modImplementation 'com.terraformersmc:modmenu:1.10.6' } } From e378ae8f93fbfa4053b1366600454df515ba0a67 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 27 Feb 2022 10:54:23 +0100 Subject: [PATCH 047/132] Fix entities disappearing on 1.16.1 and 1.15.2 client (fixes #657) --- src/main/java/com/replaymod/core/versions/MCVer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index cb7c7f00..c239d9b0 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -304,7 +304,7 @@ public static Vec3d getPosition(Particle particle, float partialTicks) { //#if MC<=11601 //$$ public static Vec3d getTrackedPosition(Entity entity) { - //$$ return new Vec3d(entity.trackedX, entity.trackedY, entity.trackedZ); + //$$ return new Vec3d(entity.trackedX / 4096.0, entity.trackedY / 4096.0, entity.trackedZ / 4096.0); //$$ } //#endif From 7e9a1e7c135761837cfa26f793664d3d5aff2bcb Mon Sep 17 00:00:00 2001 From: Andrew S Date: Sat, 25 Dec 2021 20:01:48 -0500 Subject: [PATCH 048/132] Fix Render GUI on Retina Screens (fixes #338) window.getFramebufferWidth/Height() should be used instead of window.getWidth/Height() Co-authored-by: Jonas Herzig --- .../render/mixin/MainWindowAccessor.java | 9 ++++ .../render/rendering/VideoRenderer.java | 47 +++++++++++++------ .../com/replaymod/core/versions/Window.java | 5 ++ .../render/mixin/MainWindowAccessor.java | 3 ++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index 335c9908..a54dd178 100644 --- a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -3,6 +3,7 @@ import net.minecraft.client.util.Window; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Window.class) public interface MainWindowAccessor { @@ -14,4 +15,12 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor void setFramebufferHeight(int value); + // FIXME preprocessor should be able to infer this mapping + // FIXME preprocessor should be able to remap this one when the mapping is given manually + //#if MC>=11500 + @Invoker + //#else + //$$ @Invoker("method_4483") + //#endif + void invokeUpdateFramebufferSize(); } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index dfa7e096..9eb819d5 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -120,6 +120,7 @@ public class VideoRenderer implements RenderInfo { private Framebuffer guiFramebuffer; private int displayWidth, displayHeight; + private int framebufferWidth, framebufferHeight; public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException { this.settings = settings; @@ -245,10 +246,10 @@ public boolean renderVideo() throws Throwable { public float updateForNextFrame() { // because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily MainWindowAccessor acc = (MainWindowAccessor) (Object) mc.getWindow(); - int displayWidthBefore = acc.getFramebufferWidth(); - int displayHeightBefore = acc.getFramebufferHeight(); - acc.setFramebufferWidth(displayWidth); - acc.setFramebufferHeight(displayHeight); + int framebufferWidthBefore = acc.getFramebufferWidth(); + int framebufferHeightBefore = acc.getFramebufferHeight(); + acc.setFramebufferWidth(framebufferWidth); + acc.setFramebufferHeight(framebufferHeight); if (!settings.isHighPerformance() || framesDone % fps == 0) { while (drawGui() && paused) { @@ -289,8 +290,8 @@ public float updateForNextFrame() { } // change Minecraft's display size back - acc.setFramebufferWidth(displayWidthBefore); - acc.setFramebufferHeight(displayHeightBefore); + acc.setFramebufferWidth(framebufferWidthBefore); + acc.setFramebufferHeight(framebufferHeightBefore); if (cameraPathExporter != null) { cameraPathExporter.recordFrame(timer.tickDelta); @@ -362,6 +363,7 @@ private void setup() { } updateDisplaySize(); + updateFramebufferSize(); gui.toMinecraft().init(mc, mc.getWindow().getScaledWidth(), mc.getWindow().getScaledHeight()); @@ -369,9 +371,9 @@ private void setup() { // Set up our own framebuffer to render the GUI to //#if MC>=11700 - //$$ guiFramebuffer = new WindowFramebuffer(displayWidth, displayHeight); + //$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight); //#else - guiFramebuffer = new Framebuffer(displayWidth, displayHeight, true + guiFramebuffer = new Framebuffer(framebufferWidth, framebufferHeight, true //#if MC>=11400 , false //#endif @@ -425,7 +427,7 @@ private void finish() { } // Finally, resize the Minecraft framebuffer to the actual width/height of the window - resizeMainWindow(mc, displayWidth, displayHeight); + resizeMainWindow(mc, framebufferWidth, framebufferHeight); } private void executeTaskQueue() { @@ -483,17 +485,23 @@ public boolean drawGui() { return false; } - // Resize the GUI framebuffer if the display size changed + // Check if display size has changes and force recalculate GUI framebuffer size. if (displaySizeChanged()) { updateDisplaySize(); + ((MainWindowAccessor) (Object) window).invokeUpdateFramebufferSize(); + } + + // Resize the GUI framebuffer if the display size changed + if (framebufferSizeChanged()) { + updateFramebufferSize(); //#if MC>=11400 - guiFramebuffer.resize(displayWidth, displayHeight + guiFramebuffer.resize(framebufferWidth, framebufferHeight //#if MC>=11400 , false //#endif ); //#else - //$$ guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight); + //$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight); //#endif } @@ -594,7 +602,7 @@ public boolean drawGui() { guiFramebuffer.endWrite(); popMatrix(); pushMatrix(); - guiFramebuffer.draw(displayWidth, displayHeight); + guiFramebuffer.draw(framebufferWidth, framebufferHeight); popMatrix(); //#if MC>=11500 @@ -633,12 +641,18 @@ public boolean drawGui() { private boolean displaySizeChanged() { int realWidth = mc.getWindow().getWidth(); int realHeight = mc.getWindow().getHeight(); + return displayWidth != realWidth || displayHeight != realHeight; + } + + private boolean framebufferSizeChanged() { + int realWidth = mc.getWindow().getFramebufferWidth(); + int realHeight = mc.getWindow().getFramebufferHeight(); if (realWidth == 0 || realHeight == 0) { // These can be zero on Windows if minimized. // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. return false; } - return displayWidth != realWidth || displayHeight != realHeight; + return framebufferWidth != realWidth || framebufferHeight != realHeight; } private void updateDisplaySize() { @@ -646,6 +660,11 @@ private void updateDisplaySize() { displayHeight = mc.getWindow().getHeight(); } + private void updateFramebufferSize() { + framebufferWidth = mc.getWindow().getFramebufferWidth(); + framebufferHeight = mc.getWindow().getFramebufferHeight(); + } + public int getFramesDone() { return framesDone; } diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java index fdcadef3..26d697d2 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java +++ b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java @@ -34,6 +34,11 @@ public void setFramebufferHeight(int value) { mc.displayHeight = value; } + @Override + public void invokeUpdateFramebufferSize() { + // no-op, pre-LWJGL3 MC doesn't differentiate between window and framebuffer size + } + public long getHandle() { return 0; } diff --git a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index 2d91cb58..a6166095 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -3,6 +3,7 @@ import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Minecraft.class) public interface MainWindowAccessor { @@ -14,4 +15,6 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor("displayHeight") void setFramebufferHeight(int value); + @Invoker + void invokeUpdateFramebufferSize(); } From 0b8a2a107c0c21c312aff75918f47baf8f9c1069 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 27 Feb 2022 12:59:19 +0100 Subject: [PATCH 049/132] Use separate thread to send packets, even in sync mode (fixes #674) --- .../java/com/replaymod/extras/QuickMode.java | 2 +- .../replaymod/replay/FullReplaySender.java | 185 +++++++++++++----- .../replaymod/replay/QuickReplaySender.java | 4 +- 3 files changed, 137 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/replaymod/extras/QuickMode.java b/src/main/java/com/replaymod/extras/QuickMode.java index 454d20be..ab1f677d 100644 --- a/src/main/java/com/replaymod/extras/QuickMode.java +++ b/src/main/java/com/replaymod/extras/QuickMode.java @@ -25,7 +25,7 @@ public void register(final ReplayMod mod) { return; } replayHandler.getReplaySender().setSyncModeAndWait(); - mod.runLater(() -> { + mod.runLaterWithoutLock(() -> { replayHandler.ensureQuickModeInitialized(() -> { boolean enabled = !replayHandler.isQuickMode(); updateIndicator(replayHandler.getOverlay(), enabled); diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 95b944a5..2aee5e57 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -125,6 +125,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.replaystudio.util.Utils.readInt; @@ -349,6 +352,7 @@ public void terminateReplay() { return; } terminate = true; + syncSender.shutdown(); events.unregister(); try { channelInactive(ctx); @@ -403,53 +407,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) super.channelRead(ctx, p); } - // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world - // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. - // We do this after every SpawnX packet and after the destroy entities packet. - if (!asyncMode && mc.world != null) { - if (p instanceof PlayerSpawnS2CPacket - || p instanceof EntitySpawnS2CPacket - || p instanceof MobSpawnS2CPacket - //#if MC<11600 - //$$ || p instanceof EntitySpawnGlobalS2CPacket - //#endif - || p instanceof PaintingSpawnS2CPacket - || p instanceof ExperienceOrbSpawnS2CPacket - || p instanceof EntitiesDestroyS2CPacket) { - ClientWorld world = mc.world; - //#if MC>=11700 - //$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener) - //#elseif MC>=11400 - // Note: Not sure if it's still required but there's this really handy method anyway - world.finishRemovingEntities(); - //#else - //$$ Iterator iter = world.loadedEntityList.iterator(); - //$$ while (iter.hasNext()) { - //$$ Entity entity = iter.next(); - //$$ if (entity.isDead) { - //$$ int chunkX = entity.chunkCoordX; - //$$ int chunkY = entity.chunkCoordZ; - //$$ - //#if MC>=11400 - //$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) { - //#else - //#if MC>=10904 - //$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { - //#else - //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { - //#endif - //#endif - //$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); - //$$ } - //$$ - //$$ iter.remove(); - //$$ world.onEntityRemoved(entity); - //$$ } - //$$ - //$$ } - //#endif - } - } + maybeRemoveDeadEntities(p); //#if MC>=11400 if (p instanceof ChunkDataS2CPacket) { @@ -504,6 +462,69 @@ private Packet deserializePacket(byte[] bytes) throws IOException, IllegalAccess return p; } + // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world + // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. + // We do this after every SpawnX packet and after the destroy entities packet. + private void maybeRemoveDeadEntities(Packet packet) { + if (asyncMode) { + return; // MC should have enough time to tick + } + + boolean relevantPacket = packet instanceof PlayerSpawnS2CPacket + || packet instanceof EntitySpawnS2CPacket + || packet instanceof MobSpawnS2CPacket + //#if MC<11600 + //$$ || packet instanceof EntitySpawnGlobalS2CPacket + //#endif + || packet instanceof PaintingSpawnS2CPacket + || packet instanceof ExperienceOrbSpawnS2CPacket + || packet instanceof EntitiesDestroyS2CPacket; + if (!relevantPacket) { + return; // don't want to do it too often, only when there's likely to be a dead entity + } + + mc.send(() -> { + ClientWorld world = mc.world; + if (world != null) { + removeDeadEntities(world); + } + }); + } + + private void removeDeadEntities(ClientWorld world) { + //#if MC>=11700 + //$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener) + //#elseif MC>=11400 + // Note: Not sure if it's still required but there's this really handy method anyway + world.finishRemovingEntities(); + //#else + //$$ Iterator iter = world.loadedEntityList.iterator(); + //$$ while (iter.hasNext()) { + //$$ Entity entity = iter.next(); + //$$ if (entity.isDead) { + //$$ int chunkX = entity.chunkCoordX; + //$$ int chunkY = entity.chunkCoordZ; + //$$ + //#if MC>=11400 + //$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) { + //#else + //#if MC>=10904 + //$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { + //#else + //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { + //#endif + //#endif + //$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); + //$$ } + //$$ + //$$ iter.remove(); + //$$ world.onEntityRemoved(entity); + //$$ } + //$$ + //$$ } + //#endif + } + /** * Process a packet and return the result. * @param p The packet to process @@ -824,7 +845,13 @@ public void run() { } } - return asyncMode ? processPacketAsync(p) : processPacketSync(p); + if (asyncMode) { + return processPacketAsync(p); + } else { + Packet fp = p; + mc.send(() -> processPacketSync(fp)); + return p; + } } @Override @@ -1085,6 +1112,10 @@ protected Packet processPacketAsync(Packet p) { // Synchronous packet processing // ///////////////////////////////////////////////////////// + // Even in sync mode, we send from another thread because mods may rely on that + private final ExecutorService syncSender = Executors.newSingleThreadExecutor(runnable -> + new Thread(runnable, "replaymod-sync-sender")); + /** * Sends all packets until the specified timestamp is reached (inclusive). * If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning. @@ -1093,6 +1124,36 @@ protected Packet processPacketAsync(Packet p) { @Override public void sendPacketsTill(int timestamp) { Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead."); + + // Submit our target to the sender thread and track its progress + AtomicBoolean doneSending = new AtomicBoolean(); + syncSender.submit(() -> { + try { + doSendPacketsTill(timestamp); + } finally { + doneSending.set(true); + } + }); + + // Drain the task queue while we are sending (in case a mod blocks the io thread waiting for the main thread) + while (!doneSending.get()) { + executeTaskQueue(); + + // Wait until the sender thread has made progress + try { + //noinspection BusyWait + Thread.sleep(0, 100_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + + // Everything has been sent, drain the queue one last time + executeTaskQueue(); + } + + private void doSendPacketsTill(int timestamp) { try { while (ctx == null && !terminate) { // Make sure channel is ready Thread.sleep(10); @@ -1112,7 +1173,7 @@ public void sendPacketsTill(int timestamp) { loginPhase = true; startFromBeginning = false; nextPacket = null; - replayHandler.restartedReplay(); + ReplayMod.instance.runSync(replayHandler::restartedReplay); } if (replayIn == null) { @@ -1159,7 +1220,30 @@ public void sendPacketsTill(int timestamp) { } } - protected Packet processPacketSync(Packet p) { + private void executeTaskQueue() { + //#if MC>=11400 + ((MCVer.MinecraftMethodAccessor) mc).replayModExecuteTaskQueue(); + //#else + //$$ java.util.Queue> scheduledTasks = ((MinecraftAccessor) mc).getScheduledTasks(); + //$$ + //$$ // Live-lock detection: if we already hold the lock, then the sender thread will never be able to queue its + //$$ // tasks + //$$ if (Thread.holdsLock(scheduledTasks)) { + //$$ throw new IllegalStateException("Task queue already locked. " + + //$$ "You may want to use `Scheduler.runLaterWithoutLock` to run while the lock is not taken."); + //$$ } + //$$ + //$$ //noinspection SynchronizationOnLocalVariableOrMethodParameter + //$$ synchronized (scheduledTasks) { + //$$ while (!scheduledTasks.isEmpty()) { + //$$ scheduledTasks.poll().run(); + //$$ } + //$$ } + //#endif + ReplayMod.instance.runTasks(); + } + + protected void processPacketSync(Packet p) { //#if MC>=10904 if (p instanceof UnloadChunkS2CPacket) { UnloadChunkS2CPacket packet = (UnloadChunkS2CPacket) p; @@ -1280,7 +1364,6 @@ protected Packet processPacketSync(Packet p) { } } } - return p; // During synchronous playback everything is sent normally } private void forcePositionForVehicleAndSelf(Entity entity) { diff --git a/src/main/java/com/replaymod/replay/QuickReplaySender.java b/src/main/java/com/replaymod/replay/QuickReplaySender.java index 5f5d7462..0b613f4a 100644 --- a/src/main/java/com/replaymod/replay/QuickReplaySender.java +++ b/src/main/java/com/replaymod/replay/QuickReplaySender.java @@ -137,13 +137,13 @@ public ListenableFuture initialize(Consumer progress) { LOGGER.info("Initialized quick replay sender in " + (System.currentTimeMillis() - start) + "ms"); } catch (Throwable e) { LOGGER.error("Initializing quick replay sender:", e); - mod.getCore().runLater(() -> { + mod.getCore().runLaterWithoutLock(() -> { mod.getCore().printWarningToChat("Error initializing quick replay sender: %s", e.getLocalizedMessage()); promise.setException(e); }); return; } - mod.getCore().runLater(() -> promise.set(null)); + mod.getCore().runLaterWithoutLock(() -> promise.set(null)); }).start(); return promise; } From abfb3d46f3abc478e4ff1f6ae85daa26b1ecc302 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 27 Feb 2022 13:28:10 +0100 Subject: [PATCH 050/132] Use per-version folder for pack.mcmeta Comments are not supported here, and depending on the exact environment, that may break stuff (seems to only happen on fabric in dev). --- src/main/resources/pack.mcmeta | 8 -------- versions/1.12.2/src/main/resources/pack.mcmeta | 6 ++++++ versions/1.9.4/src/main/resources/pack.mcmeta | 6 ++++++ 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 versions/1.12.2/src/main/resources/pack.mcmeta create mode 100644 versions/1.9.4/src/main/resources/pack.mcmeta diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index e491ce51..6afaebbc 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,14 +1,6 @@ { "pack": { "description": "ReplayMod resources", - //#if MC>=11400 "pack_format": 4 - //#else - //#if MC>=11002 - //$$ "pack_format": 2 - //#else - //$$ "pack_format": 1 - //#endif - //#endif } } diff --git a/versions/1.12.2/src/main/resources/pack.mcmeta b/versions/1.12.2/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..5259b825 --- /dev/null +++ b/versions/1.12.2/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "ReplayMod resources", + "pack_format": 2 + } +} diff --git a/versions/1.9.4/src/main/resources/pack.mcmeta b/versions/1.9.4/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..9a0e3411 --- /dev/null +++ b/versions/1.9.4/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "ReplayMod resources", + "pack_format": 1 + } +} From 6cc370a117756afbe2cb50c02a7da1101d83f83c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 28 Feb 2022 15:57:35 +0100 Subject: [PATCH 051/132] Workaround Java breaking with symlinks (fixes #660) --- .../com/replaymod/core/SettingsRegistryBackend.java | 3 ++- .../replaymod/core/files/ReplayFoldersService.java | 10 ++++++---- src/main/java/com/replaymod/core/utils/Utils.java | 13 +++++++++++++ .../com/replaymod/editor/gui/MarkerProcessor.java | 1 - .../replaymod/recording/packet/PacketListener.java | 1 - 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/replaymod/core/SettingsRegistryBackend.java b/src/main/java/com/replaymod/core/SettingsRegistryBackend.java index eb996915..90f50d86 100644 --- a/src/main/java/com/replaymod/core/SettingsRegistryBackend.java +++ b/src/main/java/com/replaymod/core/SettingsRegistryBackend.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import static com.replaymod.core.utils.Utils.ensureDirectoryExists; import static com.replaymod.core.versions.MCVer.getMinecraft; class SettingsRegistryBackend { @@ -179,7 +180,7 @@ public void save() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); String config = gson.toJson(root); try { - Files.createDirectories(configFile.getParent()); + ensureDirectoryExists(configFile.getParent()); Files.write(configFile, config.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/replaymod/core/files/ReplayFoldersService.java b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java index 6587704d..2803b891 100644 --- a/src/main/java/com/replaymod/core/files/ReplayFoldersService.java +++ b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java @@ -10,6 +10,8 @@ import java.nio.file.Files; import java.nio.file.Path; +import static com.replaymod.core.utils.Utils.ensureDirectoryExists; + public class ReplayFoldersService { private final Path mcDir = MinecraftClient.getInstance().runDirectory.toPath(); private final SettingsRegistry settings; @@ -19,14 +21,14 @@ public ReplayFoldersService(SettingsRegistry settings) { } public Path getReplayFolder() throws IOException { - return Files.createDirectories(mcDir.resolve(settings.get(Setting.RECORDING_PATH))); + return ensureDirectoryExists(mcDir.resolve(settings.get(Setting.RECORDING_PATH))); } /** * Folder into which replay backups are saved before the MarkerProcessor is unleashed. */ public Path getRawReplayFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("raw")); + return ensureDirectoryExists(getReplayFolder().resolve("raw")); } /** @@ -34,7 +36,7 @@ public Path getRawReplayFolder() throws IOException { * Distinct from the main folder, so they cannot be opened while they are still saving. */ public Path getRecordingFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("recording")); + return ensureDirectoryExists(getReplayFolder().resolve("recording")); } /** @@ -42,7 +44,7 @@ public Path getRecordingFolder() throws IOException { * Distinct from the recording folder cause people kept confusing them with recordings. */ public Path getCacheFolder() throws IOException { - Path path = Files.createDirectories(mcDir.resolve(settings.get(Setting.CACHE_PATH))); + Path path = ensureDirectoryExists(mcDir.resolve(settings.get(Setting.CACHE_PATH))); try { Files.setAttribute(path, "dos:hidden", true); } catch (UnsupportedOperationException ignored) { diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 50c328c8..b082c276 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -57,6 +57,9 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -356,4 +359,14 @@ public static T configure(T instance, Consumer configure) { configure.accept(instance); return instance; } + + /** + * Like {@link Files#createDirectories(Path, FileAttribute[])} but doesn't explode if it's a symlink. + */ + public static Path ensureDirectoryExists(Path path) throws IOException { + // Who in their right mind thought the default behavior of throwing when the target is a link to a directory + // was the preferred behavior?! Everyone has to fall for this at least once to learn it... + // https://bugs.openjdk.java.net/browse/JDK-8130464 + return Files.createDirectories(Files.exists(path) ? path.toRealPath() : path); + } } diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index 0bce65f3..7231cecf 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -133,7 +133,6 @@ public static List> apply(Path path, Consumer for (int i = 1; Files.exists(inputPath); i++) { inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr"); } - Files.createDirectories(inputPath.getParent()); Files.move(path, inputPath); try (ReplayFile inputReplayFile = mod.files.open(inputPath)) { diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index e37fefad..f99833a5 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -285,7 +285,6 @@ public void channelInactive(ChannelHandlerContext ctx) { for (int i = 1; Files.exists(rawPath); i++) { rawPath = rawPath.resolveSibling(replayName + "." + i + ".mcpr"); } - Files.createDirectories(rawPath.getParent()); replayFile.saveTo(rawPath.toFile()); replayFile.close(); From 4a5df2ad6d7b387db5ffc9e3632fb384d8e60a36 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 13:36:30 +0100 Subject: [PATCH 052/132] Use `GameRenderer.renderHand` boolean field instead of mixin Should be more compatible with mods which replace the entire renderHand method (thereby making our mixin non-applicable), at least as long as these mods check `renderHand` as well (Iris explicitly does). This will become important once #638 is fixed. --- .../render/hooks/EntityRendererHandler.java | 7 ++++ .../render/mixin/GameRendererAccessor.java | 13 +++++++ .../mixin/Mixin_Omnidirectional_SkipHand.java | 34 ------------------- .../resources/mixins.render.replaymod.json | 2 +- 4 files changed, 21 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java delete mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java diff --git a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java index 8dbd50f7..ee68b87e 100644 --- a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java +++ b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java @@ -8,6 +8,7 @@ import com.replaymod.render.capturer.CaptureData; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.capturer.WorldRenderer; +import com.replaymod.render.mixin.GameRendererAccessor; import com.replaymod.replay.ReplayModReplay; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.client.MinecraftClient; @@ -81,11 +82,16 @@ public void renderWorld(float partialTicks, long finishTimeNano) { //#endif if (mc.world != null && mc.player != null) { + GameRendererAccessor gameRenderer = (GameRendererAccessor) mc.gameRenderer; Screen orgScreen = mc.currentScreen; boolean orgPauseOnLostFocus = mc.options.pauseOnLostFocus; + boolean orgRenderHand = gameRenderer.getRenderHand(); try { mc.currentScreen = null; // do not want to render the current screen (that'd just be the progress gui) mc.options.pauseOnLostFocus = false; // do not want the pause menu to open if the window is unfocused + if (omnidirectional) { + gameRenderer.setRenderHand(false); // makes no sense, we wouldn't even know where to put it + } //#if MC>=11400 mc.gameRenderer.render(partialTicks, finishTimeNano, true); @@ -100,6 +106,7 @@ public void renderWorld(float partialTicks, long finishTimeNano) { } finally { mc.currentScreen = orgScreen; mc.options.pauseOnLostFocus = orgPauseOnLostFocus; + gameRenderer.setRenderHand(orgRenderHand); } } diff --git a/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java b/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java new file mode 100644 index 00000000..e74e16bc --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java @@ -0,0 +1,13 @@ +package com.replaymod.render.mixin; + +import net.minecraft.client.render.GameRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(GameRenderer.class) +public interface GameRendererAccessor { + @Accessor + boolean getRenderHand(); + @Accessor + void setRenderHand(boolean value); +} diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java deleted file mode 100644 index 939f6ec8..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.replaymod.render.mixin; - -import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.GameRenderer; -import net.minecraft.client.util.math.MatrixStack; -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(GameRenderer.class) -public abstract class Mixin_Omnidirectional_SkipHand implements EntityRendererHandler.IEntityRenderer { - @Inject(method = "renderHand", at = @At("HEAD"), cancellable = true) - private void replayModRender_renderSpectatorHand( - //#if MC>=11500 - MatrixStack matrixStack, - //#endif - //#if MC>=11400 - Camera camera, - //#endif - float partialTicks, - //#if MC<11400 - //$$ int renderPass, - //#endif - CallbackInfo ci - ) { - EntityRendererHandler handler = replayModRender_getHandler(); - if (handler != null && handler.omnidirectional) { - // No spectator hands during 360° view, we wouldn't even know where to put it - ci.cancel(); - } - } -} diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 108c1484..ba783acd 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -16,7 +16,6 @@ "Mixin_Omnidirectional_Camera", "Mixin_Omnidirectional_DisableFrustumCulling", "Mixin_Omnidirectional_Rotation", - "Mixin_Omnidirectional_SkipHand", "Mixin_PreserveDepthDuringGuiRendering", "Mixin_SkipBlockOutlinesDuringRender", "Mixin_SkipHudDuringRender", @@ -39,6 +38,7 @@ "Mixin_PreserveDepthDuringHandRendering", "Mixin_WindowsWorkaroundForTinyEXRNatives", //#endif + "GameRendererAccessor", "MainWindowAccessor", "WorldRendererAccessor", //#if MC>=10904 From fb7900d80502b93c18188a0d3a4f03527a23f2a2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 28 Feb 2022 16:29:57 +0100 Subject: [PATCH 053/132] Fix missing hand with Iris 1.1.3 and above (fixes #638) --- .../core/ReplayModMixinConfigPlugin.java | 10 ++++++ .../mixin/Mixin_ShowSpectatedHand_Iris.java | 35 +++++++++++++++++++ .../resources/mixins.replay.replaymod.json | 1 + 3 files changed, 46 insertions(+) create mode 100644 src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java diff --git a/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java b/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java index 08259e21..443a21e0 100644 --- a/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java +++ b/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java @@ -9,6 +9,10 @@ import java.util.List; import java.util.Set; +//#if FABRIC +import net.fabricmc.loader.api.FabricLoader; +//#endif + //#if MC>=11400 import java.io.InputStream; //#else @@ -34,6 +38,9 @@ static boolean hasClass(String name) throws IOException { private final Logger logger = LogManager.getLogger("replaymod/mixin"); private final boolean hasOF = hasClass("optifine.OptiFineForgeTweaker") || hasClass("me.modmuss50.optifabric.mod.Optifabric"); + //#if FABRIC + private final boolean hasIris = FabricLoader.getInstance().isModLoaded("iris"); + //#endif { logger.debug("hasOF: " + hasOF); @@ -49,6 +56,9 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { } if (mixinClassName.endsWith("_OF")) return hasOF; if (mixinClassName.endsWith("_NoOF")) return !hasOF; + //#if FABRIC + if (mixinClassName.endsWith("_Iris")) return hasIris; + //#endif return true; } diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java b/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java new file mode 100644 index 00000000..48e8c8a3 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java @@ -0,0 +1,35 @@ +//#if MC>=11400 +package com.replaymod.replay.mixin; + +import com.replaymod.replay.camera.CameraEntity; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.world.GameMode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import static com.replaymod.core.versions.MCVer.getMinecraft; + +@Pseudo +@Mixin(targets = "net.coderbot.iris.pipeline.HandRenderer", remap = false) +public abstract class Mixin_ShowSpectatedHand_Iris { + @Redirect( + method = "*", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;getCurrentGameMode()Lnet/minecraft/world/GameMode;", + remap = true + ) + ) + private GameMode getGameMode(ClientPlayerInteractionManager interactionManager) { + ClientPlayerEntity camera = getMinecraft().player; + if (camera instanceof CameraEntity) { + // alternative doesn't really matter, the caller only checks for equality to SPECTATOR + return camera.isSpectator() ? GameMode.SPECTATOR : GameMode.SURVIVAL; + } + return interactionManager.getCurrentGameMode(); + } +} +//#endif diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index 356d50cc..f4205619 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -23,6 +23,7 @@ "ClientWorldAccessor", "EntityLivingBaseAccessor", //#if MC>=11400 + "Mixin_ShowSpectatedHand_Iris", "Mixin_ShowSpectatedHand_NoOF", "Mixin_ShowSpectatedHand_OF", //#else From fe67d7fa1418ffdb5858ceca63b7c46d19642bbd Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 28 Feb 2022 17:22:15 +0100 Subject: [PATCH 054/132] Link to our docs in the "Missing ffmpeg" screen (closes #663) --- .../com/replaymod/render/gui/GuiNoFfmpeg.java | 48 +++++++++++++++++++ .../replaymod/render/gui/GuiRenderQueue.java | 13 +---- .../render/gui/GuiRenderSettings.java | 13 +---- src/main/resources/assets/replaymod/lang | 2 +- 4 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java diff --git a/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java b/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java new file mode 100644 index 00000000..6261f861 --- /dev/null +++ b/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java @@ -0,0 +1,48 @@ +package com.replaymod.render.gui; + +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; + +import java.net.URI; + +import static com.replaymod.core.versions.MCVer.openURL; +import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString; + +public class GuiNoFfmpeg extends GuiScreen { + + private static final String LINK = "https://www.replaymod.com/docs/#installing-ffmpeg"; + + private final GuiLabel message = new GuiLabel() + .setI18nText("replaymod.gui.rendering.error.message"); + private final GuiLabel link = new GuiLabel() + .setText(LINK); + private final GuiButton openLinkButton = new GuiButton() + .setI18nLabel("chat.link.open") + .setSize(100, 20) + .onClick(() -> openURL(URI.create(LINK))); + private final GuiButton copyToClipboardButton = new GuiButton() + .setI18nLabel("chat.copy") + .setSize(100, 20) + .onClick(() -> setClipboardString(LINK)); + private final GuiButton backButton = new GuiButton() + .setI18nLabel("gui.back") + .setSize(100, 20); + private final GuiPanel buttons = new GuiPanel() + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(4)) + .addElements(null, openLinkButton, copyToClipboardButton, backButton); + + { + setBackground(Background.DIRT); + setTitle(new GuiLabel().setI18nText("replaymod.gui.rendering.error.title")); + setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(30)); + addElements(new VerticalLayout.Data(0.5), message, link, buttons); + } + + public GuiNoFfmpeg(Runnable goBack) { + backButton.onClick(goBack); + } +} diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index db37e673..a1b8b6a2 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -37,7 +37,6 @@ import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.NoticeScreen; import net.minecraft.util.crash.CrashReport; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; @@ -181,17 +180,7 @@ private static void processQueue(AbstractGuiScreen container, ReplayHandler r videoRenderer.renderVideo(); } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); - NoticeScreen errorScreen = new NoticeScreen( - //#if MC>=11400 - container::display, - new TranslatableText("replaymod.gui.rendering.error.title"), - new TranslatableText("replaymod.gui.rendering.error.message") - //#else - //$$ I18n.format("replaymod.gui.rendering.error.title"), - //$$ I18n.format("replaymod.gui.rendering.error.message") - //#endif - ); - mc.openScreen(errorScreen); + mc.openScreen(new GuiNoFfmpeg(container::display).toMinecraft()); return; } catch (FFmpegWriter.FFmpegStartupException e) { int jobsToSkip = jobsDone; diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 38d80a27..b8f67b17 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -30,7 +30,6 @@ import de.johni0702.minecraft.gui.utils.lwjgl.Color; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; -import net.minecraft.client.gui.screen.NoticeScreen; import net.minecraft.client.resource.language.I18n; import net.minecraft.util.crash.CrashReport; @@ -240,17 +239,7 @@ public void run() { videoRenderer.renderVideo(); } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); - NoticeScreen errorScreen = new NoticeScreen( - //#if MC>=11400 - getScreen()::display, - new TranslatableText("replaymod.gui.rendering.error.title"), - new TranslatableText("replaymod.gui.rendering.error.message") - //#else - //$$ I18n.format("replaymod.gui.rendering.error.title"), - //$$ I18n.format("replaymod.gui.rendering.error.message") - //#endif - ); - getMinecraft().openScreen(errorScreen); + getMinecraft().openScreen(new GuiNoFfmpeg(getScreen()::display).toMinecraft()); } catch (FFmpegWriter.FFmpegStartupException e) { GuiExportFailed.tryToRecover(e, newSettings -> { // Update settings with fixed ffmpeg arguments diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 54ab62bd..4b1698fc 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 54ab62bdbb646c6b4f4b6d61a26183258e5a017d +Subproject commit 4b1698fcf805fea05d6c3f0ca6c14c63d7e731db From d9da2e135cb21c391f456e8829b2ea8f089fcb77 Mon Sep 17 00:00:00 2001 From: Jochem <29899660+JochCool@users.noreply.github.com> Date: Tue, 16 Nov 2021 00:10:21 +0100 Subject: [PATCH 055/132] Sync world border animation & movement to Replay speed Fixes an issue where the world border would move at normal speed when speeding up or slowing down the replay, and also at an incorrect speed when rendering the replay, causing it to get out of sync with the rest of the world. Co-authored-by: Jonas Herzig --- .../Mixin_UseReplayTime_ForMovement.java | 38 +++++++++++++++++++ .../Mixin_UseReplayTime_ForTexture.java | 29 ++++++++++++++ .../resources/mixins.replay.replaymod.json | 2 + 3 files changed, 69 insertions(+) create mode 100644 src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java create mode 100644 src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java diff --git a/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java new file mode 100644 index 00000000..1e8f9e2b --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java @@ -0,0 +1,38 @@ +package com.replaymod.replay.mixin.world_border; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Normally Minecraft's world border movement is based off real time; + * this redirect ensures that it is synced with the time in the Replay instead. + */ +//#if MC>=11400 +// FIXME: preprocessor should be able to remap between fabric and forge +//#if FABRIC +@Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingArea") +//#else +//$$ @Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingBorderInfo") +//#endif +//#else +//$$ @Mixin(net.minecraft.world.border.WorldBorder.class) +//#endif +public class Mixin_UseReplayTime_ForMovement { + + //#if MC>=11400 + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J")) + //#else + //$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Ljava/lang/System;currentTimeMillis()J")) + //#endif + private long getWorldBorderTime() { + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + if (replayHandler != null) { + return replayHandler.getReplaySender().currentTimeStamp(); + } + return MCVer.milliTime(); + } +} diff --git a/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java new file mode 100644 index 00000000..9abf28c4 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java @@ -0,0 +1,29 @@ +package com.replaymod.replay.mixin.world_border; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.client.render.WorldRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Normally Minecraft's world border texture animation is based off real time; + * this redirect ensures that it is synced with the time in the Replay instead. + */ +@Mixin(WorldRenderer.class) +public class Mixin_UseReplayTime_ForTexture { + //#if MC>=11400 + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J")) + //#else + //$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) + //#endif + private long getWorldBorderTime() { + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + if (replayHandler != null) { + return replayHandler.getReplaySender().currentTimeStamp(); + } + return MCVer.milliTime(); + } +} diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index f4205619..02e6fe65 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -7,6 +7,8 @@ "client": [ "entity_tracking.Mixin_EntityExt", "entity_tracking.Mixin_FixPartialUpdates", + "world_border.Mixin_UseReplayTime_ForMovement", + "world_border.Mixin_UseReplayTime_ForTexture", "Mixin_FixNPCSkinCaching", //#if MC>=11800 //$$ "Mixin_FixEntityNotTracking", From 7615499cef5dd842bbaa91000901e8a0d1f7194a Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 16:39:30 +0100 Subject: [PATCH 056/132] Add option to preserve alpha channel in export (closes #661) Default off cause it being on by default had confused quite a few people. --- .../advancedscreenshots/GuiCreateScreenshot.java | 2 +- src/main/java/com/replaymod/render/EXRWriter.java | 14 ++++++++++---- src/main/java/com/replaymod/render/PNGWriter.java | 7 +++++-- .../java/com/replaymod/render/RenderSettings.java | 10 ++++++++++ .../com/replaymod/render/gui/GuiExportFailed.java | 1 + .../replaymod/render/gui/GuiRenderSettings.java | 7 ++++++- .../replaymod/render/rendering/VideoRenderer.java | 4 ++-- src/main/resources/assets/replaymod/lang | 2 +- 8 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java index 62d316cf..cda0409d 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java @@ -40,7 +40,7 @@ public GuiCreateScreenshot(ReplayMod mod) { new GuiLabel().setI18nText("replaymod.gui.advancedscreenshots.resolution"), videoResolutionPanel, new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton); - resetChildren(advancedPanel).addElements(null, nametagCheckbox, new GuiPanel().setLayout( + resetChildren(advancedPanel).addElements(null, nametagCheckbox, alphaCheckbox , new GuiPanel().setLayout( new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15)) .addElements(new GridLayout.Data(0, 0.5), new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index 1b2348fe..0bf981d8 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -28,9 +28,11 @@ public class EXRWriter implements FrameConsumer { private final Path outputFolder; + private final boolean keepAlpha; - public EXRWriter(Path outputFolder) throws IOException { + public EXRWriter(Path outputFolder, boolean keepAlpha) throws IOException { this.outputFolder = outputFolder; + this.keepAlpha = keepAlpha; Files.createDirectories(outputFolder); } @@ -92,11 +94,15 @@ public void consume(Map channels) { bgrChannels[(i + 3) % 4] = channel; } } + + int alphaMask = keepAlpha ? 0 : 0xff; + for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - for (FloatBuffer channel : bgrChannels) { - channel.put(((int) bgra.get() & 0xff) / 255f); - } + bgrChannels[0].put(((int) bgra.get() & 0xff) / 255f); // b + bgrChannels[1].put(((int) bgra.get() & 0xff) / 255f); // g + bgrChannels[2].put(((int) bgra.get() & 0xff) / 255f); // r + bgrChannels[3].put(((int) bgra.get() & 0xff | alphaMask) / 255f); // a } } if (depthFrame != null && depthChannel != null) { diff --git a/src/main/java/com/replaymod/render/PNGWriter.java b/src/main/java/com/replaymod/render/PNGWriter.java index 846e63be..525d154d 100644 --- a/src/main/java/com/replaymod/render/PNGWriter.java +++ b/src/main/java/com/replaymod/render/PNGWriter.java @@ -19,9 +19,11 @@ public class PNGWriter implements FrameConsumer { private final Path outputFolder; + private final boolean keepAlpha; - public PNGWriter(Path outputFolder) throws IOException { + public PNGWriter(Path outputFolder, boolean keepAlpha) throws IOException { this.outputFolder = outputFolder; + this.keepAlpha = keepAlpha; Files.createDirectories(outputFolder); } @@ -47,6 +49,7 @@ public void consume(Map channels) { } private void withImage(BitmapFrame frame, IOConsumer consumer) throws IOException { + byte alphaMask = (byte) (keepAlpha ? 0 : 0xff); ByteBuffer buffer = frame.getByteBuffer(); ReadableDimension size = frame.getSize(); int width = size.getWidth(); @@ -58,7 +61,7 @@ private void withImage(BitmapFrame frame, IOConsumer consumer) throws IOE byte g = buffer.get(); byte r = buffer.get(); byte a = buffer.get(); - image.setRGBA(x, y, r, g, b, a); + image.setRGBA(x, y, r, g, b, a | alphaMask); } } consumer.accept(image); diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index e83d4947..b358b718 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -151,6 +151,7 @@ public String toString() { private final File outputFile; private final boolean renderNameTags; + private final boolean includeAlphaChannel; private final boolean stabilizeYaw; private final boolean stabilizePitch; private final boolean stabilizeRoll; @@ -187,6 +188,7 @@ public RenderSettings() { false, false, false, + false, null, 360, 180, @@ -209,6 +211,7 @@ public RenderSettings( int bitRate, File outputFile, boolean renderNameTags, + boolean includeAlphaChannel, boolean stabilizeYaw, boolean stabilizePitch, boolean stabilizeRoll, @@ -231,6 +234,7 @@ public RenderSettings( this.bitRate = bitRate; this.outputFile = outputFile; this.renderNameTags = renderNameTags; + this.includeAlphaChannel = includeAlphaChannel; this.stabilizeYaw = stabilizeYaw; this.stabilizePitch = stabilizePitch; this.stabilizeRoll = stabilizeRoll; @@ -256,6 +260,7 @@ public RenderSettings withEncodingPreset(EncodingPreset encodingPreset) { bitRate, outputFile, renderNameTags, + includeAlphaChannel, stabilizeYaw, stabilizePitch, stabilizeRoll, @@ -415,6 +420,10 @@ public boolean isRenderNameTags() { return renderNameTags; } + public boolean isIncludeAlphaChannel() { + return includeAlphaChannel; + } + public boolean isStabilizeYaw() { return stabilizeYaw; } @@ -478,6 +487,7 @@ public String toString() { ", bitRate=" + bitRate + ", outputFile=" + outputFile + ", renderNameTags=" + renderNameTags + + ", includeAlphaChannel=" + includeAlphaChannel + ", stabilizeYaw=" + stabilizeYaw + ", stabilizePitch=" + stabilizePitch + ", stabilizeRoll=" + stabilizeRoll + diff --git a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java index de5e9c0e..56f9229f 100644 --- a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java +++ b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java @@ -90,6 +90,7 @@ public GuiExportFailed(FFmpegWriter.FFmpegStartupException e, Consumer frameConsumer; if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) { //#if MC>=11400 - frameConsumer = new EXRWriter(settings.getOutputFile().toPath()); + frameConsumer = new EXRWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); //#else //$$ throw new UnsupportedOperationException("EXR requires LWJGL3"); //#endif } else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) { - frameConsumer = new PNGWriter(settings.getOutputFile().toPath()); + frameConsumer = new PNGWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else { frameConsumer = new FFmpegWriter(this); } diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 4b1698fc..188e85c4 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 4b1698fcf805fea05d6c3f0ca6c14c63d7e731db +Subproject commit 188e85c4dbc9296c81231c37d7c18bb92560b2e7 From a8d05a2bca967a5f7620c652cc4e2802046bb53a Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 16:44:31 +0100 Subject: [PATCH 057/132] Skip mod compat warning screen when in render queue (fixes #653) --- src/main/java/com/replaymod/render/gui/GuiRenderQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index a1b8b6a2..86ca19dc 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -218,7 +218,7 @@ public static void processMultipleReplays( ReplayFile replayFile = null; try { replayFile = mod.getCore().files.open(next.getKey().toPath()); - replayHandler = mod.startReplay(replayFile, true, false); + replayHandler = mod.startReplay(replayFile, false, false); } catch (IOException e) { Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {}); container.display(); // Re-show the queue popup and the new error popup From e26480f0412d7e68c110458bd0887dfdb748c083 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 16:48:36 +0100 Subject: [PATCH 058/132] Fix "Please wait" when jumping not being centered (fixes #646) --- src/main/java/com/replaymod/replay/ReplayHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index adac3806..bb74e0dc 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -603,6 +603,7 @@ public void doJump(int targetTime, boolean retainCameraPosition) { // Render our please-wait-screen GuiScreen guiScreen = new GuiScreen(); guiScreen.setBackground(AbstractGuiScreen.Background.DIRT); + guiScreen.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER)); guiScreen.addElements(new HorizontalLayout.Data(0.5), new GuiLabel().setI18nText("replaymod.gui.pleasewait")); From 2e34a2abd91a448a6ed06c3dada8438e18644833 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 16:54:59 +0100 Subject: [PATCH 059/132] Fix render timer breaking when system time changes (fixes #633) Unlike `currentTimeMillis`, `nanoTime` is guaranteed to be relative to a fixed point in time, regardless of system or wall-clock time. --- src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java index bc69f061..cd9c6268 100644 --- a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java +++ b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java @@ -141,7 +141,7 @@ public GuiVideoRenderer(VideoRenderer renderer) { @Override public void tick() { - long current = System.currentTimeMillis(); + long current = System.nanoTime() / 1_000_000; //first, update the total render time (only if rendering is not paused and has already started) if(!renderer.isPaused() && renderer.getFramesDone() > 0 && prevTime > -1) { From b8cf7e6df4619278e552028524c32a6a9dd21af3 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 17:58:10 +0100 Subject: [PATCH 060/132] Fix scrolling while replay paused on 1.12.2 and below (fixes #590) --- .../java/com/replaymod/core/mixin/MixinMinecraft.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java b/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java index 62682744..754cd090 100644 --- a/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java +++ b/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java @@ -63,6 +63,7 @@ private void postRender(boolean unused, CallbackInfo ci) { PostRenderCallback.EVENT.invoker().postRender(); } //#else + //$$ @Shadow long systemTime; //#if MC>=10904 //$$ @Shadow protected abstract void runTickKeyboard() throws IOException; //$$ @Shadow protected abstract void runTickMouse() throws IOException; @@ -80,6 +81,8 @@ private void postRender(boolean unused, CallbackInfo ci) { //$$ public void replayModRunTickMouse() { //$$ try { //$$ runTickMouse(); + //$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms) + //$$ systemTime = Minecraft.getSystemTime(); //$$ } catch (IOException e) { //$$ e.printStackTrace(); //$$ } @@ -94,7 +97,12 @@ private void postRender(boolean unused, CallbackInfo ci) { //$$ //$$ @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;sendClickBlockToController(Z)V"), cancellable = true) //$$ private void doEarlyReturnFromRunTick(CallbackInfo ci) { - //$$ if (earlyReturn) ci.cancel(); + //$$ if (earlyReturn) { + //$$ ci.cancel(); + //$$ + //$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms) + //$$ systemTime = Minecraft.getSystemTime(); + //$$ } //$$ } //#endif //$$ @Redirect( From d178c0980e8fe84ca01c7cc3d6095a6e6b420382 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 18:10:52 +0100 Subject: [PATCH 061/132] Log when replays are opened (closes #565) --- src/main/java/com/replaymod/core/gui/RestoreReplayGui.java | 5 +++++ src/main/java/com/replaymod/editor/gui/GuiEditReplay.java | 6 ++++++ .../com/replaymod/replay/gui/screen/GuiReplayViewer.java | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index 3a9f13c0..c68a3210 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -56,11 +56,16 @@ public RestoreReplayGui(ReplayMod core, GuiScreen parent, File file) { new GuiLabel().setI18nText("replaymod.gui.restorereplay1"), new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())), new GuiLabel().setI18nText("replaymod.gui.restorereplay3")); + + LOGGER.info("Found partially saved replay, offering recovery: " + file); + yesButton.onClick(() -> { + LOGGER.info("Attempting recovery: " + file); recoverInBackground(); parent.display(); }); noButton.onClick(() -> { + LOGGER.info("Recovery rejected, marking for deletion: " + file); try { File tmp = new File(file.getParentFile(), file.getName() + ".tmp"); File deleted = new File(file.getParentFile(), file.getName() + ".del"); diff --git a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java index 205d4059..0d5aefc8 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java +++ b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java @@ -21,6 +21,8 @@ import de.johni0702.minecraft.gui.utils.lwjgl.Color; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.util.crash.CrashReport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.IOException; import java.nio.file.Path; @@ -31,6 +33,8 @@ import java.util.stream.Collectors; public class GuiEditReplay extends AbstractGuiPopup { + private static final Logger LOGGER = LogManager.getLogger(); + private final Path inputPath; private final EditTimeline timeline; @@ -60,6 +64,8 @@ protected GuiEditReplay(GuiContainer container, Path inputPath) throws IOExcepti super(container); this.inputPath = inputPath; + LOGGER.info("Opening replay in editor: " + inputPath); + try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) { markers = replayFile.getMarkers().or(HashSet::new); timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers); 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 de4279aa..5525618e 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -95,8 +95,10 @@ public void run() { List selected = list.getSelected(); if (selected.size() == 1) { + File file = selected.get(0).file; + LOGGER.info("Opening replay in viewer: " + file); try { - mod.startReplay(selected.get(0).file); + mod.startReplay(file); } catch (IOException e) { e.printStackTrace(); } From f9c58a11652713866f3fd8a91800066d4e802e88 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 2 Mar 2022 18:40:46 +0100 Subject: [PATCH 062/132] Try raw file name before percent encoding it (closes #536) --- .../java/com/replaymod/core/utils/Utils.java | 43 ++++++++++++++++++- .../recording/gui/GuiSavingReplay.java | 4 +- .../handler/ConnectionEventHandler.java | 2 +- .../replay/gui/screen/GuiReplayViewer.java | 12 +++--- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index b082c276..688edc7c 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -55,10 +55,12 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; import java.security.KeyManagementException; import java.security.KeyStore; @@ -169,8 +171,45 @@ public static boolean isValidEmailAddress(String mail) { private static final PercentEscaper REPLAY_NAME_ENCODER = new PercentEscaper(".-_ ", false); - public static String replayNameToFileName(String replayName) { - return REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"; + public static Path replayNameToPath(Path folder, String replayName) { + // If we can, prefer directly using the replay name as the file name + if (isUsable(folder, replayName + ".mcpr")) { + return folder.resolve(replayName + ".mcpr"); + } else { + // otherwise, fall back to percent encoding + return folder.resolve(REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"); + } + } + + /** + * Checks whether a given file name is actually usable with the file system / operating system at the given folder. + */ + private static boolean isUsable(Path folder, String fileName) { + Path path = folder.resolve(fileName); + if (Files.exists(path)) { + return true; // if it already exits, it's definitely usable + } + + // Otherwise, there's no sure way to know, so we just gotta try + try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) { + outputStream.flush(); + } catch (IOException e) { + return false; + } + + // Looking good, but now we gotta clean up that mess (and Anti-Virus / Cloud Sync are know to lock them) + int attempts = 0; + while (true) { + try { + Files.delete(path); + return true; + } catch (IOException e) { + if (attempts++ > 100) { + LOGGER.warn("Repeatedly failed to clean up temporary test file at " + path + ": ", e); + return false; // while we were able to use it, it's taken now and we can't get it back + } + } + } } public static String fileNameToReplayName(String fileName) { diff --git a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java index 6fb66db9..9e33903a 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java +++ b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java @@ -155,9 +155,9 @@ private void applyOutput(Path path, String newName) { try { Path replaysFolder = core.folders.getReplayFolder(); - Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName)); + Path newPath = Utils.replayNameToPath(replaysFolder, newName); for (int i = 1; Files.exists(newPath); i++) { - newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")")); + newPath = Utils.replayNameToPath(replaysFolder, newName + " (" + i + ")"); } Files.move(path, newPath); } catch (IOException e) { diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 60a43a50..0211c8e0 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -127,7 +127,7 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { } String name = sdf.format(Calendar.getInstance().getTime()); - Path outputPath = core.folders.getRecordingFolder().resolve(Utils.replayNameToFileName(name)); + Path outputPath = Utils.replayNameToPath(core.folders.getRecordingFolder(), name); ReplayFile replayFile = core.files.open(outputPath); replayFile.writeModInfo(ModCompat.getInstalledNetworkMods()); 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 5525618e..9a2dec3c 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -49,6 +49,8 @@ import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -132,8 +134,8 @@ public void run() { public final GuiButton renameButton = new GuiButton().onClick(new Runnable() { @Override public void run() { - final File file = list.getSelected().get(0).file; - String name = Utils.fileNameToReplayName(file.getName()); + final Path path = list.getSelected().get(0).file.toPath(); + String name = Utils.fileNameToReplayName(path.getFileName().toString()); final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(name); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this, new GuiLabel().setI18nText("replaymod.gui.viewer.rename.name").setColor(Colors.BLACK), @@ -149,16 +151,16 @@ public void run() { } }).onTextChanged(obj -> { popup.getYesButton().setEnabled(!nameField.getText().isEmpty() - && !new File(file.getParentFile(), Utils.replayNameToFileName(nameField.getText())).exists()); + && Files.notExists(Utils.replayNameToPath(path.getParent(), nameField.getText()))); }); popup.onAccept(() -> { // Sanitize their input String newName = nameField.getText().trim(); // This file is what they want - File targetFile = new File(file.getParentFile(), Utils.replayNameToFileName(newName)); + Path targetPath = Utils.replayNameToPath(path.getParent(), newName); try { // Finally, try to move it - FileUtils.moveFile(file, targetFile); + Files.move(path, targetPath); } catch (IOException e) { // We failed (might also be their OS) e.printStackTrace(); From 0356fef61d9d7128fd19febc7345bfe5a22456c2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 5 Mar 2022 11:23:00 +0100 Subject: [PATCH 063/132] Fix crash when receiving resource pack on 1.8-1.12.2 (fixes #86) --- src/main/resources/mixins.recording.replaymod.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index e6c9ee41..09545afc 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -11,8 +11,10 @@ "SPacketSpawnMobAccessor", "SPacketSpawnPlayerAccessor", "MixinServerInfo", - //#if MC>=11400 + //#if MC>=10800 "MixinDownloadingPackFinder", + //#endif + //#if MC>=11400 "MixinMouseHelper", //#endif //#if MC>=10904 From 8573a9b208f0574e2be6a0db92f400bdaa601f2d Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 5 Mar 2022 11:37:36 +0100 Subject: [PATCH 064/132] Prevent jumping while path is playing (closes #694) --- src/main/java/com/replaymod/replay/ReplayHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index bb74e0dc..75370265 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -534,6 +534,10 @@ public void moveCameraToTargetPosition() { } public void doJump(int targetTime, boolean retainCameraPosition) { + if (!getReplaySender().isAsyncMode()) { + return; // path playback, rendering, etc. -> no jumping allowed + } + //#if MC>=10904 if (getReplaySender() == quickReplaySender) { // Always round to full tick From 1efb17e144922338043d5eb752c8ac040acd4edb Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 5 Mar 2022 13:25:31 +0100 Subject: [PATCH 065/132] Fix ODS rendering on Iris 1.1.3+ (fixes #693) --- build.gradle | 4 +++- .../render/capturer/IrisODSFrameCapturer.java | 12 ++++++++---- .../render/mixin/Mixin_LoadIrisOdsShaderPack.java | 5 ++--- src/main/resources/fabric.mod.json | 4 ++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index b486fbaf..18be4489 100644 --- a/build.gradle +++ b/build.gradle @@ -362,7 +362,9 @@ dependencies { } if (mcVersion >= 11600) { - modCompileOnly 'com.github.IrisShaders:Iris:1.0.0' + modCompileOnly("com.github.IrisShaders:Iris:1.18.x~v1.2.0") { + transitive = false // we do not want to upgrade our libs, we only need this to compile our mixins + } } testImplementation 'junit:junit:4.11' diff --git a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java index a1d31055..2dd2d269 100644 --- a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java @@ -28,6 +28,7 @@ public class IrisODSFrameCapturer implements FrameCapturer { public static IrisODSFrameCapturer INSTANCE; private final CubicPboOpenGlFrameCapturer left, right; private final String prevShaderPack; + private final boolean prevShadersEnabled; private int direction; private boolean isLeftEye; @@ -67,13 +68,16 @@ public RenderSettings getRenderSettings() { right = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize); INSTANCE = this; - prevShaderPack = Iris.getIrisConfig().getShaderPackName().orElse(null); - setShaderPack(SHADER_PACK_NAME); + IrisConfig irisConfig = Iris.getIrisConfig(); + prevShaderPack = irisConfig.getShaderPackName().orElse(null); + prevShadersEnabled = irisConfig.areShadersEnabled(); + setShaderPack(SHADER_PACK_NAME, true); } - private static void setShaderPack(String name) { + private static void setShaderPack(String name, boolean enabled) { IrisConfig irisConfig = Iris.getIrisConfig(); irisConfig.setShaderPackName(name); + irisConfig.setShadersEnabled(enabled); try { irisConfig.save(); Iris.reload(); @@ -121,7 +125,7 @@ public void close() throws IOException { left.close(); right.close(); INSTANCE = null; - setShaderPack(prevShaderPack); + setShaderPack(prevShaderPack, prevShadersEnabled); } private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer { diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java index d8756e7b..56d4564b 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java @@ -4,7 +4,6 @@ import com.replaymod.render.capturer.IrisODSFrameCapturer; import net.coderbot.iris.Iris; import net.fabricmc.loader.api.FabricLoader; -import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.injection.At; @@ -15,14 +14,14 @@ @Pseudo @Mixin(value = Iris.class, remap = false) public class Mixin_LoadIrisOdsShaderPack { - @Redirect(method = "loadExternalShaderpack", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/coderbot/iris/Iris;SHADERPACKS_DIRECTORY:Ljava/nio/file/Path;")) + @Redirect(method = "loadExternalShaderpack", at = @At(value = "INVOKE", target = "Lnet/coderbot/iris/Iris;getShaderpacksDirectory()Ljava/nio/file/Path;")) private static Path loadReplayModOdsPack(String name) { if (IrisODSFrameCapturer.INSTANCE != null && IrisODSFrameCapturer.SHADER_PACK_NAME.equals(name)) { return FabricLoader.getInstance().getModContainer("replaymod") .orElseThrow(() -> new RuntimeException("Failed to get mod container for ReplayMod")) .getRootPath(); } else { - return Iris.SHADERPACKS_DIRECTORY; + return Iris.getShaderpacksDirectory(); } } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6eb336ce..73fe9d6e 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -47,6 +47,10 @@ "fabric-resource-loader-v0": "*" }, + "conflicts": { + "iris": "<1.1.3" + }, + "custom": { "mm:early_risers": [ "com.replaymod.core.ReplayModMMLauncher" From ee6b53b24771b440debb4c501f5ddc2e9ca68b53 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 10:23:53 +0100 Subject: [PATCH 066/132] Fix incorrect FOV and aspect during 360 render on 1.16+ --- .../replaymod/render/mixin/Mixin_Omnidirectional_Camera.java | 2 +- versions/mapping-fabric-1.16.1-1.15.2.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java index fb58ab87..8fa79479 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java @@ -9,7 +9,7 @@ @Mixin(GameRenderer.class) public abstract class Mixin_Omnidirectional_Camera implements EntityRendererHandler.IEntityRenderer { - @Redirect(method = "method_22973", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;")) + @Redirect(method = "getBasicProjectionMatrix", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;")) private Matrix4f replayModRender_perspective$0(double fovY, float aspect, float zNear, float zFar) { return replayModRender_perspective((float) fovY, aspect, zNear, zFar); } diff --git a/versions/mapping-fabric-1.16.1-1.15.2.txt b/versions/mapping-fabric-1.16.1-1.15.2.txt index 1f8df9e4..25f2e2db 100644 --- a/versions/mapping-fabric-1.16.1-1.15.2.txt +++ b/versions/mapping-fabric-1.16.1-1.15.2.txt @@ -1 +1,2 @@ net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140() +net.minecraft.client.render.GameRenderer getBasicProjectionMatrix() method_22973() From c146aa145ce2e34b25caf6428f06b24f3a53ec57 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 10:30:29 +0100 Subject: [PATCH 067/132] Fix stereoscopic eye offset on 1.16+ The mapping between intermediary name on 1.15.2 and yarn on 1.16.1 has already been added in the previous commit. --- .../com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java index 945ac107..885608e3 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java @@ -13,7 +13,7 @@ @Mixin(GameRenderer.class) public abstract class Mixin_Stereoscopic_Camera implements EntityRendererHandler.IEntityRenderer { - @Inject(method = "method_22973", at = @At("RETURN"), cancellable = true) + @Inject(method = "getBasicProjectionMatrix", at = @At("RETURN"), cancellable = true) private void replayModRender_setupStereoscopicProjection(CallbackInfoReturnable ci) { if (replayModRender_getHandler() != null) { Matrix4f offset; From 74049dad775faa31e6f1cef059ac6a3829c38eba Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 11:43:46 +0100 Subject: [PATCH 068/132] Update ReplayStudio 9c5dec8 Fix writing of empty chunk load packet on 1.8.x (fixes #636) 3d8573f Fix writing of respawn packet on 1.8.x 39a85f7 Fix exception when cache file has invalid gzip header (fixes #535) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 18be4489..cceffd06 100644 --- a/build.gradle +++ b/build.gradle @@ -338,7 +338,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:70f59ef", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:9c5dec8", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those From 1fa027f36248debb0c4af692c52b11e568024d4b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 11:46:01 +0100 Subject: [PATCH 069/132] Enable Quick Mode for 1.8.x --- build.gradle | 2 +- docs/content.md | 4 +--- .../replaymod/replay/QuickReplaySender.java | 6 +++++- .../com/replaymod/replay/ReplayHandler.java | 18 +++++++++--------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index cceffd06..99f3dd15 100644 --- a/build.gradle +++ b/build.gradle @@ -338,7 +338,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:9c5dec8", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:6d081f6", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those diff --git a/docs/content.md b/docs/content.md index faea3ff2..0681006f 100755 --- a/docs/content.md +++ b/docs/content.md @@ -465,7 +465,7 @@ If you have a Replay in a dark setting (for example at nighttime, or in a cave) This works as a replacement for the **Night Vision Potion Effect**, without the side effect of a weird sky color. -## Quick Mode [quickmode] (Minecraft 1.9 and up) +## Quick Mode [quickmode] ![](img/quickmode-icon.jpg) In **Quick Mode**, this clock symbol is displayed in the lower right corner of the screen. @@ -473,8 +473,6 @@ When you first enable **Quick Mode** in a replay, an internal reference of certa As a side effect, certain features like particles and second skin layers will not be rendered in the preview. By default, **Quick Mode** is toggled with `Q`. -**Quick Mode** is available in ReplayMod for Minecraft 1.9.4 and up. - ## Player Overview [overview] ![](img/player-overview.jpg) The **Player Overview** Screen diff --git a/src/main/java/com/replaymod/replay/QuickReplaySender.java b/src/main/java/com/replaymod/replay/QuickReplaySender.java index 0b613f4a..6592b47d 100644 --- a/src/main/java/com/replaymod/replay/QuickReplaySender.java +++ b/src/main/java/com/replaymod/replay/QuickReplaySender.java @@ -1,4 +1,4 @@ -//#if MC>=10904 +//#if MC>=10800 package com.replaymod.replay; import com.google.common.util.concurrent.FutureCallback; @@ -78,7 +78,11 @@ protected void dispatch(com.replaymod.replaystudio.protocol.Packet packet) { wrappedBuf.writerIndex(size); PacketByteBuf packetByteBuf = new PacketByteBuf(wrappedBuf); + //#if MC>=10809 Packet mcPacket; + //#else + //$$ Packet mcPacket; + //#endif //#if MC>=11700 //$$ mcPacket = NetworkState.PLAY.getPacketHandler(NetworkSide.CLIENTBOUND, packet.getId(), packetByteBuf); //#elseif MC>=11500 diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 75370265..99e90c36 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -69,7 +69,7 @@ //$$ import io.netty.channel.ChannelOutboundHandlerAdapter; //#endif -//#if MC<10904 +//#if MC<10800 //$$ import de.johni0702.minecraft.gui.element.GuiLabel; //$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup; //$$ import de.johni0702.minecraft.gui.utils.Colors; @@ -119,11 +119,11 @@ public class ReplayHandler { * Decodes and sends packets into channel. */ private final FullReplaySender fullReplaySender; - //#if MC>=10904 + //#if MC>=10800 private final QuickReplaySender quickReplaySender; private boolean quickMode = false; //#else - //$$ private static final String QUICK_MODE_MIN_MC = "1.9.4"; + //$$ private static final String QUICK_MODE_MIN_MC = "1.8"; //#endif /** @@ -160,7 +160,7 @@ public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOExceptio markers = replayFile.getMarkers().or(Collections.emptySet()); fullReplaySender = new FullReplaySender(this, replayFile, false); - //#if MC>=10904 + //#if MC>=10800 quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile); //#endif @@ -206,7 +206,7 @@ public void endReplay() throws IOException { ReplayClosingCallback.EVENT.invoker().replayClosing(this); fullReplaySender.terminateReplay(); - //#if MC>=10904 + //#if MC>=10800 if (quickMode) { quickReplaySender.unregister(); } @@ -308,7 +308,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { //$$ channel = new EmbeddedChannel(dummyHandler); //$$ channel.pipeline().remove(dummyHandler); //#endif - //#if MC>=10904 + //#if MC>=10800 channel.pipeline().addLast("ReplayModReplay_quickReplaySender", quickReplaySender); //#endif channel.pipeline().addLast("ReplayModReplay_replaySender", fullReplaySender); @@ -329,7 +329,7 @@ public Restrictions getRestrictions() { } public ReplaySender getReplaySender() { - //#if MC>=10904 + //#if MC>=10800 return quickMode ? quickReplaySender : fullReplaySender; //#else //$$ return fullReplaySender; @@ -340,7 +340,7 @@ public GuiReplayOverlay getOverlay() { return overlay; } - //#if MC>=10904 + //#if MC>=10800 public void ensureQuickModeInitialized(Runnable andThen) { if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return; ListenableFuture future = quickReplaySender.getInitializationPromise(); @@ -538,7 +538,7 @@ public void doJump(int targetTime, boolean retainCameraPosition) { return; // path playback, rendering, etc. -> no jumping allowed } - //#if MC>=10904 + //#if MC>=10800 if (getReplaySender() == quickReplaySender) { // Always round to full tick targetTime = targetTime + targetTime % 50; From 4aa667fe044bf7c8b451f4eefa9032d81d720eee Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 12:33:13 +0100 Subject: [PATCH 070/132] Port to MC 1.18.2 (closes #697) --- build.gradle | 9 +++++++-- jGui | 2 +- root.gradle.kts | 2 ++ settings.gradle.kts | 2 ++ .../replaymod/recording/mixin/MixinMouseHelper.java | 4 +++- .../replaymod/recording/mixin/MixinWorldClient.java | 11 ++++++++++- .../render/mixin/Mixin_ChromaKeyColorSky.java | 7 ++++++- .../java/com/replaymod/replay/FullReplaySender.java | 4 ++++ .../java/com/replaymod/replay/InputReplayTimer.java | 13 +++++++++++++ .../com/replaymod/replay/camera/CameraEntity.java | 12 +++++++++++- .../replaymod/replay/mixin/MixinGuiSpectator.java | 2 +- versions/1.18.2/.gitkeep | 0 12 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 versions/1.18.2/.gitkeep diff --git a/build.gradle b/build.gradle index 99f3dd15..e9ba988b 100644 --- a/build.gradle +++ b/build.gradle @@ -245,6 +245,7 @@ dependencies { 11701: '1.17.1', 11800: '1.18', 11801: '1.18.1', + 11802: '1.18.2', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -256,6 +257,7 @@ dependencies { 11701: '1.17.1+build.29:v2', 11800: '1.18+build.1:v2', 11801: '1.18.1+build.1:v2', + 11802: '1.18.2+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ @@ -268,6 +270,7 @@ dependencies { 11701: '0.37.1+1.17', 11800: '0.43.1+1.18', 11801: '0.43.1+1.18', + 11802: '0.47.9+1.18.2', ][mcVersion] def fabricApiModules = [ "api-base", @@ -338,7 +341,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:6d081f6", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:b5539d1", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those @@ -346,7 +349,9 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11800) { + if (mcVersion >= 11802) { + modImplementation 'com.terraformersmc:modmenu:3.1.0' + } else if (mcVersion >= 11800) { modImplementation 'com.terraformersmc:modmenu:3.0.0' } else if (mcVersion >= 11700) { modImplementation 'com.terraformersmc:modmenu:2.0.0-beta.7' diff --git a/jGui b/jGui index c79b62a7..5e41452b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit c79b62a73e649fd6d16a1ffbd9e320555834bc46 +Subproject commit 5e41452b0e17700691efd38b8ce793b5200145c8 diff --git a/root.gradle.kts b/root.gradle.kts index f94b12fc..8ee13ad4 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -189,6 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11802 = createNode("1.18.2", 11802, "yarn") val mc11801 = createNode("1.18.1", 11801, "yarn") val mc11701 = createNode("1.17.1", 11701, "yarn") val mc11700 = createNode("1.17", 11700, "yarn") @@ -208,6 +209,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11802.link(mc11801) mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt")) mc11701.link(mc11700) mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2030c3cd..a77be868 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,6 +32,7 @@ val jGuiVersions = listOf( "1.17", "1.17.1", "1.18.1", + "1.18.2", ) val replayModVersions = listOf( // "1.7.10", @@ -52,6 +53,7 @@ val replayModVersions = listOf( "1.17", "1.17.1", "1.18.1", + "1.18.2", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java index 01e9572f..c7e8f5d4 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java @@ -33,7 +33,9 @@ private void handleReplayModScroll( long _p0, double _p1, double _p2, CallbackInfo ci, double _l1, - //#if MC>=11400 + //#if MC>=11802 + //$$ int yOffsetAccumulated + //#elseif MC>=11400 float yOffsetAccumulated //#else //$$ double yOffsetAccumulated diff --git a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java index e5c79889..55bc21b2 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java @@ -16,6 +16,10 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11802 +//$$ import net.minecraft.util.registry.RegistryEntry; +//#endif + //#if MC>=11600 import net.minecraft.util.registry.RegistryKey; import net.minecraft.world.MutableWorldProperties; @@ -52,7 +56,12 @@ protected MixinWorldClient(MutableWorldProperties mutableWorldProperties, Regist //#if MC<11602 //$$ RegistryKey registryKey2, //#endif - DimensionType dimensionType, Supplier profiler, boolean bl, boolean bl2, long l) { + //#if MC>=11802 + //$$ RegistryEntry dimensionType, + //#else + DimensionType dimensionType, + //#endif + Supplier profiler, boolean bl, boolean bl2, long l) { super(mutableWorldProperties, registryKey, //#if MC<11602 //$$ registryKey2, diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java index bb60ab4e..28872496 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java @@ -21,7 +21,12 @@ public abstract class Mixin_ChromaKeyColorSky { @Shadow @Final private MinecraftClient client; //#if MC>=11800 - //$$ @Inject(method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V", + //$$ @Inject( + //#if MC>=11802 + //$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLnet/minecraft/client/render/Camera;ZLjava/lang/Runnable;)V", + //#else + //$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V", + //#endif //$$ at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", remap = false, shift = At.Shift.AFTER), //$$ cancellable = true) //#elseif MC>=11400 || 10710>=MC diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 2aee5e57..b44febe9 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -648,7 +648,11 @@ protected Packet processPacket(Packet p) throws Exception { //#if MC>=11600 //#if MC>=11603 packet.getDimensionIds(), + //#if MC>=11800 + //$$ packet.registryManager(), + //#else (net.minecraft.util.registry.DynamicRegistryManager.Impl) packet.getRegistryManager(), + //#endif packet.getDimensionType(), //#else //$$ packet.method_29443(), diff --git a/src/main/java/com/replaymod/replay/InputReplayTimer.java b/src/main/java/com/replaymod/replay/InputReplayTimer.java index 394fdd50..0b3e8b4a 100644 --- a/src/main/java/com/replaymod/replay/InputReplayTimer.java +++ b/src/main/java/com/replaymod/replay/InputReplayTimer.java @@ -8,6 +8,10 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.RenderTickCounter; +//#if MC>=11802 +//$$ import net.minecraft.client.gui.screen.DownloadingTerrainScreen; +//#endif + //#if MC>=11400 import org.lwjgl.glfw.GLFW; //#else @@ -111,6 +115,15 @@ public InputReplayTimer(RenderTickCounter wrapped, ReplayModReplay mod) { //#endif //$$ } //#endif + + //#if MC>=11802 + //$$ // As of 1.18.2, this screen always stays open for at least two seconds, and requires ticking to close. + //$$ // Thanks, but we'll have none of that (at least while in a replay). + //$$ if (mc.currentScreen instanceof DownloadingTerrainScreen) { + //$$ mc.currentScreen.close(); + //$$ } + //#endif + } //#if MC>=11600 return ticksThisFrame; diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 5ae3fcec..35bd016c 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -31,6 +31,10 @@ import net.minecraft.util.Identifier; import net.minecraft.util.math.Box; +//#if MC>=11802 +//$$ import net.minecraft.tag.TagKey; +//#endif + //#if FABRIC>=1 //#else //$$ import net.minecraftforge.client.event.EntityViewRenderEvent; @@ -354,7 +358,13 @@ public boolean isInsideWall() { //#if MC>=11400 @Override - public boolean isSubmergedIn(Tag fluid) { + public boolean isSubmergedIn( + //#if MC>=11802 + //$$ TagKey fluid + //#else + Tag fluid + //#endif + ) { return falseUnlessSpectating(entity -> entity.isSubmergedIn(fluid)); } diff --git a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java index 9d938f7e..2257eceb 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java @@ -18,7 +18,7 @@ public abstract class MixinGuiSpectator { //$$ @Inject(method = "func_175260_a", at = @At("HEAD"), cancellable = true) //#endif public void isInReplay( - //#if MC>=11400 + //#if MC>=11400 && MC<11802 double i, //#else //$$ int i, diff --git a/versions/1.18.2/.gitkeep b/versions/1.18.2/.gitkeep new file mode 100644 index 00000000..e69de29b From 9df2035ca9ad82cfc1c0127a0d1aa190306e130b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 12:38:38 +0100 Subject: [PATCH 071/132] Update translations --- src/main/resources/assets/replaymod/lang | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 188e85c4..7614b69e 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 188e85c4dbc9296c81231c37d7c18bb92560b2e7 +Subproject commit 7614b69e249eb728d219c02c3c71effcc5fc00a2 From f2425a693a1c557725b27c276228c7cd7f3d3be1 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 14:07:03 +0100 Subject: [PATCH 072/132] DRY (Iris)ODSFrameCapturer --- .../render/capturer/IrisODSFrameCapturer.java | 26 +------------------ .../render/capturer/ODSFrameCapturer.java | 26 +------------------ 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java index 2dd2d269..0f581d99 100644 --- a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java @@ -1,7 +1,6 @@ //#if MC>=11600 package com.replaymod.render.capturer; -import com.mojang.blaze3d.platform.GlStateManager; import com.replaymod.render.RenderSettings; import com.replaymod.render.frame.CubicOpenGlFrame; import com.replaymod.render.frame.ODSOpenGlFrame; @@ -16,12 +15,6 @@ import java.util.HashMap; import java.util.Map; -import static com.replaymod.core.versions.MCVer.popMatrix; -import static com.replaymod.core.versions.MCVer.pushMatrix; -import static com.replaymod.core.versions.MCVer.resizeMainWindow; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; - public class IrisODSFrameCapturer implements FrameCapturer { public static final String SHADER_PACK_NAME = "assets/replaymod/iris/ods"; @@ -135,25 +128,8 @@ public CubicStereoFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderIn @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { - resizeMainWindow(mc, getFrameWidth(), getFrameHeight()); - - pushMatrix(); - frameBuffer().beginWrite(true); - - GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT - //#if MC>=11400 - , false - //#endif - ); - GlStateManager.enableTexture(); - direction = captureData.ordinal(); - worldRenderer.renderWorld(partialTicks, null); - - frameBuffer().endWrite(); - popMatrix(); - - return captureFrame(frameId, captureData); + return super.renderFrame(frameId, partialTicks, null); } } } diff --git a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java index 4df2fa18..c0ec8c3d 100644 --- a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java @@ -1,6 +1,5 @@ package com.replaymod.render.capturer; -import com.mojang.blaze3d.platform.GlStateManager; import com.replaymod.render.rendering.Channel; import de.johni0702.minecraft.gui.utils.EventRegistrations; import com.replaymod.render.RenderSettings; @@ -20,12 +19,6 @@ import java.util.HashMap; import java.util.Map; -import static com.replaymod.core.versions.MCVer.popMatrix; -import static com.replaymod.core.versions.MCVer.pushMatrix; -import static com.replaymod.core.versions.MCVer.resizeMainWindow; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; - public class ODSFrameCapturer implements FrameCapturer { private static final Identifier vertexResource = new Identifier("replaymod", "shader/ods.vert"); private static final Identifier fragmentResource = new Identifier("replaymod", "shader/ods.frag"); @@ -166,25 +159,8 @@ public CubicStereoFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderIn @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { - resizeMainWindow(mc, getFrameWidth(), getFrameHeight()); - - pushMatrix(); - frameBuffer().beginWrite(true); - - GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT - //#if MC>=11400 - , false - //#endif - ); - GlStateManager.enableTexture(); - directionVariable.set(captureData.ordinal()); - worldRenderer.renderWorld(partialTicks, null); - - frameBuffer().endWrite(); - popMatrix(); - - return captureFrame(frameId, captureData); + return super.renderFrame(frameId, partialTicks, null); } } } From c010a437fcca087c9ff828504915482d778e7d8b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 6 Mar 2022 14:30:24 +0100 Subject: [PATCH 073/132] Orient camera for ODS in game instead of in vertex shader This makes it work more like regular 360 mode and is generally more compatible because it doesn't require the frustum culling workarounds which the GPU solution needs. In particular, this fixes frustum culling with newer sodium versions (which no longer use the vanilla intersection checking method) and fixes the clouds on modern Iris (which seems to be using a different shader program for clouds). --- .../render/capturer/IrisODSFrameCapturer.java | 2 +- .../render/capturer/ODSFrameCapturer.java | 2 +- ...Omnidirectional_DisableFrustumCulling.java | 37 ----------- .../ods/shaders/gbuffers_textured_lit.vsh | 65 ++++++++++++------- .../assets/replaymod/shader/ods.vert | 65 ++++++++++++------- .../resources/mixins.render.replaymod.json | 1 - 6 files changed, 84 insertions(+), 88 deletions(-) delete mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java diff --git a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java index 0f581d99..6671d80a 100644 --- a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java @@ -129,7 +129,7 @@ public CubicStereoFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderIn @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { direction = captureData.ordinal(); - return super.renderFrame(frameId, partialTicks, null); + return super.renderFrame(frameId, partialTicks, captureData); } } } diff --git a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java index c0ec8c3d..925257fe 100644 --- a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java @@ -160,7 +160,7 @@ public CubicStereoFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderIn @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { directionVariable.set(captureData.ordinal()); - return super.renderFrame(frameId, partialTicks, null); + return super.renderFrame(frameId, partialTicks, captureData); } } } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java deleted file mode 100644 index f174e0a3..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.replaymod.render.mixin; - -import com.replaymod.core.versions.MCVer; -import com.replaymod.render.hooks.EntityRendererHandler; -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; - -//#if MC>=10800 -import net.minecraft.client.render.Frustum; -//#else -//$$ import net.minecraft.client.renderer.culling.Frustrum; -//#endif - -//#if MC>=10800 -@Mixin(Frustum.class) -//#else -//$$ @Mixin(Frustrum.class) -//#endif -public abstract class Mixin_Omnidirectional_DisableFrustumCulling { - //#if MC>=11500 - @Inject(method = "isAnyCornerVisible", at = @At("HEAD"), cancellable = true) - //#else - //$$ @Inject(method = "intersects", at = @At("HEAD"), cancellable = true) - //#endif - public void intersects(CallbackInfoReturnable ci) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().gameRenderer).replayModRender_getHandler(); - if (handler != null && handler.omnidirectional) { - // Note the following used to be true but for simplicity non-ODS omnidirectional is the same now too. - // Normally the camera is always facing the direction of the omnidirectional image face that is currently - // getting rendered. With ODS however, the camera is always facing forwards and the turning happens in the - // vertex shader (non-trivial due to stereo). As such, all chunks need to be rendered all the time for ODS. - ci.setReturnValue(true); - } - } -} diff --git a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh index 3b53c589..68e7e0fb 100644 --- a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh +++ b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh @@ -9,10 +9,49 @@ uniform int direction; const float eyeDistance = 0.14; +void orient(vec4 position, int orientation) { + float z; + if (orientation == 0) { // LEFT + z = position.z; + position.z = position.x; + position.x = -z; + } else if (orientation == 1) { // RIGHT + z = position.z; + position.z = -position.x; + position.x = z; + } else if (orientation == 2) { // FRONT + // No changes required + } else if (orientation == 3) { // BACK + position.x = -position.x; + position.z = -position.z; + } else if (orientation == 4) { // TOP + z = position.z; + position.z = -position.y; + position.y = z; + } else if (orientation == 5) { // BOTTOM + z = position.z; + position.z = position.y; + position.y = -z; + } +} + +void orientInverse(vec4 position, int orientation) { + if (orientation < 2) { + orient(position, 1 - orientation); // LEFT and RIGHT flip + } else if (orientation < 4) { + orient(position, orientation); // FRONT and BACK are their own inverses + } else { + orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip + } +} + void main() { // Transform to view space vec4 position = gl_ModelViewMatrix * gl_Vertex; + // Undo the camera rotation, so we always apply our stereo effect looking in the same direction + orientInverse(position, direction); + // Distort for ODS // O := The origin // P := The current vertex/point @@ -34,30 +73,8 @@ void main() { // Calculate the vector between O and T and finally move the vertex by that vector position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0); - // Rotate for different cubic views - float z; - if (direction == 0) { // LEFT - z = position.z; - position.z = position.x; - position.x = -z; - } else if (direction == 1) { // RIGHT - z = position.z; - position.z = -position.x; - position.x = z; - } else if (direction == 2) { // FRONT - // No changes required - } else if (direction == 3) { // BACK - position.x = -position.x; - position.z = -position.z; - } else if (direction == 4) { // TOP - z = position.z; - position.z = -position.y; - position.y = z; - } else if (direction == 5) { // BOTTOM - z = position.z; - position.z = position.y; - position.y = -z; - } + // Rotate back into the correct cubic view + orient(position, direction); // Transform to screen space gl_Position = gl_ProjectionMatrix * position; diff --git a/src/main/resources/assets/replaymod/shader/ods.vert b/src/main/resources/assets/replaymod/shader/ods.vert index 43160450..6a654a6b 100644 --- a/src/main/resources/assets/replaymod/shader/ods.vert +++ b/src/main/resources/assets/replaymod/shader/ods.vert @@ -14,10 +14,49 @@ uniform int direction; const float eyeDistance = 0.14; +void orient(vec4 position, int orientation) { + float z; + if (orientation == 0) { // LEFT + z = position.z; + position.z = position.x; + position.x = -z; + } else if (orientation == 1) { // RIGHT + z = position.z; + position.z = -position.x; + position.x = z; + } else if (orientation == 2) { // FRONT + // No changes required + } else if (orientation == 3) { // BACK + position.x = -position.x; + position.z = -position.z; + } else if (orientation == 4) { // TOP + z = position.z; + position.z = -position.y; + position.y = z; + } else if (orientation == 5) { // BOTTOM + z = position.z; + position.z = position.y; + position.y = -z; + } +} + +void orientInverse(vec4 position, int orientation) { + if (orientation < 2) { + orient(position, 1 - orientation); // LEFT and RIGHT flip + } else if (orientation < 4) { + orient(position, orientation); // FRONT and BACK are their own inverses + } else { + orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip + } +} + void main() { // Transform to view space vec4 position = gl_ModelViewMatrix * gl_Vertex; + // Undo the camera rotation, so we always apply our stereo effect looking in the same direction + orientInverse(position, direction); + // Distort for ODS // O := The origin // P := The current vertex/point @@ -39,30 +78,8 @@ void main() { // Calculate the vector between O and T and finally move the vertex by that vector position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0); - // Rotate for different cubic views - float z; - if (direction == 0) { // LEFT - z = position.z; - position.z = position.x; - position.x = -z; - } else if (direction == 1) { // RIGHT - z = position.z; - position.z = -position.x; - position.x = z; - } else if (direction == 2) { // FRONT - // No changes required - } else if (direction == 3) { // BACK - position.x = -position.x; - position.z = -position.z; - } else if (direction == 4) { // TOP - z = position.z; - position.z = -position.y; - position.y = z; - } else if (direction == 5) { // BOTTOM - z = position.z; - position.z = position.y; - position.y = -z; - } + // Rotate back into the correct cubic view + orient(position, direction); // Transform to screen space gl_Position = gl_ProjectionMatrix * position; diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index ba783acd..092aa958 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -14,7 +14,6 @@ "Mixin_HideNameTags", "Mixin_HideNameTags_LivingEntity", "Mixin_Omnidirectional_Camera", - "Mixin_Omnidirectional_DisableFrustumCulling", "Mixin_Omnidirectional_Rotation", "Mixin_PreserveDepthDuringGuiRendering", "Mixin_SkipBlockOutlinesDuringRender", From 463b947c2b13cf8490942a59821ef5d4469e728e Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 13:35:20 +0200 Subject: [PATCH 074/132] Suppress fabric-screen-handler-api during playback (fixes #713) --- src/main/java/com/replaymod/replay/FullReplaySender.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index b44febe9..d03c7362 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -585,6 +585,12 @@ protected Packet processPacket(Packet p) throws Exception { //#else //$$ String channelName = packet.getChannelName(); //#endif + String channelNameStr = channelName.toString(); + + if (channelNameStr.startsWith("fabric-screen-handler-api-v")) { + return null; // we do not want to show modded screens which got opened for the recording player + } + // On 1.14+ there's a dedicated OpenWrittenBookS2CPacket now //#if MC<11400 //#if MC>=11400 From d6c6220bdd1781a2cf5bfd3de629d42bc358a634 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 13:48:47 +0200 Subject: [PATCH 075/132] Fix invalid characters in replay name on Windows (fixes #715) --- src/main/java/com/replaymod/core/utils/Utils.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 688edc7c..39634262 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -59,6 +59,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; @@ -185,7 +186,12 @@ public static Path replayNameToPath(Path folder, String replayName) { * Checks whether a given file name is actually usable with the file system / operating system at the given folder. */ private static boolean isUsable(Path folder, String fileName) { - Path path = folder.resolve(fileName); + Path path; + try { + path = folder.resolve(fileName); + } catch (InvalidPathException e) { + return false; // file name contains invalid characters, definitely not usable + } if (Files.exists(path)) { return true; // if it already exits, it's definitely usable } From fb38a20465e3a6ae8413a4d30152261131ad814d Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 13:49:32 +0200 Subject: [PATCH 076/132] Fix path separator in replay name Would probably have been caught by the write check as well, except in the specific case where the folder exists, e.g. `../` --- src/main/java/com/replaymod/core/utils/Utils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 39634262..1b518534 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -186,6 +186,10 @@ public static Path replayNameToPath(Path folder, String replayName) { * Checks whether a given file name is actually usable with the file system / operating system at the given folder. */ private static boolean isUsable(Path folder, String fileName) { + if (fileName.contains(folder.getFileSystem().getSeparator())) { + return false; // file name contains the name separator, definitely not usable + } + Path path; try { path = folder.resolve(fileName); From a6c09bcb8ab46e83f918f36d3e3aec9f7183600b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 15:33:42 +0200 Subject: [PATCH 077/132] Fix crash when window is minimized on Windows --- .../java/com/replaymod/render/rendering/VideoRenderer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 56d1daf6..846daecc 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -641,6 +641,11 @@ public boolean drawGui() { private boolean displaySizeChanged() { int realWidth = mc.getWindow().getWidth(); int realHeight = mc.getWindow().getHeight(); + if (realWidth == 0 || realHeight == 0) { + // These can be zero on Windows if minimized. + // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. + return false; + } return displayWidth != realWidth || displayHeight != realHeight; } From 01d37ab2cb9fa29b5d33b13aaec6b0ffd7c5e2b8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 16:27:38 +0200 Subject: [PATCH 078/132] Split gui window/framebuffer management code from VideoRenderer --- .../render/gui/progress/VirtualWindow.java | 150 ++++++++++++++++++ .../render/rendering/VideoRenderer.java | 116 ++------------ 2 files changed, 161 insertions(+), 105 deletions(-) create mode 100644 src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java diff --git a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java new file mode 100644 index 00000000..5f2ef91d --- /dev/null +++ b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java @@ -0,0 +1,150 @@ +package com.replaymod.render.gui.progress; + +import com.replaymod.render.mixin.MainWindowAccessor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.Framebuffer; +import net.minecraft.client.util.Window; + +//#if MC>=11700 +//$$ import net.minecraft.client.gl.WindowFramebuffer; +//#endif + +public class VirtualWindow { + private final MinecraftClient mc; + private final Window window; + private final MainWindowAccessor acc; + + private final Framebuffer guiFramebuffer; + private int displayWidth, displayHeight; + private int framebufferWidth, framebufferHeight; + + private int gameWidth, gameHeight; + + + public VirtualWindow(MinecraftClient mc) { + this.mc = mc; + this.window = mc.getWindow(); + this.acc = (MainWindowAccessor) (Object) this.window; + + updateDisplaySize(); + updateFramebufferSize(); + + //#if MC>=11700 + //$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight); + //#else + guiFramebuffer = new Framebuffer(framebufferWidth, framebufferHeight, true + //#if MC>=11400 + , false + //#endif + ); + //#endif + } + + public void bind() { + gameWidth = acc.getFramebufferWidth(); + gameHeight = acc.getFramebufferHeight(); + acc.setFramebufferWidth(framebufferWidth); + acc.setFramebufferHeight(framebufferHeight); + } + + public void unbind() { + acc.setFramebufferWidth(gameWidth); + acc.setFramebufferHeight(gameHeight); + } + + public void beginWrite() { + guiFramebuffer.beginWrite(true); + } + + public void endWrite() { + guiFramebuffer.endWrite(); + } + + public void flip() { + guiFramebuffer.draw(framebufferWidth, framebufferHeight); + + //#if MC>=11500 + window.swapBuffers(); + //#else + //#if MC>=11400 + //$$ window.setFullscreen(false); + //#else + //#if MC>=10800 + //$$ mc.updateDisplay(); + //#else + //$$ mc.resetSize(); + //#endif + //#endif + //#endif + } + + public void updateSize() { + // Check if display size has changes and force recalculate GUI framebuffer size. + if (displaySizeChanged()) { + updateDisplaySize(); + acc.invokeUpdateFramebufferSize(); + } + + // Resize the GUI framebuffer if the display size changed + if (framebufferSizeChanged()) { + updateFramebufferSize(); + //#if MC>=11400 + guiFramebuffer.resize(framebufferWidth, framebufferHeight + //#if MC>=11400 + , false + //#endif + ); + //#else + //$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight); + //#endif + } + } + + private boolean displaySizeChanged() { + int realWidth = mc.getWindow().getWidth(); + int realHeight = mc.getWindow().getHeight(); + if (realWidth == 0 || realHeight == 0) { + // These can be zero on Windows if minimized. + // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. + return false; + } + return displayWidth != realWidth || displayHeight != realHeight; + } + + private boolean framebufferSizeChanged() { + int realWidth = mc.getWindow().getFramebufferWidth(); + int realHeight = mc.getWindow().getFramebufferHeight(); + if (realWidth == 0 || realHeight == 0) { + // These can be zero on Windows if minimized. + // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. + return false; + } + return framebufferWidth != realWidth || framebufferHeight != realHeight; + } + + private void updateDisplaySize() { + displayWidth = mc.getWindow().getWidth(); + displayHeight = mc.getWindow().getHeight(); + } + + private void updateFramebufferSize() { + framebufferWidth = mc.getWindow().getFramebufferWidth(); + framebufferHeight = mc.getWindow().getFramebufferHeight(); + } + + public int getDisplayWidth() { + return displayWidth; + } + + public int getDisplayHeight() { + return displayHeight; + } + + public int getFramebufferWidth() { + return framebufferWidth; + } + + public int getFramebufferHeight() { + return framebufferHeight; + } +} diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 846daecc..2ea98438 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -6,7 +6,6 @@ import com.replaymod.core.utils.WrappedTimer; import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.player.AbstractTimelinePlayer; -import com.replaymod.pathing.player.ReplayTimer; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.CameraPathExporter; import com.replaymod.render.PNGWriter; @@ -19,9 +18,9 @@ import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.gui.GuiRenderingDone; import com.replaymod.render.gui.GuiVideoRenderer; +import com.replaymod.render.gui.progress.VirtualWindow; import com.replaymod.render.hooks.ForceChunkLoadingHook; import com.replaymod.render.metadata.MetadataInjector; -import com.replaymod.render.mixin.MainWindowAccessor; import com.replaymod.render.mixin.WorldRendererAccessor; import com.replaymod.render.utils.FlawlessFrames; import com.replaymod.replay.ReplayHandler; @@ -32,7 +31,6 @@ import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.client.MinecraftClient; import com.mojang.blaze3d.platform.GLX; -import net.minecraft.client.gl.Framebuffer; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.client.util.Window; import net.minecraft.sound.SoundEvent; @@ -43,7 +41,6 @@ import org.lwjgl.glfw.GLFW; //#if MC>=11700 -//$$ import net.minecraft.client.gl.WindowFramebuffer; //$$ import net.minecraft.client.render.DiffuseLighting; //$$ import net.minecraft.util.math.Matrix4f; //#endif @@ -113,15 +110,12 @@ public class VideoRenderer implements RenderInfo { private int framesDone; private int totalFrames; + private final VirtualWindow guiWindow = new VirtualWindow(mc); private final GuiVideoRenderer gui; private boolean paused; private boolean cancelled; private volatile Throwable failureCause; - private Framebuffer guiFramebuffer; - private int displayWidth, displayHeight; - private int framebufferWidth, framebufferHeight; - public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException { this.settings = settings; this.replayHandler = replayHandler; @@ -245,11 +239,7 @@ public boolean renderVideo() throws Throwable { @Override public float updateForNextFrame() { // because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily - MainWindowAccessor acc = (MainWindowAccessor) (Object) mc.getWindow(); - int framebufferWidthBefore = acc.getFramebufferWidth(); - int framebufferHeightBefore = acc.getFramebufferHeight(); - acc.setFramebufferWidth(framebufferWidth); - acc.setFramebufferHeight(framebufferHeight); + guiWindow.bind(); if (!settings.isHighPerformance() || framesDone % fps == 0) { while (drawGui() && paused) { @@ -290,8 +280,7 @@ public float updateForNextFrame() { } // change Minecraft's display size back - acc.setFramebufferWidth(framebufferWidthBefore); - acc.setFramebufferHeight(framebufferHeightBefore); + guiWindow.unbind(); if (cameraPathExporter != null) { cameraPathExporter.recordFrame(timer.tickDelta); @@ -362,23 +351,9 @@ private void setup() { cameraPathExporter.setup(totalFrames); } - updateDisplaySize(); - updateFramebufferSize(); - gui.toMinecraft().init(mc, mc.getWindow().getScaledWidth(), mc.getWindow().getScaledHeight()); forceChunkLoadingHook = new ForceChunkLoadingHook(mc.worldRenderer); - - // Set up our own framebuffer to render the GUI to - //#if MC>=11700 - //$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight); - //#else - guiFramebuffer = new Framebuffer(framebufferWidth, framebufferHeight, true - //#if MC>=11400 - , false - //#endif - ); - //#endif } private void finish() { @@ -427,7 +402,7 @@ private void finish() { } // Finally, resize the Minecraft framebuffer to the actual width/height of the window - resizeMainWindow(mc, framebufferWidth, framebufferHeight); + resizeMainWindow(mc, guiWindow.getFramebufferWidth(), guiWindow.getFramebufferHeight()); } private void executeTaskQueue() { @@ -485,25 +460,7 @@ public boolean drawGui() { return false; } - // Check if display size has changes and force recalculate GUI framebuffer size. - if (displaySizeChanged()) { - updateDisplaySize(); - ((MainWindowAccessor) (Object) window).invokeUpdateFramebufferSize(); - } - - // Resize the GUI framebuffer if the display size changed - if (framebufferSizeChanged()) { - updateFramebufferSize(); - //#if MC>=11400 - guiFramebuffer.resize(framebufferWidth, framebufferHeight - //#if MC>=11400 - , false - //#endif - ); - //#else - //$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight); - //#endif - } + guiWindow.updateSize(); pushMatrix(); GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT @@ -512,7 +469,7 @@ public boolean drawGui() { //#endif ); GlStateManager.enableTexture(); - guiFramebuffer.beginWrite(true); + guiWindow.beginWrite(); //#if MC>=11500 RenderSystem.clear(256, MinecraftClient.IS_SYSTEM_MAC); @@ -568,8 +525,8 @@ public boolean drawGui() { //#endif //#if MC>=11400 - int mouseX = (int) mc.mouse.getX() * window.getScaledWidth() / displayWidth; - int mouseY = (int) mc.mouse.getY() * window.getScaledHeight() / displayHeight; + int mouseX = (int) mc.mouse.getX() * window.getScaledWidth() / guiWindow.getDisplayWidth(); + int mouseY = (int) mc.mouse.getY() * window.getScaledHeight() / guiWindow.getDisplayHeight(); if (mc.getOverlay() != null) { Screen orgScreen = mc.currentScreen; @@ -599,31 +556,12 @@ public boolean drawGui() { //$$ gui.toMinecraft().drawScreen(mouseX, mouseY, 0); //#endif - guiFramebuffer.endWrite(); + guiWindow.endWrite(); popMatrix(); pushMatrix(); - guiFramebuffer.draw(framebufferWidth, framebufferHeight); + guiWindow.flip(); popMatrix(); - //#if MC>=11500 - window.swapBuffers(); - //#else - //#if MC>=11400 - //$$ window.setFullscreen(false); - //#else - //$$ // 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()) { - //$$ Display.update(); - //$$ } else { - //#if MC>=10800 - //$$ mc.updateDisplay(); - //#else - //$$ mc.resetSize(); - //#endif - //$$ } - //#endif - //#endif //#if MC>=11400 if (mc.mouse.isCursorLocked()) { mc.mouse.unlockCursor(); @@ -638,38 +576,6 @@ public boolean drawGui() { } while (true); } - private boolean displaySizeChanged() { - int realWidth = mc.getWindow().getWidth(); - int realHeight = mc.getWindow().getHeight(); - if (realWidth == 0 || realHeight == 0) { - // These can be zero on Windows if minimized. - // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. - return false; - } - return displayWidth != realWidth || displayHeight != realHeight; - } - - private boolean framebufferSizeChanged() { - int realWidth = mc.getWindow().getFramebufferWidth(); - int realHeight = mc.getWindow().getFramebufferHeight(); - if (realWidth == 0 || realHeight == 0) { - // These can be zero on Windows if minimized. - // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. - return false; - } - return framebufferWidth != realWidth || framebufferHeight != realHeight; - } - - private void updateDisplaySize() { - displayWidth = mc.getWindow().getWidth(); - displayHeight = mc.getWindow().getHeight(); - } - - private void updateFramebufferSize() { - framebufferWidth = mc.getWindow().getFramebufferWidth(); - framebufferHeight = mc.getWindow().getFramebufferHeight(); - } - public int getFramesDone() { return framesDone; } From fb36fc2ba86835ba04980310ec87336d40d0567c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 17:07:08 +0200 Subject: [PATCH 079/132] Fix render progress framebuffer never being deleted --- .../com/replaymod/render/gui/progress/VirtualWindow.java | 8 +++++++- .../com/replaymod/render/rendering/VideoRenderer.java | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java index 5f2ef91d..c9d5769e 100644 --- a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java +++ b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java @@ -1,6 +1,7 @@ package com.replaymod.render.gui.progress; import com.replaymod.render.mixin.MainWindowAccessor; +import de.johni0702.minecraft.gui.function.Closeable; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gl.Framebuffer; import net.minecraft.client.util.Window; @@ -9,7 +10,7 @@ //$$ import net.minecraft.client.gl.WindowFramebuffer; //#endif -public class VirtualWindow { +public class VirtualWindow implements Closeable { private final MinecraftClient mc; private final Window window; private final MainWindowAccessor acc; @@ -40,6 +41,11 @@ public VirtualWindow(MinecraftClient mc) { //#endif } + @Override + public void close() { + guiFramebuffer.delete(); + } + public void bind() { gameWidth = acc.getFramebufferWidth(); gameHeight = acc.getFramebufferHeight(); diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 2ea98438..c7a5720c 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -363,6 +363,8 @@ private void finish() { // Tear down of the timeline player might only happen the next tick after it was cancelled timelinePlayer.onTick(); + guiWindow.close(); + // FBOs are always used in 1.14+ //#if MC<11400 //$$ if (!OpenGlHelper.isFramebufferEnabled()) { From 45587930301869697d55bee280549f564a0fc586 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 17:53:38 +0200 Subject: [PATCH 080/132] Remove unless window size tracking The only thing we care about is the framebuffer size, cause that will differ between game and progress gui rendering. The window size will be the same (real) size for both. --- .../render/gui/progress/VirtualWindow.java | 32 ------------------- .../render/rendering/VideoRenderer.java | 4 +-- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java index c9d5769e..8dd8617b 100644 --- a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java +++ b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java @@ -16,7 +16,6 @@ public class VirtualWindow implements Closeable { private final MainWindowAccessor acc; private final Framebuffer guiFramebuffer; - private int displayWidth, displayHeight; private int framebufferWidth, framebufferHeight; private int gameWidth, gameHeight; @@ -27,7 +26,6 @@ public VirtualWindow(MinecraftClient mc) { this.window = mc.getWindow(); this.acc = (MainWindowAccessor) (Object) this.window; - updateDisplaySize(); updateFramebufferSize(); //#if MC>=11700 @@ -85,12 +83,6 @@ public void flip() { } public void updateSize() { - // Check if display size has changes and force recalculate GUI framebuffer size. - if (displaySizeChanged()) { - updateDisplaySize(); - acc.invokeUpdateFramebufferSize(); - } - // Resize the GUI framebuffer if the display size changed if (framebufferSizeChanged()) { updateFramebufferSize(); @@ -106,17 +98,6 @@ public void updateSize() { } } - private boolean displaySizeChanged() { - int realWidth = mc.getWindow().getWidth(); - int realHeight = mc.getWindow().getHeight(); - if (realWidth == 0 || realHeight == 0) { - // These can be zero on Windows if minimized. - // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. - return false; - } - return displayWidth != realWidth || displayHeight != realHeight; - } - private boolean framebufferSizeChanged() { int realWidth = mc.getWindow().getFramebufferWidth(); int realHeight = mc.getWindow().getFramebufferHeight(); @@ -128,24 +109,11 @@ private boolean framebufferSizeChanged() { return framebufferWidth != realWidth || framebufferHeight != realHeight; } - private void updateDisplaySize() { - displayWidth = mc.getWindow().getWidth(); - displayHeight = mc.getWindow().getHeight(); - } - private void updateFramebufferSize() { framebufferWidth = mc.getWindow().getFramebufferWidth(); framebufferHeight = mc.getWindow().getFramebufferHeight(); } - public int getDisplayWidth() { - return displayWidth; - } - - public int getDisplayHeight() { - return displayHeight; - } - public int getFramebufferWidth() { return framebufferWidth; } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index c7a5720c..d51cc04b 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -527,8 +527,8 @@ public boolean drawGui() { //#endif //#if MC>=11400 - int mouseX = (int) mc.mouse.getX() * window.getScaledWidth() / guiWindow.getDisplayWidth(); - int mouseY = (int) mc.mouse.getY() * window.getScaledHeight() / guiWindow.getDisplayHeight(); + int mouseX = (int) mc.mouse.getX() * window.getScaledWidth() / Math.max(window.getWidth(), 1); + int mouseY = (int) mc.mouse.getY() * window.getScaledHeight() / Math.max(window.getHeight(), 1); if (mc.getOverlay() != null) { Screen orgScreen = mc.currentScreen; From fd9be55f7918c62fa0fc9a919c6b1aede89ab8e2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 18:58:34 +0200 Subject: [PATCH 081/132] Remove caching of ScaledResolution from Window shim It'll become stale if the code holds on to the Window shim object for a longer duration, and there's no reliable mechanism to invalidate it. --- .../java/com/replaymod/core/versions/Window.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java index 26d697d2..40e2278f 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java +++ b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java @@ -8,7 +8,6 @@ public class Window implements MainWindowAccessor { private final Minecraft mc; - private ScaledResolution scaledResolution; public Window(Minecraft mc) { this.mc = mc; @@ -52,16 +51,11 @@ public int getHeight() { } private ScaledResolution scaledResolution() { - ScaledResolution scaledResolution = this.scaledResolution; - if (scaledResolution == null) { - //#if MC>=10809 - scaledResolution = new ScaledResolution(mc); - //#else - //$$ scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); - //#endif - this.scaledResolution = scaledResolution; - } - return scaledResolution; + //#if MC>=10809 + return new ScaledResolution(mc); + //#else + //$$ return new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + //#endif } public int getScaledWidth() { From 5be57681afd2e6d36c2e203379e68dd000710512 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 18:36:36 +0200 Subject: [PATCH 082/132] Completely bypass Minecraft when window is resized during rendering Should fix issues where resizing affects the render result. --- .../render/gui/progress/VirtualWindow.java | 74 ++++++++++++------- .../render/hooks/MinecraftClientExt.java | 12 +++ .../render/mixin/MainWindowAccessor.java | 8 -- ...SuppressFramebufferResizeDuringRender.java | 39 ++++++++++ .../render/rendering/VideoRenderer.java | 2 - .../resources/mixins.render.replaymod.json | 1 + .../com/replaymod/core/versions/Window.java | 5 -- .../render/mixin/MainWindowAccessor.java | 2 - versions/1.14.4-forge/mapping.txt | 1 + versions/1.8.9/mapping.txt | 1 + 10 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/replaymod/render/hooks/MinecraftClientExt.java create mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_SuppressFramebufferResizeDuringRender.java diff --git a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java index 8dd8617b..82977526 100644 --- a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java +++ b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java @@ -1,5 +1,6 @@ package com.replaymod.render.gui.progress; +import com.replaymod.render.hooks.MinecraftClientExt; import com.replaymod.render.mixin.MainWindowAccessor; import de.johni0702.minecraft.gui.function.Closeable; import net.minecraft.client.MinecraftClient; @@ -16,6 +17,7 @@ public class VirtualWindow implements Closeable { private final MainWindowAccessor acc; private final Framebuffer guiFramebuffer; + private boolean isBound; private int framebufferWidth, framebufferHeight; private int gameWidth, gameHeight; @@ -26,7 +28,8 @@ public VirtualWindow(MinecraftClient mc) { this.window = mc.getWindow(); this.acc = (MainWindowAccessor) (Object) this.window; - updateFramebufferSize(); + framebufferWidth = acc.getFramebufferWidth(); + framebufferHeight = acc.getFramebufferHeight(); //#if MC>=11700 //$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight); @@ -37,11 +40,15 @@ public VirtualWindow(MinecraftClient mc) { //#endif ); //#endif + + MinecraftClientExt.get(mc).setWindowDelegate(this); } @Override public void close() { guiFramebuffer.delete(); + + MinecraftClientExt.get(mc).setWindowDelegate(null); } public void bind() { @@ -49,11 +56,13 @@ public void bind() { gameHeight = acc.getFramebufferHeight(); acc.setFramebufferWidth(framebufferWidth); acc.setFramebufferHeight(framebufferHeight); + isBound = true; } public void unbind() { acc.setFramebufferWidth(gameWidth); acc.setFramebufferHeight(gameHeight); + isBound = false; } public void beginWrite() { @@ -82,36 +91,45 @@ public void flip() { //#endif } - public void updateSize() { - // Resize the GUI framebuffer if the display size changed - if (framebufferSizeChanged()) { - updateFramebufferSize(); - //#if MC>=11400 - guiFramebuffer.resize(framebufferWidth, framebufferHeight - //#if MC>=11400 - , false - //#endif - ); - //#else - //$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight); - //#endif - } - } - - private boolean framebufferSizeChanged() { - int realWidth = mc.getWindow().getFramebufferWidth(); - int realHeight = mc.getWindow().getFramebufferHeight(); - if (realWidth == 0 || realHeight == 0) { + /** + * Updates the size of the window's framebuffer. Must only be called while this window is bound. + */ + public void onResolutionChanged(int newWidth, int newHeight) { + if (newWidth == 0 || newHeight == 0) { // These can be zero on Windows if minimized. // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. - return false; + return; + } + + if (framebufferWidth == newWidth && framebufferHeight == newHeight) { + return; // size is unchanged, nothing to do + } + + framebufferWidth = newWidth; + framebufferHeight = newHeight; + + //#if MC>=11400 + guiFramebuffer.resize(newWidth, newHeight + //#if MC>=11400 + , false + //#endif + ); + //#else + //$$ guiFramebuffer.createBindFramebuffer(newWidth, newHeight); + //#endif + + applyScaleFactor(); + if (mc.currentScreen != null) { + mc.currentScreen.resize(mc, window.getScaledWidth(), window.getScaledHeight()); } - return framebufferWidth != realWidth || framebufferHeight != realHeight; } - private void updateFramebufferSize() { - framebufferWidth = mc.getWindow().getFramebufferWidth(); - framebufferHeight = mc.getWindow().getFramebufferHeight(); + private void applyScaleFactor() { + //#if MC>=11400 + window.setScaleFactor(window.calculateScaleFactor(mc.options.guiScale, mc.forcesUnicodeFont())); + //#else + //$$ // Nothing to do, ScaledResolution re-computes the scale factor every time it is created + //#endif } public int getFramebufferWidth() { @@ -121,4 +139,8 @@ public int getFramebufferWidth() { public int getFramebufferHeight() { return framebufferHeight; } + + public boolean isBound() { + return isBound; + } } diff --git a/src/main/java/com/replaymod/render/hooks/MinecraftClientExt.java b/src/main/java/com/replaymod/render/hooks/MinecraftClientExt.java new file mode 100644 index 00000000..6b4295bb --- /dev/null +++ b/src/main/java/com/replaymod/render/hooks/MinecraftClientExt.java @@ -0,0 +1,12 @@ +package com.replaymod.render.hooks; + +import com.replaymod.render.gui.progress.VirtualWindow; +import net.minecraft.client.MinecraftClient; + +public interface MinecraftClientExt { + void setWindowDelegate(VirtualWindow window); + + static MinecraftClientExt get(MinecraftClient mc) { + return (MinecraftClientExt) mc; + } +} diff --git a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index a54dd178..c24af52b 100644 --- a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -15,12 +15,4 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor void setFramebufferHeight(int value); - // FIXME preprocessor should be able to infer this mapping - // FIXME preprocessor should be able to remap this one when the mapping is given manually - //#if MC>=11500 - @Invoker - //#else - //$$ @Invoker("method_4483") - //#endif - void invokeUpdateFramebufferSize(); } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_SuppressFramebufferResizeDuringRender.java b/src/main/java/com/replaymod/render/mixin/Mixin_SuppressFramebufferResizeDuringRender.java new file mode 100644 index 00000000..707f6952 --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/Mixin_SuppressFramebufferResizeDuringRender.java @@ -0,0 +1,39 @@ +package com.replaymod.render.mixin; + +import com.replaymod.render.gui.progress.VirtualWindow; +import com.replaymod.render.hooks.MinecraftClientExt; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.util.Window; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class Mixin_SuppressFramebufferResizeDuringRender implements MinecraftClientExt { + + @Unique + private VirtualWindow windowDelegate; + + @Override + public void setWindowDelegate(VirtualWindow window) { + this.windowDelegate = window; + } + + //#if MC>=11400 + @Inject(method = "onResolutionChanged", at = @At("HEAD"), cancellable = true) + //#else + //$$ @Inject(method = "resize", at = @At("HEAD"), cancellable = true) + //#endif + private void suppressResizeDuringRender(CallbackInfo ci) { + VirtualWindow delegate = this.windowDelegate; + if (delegate != null && delegate.isBound()) { + Window window = ((MinecraftClient) (Object) this).getWindow(); + delegate.onResolutionChanged(window.getFramebufferWidth(), window.getFramebufferHeight()); + 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 d51cc04b..a6b31504 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -462,8 +462,6 @@ public boolean drawGui() { return false; } - guiWindow.updateSize(); - pushMatrix(); GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT //#if MC>=11400 diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 092aa958..ff833a38 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -21,6 +21,7 @@ "Mixin_StabilizeCamera", "Mixin_Stereoscopic_Camera", "Mixin_Stereoscopic_HandRenderPass", + "Mixin_SuppressFramebufferResizeDuringRender", //#if MC>=11600 "Mixin_AddIrisOdsShaderUniforms", "Mixin_LoadIrisOdsShaderPack", diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java index 40e2278f..3f620619 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java +++ b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java @@ -33,11 +33,6 @@ public void setFramebufferHeight(int value) { mc.displayHeight = value; } - @Override - public void invokeUpdateFramebufferSize() { - // no-op, pre-LWJGL3 MC doesn't differentiate between window and framebuffer size - } - public long getHandle() { return 0; } diff --git a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index a6166095..697dd601 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -15,6 +15,4 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor("displayHeight") void setFramebufferHeight(int value); - @Invoker - void invokeUpdateFramebufferSize(); } diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index 2096663b..52a22ceb 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -181,6 +181,7 @@ net.minecraft.client.gui.screen.Screen hasShiftDown() isShiftKeyDown() net.minecraft.client.gui.screen.Screen init() setWorldAndResolution() net.minecraft.client.gui.screen.Screen minecraft mc net.minecraft.client.gui.screen.Screen net.minecraft.client.gui.GuiScreen +net.minecraft.client.gui.screen.Screen resize() onResize() net.minecraft.client.gui.screen.Screen passEvents allowUserInput net.minecraft.client.gui.screen.Screen removed() onGuiClosed() net.minecraft.client.gui.screen.Screen renderBackground() drawDefaultBackground() diff --git a/versions/1.8.9/mapping.txt b/versions/1.8.9/mapping.txt index 43123ad0..2dfdbda5 100644 --- a/versions/1.8.9/mapping.txt +++ b/versions/1.8.9/mapping.txt @@ -1,3 +1,4 @@ +net.minecraft.client.gui.GuiScreen onResize() func_175273_b() net.minecraft.entity.player.EntityPlayer isWearing() func_175148_a() net.minecraft.client.renderer.WorldRenderer begin() startDrawing() net.minecraft.network.play.server.S38PacketPlayerListItem getAction() func_179768_b() From 497b8440cad0c1da2069d99c59d171e12be22afd Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 10 Apr 2022 19:53:15 +0200 Subject: [PATCH 083/132] Call Window.onFramebufferSizeChanged to resize (fixes #705) Instead of setting the the values via accessor and calling the handler directly. This allows mods like ResolutionControl+ to properly resize its internal framebuffers as well. --- .../java/com/replaymod/core/versions/MCVer.java | 15 ++++----------- .../render/gui/progress/VirtualWindow.java | 2 ++ .../render/mixin/MainWindowAccessor.java | 2 ++ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index c239d9b0..e53ed093 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -27,9 +27,9 @@ //#if MC>=11400 import com.replaymod.render.mixin.MainWindowAccessor; import net.minecraft.SharedConstants; -import net.minecraft.client.gl.Framebuffer; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.AbstractButtonWidget; +import net.minecraft.client.util.Window; import java.util.concurrent.CompletableFuture; @@ -106,17 +106,10 @@ public static PacketTypeRegistry getPacketTypeRegistry(boolean loginPhase) { public static void resizeMainWindow(MinecraftClient mc, int width, int height) { //#if MC>=11400 - Framebuffer fb = mc.getFramebuffer(); - if (fb.viewportWidth != width || fb.viewportHeight != height) { - fb.resize(width, height, false); - } + Window window = mc.getWindow(); + MainWindowAccessor mainWindow = (MainWindowAccessor) (Object) window; //noinspection ConstantConditions - MainWindowAccessor mainWindow = (MainWindowAccessor) (Object) mc.getWindow(); - mainWindow.setFramebufferWidth(width); - mainWindow.setFramebufferHeight(height); - //#if MC>=11500 - mc.gameRenderer.onResized(width, height); - //#endif + mainWindow.invokeOnFramebufferSizeChanged(window.getHandle(), width, height); //#else //$$ if (width != mc.displayWidth || height != mc.displayHeight) { //$$ mc.resize(width, height); diff --git a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java index 82977526..6f2b2bc0 100644 --- a/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java +++ b/src/main/java/com/replaymod/render/gui/progress/VirtualWindow.java @@ -56,12 +56,14 @@ public void bind() { gameHeight = acc.getFramebufferHeight(); acc.setFramebufferWidth(framebufferWidth); acc.setFramebufferHeight(framebufferHeight); + applyScaleFactor(); isBound = true; } public void unbind() { acc.setFramebufferWidth(gameWidth); acc.setFramebufferHeight(gameHeight); + applyScaleFactor(); isBound = false; } diff --git a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index c24af52b..6bfa8e67 100644 --- a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -15,4 +15,6 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor void setFramebufferHeight(int value); + @Invoker + void invokeOnFramebufferSizeChanged(long window, int width, int height); } From f2c96174bc44115725220451759eedf0bf3a984b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 17 Apr 2022 11:20:58 +0200 Subject: [PATCH 084/132] Fix terrain culling not being updated for omnidirectional rendering Terrain culling is only updated when the camera position or rotation changes. For omnidirectional rendering, we don't change the camera directly though, we rotate way earlier so we can consistently get before any other effects. As a result, the terrain frustum culling wasn't updated between render passes resulting in missing terrain. This was largely masked by the fact that we used to call GameRenderer.onResize every pass (even when the size hadn't changed) but fixing that made this issue become apparent. --- .../replaymod/render/mixin/Mixin_Omnidirectional_Rotation.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Rotation.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Rotation.java index 6107028e..e7d567c7 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Rotation.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Rotation.java @@ -83,6 +83,8 @@ private void replayModRender_setupCubicFrameRotation( //#else //$$ GL11.glRotatef(angle, x, y, 0); //#endif + + getMinecraft().worldRenderer.scheduleTerrainUpdate(); } //#if MC<11500 //$$ if (getHandler() != null && getHandler().omnidirectional) { From 8b2d42f801ec9ef50f5bfbcbd7132a9ab95827f3 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 1 May 2022 20:05:00 +0200 Subject: [PATCH 085/132] Fix broken fog when using Chroma Key with Sodium (fixes #726) --- .../render/mixin/Mixin_ChromaKeyDisableFog.java | 12 ++++++++++++ versions/mapping-fabric-1.17-1.16.4.txt | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyDisableFog.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyDisableFog.java index 48719c54..948038e9 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyDisableFog.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyDisableFog.java @@ -1,5 +1,6 @@ package com.replaymod.render.mixin; +import com.mojang.blaze3d.platform.GlStateManager; import com.replaymod.core.versions.MCVer; import com.replaymod.render.hooks.EntityRendererHandler; import net.minecraft.client.render.BackgroundRenderer; @@ -20,6 +21,17 @@ void replayModRender_onSetupFog(CallbackInfo ci) { ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().gameRenderer).replayModRender_getHandler(); if (handler == null) return; if (handler.getSettings().getChromaKeyingColor() != null) { + // Starting with 1.15, fog is no longer enabled in this method but is instead managed by the RenderLayer + // system (and with 1.17, they are enabled permanently / depend only on the shader). Therefore, cancelling + // this method is no longer sufficient, and we additionally also need to set the start value to get rid of + // fog (this doesn't hurt on 1.14 either). + // Note: This only becomes noticeable with Sodium because Vanilla would already set the start to max for + // unrelated reasons. But Sodium does some math which gives wrong results if end isn't greater than + // start, as would be the case in these cases. Sodium doing math is also the reason we don't set start + // equal to end (that'll result in undefined behavior because it sticks those into a smoothstep on old + // versions), and we don't set it to MAX_VALUE because that also gives wrong results. + GlStateManager.fogStart(1E10F); + GlStateManager.fogEnd(2E10F); ci.cancel(); } } diff --git a/versions/mapping-fabric-1.17-1.16.4.txt b/versions/mapping-fabric-1.17-1.16.4.txt index 300a639a..193eb19b 100644 --- a/versions/mapping-fabric-1.17-1.16.4.txt +++ b/versions/mapping-fabric-1.17-1.16.4.txt @@ -1,4 +1,6 @@ com.mojang.blaze3d.systems.RenderSystem com.mojang.blaze3d.platform.GlStateManager +com.mojang.blaze3d.systems.RenderSystem setShaderFogStart() fogStart() +com.mojang.blaze3d.systems.RenderSystem setShaderFogEnd() fogEnd() net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket getDimensionType() method_29445() net.minecraft.entity.Entity getId() getEntityId() net.minecraft.client.network.ClientPlayerEntity init() net.minecraft.entity.player.PlayerEntity afterSpawn() From 4364347a19a1ca96f33e0cc086e17d02305479e9 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 1 May 2022 20:34:26 +0200 Subject: [PATCH 086/132] Update ReplayStudio 6fc8e20 Fix incorrect relative packet order in squash filter (fixes #718) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e9ba988b..c57fc62f 100644 --- a/build.gradle +++ b/build.gradle @@ -341,7 +341,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:b5539d1", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:6fc8e20", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those From 716df74893d7c1f7f626d6133ab4bffd923e724e Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 1 May 2022 21:06:24 +0200 Subject: [PATCH 087/132] Fix first person (cross)bow model animation on 1.9+ (fixes #708) --- src/main/java/com/replaymod/replay/camera/CameraEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 35bd016c..d990240e 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -688,6 +688,7 @@ private void syncInventory() { //#if MC>=10904 cameraA.setItemStackMainHand(viewPlayerA != null ? viewPlayerA.getItemStackMainHand() : empty); this.preferredHand = viewPlayer != null ? viewPlayer.preferredHand : Hand.MAIN_HAND; + this.activeItemStack = viewPlayer != null ? viewPlayer.getActiveItem() : empty; cameraA.setActiveItemStackUseCount(viewPlayerA != null ? viewPlayerA.getActiveItemStackUseCount() : 0); //#else //$$ cameraA.setItemInUse(viewPlayerA != null ? viewPlayerA.getItemInUse() : empty); From 996485237a51d2319fc3676fe30214684fd8bc90 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 2 May 2022 09:34:41 +0200 Subject: [PATCH 088/132] Fix build for all versions --- build.gradle | 3 ++- .../java/com/replaymod/core/versions/MCVer.java | 2 +- .../com/replaymod/core/gui/common/UI4Slice.kt | 11 +++++------ .../com/replaymod/core/gui/common/UI9Slice.kt | 11 +++++------ .../com/replaymod/core/gui/common/UIButton.kt | 5 ++--- .../com/replaymod/core/gui/common/UITexture.kt | 11 +++++------ .../replaymod/simplepathing/gui/GuiPathingKt.kt | 9 +++++++++ .../simplepathing/gui/UITimelineKeyframes.kt | 16 ++++++++++------ versions/1.14.4-forge/mapping.txt | 2 ++ 9 files changed, 41 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index 50980a6d..33bd9abd 100644 --- a/build.gradle +++ b/build.gradle @@ -97,6 +97,7 @@ preprocess { keywords.set([ ".java": PreprocessTask.DEFAULT_KEYWORDS, + ".kt": PreprocessTask.DEFAULT_KEYWORDS, ".json": PreprocessTask.DEFAULT_KEYWORDS, ".mcmeta": PreprocessTask.DEFAULT_KEYWORDS, ".cfg": PreprocessTask.CFG_KEYWORDS, @@ -338,7 +339,7 @@ dependencies { } else { elementaMcVersion = '1.8.9' } - def elementaVersion = '391' + def elementaVersion = '458+pull-58' if (fabric) { modImplementation(shadow("gg.essential:elementa-$elementaMcVersion-fabric:$elementaVersion")) } else { diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index a852644c..65dab728 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -478,7 +478,7 @@ public static abstract class Keyboard { //#else //$$ public static final int KEY_LCONTROL = org.lwjgl.input.Keyboard.KEY_LCONTROL; //$$ public static final int KEY_LSHIFT = org.lwjgl.input.Keyboard.KEY_LSHIFT; - //$$ public static final int KEY_LALT = org.lwjgl.input.Keyboard.KEY_LALT; + //$$ public static final int KEY_LALT = org.lwjgl.input.Keyboard.KEY_LMENU; //$$ public static final int KEY_ESCAPE = org.lwjgl.input.Keyboard.KEY_ESCAPE; //$$ public static final int KEY_HOME = org.lwjgl.input.Keyboard.KEY_HOME; //$$ public static final int KEY_END = org.lwjgl.input.Keyboard.KEY_END; diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt b/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt index 2ecb88a4..ddcac634 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/UI4Slice.kt @@ -7,7 +7,6 @@ import gg.essential.elementa.state.State import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import gg.essential.universal.shader.BlendState -import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier import java.awt.Color @@ -57,26 +56,26 @@ class UI4Slice( val alpha = color.alpha.toFloat() / 255f val buffer = UGraphics.getFromTessellator() - buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) fun UGraphics.texS(u: Double, v: Double) = tex(u / data.textureWidth, v / data.textureHeight) fun drawTexturedRect(x: Double, y: Double, u: Double, v: Double, width: Double, height: Double) { buffer.pos(matrixStack, x, y + height, 0.0) - .color(red, green, blue, alpha) .texS(u, v + height) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x + width, y + height, 0.0) - .color(red, green, blue, alpha) .texS(u + width, v + height) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x + width, y, 0.0) - .color(red, green, blue, alpha) .texS(u + width, v) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x, y, 0.0) - .color(red, green, blue, alpha) .texS(u, v) + .color(red, green, blue, alpha) .endVertex() } diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt b/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt index d33f94ea..64cf0c58 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/UI9Slice.kt @@ -6,7 +6,6 @@ import gg.essential.elementa.state.State import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import gg.essential.universal.shader.BlendState -import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier import java.awt.Color @@ -56,26 +55,26 @@ class UI9Slice( val alpha = color.alpha.toFloat() / 255f val buffer = UGraphics.getFromTessellator() - buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) fun UGraphics.texS(u: Double, v: Double) = tex(u / data.textureWidth, v / data.textureHeight) fun drawTexturedRect(x: Double, y: Double, u: Double, v: Double, width: Double, height: Double) { buffer.pos(matrixStack, x, y + height, 0.0) - .color(red, green, blue, alpha) .texS(u, v + height) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x + width, y + height, 0.0) - .color(red, green, blue, alpha) .texS(u + width, v + height) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x + width, y, 0.0) - .color(red, green, blue, alpha) .texS(u + width, v) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, x, y, 0.0) - .color(red, green, blue, alpha) .texS(u, v) + .color(red, green, blue, alpha) .endVertex() } diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt index a9f0139d..c3c42fd0 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/UIButton.kt @@ -14,7 +14,6 @@ import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import net.minecraft.client.MinecraftClient import net.minecraft.client.sound.PositionedSoundInstance -import net.minecraft.sound.SoundEvents import net.minecraft.util.Identifier import java.awt.Color import java.awt.image.BufferedImage @@ -112,8 +111,8 @@ class UIButton : UIComponent() { } companion object { - //#if MC>=11900 - private val BUTTON_SOUND = SoundEvents.UI_BUTTON_CLICK + //#if MC>=10900 + private val BUTTON_SOUND = net.minecraft.sound.SoundEvents.UI_BUTTON_CLICK //#else //$$ private val BUTTON_SOUND = ResourceLocation("gui.button.press") //#endif diff --git a/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt b/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt index 38809c87..f9c2a7ee 100644 --- a/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt +++ b/src/main/kotlin/com/replaymod/core/gui/common/UITexture.kt @@ -6,7 +6,6 @@ import gg.essential.elementa.state.State import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import gg.essential.universal.shader.BlendState -import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier import java.awt.Color @@ -51,26 +50,26 @@ class UITexture( UGraphics.enableAlpha() val buffer = UGraphics.getFromTessellator() - buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE) + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) with(data) { with(color) { fun UGraphics.texS(u: Double, v: Double) = tex(u / textureWidth, v / textureHeight) buffer.pos(matrixStack, l, b, 0.0) - .color(red, green, blue, alpha) .texS(lt, bt) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, r, b, 0.0) - .color(red, green, blue, alpha) .texS(rt, bt) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, r, t, 0.0) - .color(red, green, blue, alpha) .texS(rt, tt) + .color(red, green, blue, alpha) .endVertex() buffer.pos(matrixStack, l, t, 0.0) - .color(red, green, blue, alpha) .texS(lt, tt) + .color(red, green, blue, alpha) .endVertex() } } diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt index 87ab1c13..9ef62618 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/GuiPathingKt.kt @@ -119,7 +119,11 @@ class GuiPathingKt( } overlay.isCloseable = true } + //#if MC>=11800 + //$$ }, Runnable::run) + //#else }) + //#endif } } childOf secondRow @@ -401,7 +405,12 @@ class GuiPathingKt( val camera = replayHandler.cameraEntity var spectatedId = -1 if (!replayHandler.isCameraView && !neverSpectator) { + // FIXME preprocessor bug: there are mappings for this + //#if MC>=11700 + // spectatedId = replayHandler.overlay.minecraft.getCameraEntity()!!.id + //#else spectatedId = replayHandler.overlay.minecraft.getCameraEntity()!!.entityId + //#endif } timeline.addPositionKeyframe(time.inWholeMilliseconds, camera.x, camera.y, camera.z, diff --git a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt index c8df07ab..7f009050 100644 --- a/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt +++ b/src/main/kotlin/com/replaymod/simplepathing/gui/UITimelineKeyframes.kt @@ -19,10 +19,10 @@ import gg.essential.elementa.constraints.* import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.Effect import gg.essential.elementa.effects.ScissorEffect +import gg.essential.universal.UGraphics import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack import net.minecraft.client.render.Tessellator -import net.minecraft.client.render.VertexFormats import org.lwjgl.opengl.GL11 import java.awt.Color import kotlin.math.absoluteValue @@ -271,9 +271,13 @@ class UITimelineKeyframes( val keyframeX = boundComponent.getLeft() + boundComponent.getWidth() / 2 val color = -0xffff01 - val tessellator = Tessellator.getInstance() - val buffer = tessellator.buffer - buffer.begin(GL11.GL_LINE_STRIP, VertexFormats.POSITION_COLOR) + val tessellator = UGraphics.getFromTessellator() + val buffer = Tessellator.getInstance().buffer + //#if MC>=11700 + //$$ tessellator.beginWithActiveShader(UGraphics.DrawMode.LINE_STRIP, net.minecraft.client.render.VertexFormats.LINES) + //#else + tessellator.beginWithActiveShader(UGraphics.DrawMode.LINE_STRIP, UGraphics.CommonVertexFormats.POSITION_COLOR) + //#endif // Start just below the top border of the replay timeline val p1 = Vector2f(replayTimelineLeft + positionXReplayTimeline, (replayTimelineTop + BORDER_TOP).toFloat()) @@ -289,7 +293,7 @@ class UITimelineKeyframes( MCVer.emitLine(buffer, p3, p4, color) //#if MC>=11700 - //$$ RenderSystem.setShader(GameRenderer::getRenderTypeLinesShader); + //$$ com.mojang.blaze3d.systems.RenderSystem.setShader(net.minecraft.client.render.GameRenderer::getRenderTypeLinesShader) //#else GL11.glEnable(GL11.GL_LINE_SMOOTH) GL11.glDisable(GL11.GL_TEXTURE_2D) @@ -297,7 +301,7 @@ class UITimelineKeyframes( GL11.glLineWidth(2f) scissorEffect.beforeDraw(matrixStack) - tessellator.draw() + tessellator.drawDirect() scissorEffect.afterDraw(matrixStack) //#if MC<11700 diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index 52a22ceb..1f63d8aa 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -1,3 +1,5 @@ +net.minecraft.util.SoundEvents net.minecraft.init.SoundEvents +net.minecraft.util.SharedConstants net.minecraft.util.ChatAllowedCharacters net.minecraft.entity.player.PlayerInventory net.minecraft.entity.player.InventoryPlayer net.minecraft.potion.EffectInstance net.minecraft.potion.PotionEffect net.minecraft.client.gui.screen.AddServerScreen net.minecraft.client.gui.GuiScreenAddServer From 9f8957f468cf99d1f4582d4277b60d7c67c98edc Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 24 May 2022 10:44:30 +0200 Subject: [PATCH 089/132] Migrate away from deprecated fabric-api modules --- build.gradle | 2 + .../replaymod/core/KeyBindingRegistry.java | 22 +++++-- .../recording/ReplayModRecording.java | 8 +++ src/main/resources/fabric.mod.json | 2 +- .../1.15.2/src/main/resources/fabric.mod.json | 60 +++++++++++++++++++ .../1.17/src/main/resources/fabric.mod.json | 60 +++++++++++++++++++ 6 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 versions/1.15.2/src/main/resources/fabric.mod.json create mode 100644 versions/1.17/src/main/resources/fabric.mod.json diff --git a/build.gradle b/build.gradle index c57fc62f..d809e9ba 100644 --- a/build.gradle +++ b/build.gradle @@ -279,9 +279,11 @@ dependencies { "resource-loader-v0", ] if (mcVersion >= 11600) { + fabricApiModules.remove("keybindings-v0") fabricApiModules.add("key-binding-api-v1") } if (mcVersion >= 11700) { + fabricApiModules.remove("networking-v0") fabricApiModules.add("networking-api-v1") } fabricApiModules.each { module -> diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java index 6223717c..40eff2ee 100644 --- a/src/main/java/com/replaymod/core/KeyBindingRegistry.java +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -14,7 +14,11 @@ //#if FABRIC>=1 import com.replaymod.core.versions.LangResourcePack; -import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding; +//#if MC>=11600 +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +//#else +//$$ import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding; +//#endif import net.minecraft.client.util.InputUtil; import net.minecraft.util.Identifier; import static com.replaymod.core.ReplayMod.MOD_ID; @@ -35,8 +39,8 @@ public class KeyBindingRegistry extends EventRegistrations { private static final String CATEGORY = "replaymod.title"; - //#if FABRIC>=1 - static { net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.addCategory(CATEGORY); } + //#if FABRIC>=1 && MC<11600 + //$$ static { net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.addCategory(CATEGORY); } //#endif private final Map bindings = new HashMap<>(); @@ -63,9 +67,15 @@ private Binding registerKeyBinding(String name, int keyCode, boolean onlyInRepay keyCode = -1; } Identifier id = new Identifier(MOD_ID, name.substring(LangResourcePack.LEGACY_KEY_PREFIX.length())); - FabricKeyBinding fabricKeyBinding = FabricKeyBinding.Builder.create(id, InputUtil.Type.KEYSYM, keyCode, CATEGORY).build(); - net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.register(fabricKeyBinding); - KeyBinding keyBinding = fabricKeyBinding; + //#if MC>=11600 + String key = String.format("key.%s.%s", id.getNamespace(), id.getPath()); + KeyBinding keyBinding = new KeyBinding(key, InputUtil.Type.KEYSYM, keyCode, CATEGORY); + KeyBindingHelper.registerKeyBinding(keyBinding); + //#else + //$$ FabricKeyBinding fabricKeyBinding = FabricKeyBinding.Builder.create(id, InputUtil.Type.KEYSYM, keyCode, CATEGORY).build(); + //$$ net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.register(fabricKeyBinding); + //$$ KeyBinding keyBinding = fabricKeyBinding; + //#endif //#else //$$ KeyBinding keyBinding = new KeyBinding(name, keyCode, CATEGORY); //$$ ClientRegistry.registerKeyBinding(keyBinding); diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java index 94ae0902..f65e4467 100644 --- a/src/main/java/com/replaymod/recording/ReplayModRecording.java +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -16,7 +16,11 @@ import org.apache.logging.log4j.Logger; //#if FABRIC>=1 +//#if MC>=11700 +//$$ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +//#else import net.fabricmc.fabric.api.network.ClientSidePacketRegistry; +//#endif //#else //$$ import net.minecraftforge.fml.network.NetworkRegistry; //#endif @@ -68,7 +72,11 @@ public void initClient() { new GuiHandler(core).register(); //#if FABRIC>=1 + //#if MC>=11700 + //$$ ClientPlayNetworking.registerGlobalReceiver(Restrictions.PLUGIN_CHANNEL, (client, handler, buf, resp) -> {}); + //#else ClientSidePacketRegistry.INSTANCE.register(Restrictions.PLUGIN_CHANNEL, (packetContext, packetByteBuf) -> {}); + //#endif //#else //#if MC>=11400 //$$ NetworkRegistry.newEventChannel(Restrictions.PLUGIN_CHANNEL, () -> "0", any -> true, any -> true); diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 73fe9d6e..ef5522ec 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -43,7 +43,7 @@ "depends": { "fabricloader": ">=0.7.0", "fabric-networking-v0": "*", - "fabric-keybindings-v0": "*", + "fabric-key-binding-api-v1": "*", "fabric-resource-loader-v0": "*" }, diff --git a/versions/1.15.2/src/main/resources/fabric.mod.json b/versions/1.15.2/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..73fe9d6e --- /dev/null +++ b/versions/1.15.2/src/main/resources/fabric.mod.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "id": "replaymod", + "version": "${version}", + + "name": "Replay Mod", + "description": "A Mod which allows you to record, replay and share your Minecraft experience.", + "authors": [ + "CrushedPixel", + "johni0702" + ], + "contact": { + "homepage": "https://replaymod.com/", + "sources": "https://github.com/ReplayMod/ReplayMod" + }, + + "license": "GPL-3.0-or-later", + "icon": "assets/replaymod/favicon_logo.png", + + "environment": "client", + "entrypoints": { + "client": [ + "com.replaymod.core.ReplayModBackend" + ], + "modmenu": [ + "com.replaymod.core.gui.ModMenuApiImpl" + ], + "frex_flawless_frames": [ + "com.replaymod.render.utils.FlawlessFrames::registerConsumer" + ], + "preLaunch": [ + "com.replaymod.core.DummyChainLoadEntryPoint" + ], + "mm:early_risers": [ + "com.replaymod.core.ReplayModMMLauncher" + ] + }, + "mixins": [ + "mixins.jgui.json", + "mixins.nonmmlauncher.replaymod.json" + ], + + "depends": { + "fabricloader": ">=0.7.0", + "fabric-networking-v0": "*", + "fabric-keybindings-v0": "*", + "fabric-resource-loader-v0": "*" + }, + + "conflicts": { + "iris": "<1.1.3" + }, + + "custom": { + "mm:early_risers": [ + "com.replaymod.core.ReplayModMMLauncher" + ], + "modmenu:clientsideOnly": true + } +} diff --git a/versions/1.17/src/main/resources/fabric.mod.json b/versions/1.17/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..dbb99550 --- /dev/null +++ b/versions/1.17/src/main/resources/fabric.mod.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "id": "replaymod", + "version": "${version}", + + "name": "Replay Mod", + "description": "A Mod which allows you to record, replay and share your Minecraft experience.", + "authors": [ + "CrushedPixel", + "johni0702" + ], + "contact": { + "homepage": "https://replaymod.com/", + "sources": "https://github.com/ReplayMod/ReplayMod" + }, + + "license": "GPL-3.0-or-later", + "icon": "assets/replaymod/favicon_logo.png", + + "environment": "client", + "entrypoints": { + "client": [ + "com.replaymod.core.ReplayModBackend" + ], + "modmenu": [ + "com.replaymod.core.gui.ModMenuApiImpl" + ], + "frex_flawless_frames": [ + "com.replaymod.render.utils.FlawlessFrames::registerConsumer" + ], + "preLaunch": [ + "com.replaymod.core.DummyChainLoadEntryPoint" + ], + "mm:early_risers": [ + "com.replaymod.core.ReplayModMMLauncher" + ] + }, + "mixins": [ + "mixins.jgui.json", + "mixins.nonmmlauncher.replaymod.json" + ], + + "depends": { + "fabricloader": ">=0.7.0", + "fabric-networking-api-v1": "*", + "fabric-key-binding-api-v1": "*", + "fabric-resource-loader-v0": "*" + }, + + "conflicts": { + "iris": "<1.1.3" + }, + + "custom": { + "mm:early_risers": [ + "com.replaymod.core.ReplayModMMLauncher" + ], + "modmenu:clientsideOnly": true + } +} From 9ce25f711f6fe665994c1198d3c37b7343f971df Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 27 May 2022 07:03:35 +0200 Subject: [PATCH 090/132] Port to MC 1.19-pre3 --- build.gradle | 11 ++- jGui | 2 +- root.gradle.kts | 6 +- settings.gradle.kts | 2 + .../java/com/replaymod/core/ReplayMod.java | 9 +- .../core/mixin/SimpleOptionAccessor.java | 1 + .../core/versions/LangResourcePack.java | 11 ++- .../com/replaymod/core/versions/MCVer.java | 3 +- .../com/replaymod/core/versions/Patterns.java | 98 ++++++++++++++++++- .../com/replaymod/extras/FullBrightness.java | 2 +- .../playeroverview/PlayerOverviewGui.java | 4 +- .../handler/RecordingEventHandler.java | 15 +-- .../mixin/MixinDownloadingPackFinder.java | 4 + .../recording/mixin/MixinWorldClient.java | 35 +++++-- .../mixin/SPacketSpawnMobAccessor.java | 32 +++--- .../mixin/SPacketSpawnPlayerAccessor.java | 32 +++--- .../recording/packet/PacketListener.java | 10 +- .../packet/ResourcePackRecorder.java | 18 +++- .../render/blend/BlendMeshBuilder.java | 2 +- .../replaymod/render/blend/BlendState.java | 4 +- .../blend/exporters/EntityExporter.java | 2 +- .../render/blend/exporters/ItemExporter.java | 2 +- .../exporters/ModelRendererExporter.java | 2 +- .../blend/exporters/ParticlesExporter.java | 2 +- .../render/blend/exporters/RenderState.java | 2 +- .../blend/exporters/TileEntityExporter.java | 2 +- .../render/blend/mixin/MixinRenderGlobal.java | 2 +- .../render/blend/mixin/MixinRenderItem.java | 2 +- .../blend/mixin/MixinRenderLivingBase.java | 2 +- .../blend/mixin/MixinRenderManager.java | 2 +- .../render/mixin/MixinParticleManager.java | 8 +- .../replaymod/replay/FullReplaySender.java | 18 +++- .../replaymod/replay/camera/CameraEntity.java | 12 ++- .../replaymod/replay/handler/GuiHandler.java | 10 +- src/main/resources/mixins.core.replaymod.json | 3 + .../resources/mixins.recording.replaymod.json | 6 +- .../mixins.render.blend.replaymod.json | 2 + versions/1.14.4-forge/mapping.txt | 2 + versions/1.19/.gitkeep | 0 .../core/mixin/SimpleOptionAccessor.java | 11 +++ versions/mapping-fabric-1.19-1.18.2.txt | 4 + 41 files changed, 299 insertions(+), 98 deletions(-) create mode 100644 src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java create mode 100644 versions/1.19/.gitkeep create mode 100644 versions/1.19/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java create mode 100644 versions/mapping-fabric-1.19-1.18.2.txt diff --git a/build.gradle b/build.gradle index d809e9ba..3a3664ec 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ buildscript { dependencies { classpath 'gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0' if (fabric) { - classpath 'fabric-loom:fabric-loom.gradle.plugin:0.10-SNAPSHOT' + classpath 'fabric-loom:fabric-loom.gradle.plugin:0.11-SNAPSHOT' } else if (mcVersion >= 11400) { classpath('net.minecraftforge.gradle:ForgeGradle:5.0.5') { // the FG people still haven't learned to not do breaking changes exclude group: 'trove', module: 'trove' // preprocessor/idea requires more recent one @@ -246,6 +246,7 @@ dependencies { 11800: '1.18', 11801: '1.18.1', 11802: '1.18.2', + 11900: '1.19-pre3', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -258,8 +259,9 @@ dependencies { 11800: '1.18+build.1:v2', 11801: '1.18.1+build.1:v2', 11802: '1.18.2+build.1:v2', + 11900: '1.19-pre3+build.3:v2', ][mcVersion] - modImplementation 'net.fabricmc:fabric-loader:0.12.5' + modImplementation 'net.fabricmc:fabric-loader:0.14.6' def fabricApiVersion = [ 11404: '0.4.3+build.247-1.14', 11502: '0.5.1+build.294-1.15', @@ -271,6 +273,7 @@ dependencies { 11800: '0.43.1+1.18', 11801: '0.43.1+1.18', 11802: '0.47.9+1.18.2', + 11900: '0.53.4+1.19', ][mcVersion] def fabricApiModules = [ "api-base", @@ -351,7 +354,9 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11802) { + if (mcVersion >= 11900) { + modCompileOnly 'com.terraformersmc:modmenu:3.1.0' // FIXME update + } else if (mcVersion >= 11802) { modImplementation 'com.terraformersmc:modmenu:3.1.0' } else if (mcVersion >= 11800) { modImplementation 'com.terraformersmc:modmenu:3.0.0' diff --git a/jGui b/jGui index 5e41452b..94278dfd 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 5e41452b0e17700691efd38b8ce793b5200145c8 +Subproject commit 94278dfddbc3f325cf24c1c1f08d6a3547b30f8c diff --git a/root.gradle.kts b/root.gradle.kts index 8ee13ad4..276002e6 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -2,8 +2,8 @@ import groovy.json.JsonOutput import java.io.ByteArrayOutputStream plugins { - id("fabric-loom") version "0.10-SNAPSHOT" apply false - id("com.replaymod.preprocess") version "7746c47" + id("fabric-loom") version "0.11-SNAPSHOT" apply false + id("com.replaymod.preprocess") version "48e02ad" id("com.github.hierynomus.license") version "0.15.0" } @@ -189,6 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11900 = createNode("1.19", 11900, "yarn") val mc11802 = createNode("1.18.2", 11802, "yarn") val mc11801 = createNode("1.18.1", 11801, "yarn") val mc11701 = createNode("1.17.1", 11701, "yarn") @@ -209,6 +210,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11900.link(mc11802, file("versions/mapping-fabric-1.19-1.18.2.txt")) mc11802.link(mc11801) mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt")) mc11701.link(mc11700) diff --git a/settings.gradle.kts b/settings.gradle.kts index a77be868..b3289dad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ val jGuiVersions = listOf( "1.17.1", "1.18.1", "1.18.2", + "1.19", ) val replayModVersions = listOf( // "1.7.10", @@ -54,6 +55,7 @@ val replayModVersions = listOf( "1.17.1", "1.18.1", "1.18.2", + "1.19", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index 39e55c12..d93876b6 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -18,7 +18,6 @@ import com.replaymod.replaystudio.util.I18n; import com.replaymod.simplepathing.ReplayModSimplePathing; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.options.Option; import net.minecraft.resource.DirectoryResourcePack; import net.minecraft.text.LiteralText; import net.minecraft.text.Style; @@ -37,6 +36,11 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +//#if MC>=11900 +//#else +import net.minecraft.client.options.Option; +//#endif + public class ReplayMod implements Module, Scheduler { public static final String MOD_ID = "replaymod"; @@ -162,7 +166,8 @@ public void initClient() { keyBindingRegistry.register(); // 1.7.10 crashes when render distance > 16 - //#if MC>=10800 + // Post 1.19 this has become non-trivial to do, install Sodium+Bobby or OptiFine if you need it + //#if MC>=10800 && MC<11900 if (!MCVer.hasOptifine()) { Option.RENDER_DISTANCE.setMax(64f); } diff --git a/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java b/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java new file mode 100644 index 00000000..07b5fddd --- /dev/null +++ b/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java @@ -0,0 +1 @@ +// 1.19+ diff --git a/src/main/java/com/replaymod/core/versions/LangResourcePack.java b/src/main/java/com/replaymod/core/versions/LangResourcePack.java index 63e4304c..a3594f08 100644 --- a/src/main/java/com/replaymod/core/versions/LangResourcePack.java +++ b/src/main/java/com/replaymod/core/versions/LangResourcePack.java @@ -158,8 +158,12 @@ public Collection findResources( String namespace, //#endif String path, + //#if MC>=11900 + //$$ Predicate filter + //#else int maxDepth, Predicate filter + //#endif ) { if (resourcePackType == ResourceType.CLIENT_RESOURCES && "lang".equals(path)) { Path base = baseLangPath(); @@ -174,8 +178,13 @@ public Collection findResources( .map(LANG_FILE_NAME_PATTERN::matcher) .filter(Matcher::matches) .map(matcher -> String.format("%s_%s.json", matcher.group(1), matcher.group(1))) - .filter(filter::test) + //#if MC<11900 + .filter(filter) + //#endif .map(name -> new Identifier(ReplayMod.MOD_ID, "lang/" + name)) + //#if MC>=11900 + //$$ .filter(filter) + //#endif .collect(Collectors.toList()); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index e53ed093..f24937ca 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -34,6 +34,7 @@ import java.util.concurrent.CompletableFuture; //#if MC>=11600 +import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; //#else //$$ import net.minecraft.client.resource.language.I18n; @@ -220,7 +221,7 @@ public static void addButton( //#if MC>=11400 public static Optional findButton(Iterable buttonList, @SuppressWarnings("unused") String text, @SuppressWarnings("unused") int id) { //#if MC>=11600 - final TranslatableText message = new TranslatableText(text); + final Text message = new TranslatableText(text); //#else //$$ final String message = I18n.translate(text); //#endif diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index 884eb955..cfd7d5a9 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -6,6 +6,7 @@ import com.replaymod.core.mixin.MinecraftAccessor; import com.replaymod.gradle.remap.Pattern; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.options.GameOptions; import net.minecraft.client.options.KeyBinding; import net.minecraft.client.render.VertexFormat; import net.minecraft.client.texture.TextureManager; @@ -14,6 +15,11 @@ import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import net.minecraft.util.crash.CrashException; import net.minecraft.util.crash.CrashReport; import net.minecraft.util.crash.CrashReportSection; @@ -61,6 +67,7 @@ //$$ import net.minecraft.entity.EntityLivingBase; //#endif +import java.io.IOException; import java.util.Collection; import java.util.List; @@ -549,7 +556,9 @@ private static void Futures_addCallback(ListenableFuture future, FutureCallback @Pattern private static void setCrashReport(MinecraftClient mc, CrashReport report) { - //#if MC>=11800 + //#if MC>=11900 + //$$ mc.setCrashReportSupplier(report); + //#elseif MC>=11800 //$$ mc.setCrashReportSupplier(() -> report); //#else mc.setCrashReport(report); @@ -573,4 +582,91 @@ private static Vec3d getTrackedPosition(Entity entity) { //$$ return com.replaymod.core.versions.MCVer.getTrackedPosition(entity); //#endif } + + @Pattern + private static Text newTextLiteral(String str) { + //#if MC>=11900 + //$$ return net.minecraft.text.Text.literal(str); + //#else + return new LiteralText(str); + //#endif + } + + @Pattern + private static Text newTextTranslatable(String key, Object...args) { + //#if MC>=11900 + //$$ return net.minecraft.text.Text.translatable(key, args); + //#else + return new TranslatableText(key, args); + //#endif + } + + //#if MC>=11500 + @Pattern + private static Vec3d getTrackedPos(Entity entity) { + //#if MC>=11900 + //$$ return entity.getTrackedPosition().withDelta(0, 0, 0); + //#else + return entity.getTrackedPosition(); + //#endif + } + //#else + //$$ @Pattern private static void getTrackedPos() {} + //#endif + + @Pattern + private static void setGamma(GameOptions options, double value) { + //#if MC>=11900 + //$$ ((com.replaymod.core.mixin.SimpleOptionAccessor) (Object) options.getGamma()).setRawValue(value); + //#elseif MC>=11400 + options.gamma = value; + //#else + //$$ options.gammaSetting = (float) value; + //#endif + } + + @Pattern + private static double getGamma(GameOptions options) { + //#if MC>=11900 + //$$ return options.getGamma().getValue(); + //#else + return options.gamma; + //#endif + } + + @Pattern + private static int getViewDistance(GameOptions options) { + //#if MC>=11900 + //$$ return options.getViewDistance().getValue(); + //#else + return options.viewDistance; + //#endif + } + + @Pattern + private static double getFov(GameOptions options) { + //#if MC>=11900 + //$$ return options.getFov().getValue(); + //#else + return options.fov; + //#endif + } + + @Pattern + private static int getGuiScale(GameOptions options) { + //#if MC>=11900 + //$$ return options.getGuiScale().getValue(); + //#else + return options.guiScale; + //#endif + } + + @Pattern + private static Resource getResource(ResourceManager manager, Identifier id) throws IOException { + //#if MC>=11900 + //$$ return manager.getResourceOrThrow(id); + //#else + return manager.getResource(id); + //#endif + } } diff --git a/src/main/java/com/replaymod/extras/FullBrightness.java b/src/main/java/com/replaymod/extras/FullBrightness.java index b6bef691..0dc22f6c 100644 --- a/src/main/java/com/replaymod/extras/FullBrightness.java +++ b/src/main/java/com/replaymod/extras/FullBrightness.java @@ -72,7 +72,7 @@ private void preRender() { Type type = getType(); if (type == Type.Gamma || type == Type.Both) { originalGamma = mc.options.gamma; - mc.options.gamma = 1000; + mc.options.gamma = 1000.0; } if (type == Type.NightVision || type == Type.Both) { if (mc.player != null) { diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java index 97e0d20f..01bf1f8f 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java @@ -116,7 +116,7 @@ public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo render }.setSize(16, 16), new GuiLabel().setText( //#if MC>=11400 - p.getName().asString() + p.getName().getString() //#else //#if MC>=10800 //$$ p.getName() @@ -181,7 +181,7 @@ public int compare(PlayerEntity o1, PlayerEntity o2) { if (isSpectator(o1) && !isSpectator(o2)) return 1; if (isSpectator(o2) && !isSpectator(o1)) return -1; //#if MC>=11400 - return o1.getName().asString().compareToIgnoreCase(o2.getName().asString()); + return o1.getName().getString().compareToIgnoreCase(o2.getName().getString()); //#else //#if MC>=10800 //$$ return o1.getName().compareToIgnoreCase(o2.getName()); diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 4ccc2d9e..a64be5e0 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -42,12 +42,9 @@ //#if MC>=10904 import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; -import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; import net.minecraft.network.packet.s2c.play.WorldEventS2CPacket; import net.minecraft.entity.EquipmentSlot; import net.minecraft.util.Hand; -import net.minecraft.sound.SoundCategory; -import net.minecraft.sound.SoundEvent; //#endif //#if MC>=10800 @@ -91,7 +88,7 @@ public void unregister() { } } - //#if MC>=11400 + //#if MC>=10904 public void onPacket(Packet packet) { packetListener.save(packet); } @@ -112,16 +109,6 @@ public void spawnRecordingPlayer() { } //#if MC>=10904 - public void onClientSound(SoundEvent sound, SoundCategory category, - double x, double y, double z, float volume, float pitch) { - try { - // Send to all other players in ServerWorldEventHandler#playSoundToAllNearExcept - packetListener.save(new PlaySoundS2CPacket(sound, category, x, y, z, volume, pitch)); - } catch(Exception e) { - e.printStackTrace(); - } - } - public void onClientEffect(int type, BlockPos pos, int data) { try { // Send to all other players in ServerWorldEventHandler#playEvent diff --git a/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java b/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java index 8a581096..196f0ad0 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java @@ -33,7 +33,11 @@ public void setRequestCallback(Consumer callback) { } //#if MC>=10800 + //#if MC>=11900 + //$$ @Inject(method = "loadServerPack(Ljava/io/File;Lnet/minecraft/resource/ResourcePackSource;)Ljava/util/concurrent/CompletableFuture;", at = @At("HEAD")) + //#else @Inject(method = "loadServerPack", at = @At("HEAD")) + //#endif private void recordDownloadedPack( File file, //#if MC>=11600 diff --git a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java index 55bc21b2..d4951d9f 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java @@ -5,6 +5,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.world.ClientWorld; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; import net.minecraft.util.profiler.Profiler; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; @@ -61,12 +62,20 @@ protected MixinWorldClient(MutableWorldProperties mutableWorldProperties, Regist //#else DimensionType dimensionType, //#endif - Supplier profiler, boolean bl, boolean bl2, long l) { + Supplier profiler, boolean bl, boolean bl2, long l + //#if MC>=11900 + //$$ , int maxChainedNeighborUpdates + //#endif + ) { super(mutableWorldProperties, registryKey, //#if MC<11602 //$$ registryKey2, //#endif - dimensionType, profiler, bl, bl2, l); + dimensionType, profiler, bl, bl2, l + //#if MC>=11900 + //$$ , maxChainedNeighborUpdates + //#endif + ); } //#else //#if MC>=11400 @@ -102,7 +111,10 @@ private RecordingEventHandler replayModRecording_getRecordingEventHandler() { // but are instead played directly by the client. The server only sends these sounds to // other clients so we have to record them manually. // E.g. Block place sounds - //#if MC>=11400 + //#if MC>=11900 + //$$ @Inject(method = "playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFJ)V", + //$$ at = @At("HEAD")) + //#elseif MC>=11400 //#if FABRIC>=1 @Inject(method = "playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FF)V", at = @At("HEAD")) @@ -114,12 +126,23 @@ private RecordingEventHandler replayModRecording_getRecordingEventHandler() { //$$ @Inject(method = "playSound(Lnet/minecraft/entity/player/EntityPlayer;DDDLnet/minecraft/util/SoundEvent;Lnet/minecraft/util/SoundCategory;FF)V", //$$ at = @At("HEAD")) //#endif - public void replayModRecording_recordClientSound(PlayerEntity player, double x, double y, double z, SoundEvent sound, SoundCategory category, - float volume, float pitch, CallbackInfo ci) { + public void replayModRecording_recordClientSound( + PlayerEntity player, double x, double y, double z, SoundEvent sound, SoundCategory category, + float volume, float pitch, + //#if MC>=11900 + //$$ long seed, + //#endif + CallbackInfo ci) { if (player == this.client.player) { RecordingEventHandler handler = replayModRecording_getRecordingEventHandler(); if (handler != null) { - handler.onClientSound(sound, category, x, y, z, volume, pitch); + // Sent to all other players in ServerWorldEventHandler#playSoundToAllNearExcept + handler.onPacket(new PlaySoundS2CPacket( + sound, category, x, y, z, volume, pitch + //#if MC>=11900 + //$$ , seed + //#endif + )); } } } diff --git a/src/main/java/com/replaymod/recording/mixin/SPacketSpawnMobAccessor.java b/src/main/java/com/replaymod/recording/mixin/SPacketSpawnMobAccessor.java index 78b66335..1cc43bbb 100644 --- a/src/main/java/com/replaymod/recording/mixin/SPacketSpawnMobAccessor.java +++ b/src/main/java/com/replaymod/recording/mixin/SPacketSpawnMobAccessor.java @@ -1,16 +1,16 @@ -package com.replaymod.recording.mixin; - -import net.minecraft.entity.data.DataTracker; -import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(MobSpawnS2CPacket.class) -public interface SPacketSpawnMobAccessor { - //#if MC<11500 - //$$ @Accessor("dataTracker") - //$$ DataTracker getDataManager(); - //$$ @Accessor("dataTracker") - //$$ void setDataManager(DataTracker value); - //#endif -} +//#if MC<11500 +//$$ package com.replaymod.recording.mixin; +//$$ +//$$ import net.minecraft.entity.data.DataTracker; +//$$ import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; +//$$ import org.spongepowered.asm.mixin.Mixin; +//$$ import org.spongepowered.asm.mixin.gen.Accessor; +//$$ +//$$ @Mixin(MobSpawnS2CPacket.class) +//$$ public interface SPacketSpawnMobAccessor { +//$$ @Accessor("dataTracker") +//$$ DataTracker getDataManager(); +//$$ @Accessor("dataTracker") +//$$ void setDataManager(DataTracker value); +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/recording/mixin/SPacketSpawnPlayerAccessor.java b/src/main/java/com/replaymod/recording/mixin/SPacketSpawnPlayerAccessor.java index 73f7f667..b2eb8607 100644 --- a/src/main/java/com/replaymod/recording/mixin/SPacketSpawnPlayerAccessor.java +++ b/src/main/java/com/replaymod/recording/mixin/SPacketSpawnPlayerAccessor.java @@ -1,16 +1,16 @@ -package com.replaymod.recording.mixin; - -import net.minecraft.entity.data.DataTracker; -import net.minecraft.network.packet.s2c.play.PlayerSpawnS2CPacket; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(PlayerSpawnS2CPacket.class) -public interface SPacketSpawnPlayerAccessor { - //#if MC<11500 - //$$ @Accessor("dataTracker") - //$$ DataTracker getDataManager(); - //$$ @Accessor("dataTracker") - //$$ void setDataManager(DataTracker value); - //#endif -} +//#if MC<11500 +//$$ package com.replaymod.recording.mixin; +//$$ +//$$ import net.minecraft.entity.data.DataTracker; +//$$ import net.minecraft.network.packet.s2c.play.PlayerSpawnS2CPacket; +//$$ import org.spongepowered.asm.mixin.Mixin; +//$$ import org.spongepowered.asm.mixin.gen.Accessor; +//$$ +//$$ @Mixin(PlayerSpawnS2CPacket.class) +//$$ public interface SPacketSpawnPlayerAccessor { +//$$ @Accessor("dataTracker") +//$$ DataTracker getDataManager(); +//$$ @Accessor("dataTracker") +//$$ void setDataManager(DataTracker value); +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index f99833a5..7fdba653 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -13,8 +13,6 @@ import com.replaymod.recording.Setting; import com.replaymod.recording.gui.GuiSavingReplay; import com.replaymod.recording.handler.ConnectionEventHandler; -import com.replaymod.recording.mixin.SPacketSpawnMobAccessor; -import com.replaymod.recording.mixin.SPacketSpawnPlayerAccessor; import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.io.ReplayOutputStream; @@ -30,7 +28,6 @@ import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; import net.minecraft.network.packet.s2c.play.DisconnectS2CPacket; import net.minecraft.network.packet.s2c.play.ItemPickupAnimationS2CPacket; -import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerSpawnS2CPacket; import net.minecraft.entity.Entity; import net.minecraft.entity.data.DataTracker; @@ -44,6 +41,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +//#if MC>=11500 +//#else +//$$ import com.replaymod.recording.mixin.SPacketSpawnMobAccessor; +//$$ import com.replaymod.recording.mixin.SPacketSpawnPlayerAccessor; +//$$ import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; +//#endif + //#if MC>=11400 import net.minecraft.network.packet.s2c.login.LoginSuccessS2CPacket; //#else diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index 689f2cfd..01fd26c9 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -11,6 +11,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +//#if MC>=11900 +//$$ import java.net.MalformedURLException; +//$$ import java.net.URL; +//#endif + //#if MC>=11400 import net.minecraft.text.TranslatableText; //#else @@ -192,7 +197,18 @@ private void downloadResourcePackFuture(ClientConnection connection, int request downloadResourcePack(final int requestId, String url, String hash) { ClientBuiltinResourcePackProvider packFinder = mc.getResourcePackDownloader(); ((IDownloadingPackFinder) packFinder).setRequestCallback(file -> recordResourcePack(file, requestId)); - //#if MC>=11700 + //#if MC>=11900 + //$$ try { + //$$ URL theUrl = new URL(url); + //$$ String protocol = theUrl.getProtocol(); + //$$ if (!"http".equals(protocol) && !"https".equals(protocol)) { + //$$ throw new MalformedURLException("Unsupported protocol."); + //$$ } + //$$ return packFinder.download(theUrl, hash, true); + //$$ } catch (MalformedURLException e) { + //$$ return CompletableFuture.failedFuture(e); + //$$ } + //#elseif MC>=11700 //$$ return packFinder.download(url, hash, true); //#else return packFinder.download(url, hash); diff --git a/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java b/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java index 8b9e2f50..de61bb5f 100644 --- a/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java +++ b/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend; import com.replaymod.render.blend.data.DMaterial; diff --git a/src/main/java/com/replaymod/render/blend/BlendState.java b/src/main/java/com/replaymod/render/blend/BlendState.java index 0ef4a29b..a13c02a3 100644 --- a/src/main/java/com/replaymod/render/blend/BlendState.java +++ b/src/main/java/com/replaymod/render/blend/BlendState.java @@ -2,7 +2,7 @@ import com.replaymod.render.blend.data.DScene; import com.replaymod.render.blend.data.Serializer; -//#if MC>=10800 +//#if MC>=10800 && MC<11900 // FIXME 1.15 //#if MC<11500 //$$ import com.replaymod.render.blend.exporters.ChunkExporter; @@ -53,7 +53,7 @@ public BlendState(File file) throws IOException { this.blenderFile = BlenderFactory.newBlenderFile(file); this.factory = new BlenderFactory(blenderFile); - //#if MC>=10800 + //#if MC>=10800 && MC<11900 RenderState renderState = new RenderState(this); register(renderState); // FIXME 1.15 diff --git a/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java b/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java index 8b6834b7..d4dfda21 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.core.versions.MCVer; diff --git a/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java index b25b6556..368e55b8 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.render.blend.BlendMeshBuilder; diff --git a/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java index 2320f236..9fba791c 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.render.blend.BlendMeshBuilder; diff --git a/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java index 2aecf1a4..ec164ed6 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.core.versions.MCVer; diff --git a/src/main/java/com/replaymod/render/blend/exporters/RenderState.java b/src/main/java/com/replaymod/render/blend/exporters/RenderState.java index 9f385ce3..72b7abb9 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/RenderState.java +++ b/src/main/java/com/replaymod/render/blend/exporters/RenderState.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.render.blend.BlendState; diff --git a/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java b/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java index 0fd15a81..18b0ce01 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.exporters; import com.replaymod.core.versions.MCVer; diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java index af495f1d..683b33f1 100644 --- a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.mixin; import com.replaymod.render.blend.BlendState; diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java index cf41e5e2..48a86d7f 100644 --- a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.mixin; import com.replaymod.render.blend.BlendState; diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java index 04efa633..fa247467 100644 --- a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.mixin; import com.replaymod.render.blend.BlendState; diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java index 57764995..1dfb808c 100644 --- a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11900 package com.replaymod.render.blend.mixin; import com.replaymod.render.blend.BlendState; diff --git a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java index 456dceac..98eafe37 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java +++ b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java @@ -3,7 +3,6 @@ //#if MC>=10904 import com.replaymod.core.versions.MCVer; import com.replaymod.render.blend.BlendState; -import com.replaymod.render.blend.exporters.ParticlesExporter; import com.replaymod.render.hooks.EntityRendererHandler; import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.ParticleManager; @@ -12,6 +11,11 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; +//#if MC>=11900 +//#else +import com.replaymod.render.blend.exporters.ParticlesExporter; +//#endif + //#if MC>=11500 import net.minecraft.client.render.VertexConsumer; import net.minecraft.util.math.Quaternion; @@ -52,10 +56,12 @@ private void buildOrientedGeometry(Particle particle, VertexConsumer vertexConsu } private void buildGeometry(Particle particle, VertexConsumer vertexConsumer, Camera camera, float partialTicks) { + //#if MC<11900 BlendState blendState = BlendState.getState(); if (blendState != null) { blendState.get(ParticlesExporter.class).onRender(particle, partialTicks); } + //#endif particle.buildGeometry(vertexConsumer, camera, partialTicks); } //#else diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index d03c7362..aee4dac1 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -44,8 +44,6 @@ import net.minecraft.network.packet.s2c.play.ScreenHandlerPropertyUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.HealthUpdateS2CPacket; import net.minecraft.network.packet.s2c.login.LoginSuccessS2CPacket; -import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; -import net.minecraft.network.packet.s2c.play.PaintingSpawnS2CPacket; import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerAbilitiesS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; @@ -59,6 +57,12 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +//#if MC>=11900 +//#else +import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; +import net.minecraft.network.packet.s2c.play.PaintingSpawnS2CPacket; +//#endif + //#if MC>=11600 //#else //$$ import net.minecraft.network.packet.s2c.play.EntitySpawnGlobalS2CPacket; @@ -472,11 +476,13 @@ private void maybeRemoveDeadEntities(Packet packet) { boolean relevantPacket = packet instanceof PlayerSpawnS2CPacket || packet instanceof EntitySpawnS2CPacket + //#if MC<11900 || packet instanceof MobSpawnS2CPacket + || packet instanceof PaintingSpawnS2CPacket + //#endif //#if MC<11600 //$$ || packet instanceof EntitySpawnGlobalS2CPacket //#endif - || packet instanceof PaintingSpawnS2CPacket || packet instanceof ExperienceOrbSpawnS2CPacket || packet instanceof EntitiesDestroyS2CPacket; if (!relevantPacket) { @@ -688,6 +694,9 @@ protected Packet processPacket(Packet p) throws Exception { , packet.isDebugWorld() , packet.isFlatWorld() //#endif + //#if MC>=11900 + //$$ , java.util.Optional.empty() + //#endif ); //#else //#if MC>=10800 @@ -744,6 +753,9 @@ protected Packet processPacket(Packet p) throws Exception { //$$ respawn.getGeneratorType(), //$$ GameMode.SPECTATOR //#endif + //#if MC>=11900 + //$$ , java.util.Optional.empty() + //#endif ); //#else //#if MC>=10809 diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index d990240e..797c0ed9 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -31,10 +31,6 @@ import net.minecraft.util.Identifier; import net.minecraft.util.math.Box; -//#if MC>=11802 -//$$ import net.minecraft.tag.TagKey; -//#endif - //#if FABRIC>=1 //#else //$$ import net.minecraftforge.client.event.EntityViewRenderEvent; @@ -47,7 +43,11 @@ //#if MC>=11400 import net.minecraft.client.world.ClientWorld; import net.minecraft.fluid.Fluid; +//#if MC>=11802 +//$$ import net.minecraft.tag.TagKey; +//#else import net.minecraft.tag.Tag; +//#endif import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.HitResult; //#else @@ -547,7 +547,11 @@ public boolean isUsingItem() { //#if MC>=11400 @Override + //#if MC>=11900 + //$$ public void onEquipStack(EquipmentSlot slot, ItemStack stack, ItemStack itemStack) { + //#else protected void onEquipStack(ItemStack itemStack_1) { + //#endif // Suppress equip sounds } //#endif diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index 2bc2f79d..00bb3d9f 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -64,11 +64,11 @@ private void injectIntoIngameMenu(Screen guiScreen, Collection=11600 - final TranslatableText BUTTON_OPTIONS = new TranslatableText("menu.options"); - final TranslatableText BUTTON_EXIT_SERVER = new TranslatableText("menu.disconnect"); - final TranslatableText BUTTON_ADVANCEMENTS = new TranslatableText("gui.advancements"); - final TranslatableText BUTTON_STATS = new TranslatableText("gui.stats"); - final TranslatableText BUTTON_OPEN_TO_LAN = new TranslatableText("menu.shareToLan"); + final Text BUTTON_OPTIONS = new TranslatableText("menu.options"); + final Text BUTTON_EXIT_SERVER = new TranslatableText("menu.disconnect"); + final Text BUTTON_ADVANCEMENTS = new TranslatableText("gui.advancements"); + final Text BUTTON_STATS = new TranslatableText("gui.stats"); + final Text BUTTON_OPEN_TO_LAN = new TranslatableText("menu.shareToLan"); //#else //#if MC>=11400 //$$ final String BUTTON_OPTIONS = I18n.translate("menu.options"); diff --git a/src/main/resources/mixins.core.replaymod.json b/src/main/resources/mixins.core.replaymod.json index d1232a20..a326dc8d 100644 --- a/src/main/resources/mixins.core.replaymod.json +++ b/src/main/resources/mixins.core.replaymod.json @@ -22,6 +22,9 @@ "GuiScreenAccessor", "KeyBindingAccessor", "MinecraftAccessor", + //#if MC>=11900 + //$$ "SimpleOptionAccessor", + //#endif "TimerAccessor" ], "compatibilityLevel": "JAVA_8", diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index 09545afc..4d8edc84 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -8,8 +8,10 @@ "EntityLivingBaseAccessor", "IntegratedServerAccessor", "NetworkManagerAccessor", - "SPacketSpawnMobAccessor", - "SPacketSpawnPlayerAccessor", + //#if MC<11500 + //$$ "SPacketSpawnMobAccessor", + //$$ "SPacketSpawnPlayerAccessor", + //#endif "MixinServerInfo", //#if MC>=10800 "MixinDownloadingPackFinder", diff --git a/src/main/resources/mixins.render.blend.replaymod.json b/src/main/resources/mixins.render.blend.replaymod.json index abf980ae..156da80c 100644 --- a/src/main/resources/mixins.render.blend.replaymod.json +++ b/src/main/resources/mixins.render.blend.replaymod.json @@ -20,11 +20,13 @@ "ItemRendererAccessor", //#endif "ParticleAccessor", + //#if MC<11900 "MixinRenderGlobal", "MixinRenderItem", "MixinRenderLivingBase", "MixinRenderManager" //#endif + //#endif ], "compatibilityLevel": "JAVA_8", "minVersion": "0.6.11", diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index 52a22ceb..3bd9a84e 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -3,6 +3,8 @@ net.minecraft.potion.EffectInstance net.minecraft.potion.PotionEffect net.minecraft.client.gui.screen.AddServerScreen net.minecraft.client.gui.GuiScreenAddServer net.minecraft.resources.AbstractResourcePack net.minecraft.client.resources.AbstractResourcePack net.minecraft.resources.FolderPack net.minecraft.client.resources.FolderResourcePack +net.minecraft.resources.IResource net.minecraft.client.resources.IResource +net.minecraft.resources.IResourceManager net.minecraft.client.resources.IResourceManager net.minecraft.client.resources.DownloadingPackFinder net.minecraft.client.resources.ResourcePackRepository net.minecraft.client.renderer.chunk.ChunkRenderTask net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator net.minecraft.client.settings.AbstractOption net.minecraft.client.settings.GameSettings.Options diff --git a/versions/1.19/.gitkeep b/versions/1.19/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/1.19/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java b/versions/1.19/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java new file mode 100644 index 00000000..e31c2de9 --- /dev/null +++ b/versions/1.19/src/main/java/com/replaymod/core/mixin/SimpleOptionAccessor.java @@ -0,0 +1,11 @@ +package com.replaymod.core.mixin; + +import net.minecraft.client.option.SimpleOption; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SimpleOption.class) +public interface SimpleOptionAccessor { + @Accessor("value") + void setRawValue(T value); +} diff --git a/versions/mapping-fabric-1.19-1.18.2.txt b/versions/mapping-fabric-1.19-1.18.2.txt new file mode 100644 index 00000000..7ac37640 --- /dev/null +++ b/versions/mapping-fabric-1.19-1.18.2.txt @@ -0,0 +1,4 @@ +# FIXME remap should be able to map these without us explicitly declaring them +net.minecraft.client.render.WorldRenderer fullUpdateFuture field_34808 +net.minecraft.client.render.WorldRenderer updateFinished field_34809 +net.minecraft.client.render.WorldRenderer shouldUpdate field_34810 From df11ba2e365aa98aeeffa8b5b8d12b8f9e57d075 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 12 Jun 2022 13:43:39 +0200 Subject: [PATCH 091/132] Port to MC 1.19 --- build.gradle | 11 +++++++---- jGui | 2 +- .../com/replaymod/editor/gui/MarkerProcessor.java | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 3a3664ec..eef8dfdd 100644 --- a/build.gradle +++ b/build.gradle @@ -246,7 +246,7 @@ dependencies { 11800: '1.18', 11801: '1.18.1', 11802: '1.18.2', - 11900: '1.19-pre3', + 11900: '1.19', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -259,7 +259,7 @@ dependencies { 11800: '1.18+build.1:v2', 11801: '1.18.1+build.1:v2', 11802: '1.18.2+build.1:v2', - 11900: '1.19-pre3+build.3:v2', + 11900: '1.19+build.2:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.14.6' def fabricApiVersion = [ @@ -273,7 +273,7 @@ dependencies { 11800: '0.43.1+1.18', 11801: '0.43.1+1.18', 11802: '0.47.9+1.18.2', - 11900: '0.53.4+1.19', + 11900: '0.55.3+1.19', ][mcVersion] def fabricApiModules = [ "api-base", @@ -346,7 +346,10 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:6fc8e20", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:39debfe", shadeExclusions + + // FIXME this should be pulled in by ReplayStudio, and IntelliJ sees it, but javac for some reason does not + implementation 'com.github.viaversion:opennbt:0a02214' // 2.0-SNAPSHOT (ViaVersion Edition) implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those diff --git a/jGui b/jGui index 94278dfd..c1e43fc9 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 94278dfddbc3f325cf24c1c1f08d6a3547b30f8c +Subproject commit c1e43fc9f0550bf6a6ab8685c1008e754e042772 diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index 7231cecf..54940eb3 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -124,7 +124,7 @@ public static List> apply(Path path, Consumer PacketTypeRegistry registry = MCVer.getPacketTypeRegistry(true); DimensionTracker dimensionTracker = new DimensionTracker(); - SquashFilter squashFilter = new SquashFilter(null, null); + SquashFilter squashFilter = new SquashFilter(null, null, null); List> outputPaths = new ArrayList<>(); From 13ac8dcf0ae53e520457782d20460e98550fdc9d Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 15 Jun 2022 13:04:33 +0200 Subject: [PATCH 092/132] Record raw packets instead of re-encoding decoded packets Until now, recording worked by injecting right before the vanilla packet handler. At that point it was receiving packet classes and had to re-encode them to store them in the replay file. However, not all decoded packets can necessarily be re-encoded without errors, requiring us to have a bunch of workarounds. This commit changes the way recording works by injecting right before the decoder. It therefore receives the raw bytebufs and no longer has to deal with the re-encode issue. --- .../handler/ConnectionEventHandler.java | 10 +- .../handler/RecordingEventHandler.java | 93 ----- .../mixin/MixinClientConnection.java | 39 +++ .../recording/packet/PacketListener.java | 331 ++++++++---------- .../resources/mixins.recording.replaymod.json | 1 + 5 files changed, 195 insertions(+), 279 deletions(-) create mode 100644 src/main/java/com/replaymod/recording/mixin/MixinClientConnection.java diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 0211c8e0..876430fc 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -3,7 +3,6 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.Utils; -import com.replaymod.core.versions.MCVer; import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.ServerInfoExt; import com.replaymod.recording.Setting; @@ -43,7 +42,6 @@ */ public class ConnectionEventHandler { - private static final String packetHandlerKey = "packet_handler"; private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss"; private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); private static final MinecraftClient mc = getMinecraft(); @@ -141,7 +139,13 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { metaData.setMcVersion(ReplayMod.instance.getMinecraftVersion()); packetListener = new PacketListener(core, outputPath, replayFile, metaData); Channel channel = ((NetworkManagerAccessor) networkManager).getChannel(); - channel.pipeline().addBefore(packetHandlerKey, "replay_recorder", packetListener); + if (channel.pipeline().get(PacketListener.DECODER_KEY) != null) { + // Regular channel, we'll inject our recorder directly before the decoder + channel.pipeline().addBefore(PacketListener.DECODER_KEY, PacketListener.RAW_RECORDER_KEY, packetListener); + } else { + // Integrated server passes packets directly, there's no splitting, decompression or decoding + channel.pipeline().addFirst(PacketListener.RAW_RECORDER_KEY, packetListener); + } recordingEventHandler = new RecordingEventHandler(packetListener); recordingEventHandler.register(); diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index a64be5e0..ac303479 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -321,99 +321,6 @@ private void onPlayerTick() { } } - //#if FABRIC>=1 - // FIXME fabric - //#else - //$$ @SubscribeEvent - //$$ public void onPickupItem(ItemPickupEvent event) { - //$$ try { - //#if MC>=11100 - //#if MC>=11200 - //#if MC>=11400 - //$$ ItemStack stack = event.getStack(); - //$$ packetListener.save(new SCollectItemPacket( - //$$ event.getOriginalEntity().getEntityId(), - //$$ event.getPlayer().getEntityId(), - //$$ event.getStack().getCount() - //$$ )); - //#else - //$$ packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(), - //$$ event.pickedUp.getItem().getMaxStackSize())); - //#endif - //#else - //$$ packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(), - //$$ event.pickedUp.getEntityItem().getMaxStackSize())); - //#endif - //#else - //$$ packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId())); - //#endif - //$$ } catch(Exception e) { - //$$ e.printStackTrace(); - //$$ } - //$$ } - //#endif - - //#if MC>=11400 - // FIXME fabric - //#else - //$$ @SubscribeEvent - //$$ public void onSleep(PlayerSleepInBedEvent event) { - //$$ try { - //#if MC>=10904 - //$$ if (event.getEntityPlayer() != mc.player) { - //$$ return; - //$$ } - //$$ - //$$ packetListener.save(new SPacketUseBed(event.getEntityPlayer(), event.getPos())); - //#else - //$$ if (event.entityPlayer != mc.thePlayer) { - //$$ return; - //$$ } - //$$ - //$$ packetListener.save(new S0APacketUseBed(event.entityPlayer, - //#if MC>=10800 - //$$ event.pos - //#else - //$$ event.x, event.y, event.z - //#endif - //$$ )); - //#endif - //$$ - //$$ wasSleeping = true; - //$$ - //$$ } catch(Exception e) { - //$$ e.printStackTrace(); - //$$ } - //$$ } - //#endif - - /* FIXME event not (yet?) on 1.13 - @SubscribeEvent - public void enterMinecart(MinecartInteractEvent event) { - try { - //#if MC>=10904 - if(event.getEntity() != mc.player) { - return; - } - - packetListener.save(new SPacketEntityAttach(event.getPlayer(), event.getMinecart())); - - lastRiding = event.getMinecart().getEntityId(); - //#else - //$$ if(event.entity != mc.thePlayer) { - //$$ return; - //$$ } - //$$ - //$$ packetListener.save(new S1BPacketEntityAttach(0, event.player, event.minecart)); - //$$ - //$$ lastRiding = event.minecart.getEntityId(); - //#endif - } catch(Exception e) { - e.printStackTrace(); - } - } - */ - //#if MC>=10800 public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) { //#else diff --git a/src/main/java/com/replaymod/recording/mixin/MixinClientConnection.java b/src/main/java/com/replaymod/recording/mixin/MixinClientConnection.java new file mode 100644 index 00000000..60d1314c --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinClientConnection.java @@ -0,0 +1,39 @@ +package com.replaymod.recording.mixin; + +import com.replaymod.recording.packet.PacketListener; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import net.minecraft.network.ClientConnection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(ClientConnection.class) +public abstract class MixinClientConnection { + @Shadow + private Channel channel; + + @Inject(method = "setCompressionThreshold", at = @At("RETURN")) + private void ensureReplayModRecorderIsAfterDecompress(CallbackInfo ci) { + ChannelHandler recorder = null; + for (Map.Entry entry : channel.pipeline()) { + String key = entry.getKey(); + if (PacketListener.RAW_RECORDER_KEY.equals(key)) { + recorder = entry.getValue(); + } + if (PacketListener.DECOMPRESS_KEY.equals(key)) { + if (recorder != null) { + // If we've already found the recorder, then that means decompress is after recorder. That's no good + // because it means the recorder is getting compressed packets, we need to move the recorder. + channel.pipeline().remove(recorder); + channel.pipeline().addBefore(PacketListener.DECODER_KEY, PacketListener.RAW_RECORDER_KEY, recorder); + return; + } + } + } + } +} diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index 7fdba653..677673ad 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -2,7 +2,6 @@ import com.github.steveice10.netty.buffer.PooledByteBufAllocator; import com.github.steveice10.packetlib.tcp.io.ByteBufNetOutput; -import com.google.common.collect.Lists; import com.google.gson.Gson; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Restrictions; @@ -16,23 +15,24 @@ import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.io.ReplayOutputStream; +import com.replaymod.replaystudio.lib.viaversion.api.protocol.packet.State; +import com.replaymod.replaystudio.protocol.Packet; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; import de.johni0702.minecraft.gui.container.VanillaGuiScreen; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.AttributeKey; import net.minecraft.client.MinecraftClient; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; import net.minecraft.network.packet.s2c.play.DisconnectS2CPacket; -import net.minecraft.network.packet.s2c.play.ItemPickupAnimationS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerSpawnS2CPacket; import net.minecraft.entity.Entity; -import net.minecraft.entity.data.DataTracker; import net.minecraft.network.NetworkState; -import net.minecraft.network.Packet; import net.minecraft.network.PacketByteBuf; import net.minecraft.text.LiteralText; import net.minecraft.util.crash.CrashReport; @@ -41,24 +41,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -//#if MC>=11500 -//#else -//$$ import com.replaymod.recording.mixin.SPacketSpawnMobAccessor; -//$$ import com.replaymod.recording.mixin.SPacketSpawnPlayerAccessor; -//$$ import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; -//#endif - -//#if MC>=11400 -import net.minecraft.network.packet.s2c.login.LoginSuccessS2CPacket; -//#else -//$$ import net.minecraftforge.fml.common.network.internal.FMLProxyPacket; -//#endif - -//#if MC>=10904 -//#else -//$$ import java.util.List; -//#endif - //#if MC>=10800 //#if MC<10904 //$$ import net.minecraft.network.play.server.S46PacketSetCompressionLevel; @@ -68,7 +50,6 @@ import net.minecraft.network.NetworkSide; //#endif -import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; @@ -86,12 +67,31 @@ import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.replaystudio.util.Utils.writeInt; +import static java.util.Objects.requireNonNull; +@ChannelHandler.Sharable // so we can re-order it public class PacketListener extends ChannelInboundHandlerAdapter { + public static final String RAW_RECORDER_KEY = "replay_recorder_raw"; + public static final String DECODED_RECORDER_KEY = "replay_recorder_decoded"; + + public static final String DECOMPRESS_KEY = "decompress"; + public static final String DECODER_KEY = "decoder"; + private static final MinecraftClient mc = getMinecraft(); private static final Logger logger = LogManager.getLogger(); + //#if MC>=11700 + //$$ private static final int PACKET_ID_RESOURCE_PACK_SEND = getPacketId(NetworkState.PLAY, new ResourcePackSendS2CPacket("", "", false, null)); + //$$ private static final int PACKET_ID_LOGIN_COMPRESSION = getPacketId(NetworkState.LOGIN, new LoginCompressionS2CPacket(0)); + //#else + private static final int PACKET_ID_RESOURCE_PACK_SEND = getPacketId(NetworkState.PLAY, new ResourcePackSendS2CPacket()); + private static final int PACKET_ID_LOGIN_COMPRESSION = getPacketId(NetworkState.LOGIN, new LoginCompressionS2CPacket()); + //#endif + //#if MC<10904 + //$$ private static final int PACKET_ID_PLAY_COMPRESSION = getPacketId(EnumConnectionState.PLAY, new S46PacketSetCompressionLevel()); + //#endif + private final ReplayMod core; private final Path outputPath; private final ReplayFile replayFile; @@ -109,13 +109,6 @@ public class PacketListener extends ChannelInboundHandlerAdapter { private long lastSentPacket; private long timePassedWhilePaused; private volatile boolean serverWasPaused; - //#if MC>=11400 - private NetworkState connectionState = NetworkState.LOGIN; - private boolean loginPhase = true; - //#else - //$$ private EnumConnectionState connectionState = EnumConnectionState.PLAY; - //$$ private boolean loginPhase = false; - //#endif /** * Used to keep track of the last metadata save job submitted to the save service and @@ -163,6 +156,17 @@ private void saveMetaData() { }); } + public void save(net.minecraft.network.Packet packet) { + Packet encoded; + try { + encoded = encodeMcPacket(getConnectionState(), packet); + } catch (Exception e) { + logger.error("Encoding packet:", e); + return; + } + save(encoded); + } + public void save(Packet packet) { // If we're not on the main thread (i.e. we're on the netty thread), then we need to schedule the saving // to happen on the main thread so we can guarantee correct ordering of inbound and inject packets. @@ -173,24 +177,12 @@ public void save(Packet packet) { return; } try { - if(packet instanceof PlayerSpawnS2CPacket) { - //#if MC>=10800 - UUID uuid = ((PlayerSpawnS2CPacket) packet).getPlayerUuid(); - //#else - //$$ UUID uuid = ((S0CPacketSpawnPlayer) packet).func_148948_e().getId(); - //#endif - Set uuids = new HashSet<>(Arrays.asList(metaData.getPlayers())); - uuids.add(uuid.toString()); - metaData.setPlayers(uuids.toArray(new String[uuids.size()])); - saveMetaData(); - } - - //#if MC>=10800 - if (packet instanceof LoginCompressionS2CPacket) { + //#if MC>=11800 + if (packet.getRegistry().getState() == State.LOGIN && packet.getId() == PACKET_ID_LOGIN_COMPRESSION) { return; // Replay data is never compressed on the packet level } //#if MC<10904 - //$$ if (packet instanceof S46PacketSetCompressionLevel) { + //$$ if (packet.getRegistry().getState() == State.PLAY && packet.getId() == PACKET_ID_PLAY_COMPRESSION) { //$$ return; // Replay data is never compressed on the packet level //$$ } //#endif @@ -203,7 +195,7 @@ public void save(Packet packet) { } int timestamp = (int) (now - startTime - timePassedWhilePaused); lastSentPacket = timestamp; - PacketData packetData = getPacketData(timestamp, packet); + PacketData packetData = new PacketData(timestamp, packet); saveService.submit(() -> { try { if (ReplayMod.isMinimalMode()) { @@ -230,18 +222,27 @@ public void save(Packet packet) { throw new RuntimeException(e); } }); - - //#if MC>=11400 - if (packet instanceof LoginSuccessS2CPacket) { - connectionState = NetworkState.PLAY; - loginPhase = false; - } - //#endif } catch(Exception e) { logger.error("Writing packet:", e); } } + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + + if (ctx.pipeline().get(DECODED_RECORDER_KEY) == null) { + if (ctx.pipeline().get(PacketListener.DECODER_KEY) != null) { + // Regular channel, we'll inject our decoded recorder directly after the decoder + ctx.pipeline().addAfter(DECODER_KEY, DECODED_RECORDER_KEY, new DecodedPacketListener()); + } else { + // Integrated server passes packets directly, there's no splitting, decompression or decoding + // The decoded packet handler can just go directly behind this hand + ctx.pipeline().addAfter(RAW_RECORDER_KEY, DECODED_RECORDER_KEY, new DecodedPacketListener()); + } + } + } + @Override public void channelInactive(ChannelHandlerContext ctx) { metaData.setDuration((int) lastSentPacket); @@ -328,139 +329,44 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } this.context = ctx; - if (msg instanceof Packet) { - try { - Packet packet = (Packet) msg; - - //#if MC>=10904 - if(packet instanceof ItemPickupAnimationS2CPacket) { - if(mc.player != null || - ((ItemPickupAnimationS2CPacket) packet).getEntityId() == mc.player.getEntityId()) { - //#else - //$$ if(packet instanceof S0DPacketCollectItem) { - //$$ if(mc.thePlayer != null || ((S0DPacketCollectItem) packet).getEntityID() == mc.thePlayer.getEntityId()) { - //#endif - super.channelRead(ctx, msg); - return; - } - } + NetworkState connectionState = getConnectionState(); - //#if MC>=10800 - if (packet instanceof ResourcePackSendS2CPacket) { - ClientConnection connection = ctx.pipeline().get(ClientConnection.class); - save(resourcePackRecorder.handleResourcePack(connection, (ResourcePackSendS2CPacket) packet)); - return; - } - //#else - //$$ if (packet instanceof S3FPacketCustomPayload) { - //$$ S3FPacketCustomPayload p = (S3FPacketCustomPayload) packet; - //$$ if ("MC|RPack".equals(p.func_149169_c())) { - //$$ save(resourcePackRecorder.handleResourcePack(p)); - //$$ return; - //$$ } - //$$ } - //#endif - - //#if MC<11400 - //$$ if (packet instanceof FMLProxyPacket) { - //$$ // This packet requires special handling - //#if MC>=10800 - //$$ ((FMLProxyPacket) packet).toS3FPackets().forEach(this::save); - //#else - //$$ save(((FMLProxyPacket) packet).toS3FPacket()); - //#endif - //$$ super.channelRead(ctx, msg); - //$$ return; - //$$ } - //#endif - - //#if MC>=10800 - if (packet instanceof CustomPayloadS2CPacket) { - // Forge may read from this ByteBuf and/or release it during handling - // We want to save the full thing however, so we create a copy and save that one instead of the - // original one - // Note: This isn't an issue with vanilla MC because our saving code runs on the main thread - // shortly before the vanilla handling code does. Forge however does some stuff on the netty - // threads which leads to this race condition - packet = new CustomPayloadS2CPacket( - ((CustomPayloadS2CPacket) packet).getChannel(), - new PacketByteBuf(((CustomPayloadS2CPacket) packet).getData().slice().retain()) - ); - } - //#endif - - save(packet); + Packet packet = null; + if (msg instanceof ByteBuf) { + // for regular connections, we're expecting to observe `ByteBuf`s here + ByteBuf buf = (ByteBuf) msg; + if (buf.readableBytes() > 0) { + packet = decodePacket(connectionState, buf); + } + } else if (msg instanceof net.minecraft.network.Packet) { + // for integrated server connections MC is passing the packet objects directly, so we need to encode them + // ourselves to be able to store them + packet = encodeMcPacket(connectionState, (net.minecraft.network.Packet) msg); + } - if (packet instanceof CustomPayloadS2CPacket) { - CustomPayloadS2CPacket p = (CustomPayloadS2CPacket) packet; - if (Restrictions.PLUGIN_CHANNEL.equals(p.getChannel())) { - packet = new DisconnectS2CPacket(new LiteralText("Please update to view this replay.")); - save(packet); - } - } - } catch(Exception e) { - logger.error("Handling packet for recording:", e); + if (packet != null) { + if (connectionState == NetworkState.PLAY && packet.getId() == PACKET_ID_RESOURCE_PACK_SEND) { + ClientConnection connection = ctx.pipeline().get(ClientConnection.class); + save(resourcePackRecorder.handleResourcePack(connection, (ResourcePackSendS2CPacket) decodeMcPacket(packet))); + return; } + save(packet); } super.channelRead(ctx, msg); } - //#if MC>=10904 - private void DataManager_set(DataTracker dataManager, DataTracker.Entry entry) { - dataManager.startTracking(entry.getData(), entry.get()); + private NetworkState getConnectionState() { + ChannelHandlerContext ctx = context; + if (ctx == null) { + return NetworkState.LOGIN; + } + AttributeKey key = ClientConnection.ATTR_KEY_PROTOCOL; + return ctx.channel().attr(key).get(); } - //#endif - - @SuppressWarnings("unchecked") - private PacketData getPacketData(int timestamp, Packet packet) throws Exception { - //#if MC<11500 - //$$ if (packet instanceof MobSpawnS2CPacket) { - //$$ MobSpawnS2CPacket p = (MobSpawnS2CPacket) packet; - //$$ SPacketSpawnMobAccessor pa = (SPacketSpawnMobAccessor) p; - //$$ if (pa.getDataManager() == null) { - //$$ pa.setDataManager(new DataTracker(null)); - //$$ if (p.getTrackedValues() != null) { - //$$ Set seen = new HashSet<>(); - //#if MC>=10904 - //$$ for (DataTracker.Entry entry : Lists.reverse(p.getTrackedValues())) { - //$$ if (!seen.add(entry.getData().getId())) continue; - //$$ DataManager_set(pa.getDataManager(), entry); - //$$ } - //#else - //$$ for(DataWatcher.WatchableObject wo : Lists.reverse((List) p.func_149027_c())) { - //$$ if (!seen.add(wo.getDataValueId())) continue; - //$$ pa.getDataManager().addObject(wo.getDataValueId(), wo.getObject()); - //$$ } - //#endif - //$$ } - //$$ } - //$$ } - //$$ - //$$ if (packet instanceof PlayerSpawnS2CPacket) { - //$$ PlayerSpawnS2CPacket p = (PlayerSpawnS2CPacket) packet; - //$$ SPacketSpawnPlayerAccessor pa = (SPacketSpawnPlayerAccessor) p; - //$$ if (pa.getDataManager() == null) { - //$$ pa.setDataManager(new DataTracker(null)); - //$$ if (p.getTrackedValues() != null) { - //$$ Set seen = new HashSet<>(); - //#if MC>=10904 - //$$ for (DataTracker.Entry entry : Lists.reverse(p.getTrackedValues())) { - //$$ if (!seen.add(entry.getData().getId())) continue; - //$$ DataManager_set(pa.getDataManager(), entry); - //$$ } - //#else - //$$ for(DataWatcher.WatchableObject wo : Lists.reverse((List) p.func_148944_c())) { - //$$ if (!seen.add(wo.getDataValueId())) continue; - //$$ pa.getDataManager().addObject(wo.getDataValueId(), wo.getObject()); - //$$ } - //#endif - //$$ } - //$$ } - //$$ } - //#endif + private static Packet encodeMcPacket(NetworkState connectionState, net.minecraft.network.Packet packet) throws Exception { //#if MC>=10800 Integer packetId = connectionState.getPacketId(NetworkSide.CLIENTBOUND, packet); //#else @@ -472,23 +378,55 @@ private PacketData getPacketData(int timestamp, Packet packet) throws Exception ByteBuf byteBuf = Unpooled.buffer(); try { packet.write(new PacketByteBuf(byteBuf)); - return new PacketData(timestamp, new com.replaymod.replaystudio.protocol.Packet( - MCVer.getPacketTypeRegistry(loginPhase), + return new Packet( + MCVer.getPacketTypeRegistry(connectionState == NetworkState.LOGIN), packetId, com.github.steveice10.netty.buffer.Unpooled.wrappedBuffer( byteBuf.array(), byteBuf.arrayOffset(), byteBuf.readableBytes() ) - )); + ); } finally { byteBuf.release(); + } + } - //#if MC>=10800 - if (packet instanceof CustomPayloadS2CPacket) { - ((CustomPayloadS2CPacket) packet).getData().release(); - } - //#endif + private static net.minecraft.network.Packet decodeMcPacket(Packet packet) throws IOException, IllegalAccessException, InstantiationException { + NetworkState connectionState = packet.getRegistry().getState() == State.LOGIN ? NetworkState.LOGIN : NetworkState.PLAY; + int packetId = packet.getId(); + PacketByteBuf packetBuf = new PacketByteBuf(Unpooled.wrappedBuffer(packet.getBuf().nioBuffer())); + + //#if MC>=11700 + //$$ return connectionState.getPacketHandler(NetworkSide.CLIENTBOUND, packetId, packetBuf); + //#else + //#if MC>=10800 + net.minecraft.network.Packet p = connectionState.getPacketHandler(NetworkSide.CLIENTBOUND, packetId); + //#else + //$$ net.minecraft.network.Packet p = net.minecraft.network.Packet.generatePacket(connectionState.func_150755_b(), packetId); + //#endif + p.read(packetBuf); + return p; + //#endif + } + + private static Packet decodePacket(NetworkState connectionState, ByteBuf buf) { + PacketByteBuf packetBuf = new PacketByteBuf(buf.slice()); + int packetId = packetBuf.readVarInt(); + byte[] bytes = new byte[packetBuf.readableBytes()]; + packetBuf.readBytes(bytes); + return new Packet( + MCVer.getPacketTypeRegistry(connectionState == NetworkState.LOGIN), + packetId, + com.github.steveice10.netty.buffer.Unpooled.wrappedBuffer(bytes) + ); + } + + private static int getPacketId(NetworkState networkState, net.minecraft.network.Packet packet) { + try { + return requireNonNull(networkState.getPacketId(NetworkSide.CLIENTBOUND, packet)); + } catch (Exception e) { + throw new RuntimeException("Failed to determine packet id for " + packet.getClass(), e); } } @@ -530,4 +468,31 @@ public long getCurrentDuration() { public void setServerWasPaused() { this.serverWasPaused = true; } + + private class DecodedPacketListener extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + + if (msg instanceof CustomPayloadS2CPacket) { + CustomPayloadS2CPacket packet = (CustomPayloadS2CPacket) msg; + if (Restrictions.PLUGIN_CHANNEL.equals(packet.getChannel())) { + save(new DisconnectS2CPacket(new LiteralText("Please update to view this replay."))); + } + } + + if (msg instanceof PlayerSpawnS2CPacket) { + //#if MC>=10800 + UUID uuid = ((PlayerSpawnS2CPacket) msg).getPlayerUuid(); + //#else + //$$ UUID uuid = ((S0CPacketSpawnPlayer) msg).func_148948_e().getId(); + //#endif + Set uuids = new HashSet<>(Arrays.asList(metaData.getPlayers())); + uuids.add(uuid.toString()); + metaData.setPlayers(uuids.toArray(new String[uuids.size()])); + saveMetaData(); + } + + super.channelRead(ctx, msg); + } + } } diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index 4d8edc84..5dec2e07 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -8,6 +8,7 @@ "EntityLivingBaseAccessor", "IntegratedServerAccessor", "NetworkManagerAccessor", + "MixinClientConnection", //#if MC<11500 //$$ "SPacketSpawnMobAccessor", //$$ "SPacketSpawnPlayerAccessor", From 814d0b7c17e4feff1a1f2d2f704eef52bcee3b30 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 15 Jun 2022 13:46:30 +0200 Subject: [PATCH 093/132] Update ReplayStudio Fixes fatal QuickMode error introduced with the 1.19 update. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eef8dfdd..573347ee 100644 --- a/build.gradle +++ b/build.gradle @@ -346,7 +346,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:39debfe", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:74d8465", shadeExclusions // FIXME this should be pulled in by ReplayStudio, and IntelliJ sees it, but javac for some reason does not implementation 'com.github.viaversion:opennbt:0a02214' // 2.0-SNAPSHOT (ViaVersion Edition) From 40f07279cffe90d73351c8ba0333836eef425ed4 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 11:57:24 +0200 Subject: [PATCH 094/132] Load tinyexr in its own class loader That way we can simply use whatever version we want and aren't tied to the version of lwjgl which MC provides. This fixes the OpenEXR export on 1.19 crashing either due to the windows workaround or due to differing lwjgl versions (depending on OS). And also enables OpenEXR export on MC versions using lwjgl2 (1.12.2 and below). --- build.gradle | 24 ++-- .../java/com/replaymod/render/EXRWriter.java | 11 +- .../com/replaymod/render/RenderSettings.java | 7 - ...in_WindowsWorkaroundForTinyEXRNatives.java | 81 ----------- .../render/rendering/VideoRenderer.java | 8 +- .../replaymod/render/utils/Lwjgl3Loader.java | 132 ++++++++++++++++++ .../resources/mixins.render.replaymod.json | 1 - 7 files changed, 156 insertions(+), 108 deletions(-) delete mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java create mode 100644 src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java diff --git a/build.gradle b/build.gradle index 573347ee..40ad0f52 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import com.replaymod.gradle.preprocess.PreprocessTask +import static gg.essential.gradle.util.PrebundleKt.prebundle buildscript { def mcVersion @@ -18,17 +19,17 @@ buildscript { name = "fabric" url = "https://maven.fabricmc.net/" } - if (!fabric) { - maven { - name = "forge" - url = "https://maven.minecraftforge.net" - } + maven { + name = "forge" + url = "https://maven.minecraftforge.net" } maven { name = "sonatype" url = "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url 'https://jitpack.io' } + maven { url "https://maven.architectury.dev" } + maven { url "https://repo.essential.gg/repository/maven-public" } } dependencies { @@ -48,6 +49,7 @@ buildscript { } else { classpath 'com.github.ReplayMod:ForgeGradle:a8a9e0ca:all' // FG 1.2 } + classpath 'gg.essential:essential-gradle-toolkit:0.1.10' } } @@ -323,13 +325,13 @@ dependencies { shadow 'com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions shadow 'com.google.oauth-client:google-oauth-client-jetty:1.20.0' - if (mcVersion >= 11400) { // need lwjgl 3 - for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos']) { - shadow('org.lwjgl:lwjgl-tinyexr:3.2.2' + suffix) { - exclude group: 'org.lwjgl', module: 'lwjgl' // comes with MC - } - } + def lwjgl = configurations.create("lwjgl") + for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos', ':natives-macos-arm64']) { + add(lwjgl.name, 'org.lwjgl:lwjgl:3.3.1' + suffix) + add(lwjgl.name, 'org.lwjgl:lwjgl-tinyexr:3.3.1' + suffix) } + compileOnly('org.lwjgl:lwjgl-tinyexr:3.3.1') + shadow(prebundle(project, lwjgl, "com/replaymod/render/utils/lwjgl.jar", {})) if (mcVersion < 11200) { // The version which MC ships is too old, we'll need to ship our own diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index 0bf981d8..17124ccc 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -1,4 +1,3 @@ -//#if MC>=11400 package com.replaymod.render; import com.replaymod.core.versions.MCVer; @@ -6,6 +5,7 @@ import com.replaymod.render.rendering.Channel; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.render.utils.ByteBufferPool; +import com.replaymod.render.utils.Lwjgl3Loader; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.util.crash.CrashReport; import org.lwjgl.PointerBuffer; @@ -27,6 +27,14 @@ public class EXRWriter implements FrameConsumer { + public static FrameConsumer create(Path outputFolder, boolean keepAlpha) { + return Lwjgl3Loader.createFrameConsumer( + EXRWriter.class, + new Class[]{ Path.class, boolean.class }, + new Object[]{ outputFolder, keepAlpha } + ); + } + private final Path outputFolder; private final boolean keepAlpha; @@ -128,4 +136,3 @@ public void consume(Map channels) { public void close() { } } -//#endif diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index b358b718..f5e92ffb 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -105,13 +105,6 @@ public String toString() { public boolean isSupported() { if (this == BLEND) { return RenderMethod.BLEND.isSupported(); - } else if (this == EXR) { - // Need LJWGL 3 - //#if MC>=11400 - return true; - //#else - //$$ return false; - //#endif } else { return true; } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java b/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java deleted file mode 100644 index 3df81c08..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java +++ /dev/null @@ -1,81 +0,0 @@ -//#if MC>=11400 -package com.replaymod.render.mixin; - -import org.lwjgl.system.Library; -import org.lwjgl.system.Platform; -import org.lwjgl.util.tinyexr.TinyEXR; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArg; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.function.Consumer; -import java.util.regex.Pattern; - -/** - * It appears like natives on Windows cannot be loaded if one of their dependencies has already been loaded by a - * different class loader. In our case we cannot load tinyexr (on the knot class loader) because lwjgl has already - * been loaded on the system class loader. - * - * If we force the tinyexr native to load on the system class loader (by calling `Library.loadSystem(absPath)`), - * it'll load but we'll get an error when we call any of the native methods. - * - * We can't really load TinyEXR itself via the system class loader because Java does not provide any methods for - * modifying the system class path at runtime and we'd have to use JVM-specific hacks. - * - * Strangely, if we use System.loadLibrary instead of System.load, then it all just works. This mixin implements - * that workaround by finding MC's natives folder, extracting the dll from our jar into that folder and then replacing - * the context class passed to Library.loadSystem (which it uses to find dlls in jars) with Library (which is on the - * system class loader) so it cannot find the dll in our jar and falls back to using System.loadLibrary. - */ -@Mixin(value = TinyEXR.class, remap = false) -public class Mixin_WindowsWorkaroundForTinyEXRNatives { - private static final String LOAD_SYSTEM_CONSUMERS = "Lorg/lwjgl/system/Library;loadSystem(Ljava/util/function/Consumer;Ljava/util/function/Consumer;Ljava/lang/Class;Ljava/lang/String;)V"; - - @ModifyArg(method = "", at = @At(value = "INVOKE", target = LOAD_SYSTEM_CONSUMERS)) - private static Class uglyWindowsHacks(Consumer load, Consumer loadLibrary, Class context, String name) throws IOException { - if (Platform.get() != Platform.WINDOWS) { - return context; // works out of the box on linux - } - - name = System.mapLibraryName(name); - - URL libURL = context.getClassLoader().getResource(name); - if (libURL == null) { - throw new UnsatisfiedLinkError("Failed to locate library: " + name); - } - - String lwjglLibName = Library.JNI_LIBRARY_NAME; - if (!lwjglLibName.endsWith(".dll")) { - lwjglLibName = System.mapLibraryName(lwjglLibName); - } - - String paths = System.getProperty("java.library.path"); - Path nativesDir = null; - for (String dir : Pattern.compile(File.pathSeparator).split(paths)) { - Path path = Paths.get(dir); - if (Files.isReadable(path.resolve(lwjglLibName))) { - nativesDir = path; - break; - } - } - if (nativesDir == null) { - throw new UnsatisfiedLinkError("Failed to locate natives folder in " + paths); - } - - Path libPath = nativesDir.resolve(name); - try (InputStream source = libURL.openStream()) { - Files.copy(source, libPath, StandardCopyOption.REPLACE_EXISTING); - } - - return Library.class; - } -} -//#endif diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index a6b31504..ff8048d2 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -8,6 +8,7 @@ import com.replaymod.pathing.player.AbstractTimelinePlayer; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.CameraPathExporter; +import com.replaymod.render.EXRWriter; import com.replaymod.render.PNGWriter; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; @@ -55,7 +56,6 @@ //#endif //#if MC>=11400 -import com.replaymod.render.EXRWriter; import net.minecraft.client.gui.screen.Screen; import java.util.concurrent.CompletableFuture; //#else @@ -129,11 +129,7 @@ public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timel } else { FrameConsumer frameConsumer; if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) { - //#if MC>=11400 - frameConsumer = new EXRWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); - //#else - //$$ throw new UnsupportedOperationException("EXR requires LWJGL3"); - //#endif + frameConsumer = EXRWriter.create(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) { frameConsumer = new PNGWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else { diff --git a/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java b/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java new file mode 100644 index 00000000..8c832acd --- /dev/null +++ b/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java @@ -0,0 +1,132 @@ +package com.replaymod.render.utils; + +import com.replaymod.core.ReplayMod; +import com.replaymod.render.rendering.Frame; +import com.replaymod.render.rendering.FrameConsumer; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.ProtectionDomain; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +public class Lwjgl3Loader extends URLClassLoader { + static { registerAsParallelCapable(); } + private static Path tempJarFile; + private static Lwjgl3Loader instance; + + private final Set implClasses = new CopyOnWriteArraySet<>(); + + private Lwjgl3Loader(Path jarFile) throws IOException, ReflectiveOperationException { + super(new URL[] { jarFile.toUri().toURL() }, Lwjgl3Loader.class.getClassLoader()); + + // Need to use a different directory for natives than MC because native files can only be loaded once + Path nativesDir = ReplayMod.instance.folders.getCacheFolder().resolve("lwjgl-natives"); + + Class configClass = Class.forName("org.lwjgl.system.Configuration", true, this); + Object extractDirField = configClass.getField("SHARED_LIBRARY_EXTRACT_DIRECTORY").get(null); + Method setMethod = configClass.getMethod("set", Object.class); + setMethod.invoke(extractDirField, nativesDir.toAbsolutePath().toString()); + } + + private boolean canBeSharedWithMc(String name) { + if (name.startsWith("org.lwjgl.")) { + return false; // MC may have a different version + } + for (String implClass : implClasses) { + if (name.startsWith(implClass)) { + return false; // depends on above lwjgl + } + } + return true; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!canBeSharedWithMc(name)) { + synchronized (getClassLoadingLock(name)) { + Class cls = findLoadedClass(name); + if (cls == null) { + cls = findClass(name); + } + if (resolve) { + resolveClass(cls); + } + return cls; + } + } else { + return super.loadClass(name, resolve); + } + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + try { + return super.findClass(name); + } catch (ClassNotFoundException e) { + String path = name.replace('.', '/').concat(".class"); + URL url = getParent().getResource(path); + if (url == null) { + throw e; + } + try { + byte[] bytes = IOUtils.toByteArray(url); + return defineClass(name, bytes, 0, bytes.length, (ProtectionDomain) null); + } catch (IOException e1) { + throw new ClassNotFoundException(name, e1); + } + } + } + + private static synchronized Path getJarFile() throws IOException { + if (tempJarFile == null) { + Path jarFile = Files.createTempFile("replaymod-lwjgl", ".jar"); + jarFile.toFile().deleteOnExit(); + try (InputStream in = Lwjgl3Loader.class.getResourceAsStream("lwjgl.jar")) { + if (in == null) { + throw new IOException("Failed to find embedded lwjgl.jar file."); + } + Files.copy(in, jarFile, REPLACE_EXISTING); + } + tempJarFile = jarFile; + } + return tempJarFile; + } + + public static synchronized Lwjgl3Loader instance() { + if (instance == null) { + try { + instance = new Lwjgl3Loader(getJarFile()); + } catch (IOException | ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + return instance; + } + + @SuppressWarnings("unchecked") + public static

FrameConsumer

createFrameConsumer( + Class> implClass, + Class[] parameterTypes, + Object[] args + ) { + try { + Lwjgl3Loader loader = instance(); + loader.implClasses.add(implClass.getName()); + Class realClass = Class.forName(implClass.getName(), true, loader); + Constructor constructor = realClass.getConstructor(parameterTypes); + return (FrameConsumer

) constructor.newInstance(args); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index ff833a38..8bd2c8b6 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -36,7 +36,6 @@ //#endif //#if MC>=11400 "Mixin_PreserveDepthDuringHandRendering", - "Mixin_WindowsWorkaroundForTinyEXRNatives", //#endif "GameRendererAccessor", "MainWindowAccessor", From 4f9a730b6d185b839de08a3a570940c06e353fc7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 12:28:07 +0200 Subject: [PATCH 095/132] Fix classic camera speed changing way too quickly (fixes #719) On my system, a single scroll tick results in 120 calls, which is fine for the vanilla camera controller because it has a range of 2000, but the classic controller only has a range of 36, making it impossible to select anything but the extremes. This commit changes the size of the range to match that of the vanilla controller. --- .../com/replaymod/replay/camera/ClassicCameraController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java index 316b4e25..4a1692fe 100644 --- a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java @@ -9,9 +9,9 @@ // TODO: Marius is responsible for this. Please, someone clean it up. public class ClassicCameraController implements CameraController { - private static final double SPEED_CHANGE = 0.5; private static final double LOWER_SPEED = 2; private static final double UPPER_SPEED = 20; + private static final double SPEED_CHANGE = (UPPER_SPEED - LOWER_SPEED) / 2000; private final CameraEntity camera; From a3f40493223f5ceae172e59ad007155069b51ceb Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 12:30:23 +0200 Subject: [PATCH 096/132] Reduces minimum speed of classic camera controller Two blocks per second is still quite fast, so this commit reduces the minimum to a tenth of that, allowing for much finer control. --- .../com/replaymod/replay/camera/ClassicCameraController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java index 4a1692fe..1d28ce25 100644 --- a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java @@ -9,7 +9,7 @@ // TODO: Marius is responsible for this. Please, someone clean it up. public class ClassicCameraController implements CameraController { - private static final double LOWER_SPEED = 2; + private static final double LOWER_SPEED = 0.2; private static final double UPPER_SPEED = 20; private static final double SPEED_CHANGE = (UPPER_SPEED - LOWER_SPEED) / 2000; From 517591d72a060a01aa01e9786992091481a4cac8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 13:32:58 +0200 Subject: [PATCH 097/132] Encode OpenEXR and PNG frames in parallel --- .../advancedscreenshots/ScreenshotWriter.java | 5 ++ .../java/com/replaymod/render/EXRWriter.java | 5 ++ .../com/replaymod/render/FFmpegWriter.java | 5 ++ .../java/com/replaymod/render/PNGWriter.java | 5 ++ .../render/rendering/FrameConsumer.java | 2 + .../replaymod/render/rendering/Pipeline.java | 59 ++++++++++++++----- .../replaymod/render/rendering/Pipelines.java | 5 ++ .../render/rendering/VideoRenderer.java | 15 ++++- 8 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java index 70522b22..987e7839 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java @@ -63,4 +63,9 @@ public void consume(Map channels) { public void close() throws IOException { } + + @Override + public boolean isParallelCapable() { + return false; + } } diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index 17124ccc..f2d9a921 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -135,4 +135,9 @@ public void consume(Map channels) { @Override public void close() { } + + @Override + public boolean isParallelCapable() { + return true; + } } diff --git a/src/main/java/com/replaymod/render/FFmpegWriter.java b/src/main/java/com/replaymod/render/FFmpegWriter.java index f4e1381c..332f346c 100644 --- a/src/main/java/com/replaymod/render/FFmpegWriter.java +++ b/src/main/java/com/replaymod/render/FFmpegWriter.java @@ -134,6 +134,11 @@ public void consume(Map channels) { } } + @Override + public boolean isParallelCapable() { + return false; + } + private void checkSize(ReadableDimension size) { checkSize(size.getWidth(), size.getHeight()); } diff --git a/src/main/java/com/replaymod/render/PNGWriter.java b/src/main/java/com/replaymod/render/PNGWriter.java index 525d154d..8912dcb0 100644 --- a/src/main/java/com/replaymod/render/PNGWriter.java +++ b/src/main/java/com/replaymod/render/PNGWriter.java @@ -71,4 +71,9 @@ private void withImage(BitmapFrame frame, IOConsumer consumer) throws IOE @Override public void close() { } + + @Override + public boolean isParallelCapable() { + return true; + } } diff --git a/src/main/java/com/replaymod/render/rendering/FrameConsumer.java b/src/main/java/com/replaymod/render/rendering/FrameConsumer.java index fd74586c..ae654ca0 100644 --- a/src/main/java/com/replaymod/render/rendering/FrameConsumer.java +++ b/src/main/java/com/replaymod/render/rendering/FrameConsumer.java @@ -7,4 +7,6 @@ public interface FrameConsumer

extends Closeable { void consume(Map channels); + boolean isParallelCapable(); + } diff --git a/src/main/java/com/replaymod/render/rendering/Pipeline.java b/src/main/java/com/replaymod/render/rendering/Pipeline.java index cf7f057d..8696fe29 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipeline.java +++ b/src/main/java/com/replaymod/render/rendering/Pipeline.java @@ -10,6 +10,7 @@ import net.minecraft.util.crash.CrashReport; import org.lwjgl.glfw.GLFW; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; @@ -25,8 +26,6 @@ public class Pipeline implements Runnable { private final FrameCapturer capturer; private final FrameProcessor processor; private final GlToAbsoluteDepthProcessor depthProcessor; - private int consumerNextFrame; - private final Object consumerLock = new Object(); private final FrameConsumer

consumer; private volatile boolean abort; @@ -35,7 +34,7 @@ public Pipeline(WorldRenderer worldRenderer, FrameCapturer capturer, FramePro this.worldRenderer = worldRenderer; this.capturer = capturer; this.processor = processor; - this.consumer = consumer; + this.consumer = new ParallelSafeConsumer<>(consumer); float near = 0.05f; float far = getMinecraft().options.viewDistance * 16 * 4; @@ -44,7 +43,6 @@ public Pipeline(WorldRenderer worldRenderer, FrameCapturer capturer, FramePro @Override public synchronized void run() { - consumerNextFrame = 0; int processors = Runtime.getRuntime().availableProcessors(); int processThreads = Math.max(1, processors - 2); // One processor for the main thread and one for ffmpeg, sorry OS :( ExecutorService processService = new ThreadPoolExecutor(processThreads, processThreads, @@ -106,7 +104,6 @@ public ProcessTask(Map rawChannels) { @Override public void run() { try { - Integer frameId = null; Map processedChannels = new HashMap<>(); for (Map.Entry entry : rawChannels.entrySet()) { P processedFrame = processor.process(entry.getValue()); @@ -114,27 +111,57 @@ public void run() { depthProcessor.process((BitmapFrame) processedFrame); } processedChannels.put(entry.getKey(), processedFrame); - frameId = processedFrame.getFrameId(); } - if (frameId == null) { + if (processedChannels.isEmpty()) { return; } - synchronized (consumerLock) { - while (consumerNextFrame != frameId) { + consumer.consume(processedChannels); + } catch (Throwable t) { + CrashReport crashReport = CrashReport.create(t, "Processing frame"); + MCVer.getMinecraft().setCrashReport(crashReport); + } + } + } + + private static class ParallelSafeConsumer

implements FrameConsumer

{ + private final FrameConsumer

inner; + + private int nextFrame; + private final Object lock = new Object(); + + private ParallelSafeConsumer(FrameConsumer

inner) { + this.inner = inner; + } + + @Override + public void consume(Map channels) { + if (inner.isParallelCapable()) { + inner.consume(channels); + } else { + int frameId = channels.values().iterator().next().getFrameId(); + synchronized (lock) { + while (nextFrame != frameId) { try { - consumerLock.wait(); + lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } - consumer.consume(processedChannels); - consumerNextFrame++; - consumerLock.notifyAll(); + inner.consume(channels); + nextFrame++; + lock.notifyAll(); } - } catch (Throwable t) { - CrashReport crashReport = CrashReport.create(t, "Processing frame"); - MCVer.getMinecraft().setCrashReport(crashReport); } } + + @Override + public boolean isParallelCapable() { + return true; + } + + @Override + public void close() throws IOException { + inner.close(); + } } } diff --git a/src/main/java/com/replaymod/render/rendering/Pipelines.java b/src/main/java/com/replaymod/render/rendering/Pipelines.java index e012fb41..bc65fba2 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipelines.java +++ b/src/main/java/com/replaymod/render/rendering/Pipelines.java @@ -128,6 +128,11 @@ public void consume(Map channels) { @Override public void close() { } + + @Override + public boolean isParallelCapable() { + return true; + } }; return new Pipeline<>(worldRenderer, capturer, new DummyProcessor<>(), consumer); } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index ff8048d2..92021f8b 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -137,11 +137,19 @@ public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timel } ffmpegWriter = frameConsumer instanceof FFmpegWriter ? (FFmpegWriter) frameConsumer : null; FrameConsumer previewingFrameConsumer = new FrameConsumer() { + private int lastFrameId = -1; + @Override public void consume(Map channels) { BitmapFrame bgra = channels.get(Channel.BRGA); if (bgra != null) { - gui.updatePreview(bgra.getByteBuffer(), bgra.getSize()); + synchronized (this) { + int frameId = bgra.getFrameId(); + if (lastFrameId < frameId) { + lastFrameId = frameId; + gui.updatePreview(bgra.getByteBuffer(), bgra.getSize()); + } + } } frameConsumer.consume(channels); } @@ -150,6 +158,11 @@ public void consume(Map channels) { public void close() throws IOException { frameConsumer.close(); } + + @Override + public boolean isParallelCapable() { + return frameConsumer.isParallelCapable(); + } }; this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, previewingFrameConsumer); } From 08c86ce57487d8e8f10bcbc7a0f407a8396c447b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 13:33:13 +0200 Subject: [PATCH 098/132] Enable compression for OpenEXR if we have the cpu power for it --- src/main/java/com/replaymod/render/EXRWriter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index f2d9a921..2b072bda 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -35,6 +35,11 @@ public static FrameConsumer create(Path outputFolder, boolean keepA ); } + // Compression is pretty slow, so we'll only use it when we've got enough cpu cores to make up for that + private static final int COMPRESSION = Runtime.getRuntime().availableProcessors() >= 8 + ? TINYEXR_COMPRESSIONTYPE_ZIPS + : TINYEXR_COMPRESSIONTYPE_NONE; + private final Path outputFolder; private final boolean keepAlpha; @@ -71,6 +76,7 @@ public void consume(Map channels) { header.channels(channelInfos); header.pixel_types(pixelTypes); header.requested_pixel_types(requestedPixelTypes); + header.compression_type(COMPRESSION); // Some readers ignore this, so we use the most expected order memASCII("A", true, channelInfos.get(0).name()); From 7457f9c13bb4a538e588a8a280e425d1c90b3d89 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 26 Jun 2022 13:54:52 +0200 Subject: [PATCH 099/132] Increase maximum camera speed by about an order of magnitude --- .../com/replaymod/replay/camera/ClassicCameraController.java | 4 ++-- .../com/replaymod/replay/camera/VanillaCameraController.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java index 1d28ce25..26c5dfb0 100644 --- a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java @@ -10,8 +10,8 @@ // TODO: Marius is responsible for this. Please, someone clean it up. public class ClassicCameraController implements CameraController { private static final double LOWER_SPEED = 0.2; - private static final double UPPER_SPEED = 20; - private static final double SPEED_CHANGE = (UPPER_SPEED - LOWER_SPEED) / 2000; + private static final double UPPER_SPEED = 200; + private static final double SPEED_CHANGE = (UPPER_SPEED - LOWER_SPEED) / 20000; private final CameraEntity camera; diff --git a/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java b/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java index 10354be0..6561436b 100644 --- a/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java @@ -9,7 +9,7 @@ * Camera controller performing vanilla creative-like camera movements. */ public class VanillaCameraController implements CameraController { - private static final int MAX_SPEED = 1000; + private static final int MAX_SPEED = 2000; private static final int MIN_SPEED = -1000; private static final Vector3f[] DIRECTIONS = new Vector3f[]{ From 42aef017c19fbed4877cdeb037956f1b274d2b30 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 17 Jul 2022 16:02:41 +0200 Subject: [PATCH 100/132] Fix skin in Player Overview and first person on 1.8.9 (fixes #749) No clue why `getResourceLocationForPlayerUUID` was implemented the way it was, that commit dates back to 2015 (332c36a), but if the skin isn't loaded yet, it won't load it, so sometimes it just won't show properly in first person and in the Player Overview. This commit just calls `AbstractClientPlayer.getSkinTexture` instead. Unclear which MC versions are affected. At some point before 1.16.4 something else was introduced which loads the skin for us, hence why modern versions were not affected. --- .../java/com/replaymod/core/utils/Utils.java | 30 ------------------- .../playeroverview/PlayerOverviewGui.java | 5 ++-- .../replaymod/replay/camera/CameraEntity.java | 4 +-- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 1b518534..826f78fa 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -1,7 +1,6 @@ package com.replaymod.core.utils; import com.google.common.base.Throwables; -import com.google.common.collect.Iterables; import com.google.common.net.PercentEscaper; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -27,7 +26,6 @@ import de.johni0702.minecraft.gui.versions.MCVer; import net.minecraft.client.gui.screen.Screen; import net.minecraft.util.crash.CrashReport; -import net.minecraft.util.Identifier; import org.apache.commons.io.Charsets; import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.LogManager; @@ -38,15 +36,6 @@ //$$ import org.lwjgl.input.Keyboard; //#endif -//#if MC>=10800 -import net.minecraft.client.network.PlayerListEntry; -import net.minecraft.client.util.DefaultSkinHelper; -//#else -//$$ import net.minecraft.client.Minecraft; -//$$ import net.minecraft.client.entity.AbstractClientPlayer; -//$$ import net.minecraft.entity.player.EntityPlayer; -//#endif - import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; @@ -233,25 +222,6 @@ public static String fileNameToReplayName(String fileName) { } } - public static Identifier getResourceLocationForPlayerUUID(UUID uuid) { - //#if MC>=10800 - PlayerListEntry info = getMinecraft().getNetworkHandler().getPlayerListEntry(uuid); - Identifier skinLocation; - if (info != null && info.hasSkinTexture()) { - skinLocation = info.getSkinTexture(); - } else { - skinLocation = DefaultSkinHelper.getTexture(uuid); - } - return skinLocation; - //#else - //$$ EntityPlayer player = Minecraft.getMinecraft().theWorld.getPlayerEntityByUUID(uuid); - //$$ if (player != null || !(player instanceof AbstractClientPlayer)) { - //$$ return AbstractClientPlayer.locationStevePng; - //$$ } - //$$ return ((AbstractClientPlayer) player).getLocationSkin(); - //#endif - } - public static boolean isCtrlDown() { //#if MC>=11400 return Screen.hasControlDown(); diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java index 01bf1f8f..78ed336e 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java @@ -1,6 +1,5 @@ package com.replaymod.extras.playeroverview; -import com.replaymod.core.utils.Utils; import com.replaymod.replay.ReplayModReplay; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; @@ -20,6 +19,7 @@ import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.util.Identifier; @@ -94,7 +94,8 @@ public PlayerOverviewGui(final PlayerOverview extra, List players) Collections.sort(players, new PlayerComparator()); // Sort by name, spectators last for (final PlayerEntity p : players) { - final Identifier texture = Utils.getResourceLocationForPlayerUUID(p.getUuid()); + if (!(p instanceof AbstractClientPlayerEntity)) continue; + final Identifier texture = ((AbstractClientPlayerEntity) p).getSkinTexture(); final GuiClickable panel = new GuiClickable().setLayout(new HorizontalLayout().setSpacing(2)).addElements( new HorizontalLayout.Data(0.5), new GuiImage() { @Override diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 797c0ed9..f9c6fe53 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -472,8 +472,8 @@ public boolean isInvisible() { @Override public Identifier getSkinTexture() { Entity view = this.client.getCameraEntity(); - if (view != this && view instanceof PlayerEntity) { - return Utils.getResourceLocationForPlayerUUID(view.getUuid()); + if (view != this && view instanceof AbstractClientPlayerEntity) { + return ((AbstractClientPlayerEntity) view).getSkinTexture(); } return super.getSkinTexture(); } From 633ac10d8927be0dc5d97908ebba35b012dba689 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 17 Jul 2022 16:25:23 +0200 Subject: [PATCH 101/132] Fix main hand in first person spectator view (fixes #731) --- .../com/replaymod/replay/camera/CameraEntity.java | 12 ++++++++++++ versions/1.14.4-forge/mapping.txt | 1 + 2 files changed, 13 insertions(+) diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index f9c6fe53..19ca60cd 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -73,6 +73,7 @@ //$$ import net.minecraft.stats.RecipeBook; //#endif //#endif +import net.minecraft.util.Arm; import net.minecraft.util.Hand; //#endif @@ -498,6 +499,17 @@ public boolean isPartVisible(PlayerModelPart modelPart) { } //#endif + //#if MC>=10904 + @Override + public Arm getMainArm() { + Entity view = this.client.getCameraEntity(); + if (view != this && view instanceof PlayerEntity) { + return ((PlayerEntity) view).getMainArm(); + } + return super.getMainArm(); + } + //#endif + @Override public float getHandSwingProgress(float renderPartialTicks) { Entity view = this.client.getCameraEntity(); diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index 3bd9a84e..90dff9e6 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -23,6 +23,7 @@ org.lwjgl.glfw.GLFW com.replaymod.core.versions.GLFW net.minecraft.client.MainWindow com.replaymod.core.versions.Window net.minecraft.client.audio.SimpleSound net.minecraft.client.audio.PositionedSoundRecord net.minecraft.client.gui.IngameGui net.minecraft.client.gui.GuiIngame +net.minecraft.util.HandSide net.minecraft.util.EnumHandSide net.minecraft.resources.FolderPack getInputStream() getInputStreamByName() net.minecraft.client.gui.GuiYesNoCallback confirmResult() confirmClicked() From 6e3d30e41be915193874455411a0866d26dc1aca Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 17 Jul 2022 16:28:56 +0200 Subject: [PATCH 102/132] Fix thread unsafety in OpenGlToBitmapProcessor Sharing the temporary `row` and `rowSwap` buffers between different calls to the processor is not safe because it will be used by multiple threads. As a result some of the rows in the image could randomly get corrupted. This commit gets rid of the cached buffers (the value of which questionable anyway) and simply re-uses the same code which other processors use. --- .../processor/OpenGlToBitmapProcessor.java | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java b/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java index 6cb60f5e..8740716d 100644 --- a/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java +++ b/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java @@ -2,42 +2,25 @@ import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.utils.ByteBufferPool; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import java.nio.ByteBuffer; -public class OpenGlToBitmapProcessor extends AbstractFrameProcessor { +import static com.replaymod.render.utils.Utils.openGlBytesToBitmap; - private byte[] row, rowSwap; +public class OpenGlToBitmapProcessor extends AbstractFrameProcessor { @Override public BitmapFrame process(OpenGlFrame rawFrame) { - // Flip whole image in place - ReadableDimension size = rawFrame.getSize(); + int width = size.getWidth(); + int height = size.getHeight(); int bpp = rawFrame.getBytesPerPixel(); - int rowSize = size.getWidth() * bpp; - if (row == null || row.length < rowSize) { - row = new byte[rowSize]; - rowSwap = new byte[rowSize]; - } - ByteBuffer buffer = rawFrame.getByteBuffer(); - int rows = size.getHeight(); - byte[] row = this.row; - byte[] rowSwap = this.rowSwap; - for (int i = 0; i < rows / 2; i++) { - int from = rowSize * i; - int to = rowSize * (rows - i - 1); - buffer.position(from); - buffer.get(row); - buffer.position(to); - buffer.get(rowSwap); - buffer.position(to); - buffer.put(row); - buffer.position(from); - buffer.put(rowSwap); - } - buffer.rewind(); - return new BitmapFrame(rawFrame.getFrameId(), size, bpp, buffer); + ByteBuffer result = ByteBufferPool.allocate(width * height * bpp); + openGlBytesToBitmap(rawFrame, 0, 0, result, width); + ByteBufferPool.release(rawFrame.getByteBuffer()); + return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width, height), bpp, result); } } From 738320e8969f61aecaf0885a2737a1103946e594 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 17 Jul 2022 23:03:44 +0200 Subject: [PATCH 103/132] Update translations --- src/main/resources/assets/replaymod/lang | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 7614b69e..d8beaa51 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 7614b69e249eb728d219c02c3c71effcc5fc00a2 +Subproject commit d8beaa51ae2966214df6d5cebec0307475fcec60 From 05387b0ac71eb28a5da88cbb709c42b23c510996 Mon Sep 17 00:00:00 2001 From: bela333 Date: Tue, 21 Jun 2022 18:01:56 +0200 Subject: [PATCH 104/132] Modified FREX notice --- src/main/java/com/replaymod/render/rendering/VideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 92021f8b..6d554e28 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -651,7 +651,7 @@ public static String[] checkCompat(RenderSettings settings) { return new String[] { "Rendering is not supported with your Sodium version.", "It is missing support for the FREX Flawless Frames API.", - "Either update to the latest version or uninstall Sodium before rendering!", + "Either use the Sodium build from replaymod.com or uninstall Sodium before rendering!", }; } //#if MC>=11700 From f0299fed66995fba7ee733a1cf11150bb3e30983 Mon Sep 17 00:00:00 2001 From: bela333 Date: Tue, 21 Jun 2022 18:18:25 +0200 Subject: [PATCH 105/132] Update documentation to include compatible Sodium --- docs/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content.md b/docs/content.md index 0681006f..e767cb76 100755 --- a/docs/content.md +++ b/docs/content.md @@ -619,7 +619,7 @@ The _Baritone_ mod can cause a crash when you're trying to load a replay. If you Minecraft may crash if you try to use _RandomPatches_ together with ReplayMod. Try removing RandomPatches if Minecraft crashes on startup. ### Sodium [sodium] -ReplayMod can record when _Sodium_ is installed but will crash during render. Disable Sodium before rendering, it can be re-enabled after that. +ReplayMod can record when _Sodium_ is installed, but currently lacks the FREX Flawless Frames API to render. A modified build of _Sodium_, that supports this API, is available from the ReplayMod downloads, by clicking the `Click to show compatible Sodium versions` button. ### Resource Loader [resourceloader] The _Resource Loader_ mod is not compatible with ReplayMod. From 89d771fdbf3b6a112cce10852e09ce6154ce0896 Mon Sep 17 00:00:00 2001 From: Kepler-17c <17955785+Kepler-17c@users.noreply.github.com> Date: Wed, 12 Jan 2022 03:38:14 +0100 Subject: [PATCH 106/132] Updated the compatibility section on shaders --- docs/content.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/content.md b/docs/content.md index e767cb76..62746584 100755 --- a/docs/content.md +++ b/docs/content.md @@ -582,7 +582,10 @@ In General, the Replay Mod _should_ be compatible with most Forge and Fabric Mod ### Shaders Mod [shaders] _Karyonix' Shaders Mod_ is no longer compatible with Minecraft Forge starting with 1.9.4. As such it is not compatible with the Replay Mod either. -Please use _Optifine_ instead. + +Below Minecraft 1.16 you can try _Optifine_ instead. Note however, that official support has ended and many versions break ReplayMod. + +On Minecraft 1.16.5 and up you can use _Iris_, which is fully supported. For the time being, you will have to use it with the custom _Sodium_ provided on our download page. The fix it includes is pending for the official version. ### Custom Main Menu [custom-main-menu] The _Custom Main Menu_ mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar. From a95edaa31df0765a0795c5ae228a8a8c8628f202 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 09:30:19 +0200 Subject: [PATCH 107/132] Update translations --- src/main/resources/assets/replaymod/lang | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index d8beaa51..ed16d95c 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit d8beaa51ae2966214df6d5cebec0307475fcec60 +Subproject commit ed16d95cd373d4b5ddd257ea4b30790d2fcff353 From 6db47ee070c23ac0382b8e3e84bf8f350f872796 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 09:34:41 +0200 Subject: [PATCH 108/132] Updated broken forge docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8d5f000..6062e5db 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ It has by now acquired a lot more sophisticated features to make it as noninvasi Please read the [preprocessor's README](https://github.com/ReplayMod/preprocessor/blob/master/README.md) to understand how it works. ### Versioning -The ReplayMod uses the versioning scheme outlined [here](http://mcforge.readthedocs.io/en/latest/conventions/versioning/) +The ReplayMod uses the versioning scheme outlined [here](https://docs.minecraftforge.net/en/1.12.x/conventions/versioning/) with three changes: - No `MAJORAPI`, the ReplayMod does not provide any external API - "Updating to a new Minecraft version" should not increment `MAJORMOD`, we maintain one version of the ReplayMod From bdea84b9a2c2428b80dd88d972de7f53066eb737 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 10:22:20 +0200 Subject: [PATCH 109/132] Remove unused GuiMainMenuAccessor --- .../core/mixin/GuiMainMenuAccessor.java | 16 ---------------- src/main/resources/mixins.core.replaymod.json | 1 - 2 files changed, 17 deletions(-) delete mode 100644 src/main/java/com/replaymod/core/mixin/GuiMainMenuAccessor.java diff --git a/src/main/java/com/replaymod/core/mixin/GuiMainMenuAccessor.java b/src/main/java/com/replaymod/core/mixin/GuiMainMenuAccessor.java deleted file mode 100644 index 3716e1ab..00000000 --- a/src/main/java/com/replaymod/core/mixin/GuiMainMenuAccessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.replaymod.core.mixin; - -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.screen.Screen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(TitleScreen.class) -public interface GuiMainMenuAccessor { - //#if MC>=10904 - @Accessor("realmsNotificationGui") - Screen getRealmsNotification(); - @Accessor("realmsNotificationGui") - void setRealmsNotification(Screen value); - //#endif -} diff --git a/src/main/resources/mixins.core.replaymod.json b/src/main/resources/mixins.core.replaymod.json index a326dc8d..7e10b9c9 100644 --- a/src/main/resources/mixins.core.replaymod.json +++ b/src/main/resources/mixins.core.replaymod.json @@ -18,7 +18,6 @@ //#endif "MixinKeyboardListener", "MixinMinecraft", - "GuiMainMenuAccessor", "GuiScreenAccessor", "KeyBindingAccessor", "MinecraftAccessor", From 400ec6bda58480c26f08aada7232e7017ba4e3a9 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 10:37:22 +0200 Subject: [PATCH 110/132] Port to MC 1.19.1 --- build.gradle | 5 ++++- jGui | 2 +- root.gradle.kts | 2 ++ settings.gradle.kts | 2 ++ src/main/java/com/replaymod/replay/handler/GuiHandler.java | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 40ad0f52..76afaae9 100644 --- a/build.gradle +++ b/build.gradle @@ -249,6 +249,7 @@ dependencies { 11801: '1.18.1', 11802: '1.18.2', 11900: '1.19', + 11901: '1.19.1', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -262,6 +263,7 @@ dependencies { 11801: '1.18.1+build.1:v2', 11802: '1.18.2+build.1:v2', 11900: '1.19+build.2:v2', + 11901: '1.19.1+build.5:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.14.6' def fabricApiVersion = [ @@ -276,6 +278,7 @@ dependencies { 11801: '0.43.1+1.18', 11802: '0.47.9+1.18.2', 11900: '0.55.3+1.19', + 11901: '0.58.5+1.19.1', ][mcVersion] def fabricApiModules = [ "api-base", @@ -348,7 +351,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:74d8465", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:b2c999d", shadeExclusions // FIXME this should be pulled in by ReplayStudio, and IntelliJ sees it, but javac for some reason does not implementation 'com.github.viaversion:opennbt:0a02214' // 2.0-SNAPSHOT (ViaVersion Edition) diff --git a/jGui b/jGui index c1e43fc9..aa031212 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit c1e43fc9f0550bf6a6ab8685c1008e754e042772 +Subproject commit aa031212c7019c475abb3a83d67fb7ade3d3455c diff --git a/root.gradle.kts b/root.gradle.kts index 276002e6..cba0c385 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -189,6 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11901 = createNode("1.19.1", 11901, "yarn") val mc11900 = createNode("1.19", 11900, "yarn") val mc11802 = createNode("1.18.2", 11802, "yarn") val mc11801 = createNode("1.18.1", 11801, "yarn") @@ -210,6 +211,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11901.link(mc11900) mc11900.link(mc11802, file("versions/mapping-fabric-1.19-1.18.2.txt")) mc11802.link(mc11801) mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt")) diff --git a/settings.gradle.kts b/settings.gradle.kts index b3289dad..2907c38f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,7 @@ val jGuiVersions = listOf( "1.18.1", "1.18.2", "1.19", + "1.19.1", ) val replayModVersions = listOf( // "1.7.10", @@ -56,6 +57,7 @@ val replayModVersions = listOf( "1.18.1", "1.18.2", "1.19", + "1.19.1", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index 00bb3d9f..f70f7d95 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -138,7 +138,7 @@ private void injectIntoIngameMenu(Screen guiScreen, Collection=11400 + //#if MC>=11400 && MC<11901 } else if (id.equals(BUTTON_OPTIONS)) { //#if MC>=11400 b.setWidth(204); From a9401a9779369b4455610d6d05230947331473eb Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 10:38:07 +0200 Subject: [PATCH 111/132] Update ModMenu --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 76afaae9..9d1db1cf 100644 --- a/build.gradle +++ b/build.gradle @@ -362,8 +362,10 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11900) { - modCompileOnly 'com.terraformersmc:modmenu:3.1.0' // FIXME update + if (mcVersion >= 11901) { + modImplementation 'com.terraformersmc:modmenu:4.0.5' + } else if (mcVersion >= 11900) { + modImplementation 'com.terraformersmc:modmenu:4.0.4' } else if (mcVersion >= 11802) { modImplementation 'com.terraformersmc:modmenu:3.1.0' } else if (mcVersion >= 11800) { From 962f08718fa63e9a4077a1f3d38854db43c460ac Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 10:38:14 +0200 Subject: [PATCH 112/132] Fix "Show Chat" setting on 1.19+ (fixes #757) --- src/main/java/com/replaymod/replay/FullReplaySender.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index aee4dac1..0806979f 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -58,6 +58,7 @@ import org.apache.commons.io.IOUtils; //#if MC>=11900 +//$$ import net.minecraft.network.packet.s2c.play.ChatMessageS2CPacket; //#else import net.minecraft.network.packet.s2c.play.MobSpawnS2CPacket; import net.minecraft.network.packet.s2c.play.PaintingSpawnS2CPacket; @@ -861,7 +862,11 @@ public void run() { } } + //#if MC>=11900 + //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket) { + //#else if (p instanceof GameMessageS2CPacket) { + //#endif if (!ReplayModReplay.instance.getCore().getSettingsRegistry().get(Setting.SHOW_CHAT)) { return null; } From af3f6ddbe2c96064b4f2af8c2a7908f1ffbe3cb8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 11:12:53 +0200 Subject: [PATCH 113/132] Always handle PlayerPositionLookS2CPacket manually --- src/main/java/com/replaymod/replay/FullReplaySender.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 0806979f..a6e8ae7d 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -832,8 +832,11 @@ public void run() { CameraEntity cent = replayHandler.getCameraEntity(); cent.setCameraPosition(ppl.getX(), ppl.getY(), ppl.getZ()); + cent.setCameraRotation(ppl.getYaw(), ppl.getPitch(), cent.roll); } }.run(); + + return null; } if(p instanceof GameStateChangeS2CPacket) { From 7550e3dcb763b75e0dc090efd7119b57e730c437 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 11:16:37 +0200 Subject: [PATCH 114/132] Fix race conditions in movement packet filtering (fixes #760) We cannot filter this packet purely on the netty thread because our filter depends on the current camera position (if it's too far, we do want to teleport it closer) and accessing the camera is only safe from the main thread. Previously the second part had not been considered which could lead to race conditions where the camera may not yet exist on the netty thread at time of handling. These appear to have gotten the wholly inappropriate "just put a null check around it" treatment, but that means that the filter sometimes doesn't work (hence the bug report). This commit changes handling such that all access to `allowMovement` is always done on the main thread and then moves the filtering there as well. And therefore filtering should now work properly. --- .../replaymod/replay/FullReplaySender.java | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index a6e8ae7d..fd41e6e2 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -267,6 +267,8 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend /** * Whether to allow (process) the next player movement packet. + * + * Must only be accessed from the main thread. */ protected boolean allowMovement; @@ -639,7 +641,7 @@ protected Packet processPacket(Packet p) throws Exception { if(p instanceof GameJoinS2CPacket) { GameJoinS2CPacket packet = (GameJoinS2CPacket) p; int entId = packet.getEntityId(); - allowMovement = true; + schedulePacketHandler(() -> allowMovement = true); actualID = entId; entId = -1789435; // Camera entity id should be negative which is an invalid id and can't be used by servers //#if MC>=11400 @@ -773,7 +775,7 @@ protected Packet processPacket(Packet p) throws Exception { //#endif //#endif - allowMovement = true; + schedulePacketHandler(() -> allowMovement = true); } if(p instanceof PlayerPositionLookS2CPacket) { @@ -789,8 +791,6 @@ protected Packet processPacket(Packet p) throws Exception { if(replayHandler.shouldSuppressCameraMovements()) return null; - CameraEntity cent = replayHandler.getCameraEntity(); - //#if MC>=10800 //#if MC>=11400 for (PlayerPositionLookS2CPacket.Flag relative : ppl.getFlags()) { @@ -812,29 +812,28 @@ protected Packet processPacket(Packet p) throws Exception { } //#endif - if(cent != null) { - if(!allowMovement && !((Math.abs(cent.getX() - ppl.getX()) > TP_DISTANCE_LIMIT) || - (Math.abs(cent.getZ() - ppl.getZ()) > TP_DISTANCE_LIMIT))) { - return null; - } else { - allowMovement = false; - } - } - - new Runnable() { + schedulePacketHandler(new Runnable() { @Override @SuppressWarnings("unchecked") public void run() { + // FIXME: world shouldn't ever be null at this point, now that we use the packet queue + // probably fine to remove on the next non-patch version (don't want to break stuff now) if (mc.world == null || !mc.isOnThread()) { ReplayMod.instance.runLater(this); return; } CameraEntity cent = replayHandler.getCameraEntity(); + if (!allowMovement && !((Math.abs(cent.getX() - ppl.getX()) > TP_DISTANCE_LIMIT) || + (Math.abs(cent.getZ() - ppl.getZ()) > TP_DISTANCE_LIMIT))) { + return; + } else { + allowMovement = false; + } cent.setCameraPosition(ppl.getX(), ppl.getY(), ppl.getZ()); cent.setCameraRotation(ppl.getYaw(), ppl.getPitch(), cent.roll); } - }.run(); + }); return null; } @@ -1273,6 +1272,22 @@ private void executeTaskQueue() { ReplayMod.instance.runTasks(); } + /** + * Runs the given runnable on the main thread as if it was a packet handler. + * Note that the packet handler queue has different behavior than the standard ReplayMod queue. + */ + private void schedulePacketHandler(Runnable runnable) { + if (mc.isOnThread()) { + runnable.run(); + } else { + //#if MC>=11400 + mc.execute(runnable); + //#else + //$$ mc.addScheduledTask(runnable); + //#endif + } + } + protected void processPacketSync(Packet p) { //#if MC>=10904 if (p instanceof UnloadChunkS2CPacket) { From 42d04d123c49797614ab3f333fbdaecbd65411fd Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 12:25:43 +0200 Subject: [PATCH 115/132] Deduplicate code --- .../handler/RecordingEventHandler.java | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index ac303479..b2e403ea 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -254,8 +254,14 @@ private void onPlayerTick() { //#if MC>=10904 for (EquipmentSlot slot : EquipmentSlot.values()) { ItemStack stack = player.getEquippedStack(slot); - if (playerItems[slot.ordinal()] != stack) { - playerItems[slot.ordinal()] = stack; + int index = slot.ordinal(); + //#else + //$$ for (int slot = 0; slot < 5; slot++) { + //$$ ItemStack stack = player.getEquipmentInSlot(slot); + //$$ int index = slot; + //#endif + if (playerItems[index] != stack) { + playerItems[index] = stack; //#if MC>=11600 packetListener.save(new EntityEquipmentUpdateS2CPacket(player.getEntityId(), Collections.singletonList(Pair.of(slot, stack)))); //#else @@ -263,37 +269,6 @@ private void onPlayerTick() { //#endif } } - //#else - //$$ if(playerItems[0] != mc.thePlayer.getHeldItem()) { - //$$ playerItems[0] = mc.thePlayer.getHeldItem(); - //$$ S04PacketEntityEquipment pee = new S04PacketEntityEquipment(player.getEntityId(), 0, playerItems[0]); - //$$ packetListener.save(pee); - //$$ } - //$$ - //$$ if(playerItems[1] != mc.thePlayer.inventory.armorInventory[0]) { - //$$ playerItems[1] = mc.thePlayer.inventory.armorInventory[0]; - //$$ S04PacketEntityEquipment pee = new S04PacketEntityEquipment(player.getEntityId(), 1, playerItems[1]); - //$$ packetListener.save(pee); - //$$ } - //$$ - //$$ if(playerItems[2] != mc.thePlayer.inventory.armorInventory[1]) { - //$$ playerItems[2] = mc.thePlayer.inventory.armorInventory[1]; - //$$ S04PacketEntityEquipment pee = new S04PacketEntityEquipment(player.getEntityId(), 2, playerItems[2]); - //$$ packetListener.save(pee); - //$$ } - //$$ - //$$ if(playerItems[3] != mc.thePlayer.inventory.armorInventory[2]) { - //$$ playerItems[3] = mc.thePlayer.inventory.armorInventory[2]; - //$$ S04PacketEntityEquipment pee = new S04PacketEntityEquipment(player.getEntityId(), 3, playerItems[3]); - //$$ packetListener.save(pee); - //$$ } - //$$ - //$$ if(playerItems[4] != mc.thePlayer.inventory.armorInventory[3]) { - //$$ playerItems[4] = mc.thePlayer.inventory.armorInventory[3]; - //$$ S04PacketEntityEquipment pee = new S04PacketEntityEquipment(player.getEntityId(), 4, playerItems[4]); - //$$ packetListener.save(pee); - //$$ } - //#endif //Leaving Ride From 62b9cd05387ba620c77b2a16ba0c1fe26fba3df2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 12:49:46 +0200 Subject: [PATCH 116/132] Use DefaultedList for equipment tracking Because on 1.11+ a `null` item stack is not something that should exist. This then also allows us to compare item stacks by value rather than only by reference, potentially saving a few redundant entity equipment update packets (but more importantly, also allows us to store a copy instead of the original, which will be important for the next commit). --- .../java/com/replaymod/core/versions/Patterns.java | 14 ++++++++++++++ .../recording/handler/RecordingEventHandler.java | 11 ++++++++--- versions/1.11.2/mapping.txt | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index cfd7d5a9..c5a81d1f 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -15,6 +15,7 @@ import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; import net.minecraft.resource.Resource; import net.minecraft.resource.ResourceManager; import net.minecraft.text.LiteralText; @@ -49,6 +50,10 @@ //$$ import net.minecraft.client.gui.GuiButton; //#endif +//#if MC>=11100 +import net.minecraft.util.collection.DefaultedList; +//#endif + //#if MC>=10904 import net.minecraft.sound.SoundEvent; import net.minecraft.util.crash.CrashCallable; @@ -669,4 +674,13 @@ private static Resource getResource(ResourceManager manager, Identifier id) thro return manager.getResource(id); //#endif } + + @Pattern + private static List DefaultedList_ofSize_ItemStack_Empty(int size) { + //#if MC>=11100 + return DefaultedList.ofSize(size, ItemStack.EMPTY); + //#else + //$$ return java.util.Arrays.asList(new ItemStack[size]); + //#endif + } } diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index b2e403ea..17427bad 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -40,6 +40,10 @@ //$$ import net.minecraft.network.play.server.SPacketUseBed; //#endif +//#if MC>=11100 +import net.minecraft.util.collection.DefaultedList; +//#endif + //#if MC>=10904 import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.WorldEventS2CPacket; @@ -53,6 +57,7 @@ //$$ import net.minecraft.util.MathHelper; //#endif +import java.util.List; import java.util.Objects; import static com.replaymod.core.versions.MCVer.*; @@ -63,7 +68,7 @@ public class RecordingEventHandler extends EventRegistrations { private final PacketListener packetListener; private Double lastX, lastY, lastZ; - private ItemStack[] playerItems = new ItemStack[6]; + private final List playerItems = DefaultedList.ofSize(6, ItemStack.EMPTY); private int ticksSinceLastCorrection; private boolean wasSleeping; private int lastRiding = -1; @@ -260,8 +265,8 @@ private void onPlayerTick() { //$$ ItemStack stack = player.getEquipmentInSlot(slot); //$$ int index = slot; //#endif - if (playerItems[index] != stack) { - playerItems[index] = stack; + if (!ItemStack.areEqual(playerItems.get(index), stack)) { + playerItems.set(index, stack); //#if MC>=11600 packetListener.save(new EntityEquipmentUpdateS2CPacket(player.getEntityId(), Collections.singletonList(Pair.of(slot, stack)))); //#else diff --git a/versions/1.11.2/mapping.txt b/versions/1.11.2/mapping.txt index 7f741972..48e74eec 100644 --- a/versions/1.11.2/mapping.txt +++ b/versions/1.11.2/mapping.txt @@ -1 +1,2 @@ net.minecraft.item.ItemStack EMPTY field_190927_a +net.minecraft.util.NonNullList withSize() func_191197_a() From c34f75d95aa15a938bb3f7d7e620991c59eae9a7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 12:53:11 +0200 Subject: [PATCH 117/132] Fix item staying visually equipped after being dropped (fixes #658) --- .../com/replaymod/recording/handler/RecordingEventHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 17427bad..822c3944 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -266,6 +266,9 @@ private void onPlayerTick() { //$$ int index = slot; //#endif if (!ItemStack.areEqual(playerItems.get(index), stack)) { + // ItemStack has internal mutability, so we need to make a copy now if we want to compare its + // current state with future states (e.g. dropping on modern versions will set the count to zero). + stack = stack != null ? stack.copy() : null; playerItems.set(index, stack); //#if MC>=11600 packetListener.save(new EntityEquipmentUpdateS2CPacket(player.getEntityId(), Collections.singletonList(Pair.of(slot, stack)))); From ff98260216914c230be6a9a306775a511e914594 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 13:45:49 +0200 Subject: [PATCH 118/132] Fix build on MC 1.8 --- versions/1.8.9/mapping.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/versions/1.8.9/mapping.txt b/versions/1.8.9/mapping.txt index 2dfdbda5..37f9ead2 100644 --- a/versions/1.8.9/mapping.txt +++ b/versions/1.8.9/mapping.txt @@ -8,6 +8,8 @@ net.minecraft.network.play.server.S48PacketResourcePackSend getHash() func_17978 net.minecraft.network.play.server.S08PacketPlayerPosLook getX() func_148932_c() net.minecraft.network.play.server.S08PacketPlayerPosLook getY() func_148928_d() net.minecraft.network.play.server.S08PacketPlayerPosLook getZ() func_148933_e() +net.minecraft.network.play.server.S08PacketPlayerPosLook getYaw() func_148931_f() +net.minecraft.network.play.server.S08PacketPlayerPosLook getPitch() func_148930_g() net.minecraft.network.play.server.S2BPacketChangeGameState getGameState() func_149138_c() net.minecraft.network.play.server.S0EPacketSpawnObject getType() func_148993_l() net.minecraft.network.play.server.S21PacketChunkData getChunkX() func_149273_e() From 82d440856746264d835c7ceabdbd0967172b2dec Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 31 Jul 2022 14:35:06 +0200 Subject: [PATCH 119/132] Add missing MC 1.19.1 .gitkeep file --- root.gradle.kts | 6 +++++- versions/1.19.1/.gitkeep | 0 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 versions/1.19.1/.gitkeep diff --git a/root.gradle.kts b/root.gradle.kts index cba0c385..88e8cc7e 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -94,7 +94,11 @@ fun generateVersionsJson(): Map { // We dropped 1.7.10 with the Gradle 7 update but still kept its source in case someone // volunteers to update FG 1.2 to Gradle 7. .filterNot { it == "1.7.10" && versionComparator.compare(version, "2.6.0") >= 0 } - mcVersions.map { "$it-$version" } + val versions = mcVersions.map { "$it-$version" }.toMutableList() + when (version) { + "2.6.7" -> versions.add("1.19.1-2.6.7") // forgot to add the .gitkeep file before merging + } + versions }.flatten() val versions = commitVersions + tagVersions.reversed() diff --git a/versions/1.19.1/.gitkeep b/versions/1.19.1/.gitkeep new file mode 100644 index 00000000..e69de29b From 35d1b7f24bcfb567614cb1ae69188df0c0c68b50 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 10 Aug 2022 13:47:08 +0200 Subject: [PATCH 120/132] Fix game crashing if client disconnects from replay during jump --- src/main/java/com/replaymod/replay/ReplayHandler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 99e90c36..8967aae4 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -704,6 +704,13 @@ public void doJump(int targetTime, boolean retainCameraPosition) { //#else //$$ .processReceivedPackets(); //#endif + + // If the packets we just sent somehow caused the client to disconnect, then the above connection tick + // call will have unloaded the world, and we'll have to abort what we were doing. + if (mc.world == null) { + return; + } + for (Entity entity : mc.world.getEntities()) { skipTeleportInterpolation(entity); entity.lastRenderX = entity.prevX = entity.getX(); From 06f1e0c36cdb4c02639d847c8d7ac5699d8b394c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 10 Aug 2022 13:33:27 +0200 Subject: [PATCH 121/132] Fix chat message validation failure when Show Chat is off on 1.19.1+ --- src/main/java/com/replaymod/replay/FullReplaySender.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index fd41e6e2..595e264c 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -57,6 +57,10 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +//#if MC>=11901 +//$$ import net.minecraft.network.packet.s2c.play.MessageHeaderS2CPacket; +//#endif + //#if MC>=11900 //$$ import net.minecraft.network.packet.s2c.play.ChatMessageS2CPacket; //#else @@ -864,7 +868,9 @@ public void run() { } } - //#if MC>=11900 + //#if MC>=11901 + //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket || p instanceof MessageHeaderS2CPacket) { + //#elseif MC>=11900 //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket) { //#else if (p instanceof GameMessageS2CPacket) { From cce65ec76df4e17481070afc05cf32d8b1747e98 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 10 Aug 2022 14:09:23 +0200 Subject: [PATCH 122/132] Ignore expired player public keys during replay These would otherwise fail to be initialized and cause the game to disconnect itself on the first chat message from the corresponding player. --- .../mixin/Mixin_AllowExpiredPlayerKeys.java | 1 + .../resources/mixins.replay.replaymod.json | 3 +++ .../mixin/Mixin_AllowExpiredPlayerKeys.java | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java create mode 100644 versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java b/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java new file mode 100644 index 00000000..be37d3e2 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java @@ -0,0 +1 @@ +// 1.19+ only diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index 02e6fe65..bdd258a6 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -10,6 +10,9 @@ "world_border.Mixin_UseReplayTime_ForMovement", "world_border.Mixin_UseReplayTime_ForTexture", "Mixin_FixNPCSkinCaching", + //#if MC>=11900 + //$$ "Mixin_AllowExpiredPlayerKeys", + //#endif //#if MC>=11800 //$$ "Mixin_FixEntityNotTracking", //#endif diff --git a/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java b/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java new file mode 100644 index 00000000..f64cbc08 --- /dev/null +++ b/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java @@ -0,0 +1,18 @@ +package com.replaymod.replay.mixin; + +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.network.encryption.PlayerPublicKey; +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(PlayerPublicKey.PublicKeyData.class) +public abstract class Mixin_AllowExpiredPlayerKeys { + @Inject(method = "isExpired", at = @At("HEAD"), cancellable = true) + private void neverExpireWhenInReplay(CallbackInfoReturnable ci) { + if (ReplayModReplay.instance.getReplayHandler() != null) { + ci.setReturnValue(false); + } + } +} From bee13fc9a1fe913334760858753ff60cdd27408c Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 5 Dec 2022 13:02:39 +0100 Subject: [PATCH 123/132] Replace projection matrix Redirect with ModifyArg Should be better for compatibility because multiple mods can ModifyArg but only one can Redirect, and will be easier to port to 1.19.3. --- .../mixin/Mixin_Omnidirectional_Camera.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java index 8fa79479..51e4f6d1 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java @@ -2,23 +2,30 @@ import com.replaymod.render.hooks.EntityRendererHandler; import net.minecraft.client.render.GameRenderer; -import net.minecraft.util.math.Matrix4f; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.ModifyArg; @Mixin(GameRenderer.class) public abstract class Mixin_Omnidirectional_Camera implements EntityRendererHandler.IEntityRenderer { - @Redirect(method = "getBasicProjectionMatrix", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;")) - private Matrix4f replayModRender_perspective$0(double fovY, float aspect, float zNear, float zFar) { - return replayModRender_perspective((float) fovY, aspect, zNear, zFar); + private static final String METHOD = "getBasicProjectionMatrix"; + private static final String TARGET = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;"; + private static final boolean TARGET_REMAP = true; + private static final float OMNIDIRECTIONAL_FOV = 90; + + @ModifyArg(method = METHOD, at = @At(value = "INVOKE", target = TARGET, remap = TARGET_REMAP), index = 0) + private double replayModRender_perspective_fov(double fovY) { + return isOmnidirectional() ? OMNIDIRECTIONAL_FOV : fovY; + } + + @ModifyArg(method = METHOD, at = @At(value = "INVOKE", target = TARGET, remap = TARGET_REMAP), index = 1) + private float replayModRender_perspective_aspect(float aspect) { + return isOmnidirectional() ? 1 : aspect; } - private Matrix4f replayModRender_perspective(float fovY, float aspect, float zNear, float zFar) { - if (replayModRender_getHandler() != null && replayModRender_getHandler().omnidirectional) { - fovY = 90; - aspect = 1; - } - return Matrix4f.viewboxMatrix(fovY, aspect, zNear, zFar); + @Unique + private boolean isOmnidirectional() { + return replayModRender_getHandler() != null && replayModRender_getHandler().omnidirectional; } } From 19404032fec50585463a5fb4bce71f567671bf6b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 6 Dec 2022 17:51:55 +0100 Subject: [PATCH 124/132] Fix jGui resource pack when using per-version run folder --- src/main/java/com/replaymod/core/ReplayMod.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index d93876b6..ebccc80e 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -115,7 +115,10 @@ public SettingsRegistry getSettingsRegistry() { private static DirectoryResourcePack createJGuiResourcePack() { File folder = new File("../jGui/src/main/resources"); if (!folder.exists()) { - return null; + folder = new File("../../../jGui/src/main/resources"); + if (!folder.exists()) { + return null; + } } return new DirectoryResourcePack(folder) { @Override From e0a682a5dfb78bc7c2dd4c73c9f55dfd4078467d Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 6 Dec 2022 17:58:58 +0100 Subject: [PATCH 125/132] Fix armor missing after dimension change (fixes #791) The respawn packet re-creates the player entity, so we need to re-send its armor (and technically also the riding and sleeping state, though that probably wouldn't ever have caused issues in practice because a respawn will implicitly reset those, unlike your inventory which is preserved). --- .../replaymod/recording/handler/RecordingEventHandler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 822c3944..f71270b2 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -108,6 +108,13 @@ public void spawnRecordingPlayer() { packetListener.save(new EntityTrackerUpdateS2CPacket(player.getEntityId(), player.getDataTracker(), true)); //#endif lastX = lastY = lastZ = null; + //#if MC>=11100 + playerItems.clear(); + //#else + //$$ Collections.fill(playerItems, null); + //#endif + lastRiding = -1; + wasSleeping = false; } catch(Exception e) { e.printStackTrace(); } From e8ea70aabcedd561e63f22e029e9fecc4055ecdd Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 6 Dec 2022 17:54:05 +0100 Subject: [PATCH 126/132] Port to MC 1.19.2 (fixes #801) --- build.gradle | 5 ++++- jGui | 2 +- root.gradle.kts | 2 ++ settings.gradle.kts | 2 ++ versions/1.19.2/.gitkeep | 0 .../replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java | 4 ++++ 6 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 versions/1.19.2/.gitkeep diff --git a/build.gradle b/build.gradle index 9d1db1cf..ee00b0cb 100644 --- a/build.gradle +++ b/build.gradle @@ -250,6 +250,7 @@ dependencies { 11802: '1.18.2', 11900: '1.19', 11901: '1.19.1', + 11902: '1.19.2', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -264,8 +265,9 @@ dependencies { 11802: '1.18.2+build.1:v2', 11900: '1.19+build.2:v2', 11901: '1.19.1+build.5:v2', + 11902: '1.19.2+build.28:v2', ][mcVersion] - modImplementation 'net.fabricmc:fabric-loader:0.14.6' + modImplementation 'net.fabricmc:fabric-loader:0.14.11' def fabricApiVersion = [ 11404: '0.4.3+build.247-1.14', 11502: '0.5.1+build.294-1.15', @@ -279,6 +281,7 @@ dependencies { 11802: '0.47.9+1.18.2', 11900: '0.55.3+1.19', 11901: '0.58.5+1.19.1', + 11902: '0.68.0+1.19.2', ][mcVersion] def fabricApiModules = [ "api-base", diff --git a/jGui b/jGui index aa031212..360b2541 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit aa031212c7019c475abb3a83d67fb7ade3d3455c +Subproject commit 360b2541d2164822ff6a9bd924fceedd52a4c1cb diff --git a/root.gradle.kts b/root.gradle.kts index 88e8cc7e..950a859d 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -193,6 +193,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11902 = createNode("1.19.2", 11902, "yarn") val mc11901 = createNode("1.19.1", 11901, "yarn") val mc11900 = createNode("1.19", 11900, "yarn") val mc11802 = createNode("1.18.2", 11802, "yarn") @@ -215,6 +216,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11902.link(mc11901) mc11901.link(mc11900) mc11900.link(mc11802, file("versions/mapping-fabric-1.19-1.18.2.txt")) mc11802.link(mc11801) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2907c38f..9aa97ab5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ val jGuiVersions = listOf( "1.18.2", "1.19", "1.19.1", + "1.19.2", ) val replayModVersions = listOf( // "1.7.10", @@ -58,6 +59,7 @@ val replayModVersions = listOf( "1.18.2", "1.19", "1.19.1", + "1.19.2", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/versions/1.19.2/.gitkeep b/versions/1.19.2/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java b/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java index f64cbc08..2f2c711e 100644 --- a/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java +++ b/versions/1.19/src/main/java/com/replaymod/replay/mixin/Mixin_AllowExpiredPlayerKeys.java @@ -9,7 +9,11 @@ @Mixin(PlayerPublicKey.PublicKeyData.class) public abstract class Mixin_AllowExpiredPlayerKeys { + //#if MC>=11902 + //$$ @Inject(method = { "isExpired()Z", "isExpired(Ljava/time/Duration;)Z" }, at = @At("HEAD"), cancellable = true) + //#else @Inject(method = "isExpired", at = @At("HEAD"), cancellable = true) + //#endif private void neverExpireWhenInReplay(CallbackInfoReturnable ci) { if (ReplayModReplay.instance.getReplayHandler() != null) { ci.setReturnValue(false); From d571e8fed5074f710a7fe243f912ccd0892453e2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 6 Dec 2022 17:58:52 +0100 Subject: [PATCH 127/132] Port to MC 1.19.3 Building against 1.19.3-rc3 for now because 1.19.3 has yet to release. ReplayStudio/ViaVersion is already targeting the release version though. --- build.gradle | 9 +- jGui | 2 +- root.gradle.kts | 2 + settings.gradle.kts | 2 + .../java/com/replaymod/core/ReplayMod.java | 32 ++- .../core/versions/LangResourcePack.java | 120 +++++++--- .../com/replaymod/core/versions/MCVer.java | 38 ++- .../com/replaymod/core/versions/Patterns.java | 216 +++++++++++++++++- .../handler/RecordingEventHandler.java | 6 +- .../mixin/MixinNetHandlerPlayClient.java | 4 + .../recording/mixin/MixinWorldClient.java | 13 +- .../mixin/Mixin_Omnidirectional_Camera.java | 10 + .../replaymod/replay/FullReplaySender.java | 16 +- .../com/replaymod/replay/ReplayHandler.java | 5 + .../replaymod/replay/handler/GuiHandler.java | 6 +- .../replay/mixin/Mixin_MoveRealmsButton.java | 12 +- versions/1.19.3/.gitkeep | 0 versions/1.9.4/mapping.txt | 1 + versions/mapping-fabric-1.19.3-1.19.2.txt | 4 + 19 files changed, 440 insertions(+), 58 deletions(-) create mode 100644 versions/1.19.3/.gitkeep create mode 100644 versions/mapping-fabric-1.19.3-1.19.2.txt diff --git a/build.gradle b/build.gradle index ee00b0cb..a6ac6066 100644 --- a/build.gradle +++ b/build.gradle @@ -251,6 +251,7 @@ dependencies { 11900: '1.19', 11901: '1.19.1', 11902: '1.19.2', + 11903: '1.19.3-rc3', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -266,6 +267,7 @@ dependencies { 11900: '1.19+build.2:v2', 11901: '1.19.1+build.5:v2', 11902: '1.19.2+build.28:v2', + 11903: '1.19.3-rc3+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.14.11' def fabricApiVersion = [ @@ -282,6 +284,7 @@ dependencies { 11900: '0.55.3+1.19', 11901: '0.58.5+1.19.1', 11902: '0.68.0+1.19.2', + 11903: '0.68.1+1.19.3', ][mcVersion] def fabricApiModules = [ "api-base", @@ -354,7 +357,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:b2c999d", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:21ef505", shadeExclusions // FIXME this should be pulled in by ReplayStudio, and IntelliJ sees it, but javac for some reason does not implementation 'com.github.viaversion:opennbt:0a02214' // 2.0-SNAPSHOT (ViaVersion Edition) @@ -365,7 +368,9 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11901) { + if (mcVersion >= 11903) { + modImplementation 'com.terraformersmc:modmenu:5.0.0-alpha.4' + } else if (mcVersion >= 11901) { modImplementation 'com.terraformersmc:modmenu:4.0.5' } else if (mcVersion >= 11900) { modImplementation 'com.terraformersmc:modmenu:4.0.4' diff --git a/jGui b/jGui index 360b2541..7a80b50c 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 360b2541d2164822ff6a9bd924fceedd52a4c1cb +Subproject commit 7a80b50c2e7cd0557c5ab670c59b46b256c0434c diff --git a/root.gradle.kts b/root.gradle.kts index 950a859d..511e29e6 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -193,6 +193,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11903 = createNode("1.19.3", 11903, "yarn") val mc11902 = createNode("1.19.2", 11902, "yarn") val mc11901 = createNode("1.19.1", 11901, "yarn") val mc11900 = createNode("1.19", 11900, "yarn") @@ -216,6 +217,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11903.link(mc11902, file("versions/mapping-fabric-1.19.3-1.19.2.txt")) mc11902.link(mc11901) mc11901.link(mc11900) mc11900.link(mc11802, file("versions/mapping-fabric-1.19-1.18.2.txt")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 9aa97ab5..fa478e0a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ val jGuiVersions = listOf( "1.19", "1.19.1", "1.19.2", + "1.19.3", ) val replayModVersions = listOf( // "1.7.10", @@ -60,6 +61,7 @@ val replayModVersions = listOf( "1.19", "1.19.1", "1.19.2", + "1.19.3", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index ebccc80e..d31a57f7 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -120,7 +120,11 @@ private static DirectoryResourcePack createJGuiResourcePack() { return null; } } + //#if MC>=11903 + //$$ return new DirectoryResourcePack(JGUI_RESOURCE_PACK_NAME, folder.toPath(), true) { + //#else return new DirectoryResourcePack(folder) { + //#endif @Override //#if MC>=11400 public String getName() { @@ -130,23 +134,37 @@ public String getName() { return JGUI_RESOURCE_PACK_NAME; } + //#if MC>=11903 + //$$ @Override + //$$ public net.minecraft.resource.InputSupplier openRoot(String... segments) { + //$$ if (segments.length == 1 && segments[0].equals("pack.mcmeta")) { + //$$ return () -> new ByteArrayInputStream(generatePackMeta()); + //$$ } + //$$ return super.openRoot(segments); + //$$ } + //#else @Override protected InputStream openFile(String resourceName) throws IOException { try { return super.openFile(resourceName); } catch (IOException e) { if ("pack.mcmeta".equals(resourceName)) { - //#if MC>=11400 - int version = 4; - //#else - //$$ int version = 1; - //#endif - return new ByteArrayInputStream(("{\"pack\": {\"description\": \"dummy pack for jGui resources in dev-env\", \"pack_format\": " - + version + "}}").getBytes(StandardCharsets.UTF_8)); + return new ByteArrayInputStream(generatePackMeta()); } throw e; } } + //#endif + + private byte[] generatePackMeta() { + //#if MC>=11400 + int version = 4; + //#else + //$$ int version = 1; + //#endif + return ("{\"pack\": {\"description\": \"dummy pack for jGui resources in dev-env\", \"pack_format\": " + + version + "}}").getBytes(StandardCharsets.UTF_8); + } }; } diff --git a/src/main/java/com/replaymod/core/versions/LangResourcePack.java b/src/main/java/com/replaymod/core/versions/LangResourcePack.java index a3594f08..41fa3e14 100644 --- a/src/main/java/com/replaymod/core/versions/LangResourcePack.java +++ b/src/main/java/com/replaymod/core/versions/LangResourcePack.java @@ -4,7 +4,6 @@ import com.google.gson.Gson; import com.replaymod.core.ReplayMod; import net.minecraft.resource.AbstractFileResourcePack; -import net.minecraft.resource.ResourceNotFoundException; import net.minecraft.resource.ResourceType; import net.minecraft.util.Identifier; import org.apache.commons.io.IOUtils; @@ -16,16 +15,18 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; +import java.util.stream.Stream; //#if FABRIC>=1 import net.fabricmc.loader.api.FabricLoader; @@ -33,6 +34,11 @@ //#else //#endif +//#if MC>=11903 +//$$ import java.util.Objects; +//$$ import net.minecraft.resource.InputSupplier; +//#endif + //#if MC>=11400 //#else //$$ import net.minecraft.resources.IPackFinder; @@ -58,7 +64,11 @@ public class LangResourcePack extends AbstractFileResourcePack { private final Path basePath; public LangResourcePack() { + //#if MC>=11903 + //$$ super(NAME, true); + //#else super(new File(NAME)); + //#endif //#if FABRIC>=1 ModContainer container = FabricLoader.getInstance().getModContainer(ReplayMod.MOD_ID).orElseThrow(IllegalAccessError::new); @@ -110,14 +120,45 @@ private String convertValue(String value) { return value; } + //#if MC>=11903 + //$$ @Override + //$$ public InputSupplier openRoot(String... segments) { + //$$ byte[] bytes; + //$$ try { + //$$ bytes = readFile(String.join("/", segments)); + //$$ } catch (IOException e) { + //$$ throw new RuntimeException(e); + //$$ } + //$$ if (bytes == null) { + //$$ return null; + //$$ } + //$$ return () -> new ByteArrayInputStream(bytes); + //$$ } + //#endif + + //#if MC>=11903 + //$$ @Override + //$$ public InputSupplier open(ResourceType type, Identifier id) { + //$$ return openRoot(type.getDirectory(), id.getNamespace(), id.getPath()); + //$$ } + //#else @Override protected InputStream openFile(String path) throws IOException { + byte[] bytes = readFile(path); + if (bytes == null) { + throw new net.minecraft.resource.ResourceNotFoundException(this.base, path); + } + return new ByteArrayInputStream(bytes); + } + //#endif + + private byte[] readFile(String path) throws IOException { if ("pack.mcmeta".equals(path)) { - return new ByteArrayInputStream("{\"pack\": {\"description\": \"ReplayMod language files\", \"pack_format\": 4}}".getBytes(StandardCharsets.UTF_8)); + return "{\"pack\": {\"description\": \"ReplayMod language files\", \"pack_format\": 4}}".getBytes(StandardCharsets.UTF_8); } Path langPath = langPath(path); - if (langPath == null) throw new ResourceNotFoundException(this.base, path); + if (langPath == null) return null; List langFile; try (InputStream in = Files.newInputStream(langPath)) { @@ -141,16 +182,25 @@ protected InputStream openFile(String path) throws IOException { properties.put(key, value); } - return new ByteArrayInputStream(GSON.toJson(properties).getBytes(StandardCharsets.UTF_8)); + return GSON.toJson(properties).getBytes(StandardCharsets.UTF_8); } + //#if MC>=11903 + //#else @Override protected boolean containsFile(String path) { Path langPath = langPath(path); return langPath != null && Files.exists(langPath); } + //#endif + //#if MC>=11903 + //$$ @Override + //$$ public void findResources(ResourceType type, String namespace, String prefix, ResultConsumer consumer) { + //$$ findResources(type, prefix, id -> consumer.accept(id, () -> new ByteArrayInputStream(Objects.requireNonNull(readFile(id.getPath()))))); + //$$ } + //#else @Override public Collection findResources( ResourceType resourcePackType, @@ -162,36 +212,42 @@ public Collection findResources( //$$ Predicate filter //#else int maxDepth, - Predicate filter + Predicate pathFilter //#endif ) { - if (resourcePackType == ResourceType.CLIENT_RESOURCES && "lang".equals(path)) { - Path base = baseLangPath(); - //#if MC<11400 - //$$ if (base == null) return Collections.emptyList(); - //#endif - try { - return Files.walk(base, 1) - .skip(1) - .filter(Files::isRegularFile) - .map(Path::getFileName).map(Path::toString) - .map(LANG_FILE_NAME_PATTERN::matcher) - .filter(Matcher::matches) - .map(matcher -> String.format("%s_%s.json", matcher.group(1), matcher.group(1))) - //#if MC<11900 - .filter(filter) - //#endif - .map(name -> new Identifier(ReplayMod.MOD_ID, "lang/" + name)) - //#if MC>=11900 - //$$ .filter(filter) - //#endif - .collect(Collectors.toList()); - } catch (IOException e) { - e.printStackTrace(); - return Collections.emptyList(); + //#if MC<11900 + Predicate filter = id -> pathFilter.test(id.getPath()); + //#endif + + List result = new ArrayList<>(); + findResources(resourcePackType, path, id -> { + if (filter.test(id)) { + result.add(id); } - } else { - return Collections.emptyList(); + }); + return result; + } + //#endif + + private void findResources(ResourceType type, String path, Consumer consumer) { + if (type != ResourceType.CLIENT_RESOURCES) return; + if (!"lang".equals(path)) return; + Path base = baseLangPath(); + //#if MC<11400 + //$$ if (base == null) return; + //#endif + try (Stream stream = Files.walk(base, 1)) { + stream + .skip(1) + .filter(Files::isRegularFile) + .map(Path::getFileName).map(Path::toString) + .map(LANG_FILE_NAME_PATTERN::matcher) + .filter(Matcher::matches) + .map(matcher -> String.format("%s_%s.json", matcher.group(1), matcher.group(1))) + .map(name -> new Identifier(ReplayMod.MOD_ID, "lang/" + name)) + .forEach(consumer); + } catch (IOException e) { + e.printStackTrace(); } } diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index f24937ca..ab6d4763 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -15,6 +15,10 @@ import net.minecraft.util.Util; import net.minecraft.util.math.Vec3d; +//#if MC>=11700 +//$$ import net.minecraft.util.math.Matrix4f; +//#endif + //#if MC>=11604 //#else //$$ import net.minecraft.entity.Entity; @@ -27,6 +31,8 @@ //#if MC>=11400 import com.replaymod.render.mixin.MainWindowAccessor; import net.minecraft.SharedConstants; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.ParentElement; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.AbstractButtonWidget; import net.minecraft.client.util.Window; @@ -219,13 +225,23 @@ public static void addButton( } //#if MC>=11400 - public static Optional findButton(Iterable buttonList, @SuppressWarnings("unused") String text, @SuppressWarnings("unused") int id) { + public static Optional findButton(Iterable buttonList, @SuppressWarnings("unused") String text, @SuppressWarnings("unused") int id) { //#if MC>=11600 final Text message = new TranslatableText(text); //#else //$$ final String message = I18n.translate(text); //#endif - for (AbstractButtonWidget b : buttonList) { + for (Element e : buttonList) { + if (e instanceof ParentElement) { + Optional button = findButton(((ParentElement) e).children(), text, id); + if (button.isPresent()) { + return button; + } + } + if (!(e instanceof AbstractButtonWidget)) { + continue; + } + AbstractButtonWidget b = (AbstractButtonWidget) e; if (message.equals(b.getMessage())) { return Optional.of(b); } @@ -361,6 +377,24 @@ public static void popMatrix() { //#endif } + //#if MC>=11700 + //$$ public static net.minecraft.util.math.Quaternion quaternion(float angle, net.minecraft.util.math.Vec3f axis) { + //#if MC>=11903 + //$$ return new org.joml.Quaternionf().fromAxisAngleDeg(axis.x, axis.y, axis.z, angle); + //#else + //$$ return new net.minecraft.util.math.Quaternion(axis, angle, true); + //#endif + //$$ } + //$$ + //$$ public static Matrix4f ortho(float left, float right, float top, float bottom, float zNear, float zFar) { + //#if MC>=11903 + //$$ return new Matrix4f().ortho(left, right, bottom, top, zNear, zFar); + //#else + //$$ return Matrix4f.projectionMatrix(left, right, top, bottom, zNear, zFar); + //#endif + //$$ } + //#endif + public static void emitLine(BufferBuilder buffer, Vector2f p1, Vector2f p2, int color) { emitLine(buffer, new Vector3f(p1.x, p1.y, 0), new Vector3f(p2.x, p2.y, 0), color); } diff --git a/src/main/java/com/replaymod/core/versions/Patterns.java b/src/main/java/com/replaymod/core/versions/Patterns.java index c5a81d1f..c794d052 100644 --- a/src/main/java/com/replaymod/core/versions/Patterns.java +++ b/src/main/java/com/replaymod/core/versions/Patterns.java @@ -18,6 +18,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.resource.Resource; import net.minecraft.resource.ResourceManager; +import net.minecraft.sound.SoundCategory; import net.minecraft.text.LiteralText; import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; @@ -39,13 +40,16 @@ //#if MC>=11600 import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Quaternion; //#else //#endif //#if MC>=11400 import net.minecraft.client.gui.widget.AbstractButtonWidget; import net.minecraft.client.util.Window; +import net.minecraft.util.registry.Registry; //#else //$$ import net.minecraft.client.gui.GuiButton; //#endif @@ -165,6 +169,42 @@ private static void Entity_setPos(Entity entity, double x, double y, double z) { //#endif } + @Pattern + private static int getX(AbstractButtonWidget button) { + //#if MC>=11903 + //$$ return button.getX(); + //#else + return button.x; + //#endif + } + + @Pattern + private static int getY(AbstractButtonWidget button) { + //#if MC>=11903 + //$$ return button.getY(); + //#else + return button.y; + //#endif + } + + @Pattern + private static void setX(AbstractButtonWidget button, int value) { + //#if MC>=11903 + //$$ button.setX(value); + //#else + button.x = value; + //#endif + } + + @Pattern + private static void setY(AbstractButtonWidget button, int value) { + //#if MC>=11903 + //$$ button.setY(value); + //#else + button.y = value; + //#endif + } + //#if MC>=11400 @Pattern private static void setWidth(AbstractButtonWidget button, int value) { @@ -529,7 +569,7 @@ private static void GL11_glTranslatef(float x, float y, float z) { @Pattern private static void GL11_glRotatef(float angle, float x, float y, float z) { //#if MC>=11700 - //$$ { float $angle = angle; com.mojang.blaze3d.systems.RenderSystem.getModelViewStack().multiply(new net.minecraft.util.math.Quaternion(new net.minecraft.util.math.Vec3f(x, y, z), $angle, true)); } + //$$ com.mojang.blaze3d.systems.RenderSystem.getModelViewStack().multiply(com.replaymod.core.versions.MCVer.quaternion(angle, new net.minecraft.util.math.Vec3f(x, y, z))); //#else GL11.glRotatef(angle, x, y, z); //#endif @@ -683,4 +723,178 @@ private static List DefaultedList_ofSize_ItemStack_Empty(int size) { //$$ return java.util.Arrays.asList(new ItemStack[size]); //#endif } + + @Pattern + private static void setSoundVolume(GameOptions options, SoundCategory category, float value) { + //#if MC>=11903 + //$$ options.getSoundVolumeOption(category).setValue((double) value); + //#else + options.setSoundVolume(category, value); + //#endif + } + + //#if MC>=10900 + @Pattern + private static SoundEvent SoundEvent_of(Identifier identifier) { + //#if MC>=11903 + //$$ return SoundEvent.of(identifier); + //#else + return new SoundEvent(identifier); + //#endif + } + //#else + //$$ @Pattern private static void SoundEvent_of() {} + //#endif + + //#if MC>=11600 + @Pattern + private static Vector3f POSITIVE_X() { + //#if MC>=11903 + //$$ return new org.joml.Vector3f(1, 0, 0); + //#else + return Vector3f.POSITIVE_X; + //#endif + } + + @Pattern + private static Vector3f POSITIVE_Y() { + //#if MC>=11903 + //$$ return new org.joml.Vector3f(0, 1, 0); + //#else + return Vector3f.POSITIVE_Y; + //#endif + } + + @Pattern + private static Vector3f POSITIVE_Z() { + //#if MC>=11903 + //$$ return new org.joml.Vector3f(0, 0, 1); + //#else + return Vector3f.POSITIVE_Z; + //#endif + } + + @Pattern + private static Quaternion getDegreesQuaternion(Vector3f axis, float angle) { + //#if MC>=11903 + //$$ return new org.joml.Quaternionf().fromAxisAngleDeg(axis, angle); + //#else + return axis.getDegreesQuaternion(angle); + //#endif + } + + @Pattern + private static void Quaternion_mul(Quaternion left, Quaternion right) { + //#if MC>=11903 + //$$ left.mul(right); + //#else + left.hamiltonProduct(right); + //#endif + } + + @Pattern + private static float Quaternion_getX(Quaternion q) { + //#if MC>=11903 + //$$ return q.x; + //#else + return q.getX(); + //#endif + } + + @Pattern + private static float Quaternion_getY(Quaternion q) { + //#if MC>=11903 + //$$ return q.y; + //#else + return q.getY(); + //#endif + } + + @Pattern + private static float Quaternion_getZ(Quaternion q) { + //#if MC>=11903 + //$$ return q.z; + //#else + return q.getZ(); + //#endif + } + + @Pattern + private static float Quaternion_getW(Quaternion q) { + //#if MC>=11903 + //$$ return q.w; + //#else + return q.getW(); + //#endif + } + + @Pattern + private static Quaternion Quaternion_copy(Quaternion source) { + //#if MC>=11903 + //$$ return new org.joml.Quaternionf(source); + //#else + return source.copy(); + //#endif + } + //#else + //$$ @Pattern private static void POSITIVE_X() {} + //$$ @Pattern private static void POSITIVE_Y() {} + //$$ @Pattern private static void POSITIVE_Z() {} + //$$ @Pattern private static void getDegreesQuaternion() {} + //$$ @Pattern private static void Quaternion_mul() {} + //$$ @Pattern private static void Quaternion_getX() {} + //$$ @Pattern private static void Quaternion_getY() {} + //$$ @Pattern private static void Quaternion_getZ() {} + //$$ @Pattern private static void Quaternion_getW() {} + //$$ @Pattern private static void Quaternion_copy() {} + //#endif + + //#if MC>=11600 + @Pattern + private static void Matrix4f_multiply(Matrix4f left, Matrix4f right) { + //#if MC>=11903 + //$$ left.mul(right); + //#else + left.multiply(right); + //#endif + } + + @Pattern + private static Matrix4f Matrix4f_translate(float x, float y, float z) { + //#if MC>=11903 + //$$ return new Matrix4f().translation(x, y, z); + //#else + return Matrix4f.translate(x, y, z); + //#endif + } + //#else + //$$ @Pattern private static void Matrix4f_multiply() {} + //$$ @Pattern private static void Matrix4f_translate() {} + //#endif + + //#if MC>=11700 + //$$ @Pattern + //$$ private static Matrix4f Matrix4f_perspectiveMatrix(float left, float right, float top, float bottom, float zNear, float zFar) { + //#if MC>=11903 + //$$ return com.replaymod.core.versions.MCVer.ortho(left, right, top, bottom, zNear, zFar); + //#else + //$$ return Matrix4f.projectionMatrix(left, right, top, bottom, zNear, zFar); + //#endif + //$$ } + //#else + @Pattern private static void Matrix4f_perspectiveMatrix() {} + //#endif + + //#if MC>=11400 + @Pattern + private static Registry> REGISTRIES() { + //#if MC>=11903 + //$$ return net.minecraft.registry.Registries.REGISTRIES; + //#else + return Registry.REGISTRIES; + //#endif + } + //#else + //$$ @Pattern private static void REGISTRIES() {} + //#endif } diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index f71270b2..fe2772e6 100644 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -32,7 +32,6 @@ //#if MC>=11600 import com.mojang.datafixers.util.Pair; -import java.util.Collections; //#endif //#if MC>=11400 @@ -57,6 +56,7 @@ //$$ import net.minecraft.util.MathHelper; //#endif +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -104,7 +104,9 @@ public void spawnRecordingPlayer() { ClientPlayerEntity player = mc.player; assert player != null; packetListener.save(new PlayerSpawnS2CPacket(player)); - //#if MC>=11500 + //#if MC>=11903 + //$$ packetListener.save(new EntityTrackerUpdateS2CPacket(player.getId(), player.getDataTracker().getChangedEntries())); + //#elseif MC>=11500 packetListener.save(new EntityTrackerUpdateS2CPacket(player.getEntityId(), player.getDataTracker(), true)); //#endif lastX = lastY = lastZ = null; diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java index 8c1841fc..8be310c4 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java @@ -61,7 +61,11 @@ public void recordOwnJoin(PlayerListS2CPacket packet, CallbackInfo ci) { if (mcStatic.player == null) return; RecordingEventHandler handler = getRecordingEventHandler(); + //#if MC>=11903 + //$$ if (handler != null && packet.getActions().contains(PlayerListS2CPacket.Action.ADD_PLAYER)) { + //#else if (handler != null && packet.getAction() == PlayerListS2CPacket.Action.ADD_PLAYER) { + //#endif // We cannot reference SPacketPlayerListItem.AddPlayerData directly for complicated (and yet to be // resolved) reasons (see https://github.com/MinecraftForge/ForgeGradle/issues/472), so we use ReplayStudio // to parse it instead. diff --git a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java index d4951d9f..e859e2a3 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java @@ -111,7 +111,10 @@ private RecordingEventHandler replayModRecording_getRecordingEventHandler() { // but are instead played directly by the client. The server only sends these sounds to // other clients so we have to record them manually. // E.g. Block place sounds - //#if MC>=11900 + //#if MC>=11903 + //$$ @Inject(method = "playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V", + //$$ at = @At("HEAD")) + //#elseif MC>=11900 //$$ @Inject(method = "playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFJ)V", //$$ at = @At("HEAD")) //#elseif MC>=11400 @@ -127,7 +130,13 @@ private RecordingEventHandler replayModRecording_getRecordingEventHandler() { //$$ at = @At("HEAD")) //#endif public void replayModRecording_recordClientSound( - PlayerEntity player, double x, double y, double z, SoundEvent sound, SoundCategory category, + PlayerEntity player, double x, double y, double z, + //#if MC>=11903 + //$$ RegistryEntry sound, + //#else + SoundEvent sound, + //#endif + SoundCategory category, float volume, float pitch, //#if MC>=11900 //$$ long seed, diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java index 51e4f6d1..b9726a4c 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java @@ -10,12 +10,22 @@ @Mixin(GameRenderer.class) public abstract class Mixin_Omnidirectional_Camera implements EntityRendererHandler.IEntityRenderer { private static final String METHOD = "getBasicProjectionMatrix"; + //#if MC>=11903 + //$$ private static final String TARGET = "Lorg/joml/Matrix4f;setPerspective(FFFF)Lorg/joml/Matrix4f;"; + //$$ private static final boolean TARGET_REMAP = false; + //$$ private static final float OMNIDIRECTIONAL_FOV = (float) Math.PI / 2; + //#else private static final String TARGET = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;"; private static final boolean TARGET_REMAP = true; private static final float OMNIDIRECTIONAL_FOV = 90; + //#endif @ModifyArg(method = METHOD, at = @At(value = "INVOKE", target = TARGET, remap = TARGET_REMAP), index = 0) + //#if MC>=11903 + //$$ private float replayModRender_perspective_fov(float fovY) { + //#else private double replayModRender_perspective_fov(double fovY) { + //#endif return isOmnidirectional() ? OMNIDIRECTIONAL_FOV : fovY; } diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 595e264c..9c9621e0 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -57,7 +57,11 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -//#if MC>=11901 +//#if MC>=11903 +//$$ import net.minecraft.network.packet.s2c.play.ProfilelessChatMessageS2CPacket; +//#endif + +//#if MC==11901 || MC==11902 //$$ import net.minecraft.network.packet.s2c.play.MessageHeaderS2CPacket; //#endif @@ -755,7 +759,11 @@ protected Packet processPacket(Packet p) throws Exception { GameMode.SPECTATOR, respawn.isDebugWorld(), respawn.isFlatWorld(), - respawn.isWritingErrorSkippable() + //#if MC>=11903 + //$$ (byte) 0 + //#else + false + //#endif //#else //$$ respawn.getGeneratorType(), //$$ GameMode.SPECTATOR @@ -868,7 +876,9 @@ public void run() { } } - //#if MC>=11901 + //#if MC>=11903 + //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket || p instanceof ProfilelessChatMessageS2CPacket) { + //#elseif MC==11901 || MC==11902 //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket || p instanceof MessageHeaderS2CPacket) { //#elseif MC>=11900 //$$ if (p instanceof GameMessageS2CPacket || p instanceof ChatMessageS2CPacket) { diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 8967aae4..800377b6 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -295,6 +295,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { networkManager, mc, null + //#if MC>=11903 + //$$ , null + //$$ , false + //$$ , null + //#endif //#if MC>=11400 , it -> {} //#endif diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index f70f7d95..a8fec2a3 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -195,7 +195,8 @@ private void moveAllButtonsInRect( buttons.stream() .filter(button -> button.x <= xEnd && button.x + button.getWidth() >= xStart) .filter(button -> button.y <= yEnd && button.y + button.getHeight() >= yStart) - .forEach(button -> button.y += moveBy); + // FIXME remap bug: needs the {} to recognize the setter (it also doesn't understand +=) + .forEach(button -> { button.y = button.y + moveBy; }); } { on(InitScreenCallback.EVENT, (screen, buttons) -> ensureReplayStopped(screen)); } @@ -417,6 +418,9 @@ public InjectedButton(Screen guiScreen, int buttonId, int x, int y, int width, i //#if MC>=11400 , self -> onClick.accept((InjectedButton) self) //#endif + //#if MC>=11903 + //$$ , DEFAULT_NARRATION_SUPPLIER + //#endif ); this.guiScreen = guiScreen; this.id = buttonId; diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java b/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java index 3066899f..2ccc1c9b 100644 --- a/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java @@ -11,11 +11,13 @@ @Mixin(TitleScreen.class) public abstract class Mixin_MoveRealmsButton { - @ModifyArg( - method = "init", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;init(Lnet/minecraft/client/MinecraftClient;II)V"), - index = 2 - ) + //#if MC>=11901 + //$$ private static final String REALMS_INIT = "Lnet/minecraft/client/realms/gui/screen/RealmsNotificationsScreen;init(Lnet/minecraft/client/MinecraftClient;II)V"; + //#else + private static final String REALMS_INIT = "Lnet/minecraft/client/gui/screen/Screen;init(Lnet/minecraft/client/MinecraftClient;II)V"; + //#endif + + @ModifyArg(method = "init", at = @At(value = "INVOKE", target = REALMS_INIT), index = 2) private int adjustRealmsButton(int height) { String setting = ReplayMod.instance.getSettingsRegistry().get(Setting.MAIN_MENU_BUTTON); if (MainMenuButtonPosition.valueOf(setting) == MainMenuButtonPosition.BIG) { diff --git a/versions/1.19.3/.gitkeep b/versions/1.19.3/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/1.9.4/mapping.txt b/versions/1.9.4/mapping.txt index ea42a2f1..0538db5a 100644 --- a/versions/1.9.4/mapping.txt +++ b/versions/1.9.4/mapping.txt @@ -17,6 +17,7 @@ net.minecraft.util.math.BlockPos net.minecraft.util.BlockPos net.minecraft.client.gui.GuiWorldSelection net.minecraft.client.gui.GuiSelectWorld net.minecraft.util.math.AxisAlignedBB net.minecraft.util.AxisAlignedBB net.minecraft.util.math.RayTraceResult net.minecraft.util.MovingObjectPosition +net.minecraft.client.renderer.Matrix4f net.minecraft.util.Matrix4f net.minecraft.client.renderer.RenderItem net.minecraft.client.renderer.entity.RenderItem net.minecraft.network.play.server.SPacketSpawnMob dataManager field_149043_l net.minecraft.network.play.server.SPacketSpawnMob getDataManagerEntries() func_149027_c() diff --git a/versions/mapping-fabric-1.19.3-1.19.2.txt b/versions/mapping-fabric-1.19.3-1.19.2.txt new file mode 100644 index 00000000..fa2e425d --- /dev/null +++ b/versions/mapping-fabric-1.19.3-1.19.2.txt @@ -0,0 +1,4 @@ +org.joml.Matrix3 net.minecraft.util.math.Matrix3f +org.joml.Matrix4f net.minecraft.util.math.Matrix4f +org.joml.Quaternionf net.minecraft.util.math.Quaternion +org.joml.Vector3f net.minecraft.util.math.Vec3f From ffcca61b5be383e10177aaaf3afc516ce334d286 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 7 Dec 2022 14:12:38 +0100 Subject: [PATCH 128/132] Fix running in 1.14.4 dev environment First version of modmenu in its repo appears to be for 1.15, so we need to disable it in 1.14. --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a6ac6066..6055b10f 100644 --- a/build.gradle +++ b/build.gradle @@ -386,8 +386,10 @@ dependencies { modImplementation('com.terraformersmc:modmenu:1.14.15') { exclude module: 'fabric-resource-loader-v0' // inappropriate version for 1.16.1 } - } else { + } else if (mcVersion >= 11500) { modImplementation 'com.terraformersmc:modmenu:1.10.6' + } else { + modCompileOnly 'com.terraformersmc:modmenu:1.10.6' } } From 000ff197498018d3e1af93df464a34bc96134a0b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 7 Dec 2022 14:32:16 +0100 Subject: [PATCH 129/132] Fix depth export while spectating on 1.12.2 and below (fixes #785) --- .../mixin/Mixin_PreserveDepthDuringHandRendering.java | 9 ++++++--- src/main/resources/mixins.render.replaymod.json | 4 +--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java index fb7e3877..1920db95 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java @@ -10,13 +10,16 @@ @Mixin(GameRenderer.class) public abstract class Mixin_PreserveDepthDuringHandRendering { @ModifyArg( - // FIXME preprocessor bug: 1.8.9 uses method with `(FJ)V` when just name would be enough - //#if MC>=10809 + //#if MC>=11400 method = "renderWorld", //#else - //$$ method = "updateCameraAndRender(F)V", + //$$ method = "renderWorldPass", //#endif + //#if MC>=11400 at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"), + //#else + //$$ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;clear(I)V", ordinal = 1), + //#endif index = 0 ) private int replayModRender_skipClearWhenRecordingDepth(int mask) { diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 8bd2c8b6..2759769b 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -16,6 +16,7 @@ "Mixin_Omnidirectional_Camera", "Mixin_Omnidirectional_Rotation", "Mixin_PreserveDepthDuringGuiRendering", + "Mixin_PreserveDepthDuringHandRendering", "Mixin_SkipBlockOutlinesDuringRender", "Mixin_SkipHudDuringRender", "Mixin_StabilizeCamera", @@ -34,9 +35,6 @@ //$$ "MixinChunkRenderWorker", //#endif //#endif - //#if MC>=11400 - "Mixin_PreserveDepthDuringHandRendering", - //#endif "GameRendererAccessor", "MainWindowAccessor", "WorldRendererAccessor", From 3816671207152b4ae0732035ecaec1e9dbc7e287 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 7 Dec 2022 14:33:27 +0100 Subject: [PATCH 130/132] Fix depth export on 1.14.4 --- .../Mixin_PreserveDepthDuringGuiRendering.java | 14 ++++++++++++-- .../Mixin_PreserveDepthDuringHandRendering.java | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringGuiRendering.java b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringGuiRendering.java index 4e380f62..d23765fa 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringGuiRendering.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringGuiRendering.java @@ -1,25 +1,35 @@ package com.replaymod.render.mixin; import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.GameRenderer; import org.lwjgl.opengl.GL11; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyArg; +//#if MC>=11500 @Mixin(GameRenderer.class) +//#elseif MC>=11400 +//$$ @Mixin(net.minecraft.client.util.Window.class) +//#else +//$$ @Mixin(EntityRenderer.class) +//#endif public abstract class Mixin_PreserveDepthDuringGuiRendering { @ModifyArg( - //#if MC>=11400 + //#if MC>=11500 method = "render", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"), index = 0 + //#elseif MC>=11400 + //$$ method = "method_4493", + //$$ at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/GlStateManager;clear(IZ)V"), index = 0 //#else //$$ method = "setupOverlayRendering", //$$ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;clear(I)V"), index = 0 //#endif ) private int replayModRender_skipClearWhenRecordingDepth(int mask) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this).replayModRender_getHandler(); + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MinecraftClient.getInstance().gameRenderer).replayModRender_getHandler(); if (handler != null && handler.getSettings().isDepthMap()) { mask = mask & ~GL11.GL_DEPTH_BUFFER_BIT; } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java index 1920db95..948ab355 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java @@ -15,8 +15,10 @@ public abstract class Mixin_PreserveDepthDuringHandRendering { //#else //$$ method = "renderWorldPass", //#endif - //#if MC>=11400 + //#if MC>=11500 at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"), + //#elseif MC>=11400 + //$$ at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/GlStateManager;clear(IZ)V", ordinal = 1), //#else //$$ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;clear(I)V", ordinal = 1), //#endif From 77ce90b2b0f80b4960fb4b6c6de89d5ee5ae08f8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 7 Dec 2022 14:58:47 +0100 Subject: [PATCH 131/132] Disable MixinShaderRenderChunk on 1.18+ It's only required for legacy Optifine doesn't even apply any more as of 1.18, so we can just disable it on modern versions so it doesn't spam the log. --- .../compat/shaders/mixin/MixinShaderRenderChunk.java | 2 +- src/main/resources/mixins.compat.shaders.replaymod.json | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java index b803ed2d..a692c6b0 100644 --- a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShaderRenderChunk.java @@ -1,4 +1,4 @@ -//#if MC>=10800 +//#if MC>=10800 && MC<11800 package com.replaymod.compat.shaders.mixin; import com.replaymod.render.hooks.EntityRendererHandler; diff --git a/src/main/resources/mixins.compat.shaders.replaymod.json b/src/main/resources/mixins.compat.shaders.replaymod.json index 163e36b0..b2826f7f 100644 --- a/src/main/resources/mixins.compat.shaders.replaymod.json +++ b/src/main/resources/mixins.compat.shaders.replaymod.json @@ -7,9 +7,11 @@ //#if MC>=11500 "MixinChunkVisibility", //#endif + //#if MC>=10800 && MC<11800 + "MixinShaderRenderChunk", + //#endif //#if MC>=10800 "MixinShaderEntityRenderer", - "MixinShaderRenderChunk", //#else //$$ "MixinShaders", //#endif From 043664ab0c66c0ce897293a2c5182041777ede49 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 9 Dec 2022 13:14:56 +0100 Subject: [PATCH 132/132] Fix multiplayer recording on 1.19.3 (fixes #803) --- .../handler/ConnectionEventHandler.java | 26 ++++++++++++++----- .../ClientLoginNetworkHandlerAccessor.java | 17 ++++++++++++ .../resources/mixins.recording.replaymod.json | 1 + 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/replaymod/recording/mixin/ClientLoginNetworkHandlerAccessor.java diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 876430fc..1137485f 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -19,6 +19,10 @@ import net.minecraft.network.ClientConnection; import org.apache.logging.log4j.Logger; +//#if MC>=11900 +//$$ import com.replaymod.recording.mixin.ClientLoginNetworkHandlerAccessor; +//#endif + //#if MC>=11600 import net.minecraft.world.World; //#else @@ -88,6 +92,15 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { } } + ServerInfo serverInfo; + //#if MC>=11903 + //$$ serverInfo = networkManager.getPacketListener() instanceof ClientLoginNetworkHandlerAccessor loginNetworkHandler + //$$ ? loginNetworkHandler.getServerInfo() + //$$ : null; + //#else + serverInfo = mc.getCurrentServerEntry(); + //#endif + String worldName; String serverName = null; boolean autoStart = core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING); @@ -98,8 +111,12 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { //$$ worldName = mc.getServer().getLevelName(); //#endif serverName = worldName; - } else if (mc.getCurrentServerEntry() != null) { - ServerInfo serverInfo = mc.getCurrentServerEntry(); + //#if MC>=11100 + } else if (mc.isConnectedToRealms()) { + // we can't access the server name without tapping too deep in the Realms Library + worldName = "A Realms Server"; + //#endif + } else if (serverInfo != null) { worldName = serverInfo.address; if (!I18n.translate("selectServer.defaultName").equals(serverInfo.name)) { serverName = serverInfo.name; @@ -109,11 +126,6 @@ public void onConnectedToServerEvent(ClientConnection networkManager) { if (autoStartServer != null) { autoStart = autoStartServer; } - //#if MC>=11100 - } else if (mc.isConnectedToRealms()) { - // we can't access the server name without tapping too deep in the Realms Library - worldName = "A Realms Server"; - //#endif } else { logger.info("Recording not started as the world is neither local nor remote (probably a replay)."); return; diff --git a/src/main/java/com/replaymod/recording/mixin/ClientLoginNetworkHandlerAccessor.java b/src/main/java/com/replaymod/recording/mixin/ClientLoginNetworkHandlerAccessor.java new file mode 100644 index 00000000..37990587 --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/ClientLoginNetworkHandlerAccessor.java @@ -0,0 +1,17 @@ +package com.replaymod.recording.mixin; + +import net.minecraft.client.network.ClientLoginNetworkHandler; +import org.spongepowered.asm.mixin.Mixin; + +//#if MC>=11903 +//$$ import net.minecraft.client.network.ServerInfo; +//$$ import org.spongepowered.asm.mixin.gen.Accessor; +//#endif + +@Mixin(ClientLoginNetworkHandler.class) +public interface ClientLoginNetworkHandlerAccessor { + //#if MC>=11903 + //$$ @Accessor + //$$ ServerInfo getServerInfo(); + //#endif +} diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index 5dec2e07..df7f09ea 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -5,6 +5,7 @@ "server": [], "client": [ "AddServerScreenAccessor", + "ClientLoginNetworkHandlerAccessor", "EntityLivingBaseAccessor", "IntegratedServerAccessor", "NetworkManagerAccessor",