From d2f27e391893d4357266a982323194fec54840f7 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 14 Nov 2021 16:08:35 +0100 Subject: [PATCH 01/27] 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 02/27] 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 net.minecraft.world.chunk.WorldChunk; 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 @@ class Patterns { 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.ReplayMod; 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 class MarkerProcessor { 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 class MarkerProcessor { 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 class MarkerProcessor { 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 class Util { } 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 @@ import java.util.concurrent.locks.ReentrantLock; 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 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 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 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend //#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 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend //#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 03/27] 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 @@ import net.minecraft.client.render.WorldRenderer; 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 @@ import org.spongepowered.asm.mixin.injection.ModifyArg; @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 04/27] 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 @@ class Patterns { 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 05/27] 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 06/27] 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 07/27] 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 org.spongepowered.asm.mixin.gen.Accessor; 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 @@ 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.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.Tessellator; 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 @@ class Patterns { 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.ReplayModReplay; 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.GameRenderer; 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 abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { @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 @@ public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { 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 @@ public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { // 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 @@ public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { 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 08/27] 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.recording.ReplayModRecording; 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.Text; 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 class ReplayMod implements Module, Scheduler { 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 class ReplayMod implements Module, Scheduler { } //#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 class ReplayMod implements Module, Scheduler { 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 @@ public class RestoreReplayGui extends AbstractGuiScreen { } 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 @@ public class GuiEditReplay extends AbstractGuiPopup { 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 @@ public class GuiEditReplay extends AbstractGuiPopup { 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 class MarkerProcessor { 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 class MarkerProcessor { 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 class MarkerProcessor { 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 class MarkerProcessor { 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 @@ public class GuiSavingReplay { } 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 class ConnectionEventHandler { } 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 class PacketListener extends ChannelInboundHandlerAdapter { // 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 class GuiRenderQueue extends AbstractGuiPopup implements 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 class ReplayModReplay implements Module { } 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 class GuiReplayViewer extends GuiScreen { @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 class GuiReplayViewer extends GuiScreen { 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 class GuiReplayViewer extends GuiScreen { 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 09/27] 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.Files; 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 class ReplayFilesService { } 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 class ReplayFilesService { 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 10/27] 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 class CameraEntity 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 11/27] 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 class RealtimeTimelinePlayer extends AbstractTimelinePlayer { @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 12/27] 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.ExecutionException; 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 abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { @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 @@ public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading { } } + // 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 13/27] 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 14/27] 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.SignEditorOpenS2CPacket; 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 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend 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 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend 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 15/27] 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 16/27] 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 class RenderJob { } 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 17/27] 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.gui.screen.Screen; 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 @@ import org.lwjgl.glfw.GLFW; //#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 class MCVer { } //#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.Entity; 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 @@ class Patterns { 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 18/27] 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.MinecraftClient; 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 class CameraEntity 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 class CameraEntity 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 @@ public class CameraEntity 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 19/27] 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 class CameraEntity this.lastRenderX = to.lastRenderX; this.lastRenderY = to.lastRenderY + yOffset; this.lastRenderZ = to.lastRenderZ; + this.wrapArmYaw(); updateBoundingBox(); } @@ -703,7 +704,35 @@ public class CameraEntity 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 20/27] 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 21/27] 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 @@ import net.minecraft.client.render.WorldRenderer; 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 22/27] 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 @@ import java.util.Collections; //#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 @@ public class RecordingEventHandler extends EventRegistrations { 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 23/27] 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 @@ public class GuiRecordingControls extends EventRegistrations { 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 24/27] 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 @@ public class GuiPathing { 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 class GuiPathing { 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 25/27] 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 class FullReplaySender extends ChannelDuplexHandler implements ReplaySend //#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 26/27] 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 27/27] 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