diff --git a/build.gradle b/build.gradle index 59fd9e7b..e9ba988b 100644 --- a/build.gradle +++ b/build.gradle @@ -245,6 +245,7 @@ dependencies { 11701: '1.17.1', 11800: '1.18', 11801: '1.18.1', + 11802: '1.18.2', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', @@ -256,6 +257,7 @@ dependencies { 11701: '1.17.1+build.29:v2', 11800: '1.18+build.1:v2', 11801: '1.18.1+build.1:v2', + 11802: '1.18.2+build.1:v2', ][mcVersion] modImplementation 'net.fabricmc:fabric-loader:0.12.5' def fabricApiVersion = [ @@ -268,6 +270,7 @@ dependencies { 11701: '0.37.1+1.17', 11800: '0.43.1+1.18', 11801: '0.43.1+1.18', + 11802: '0.47.9+1.18.2', ][mcVersion] def fabricApiModules = [ "api-base", @@ -338,7 +341,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:70f59ef", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:b5539d1", shadeExclusions implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those @@ -346,21 +349,27 @@ dependencies { shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66' if (FABRIC) { - if (mcVersion >= 11800) { + if (mcVersion >= 11802) { + modImplementation 'com.terraformersmc:modmenu:3.1.0' + } else if (mcVersion >= 11800) { modImplementation 'com.terraformersmc:modmenu:3.0.0' } else if (mcVersion >= 11700) { modImplementation 'com.terraformersmc:modmenu:2.0.0-beta.7' } else if (mcVersion >= 11602) { modImplementation 'com.terraformersmc:modmenu:1.16.8' } else if (mcVersion >= 11600) { - modImplementation 'io.github.prospector:modmenu:1.14.0+build.24' + modImplementation('com.terraformersmc:modmenu:1.14.15') { + exclude module: 'fabric-resource-loader-v0' // inappropriate version for 1.16.1 + } } else { - modImplementation 'io.github.prospector.modmenu:ModMenu:1.6.2-92' + modImplementation 'com.terraformersmc:modmenu:1.10.6' } } if (mcVersion >= 11600) { - modCompileOnly 'com.github.IrisShaders:Iris:1.0.0' + modCompileOnly("com.github.IrisShaders:Iris:1.18.x~v1.2.0") { + transitive = false // we do not want to upgrade our libs, we only need this to compile our mixins + } } testImplementation 'junit:junit:4.11' diff --git a/docs/content.md b/docs/content.md index faea3ff2..0681006f 100755 --- a/docs/content.md +++ b/docs/content.md @@ -465,7 +465,7 @@ If you have a Replay in a dark setting (for example at nighttime, or in a cave) This works as a replacement for the **Night Vision Potion Effect**, without the side effect of a weird sky color. -## Quick Mode [quickmode] (Minecraft 1.9 and up) +## Quick Mode [quickmode] ![](img/quickmode-icon.jpg) In **Quick Mode**, this clock symbol is displayed in the lower right corner of the screen. @@ -473,8 +473,6 @@ When you first enable **Quick Mode** in a replay, an internal reference of certa As a side effect, certain features like particles and second skin layers will not be rendered in the preview. By default, **Quick Mode** is toggled with `Q`. -**Quick Mode** is available in ReplayMod for Minecraft 1.9.4 and up. - ## Player Overview [overview] ![](img/player-overview.jpg) The **Player Overview** Screen diff --git a/jGui b/jGui index c79b62a7..5e41452b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit c79b62a73e649fd6d16a1ffbd9e320555834bc46 +Subproject commit 5e41452b0e17700691efd38b8ce793b5200145c8 diff --git a/root.gradle.kts b/root.gradle.kts index f94b12fc..8ee13ad4 100755 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -189,6 +189,7 @@ val doRelease by tasks.registering { defaultTasks("bundleJar") preprocess { + val mc11802 = createNode("1.18.2", 11802, "yarn") val mc11801 = createNode("1.18.1", 11801, "yarn") val mc11701 = createNode("1.17.1", 11701, "yarn") val mc11700 = createNode("1.17", 11700, "yarn") @@ -208,6 +209,7 @@ preprocess { val mc10800 = createNode("1.8", 10800, "srg") val mc10710 = createNode("1.7.10", 10710, "srg") + mc11802.link(mc11801) mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt")) mc11701.link(mc11700) mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2030c3cd..a77be868 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,6 +32,7 @@ val jGuiVersions = listOf( "1.17", "1.17.1", "1.18.1", + "1.18.2", ) val replayModVersions = listOf( // "1.7.10", @@ -52,6 +53,7 @@ val replayModVersions = listOf( "1.17", "1.17.1", "1.18.1", + "1.18.2", ) rootProject.buildFileName = "root.gradle.kts" diff --git a/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java b/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java index 08259e21..443a21e0 100644 --- a/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java +++ b/src/main/java/com/replaymod/core/ReplayModMixinConfigPlugin.java @@ -9,6 +9,10 @@ import java.io.IOException; import java.util.List; import java.util.Set; +//#if FABRIC +import net.fabricmc.loader.api.FabricLoader; +//#endif + //#if MC>=11400 import java.io.InputStream; //#else @@ -34,6 +38,9 @@ public class ReplayModMixinConfigPlugin implements IMixinConfigPlugin { private final Logger logger = LogManager.getLogger("replaymod/mixin"); private final boolean hasOF = hasClass("optifine.OptiFineForgeTweaker") || hasClass("me.modmuss50.optifabric.mod.Optifabric"); + //#if FABRIC + private final boolean hasIris = FabricLoader.getInstance().isModLoaded("iris"); + //#endif { logger.debug("hasOF: " + hasOF); @@ -49,6 +56,9 @@ public class ReplayModMixinConfigPlugin implements IMixinConfigPlugin { } if (mixinClassName.endsWith("_OF")) return hasOF; if (mixinClassName.endsWith("_NoOF")) return !hasOF; + //#if FABRIC + if (mixinClassName.endsWith("_Iris")) return hasIris; + //#endif return true; } diff --git a/src/main/java/com/replaymod/core/SettingsRegistryBackend.java b/src/main/java/com/replaymod/core/SettingsRegistryBackend.java index eb996915..90f50d86 100644 --- a/src/main/java/com/replaymod/core/SettingsRegistryBackend.java +++ b/src/main/java/com/replaymod/core/SettingsRegistryBackend.java @@ -22,6 +22,7 @@ import java.nio.file.WatchService; import java.util.List; import java.util.Map; +import static com.replaymod.core.utils.Utils.ensureDirectoryExists; import static com.replaymod.core.versions.MCVer.getMinecraft; class SettingsRegistryBackend { @@ -179,7 +180,7 @@ class SettingsRegistryBackend { Gson gson = new GsonBuilder().setPrettyPrinting().create(); String config = gson.toJson(root); try { - Files.createDirectories(configFile.getParent()); + ensureDirectoryExists(configFile.getParent()); Files.write(configFile, config.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/replaymod/core/files/ReplayFoldersService.java b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java index 6587704d..2803b891 100644 --- a/src/main/java/com/replaymod/core/files/ReplayFoldersService.java +++ b/src/main/java/com/replaymod/core/files/ReplayFoldersService.java @@ -10,6 +10,8 @@ import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Path; +import static com.replaymod.core.utils.Utils.ensureDirectoryExists; + public class ReplayFoldersService { private final Path mcDir = MinecraftClient.getInstance().runDirectory.toPath(); private final SettingsRegistry settings; @@ -19,14 +21,14 @@ public class ReplayFoldersService { } public Path getReplayFolder() throws IOException { - return Files.createDirectories(mcDir.resolve(settings.get(Setting.RECORDING_PATH))); + return ensureDirectoryExists(mcDir.resolve(settings.get(Setting.RECORDING_PATH))); } /** * Folder into which replay backups are saved before the MarkerProcessor is unleashed. */ public Path getRawReplayFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("raw")); + return ensureDirectoryExists(getReplayFolder().resolve("raw")); } /** @@ -34,7 +36,7 @@ public class ReplayFoldersService { * Distinct from the main folder, so they cannot be opened while they are still saving. */ public Path getRecordingFolder() throws IOException { - return Files.createDirectories(getReplayFolder().resolve("recording")); + return ensureDirectoryExists(getReplayFolder().resolve("recording")); } /** @@ -42,7 +44,7 @@ public class ReplayFoldersService { * Distinct from the recording folder cause people kept confusing them with recordings. */ public Path getCacheFolder() throws IOException { - Path path = Files.createDirectories(mcDir.resolve(settings.get(Setting.CACHE_PATH))); + Path path = ensureDirectoryExists(mcDir.resolve(settings.get(Setting.CACHE_PATH))); try { Files.setAttribute(path, "dos:hidden", true); } catch (UnsupportedOperationException ignored) { diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index 3a9f13c0..c68a3210 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -56,11 +56,16 @@ public class RestoreReplayGui extends AbstractGuiScreen { new GuiLabel().setI18nText("replaymod.gui.restorereplay1"), new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())), new GuiLabel().setI18nText("replaymod.gui.restorereplay3")); + + LOGGER.info("Found partially saved replay, offering recovery: " + file); + yesButton.onClick(() -> { + LOGGER.info("Attempting recovery: " + file); recoverInBackground(); parent.display(); }); noButton.onClick(() -> { + LOGGER.info("Recovery rejected, marking for deletion: " + file); try { File tmp = new File(file.getParentFile(), file.getName() + ".tmp"); File deleted = new File(file.getParentFile(), file.getName() + ".del"); diff --git a/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java b/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java index 62682744..754cd090 100644 --- a/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java +++ b/src/main/java/com/replaymod/core/mixin/MixinMinecraft.java @@ -63,6 +63,7 @@ public abstract class MixinMinecraft PostRenderCallback.EVENT.invoker().postRender(); } //#else + //$$ @Shadow long systemTime; //#if MC>=10904 //$$ @Shadow protected abstract void runTickKeyboard() throws IOException; //$$ @Shadow protected abstract void runTickMouse() throws IOException; @@ -80,6 +81,8 @@ public abstract class MixinMinecraft //$$ public void replayModRunTickMouse() { //$$ try { //$$ runTickMouse(); + //$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms) + //$$ systemTime = Minecraft.getSystemTime(); //$$ } catch (IOException e) { //$$ e.printStackTrace(); //$$ } @@ -94,7 +97,12 @@ public abstract class MixinMinecraft //$$ //$$ @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;sendClickBlockToController(Z)V"), cancellable = true) //$$ private void doEarlyReturnFromRunTick(CallbackInfo ci) { - //$$ if (earlyReturn) ci.cancel(); + //$$ if (earlyReturn) { + //$$ ci.cancel(); + //$$ + //$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms) + //$$ systemTime = Minecraft.getSystemTime(); + //$$ } //$$ } //#endif //$$ @Redirect( diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 50c328c8..688edc7c 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -55,8 +55,13 @@ import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -166,8 +171,45 @@ public class Utils { private static final PercentEscaper REPLAY_NAME_ENCODER = new PercentEscaper(".-_ ", false); - public static String replayNameToFileName(String replayName) { - return REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"; + public static Path replayNameToPath(Path folder, String replayName) { + // If we can, prefer directly using the replay name as the file name + if (isUsable(folder, replayName + ".mcpr")) { + return folder.resolve(replayName + ".mcpr"); + } else { + // otherwise, fall back to percent encoding + return folder.resolve(REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"); + } + } + + /** + * Checks whether a given file name is actually usable with the file system / operating system at the given folder. + */ + private static boolean isUsable(Path folder, String fileName) { + Path path = folder.resolve(fileName); + if (Files.exists(path)) { + return true; // if it already exits, it's definitely usable + } + + // Otherwise, there's no sure way to know, so we just gotta try + try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) { + outputStream.flush(); + } catch (IOException e) { + return false; + } + + // Looking good, but now we gotta clean up that mess (and Anti-Virus / Cloud Sync are know to lock them) + int attempts = 0; + while (true) { + try { + Files.delete(path); + return true; + } catch (IOException e) { + if (attempts++ > 100) { + LOGGER.warn("Repeatedly failed to clean up temporary test file at " + path + ": ", e); + return false; // while we were able to use it, it's taken now and we can't get it back + } + } + } } public static String fileNameToReplayName(String fileName) { @@ -356,4 +398,14 @@ public class Utils { configure.accept(instance); return instance; } + + /** + * Like {@link Files#createDirectories(Path, FileAttribute[])} but doesn't explode if it's a symlink. + */ + public static Path ensureDirectoryExists(Path path) throws IOException { + // Who in their right mind thought the default behavior of throwing when the target is a link to a directory + // was the preferred behavior?! Everyone has to fall for this at least once to learn it... + // https://bugs.openjdk.java.net/browse/JDK-8130464 + return Files.createDirectories(Files.exists(path) ? path.toRealPath() : path); + } } diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index cb7c7f00..c239d9b0 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -304,7 +304,7 @@ public class MCVer { //#if MC<=11601 //$$ public static Vec3d getTrackedPosition(Entity entity) { - //$$ return new Vec3d(entity.trackedX, entity.trackedY, entity.trackedZ); + //$$ return new Vec3d(entity.trackedX / 4096.0, entity.trackedY / 4096.0, entity.trackedZ / 4096.0); //$$ } //#endif diff --git a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java index 205d4059..0d5aefc8 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java +++ b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java @@ -21,6 +21,8 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.lwjgl.Color; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.util.crash.CrashReport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.IOException; import java.nio.file.Path; @@ -31,6 +33,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; public class GuiEditReplay extends AbstractGuiPopup { + private static final Logger LOGGER = LogManager.getLogger(); + private final Path inputPath; private final EditTimeline timeline; @@ -60,6 +64,8 @@ public class GuiEditReplay extends AbstractGuiPopup { super(container); this.inputPath = inputPath; + LOGGER.info("Opening replay in editor: " + inputPath); + try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) { markers = replayFile.getMarkers().or(HashSet::new); timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers); diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index 0bce65f3..7231cecf 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -133,7 +133,6 @@ public class MarkerProcessor { for (int i = 1; Files.exists(inputPath); i++) { inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr"); } - Files.createDirectories(inputPath.getParent()); Files.move(path, inputPath); try (ReplayFile inputReplayFile = mod.files.open(inputPath)) { diff --git a/src/main/java/com/replaymod/extras/QuickMode.java b/src/main/java/com/replaymod/extras/QuickMode.java index 454d20be..ab1f677d 100644 --- a/src/main/java/com/replaymod/extras/QuickMode.java +++ b/src/main/java/com/replaymod/extras/QuickMode.java @@ -25,7 +25,7 @@ public class QuickMode extends EventRegistrations implements Extra { return; } replayHandler.getReplaySender().setSyncModeAndWait(); - mod.runLater(() -> { + mod.runLaterWithoutLock(() -> { replayHandler.ensureQuickModeInitialized(() -> { boolean enabled = !replayHandler.isQuickMode(); updateIndicator(replayHandler.getOverlay(), enabled); diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java index 62d316cf..cda0409d 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java @@ -40,7 +40,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable { new GuiLabel().setI18nText("replaymod.gui.advancedscreenshots.resolution"), videoResolutionPanel, new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton); - resetChildren(advancedPanel).addElements(null, nametagCheckbox, new GuiPanel().setLayout( + resetChildren(advancedPanel).addElements(null, nametagCheckbox, alphaCheckbox , new GuiPanel().setLayout( new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15)) .addElements(new GridLayout.Data(0, 0.5), new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, diff --git a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java index 6fb66db9..9e33903a 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java +++ b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java @@ -155,9 +155,9 @@ public class GuiSavingReplay { try { Path replaysFolder = core.folders.getReplayFolder(); - Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName)); + Path newPath = Utils.replayNameToPath(replaysFolder, newName); for (int i = 1; Files.exists(newPath); i++) { - newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")")); + newPath = Utils.replayNameToPath(replaysFolder, newName + " (" + i + ")"); } Files.move(path, newPath); } catch (IOException e) { diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 60a43a50..0211c8e0 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -127,7 +127,7 @@ public class ConnectionEventHandler { } String name = sdf.format(Calendar.getInstance().getTime()); - Path outputPath = core.folders.getRecordingFolder().resolve(Utils.replayNameToFileName(name)); + Path outputPath = Utils.replayNameToPath(core.folders.getRecordingFolder(), name); ReplayFile replayFile = core.files.open(outputPath); replayFile.writeModInfo(ModCompat.getInstalledNetworkMods()); diff --git a/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java index 01e9572f..c7e8f5d4 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java @@ -33,7 +33,9 @@ public abstract class MixinMouseHelper { long _p0, double _p1, double _p2, CallbackInfo ci, double _l1, - //#if MC>=11400 + //#if MC>=11802 + //$$ int yOffsetAccumulated + //#elseif MC>=11400 float yOffsetAccumulated //#else //$$ double yOffsetAccumulated diff --git a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java index e5c79889..55bc21b2 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java @@ -16,6 +16,10 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11802 +//$$ import net.minecraft.util.registry.RegistryEntry; +//#endif + //#if MC>=11600 import net.minecraft.util.registry.RegistryKey; import net.minecraft.world.MutableWorldProperties; @@ -52,7 +56,12 @@ public abstract class MixinWorldClient extends World implements RecordingEventHa //#if MC<11602 //$$ RegistryKey registryKey2, //#endif - DimensionType dimensionType, Supplier profiler, boolean bl, boolean bl2, long l) { + //#if MC>=11802 + //$$ RegistryEntry dimensionType, + //#else + DimensionType dimensionType, + //#endif + Supplier profiler, boolean bl, boolean bl2, long l) { super(mutableWorldProperties, registryKey, //#if MC<11602 //$$ registryKey2, diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index e37fefad..f99833a5 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -285,7 +285,6 @@ public class PacketListener extends ChannelInboundHandlerAdapter { for (int i = 1; Files.exists(rawPath); i++) { rawPath = rawPath.resolveSibling(replayName + "." + i + ".mcpr"); } - Files.createDirectories(rawPath.getParent()); replayFile.saveTo(rawPath.toFile()); replayFile.close(); diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index 1b2348fe..0bf981d8 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -28,9 +28,11 @@ import static org.lwjgl.util.tinyexr.TinyEXR.*; public class EXRWriter implements FrameConsumer { private final Path outputFolder; + private final boolean keepAlpha; - public EXRWriter(Path outputFolder) throws IOException { + public EXRWriter(Path outputFolder, boolean keepAlpha) throws IOException { this.outputFolder = outputFolder; + this.keepAlpha = keepAlpha; Files.createDirectories(outputFolder); } @@ -92,11 +94,15 @@ public class EXRWriter implements FrameConsumer { bgrChannels[(i + 3) % 4] = channel; } } + + int alphaMask = keepAlpha ? 0 : 0xff; + for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - for (FloatBuffer channel : bgrChannels) { - channel.put(((int) bgra.get() & 0xff) / 255f); - } + bgrChannels[0].put(((int) bgra.get() & 0xff) / 255f); // b + bgrChannels[1].put(((int) bgra.get() & 0xff) / 255f); // g + bgrChannels[2].put(((int) bgra.get() & 0xff) / 255f); // r + bgrChannels[3].put(((int) bgra.get() & 0xff | alphaMask) / 255f); // a } } if (depthFrame != null && depthChannel != null) { diff --git a/src/main/java/com/replaymod/render/PNGWriter.java b/src/main/java/com/replaymod/render/PNGWriter.java index 846e63be..525d154d 100644 --- a/src/main/java/com/replaymod/render/PNGWriter.java +++ b/src/main/java/com/replaymod/render/PNGWriter.java @@ -19,9 +19,11 @@ import java.util.Map; public class PNGWriter implements FrameConsumer { private final Path outputFolder; + private final boolean keepAlpha; - public PNGWriter(Path outputFolder) throws IOException { + public PNGWriter(Path outputFolder, boolean keepAlpha) throws IOException { this.outputFolder = outputFolder; + this.keepAlpha = keepAlpha; Files.createDirectories(outputFolder); } @@ -47,6 +49,7 @@ public class PNGWriter implements FrameConsumer { } private void withImage(BitmapFrame frame, IOConsumer consumer) throws IOException { + byte alphaMask = (byte) (keepAlpha ? 0 : 0xff); ByteBuffer buffer = frame.getByteBuffer(); ReadableDimension size = frame.getSize(); int width = size.getWidth(); @@ -58,7 +61,7 @@ public class PNGWriter implements FrameConsumer { byte g = buffer.get(); byte r = buffer.get(); byte a = buffer.get(); - image.setRGBA(x, y, r, g, b, a); + image.setRGBA(x, y, r, g, b, a | alphaMask); } } consumer.accept(image); diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index e83d4947..b358b718 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -151,6 +151,7 @@ public class RenderSettings { private final File outputFile; private final boolean renderNameTags; + private final boolean includeAlphaChannel; private final boolean stabilizeYaw; private final boolean stabilizePitch; private final boolean stabilizeRoll; @@ -187,6 +188,7 @@ public class RenderSettings { false, false, false, + false, null, 360, 180, @@ -209,6 +211,7 @@ public class RenderSettings { int bitRate, File outputFile, boolean renderNameTags, + boolean includeAlphaChannel, boolean stabilizeYaw, boolean stabilizePitch, boolean stabilizeRoll, @@ -231,6 +234,7 @@ public class RenderSettings { this.bitRate = bitRate; this.outputFile = outputFile; this.renderNameTags = renderNameTags; + this.includeAlphaChannel = includeAlphaChannel; this.stabilizeYaw = stabilizeYaw; this.stabilizePitch = stabilizePitch; this.stabilizeRoll = stabilizeRoll; @@ -256,6 +260,7 @@ public class RenderSettings { bitRate, outputFile, renderNameTags, + includeAlphaChannel, stabilizeYaw, stabilizePitch, stabilizeRoll, @@ -415,6 +420,10 @@ public class RenderSettings { return renderNameTags; } + public boolean isIncludeAlphaChannel() { + return includeAlphaChannel; + } + public boolean isStabilizeYaw() { return stabilizeYaw; } @@ -478,6 +487,7 @@ public class RenderSettings { ", bitRate=" + bitRate + ", outputFile=" + outputFile + ", renderNameTags=" + renderNameTags + + ", includeAlphaChannel=" + includeAlphaChannel + ", stabilizeYaw=" + stabilizeYaw + ", stabilizePitch=" + stabilizePitch + ", stabilizeRoll=" + stabilizeRoll + diff --git a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java index a1d31055..6671d80a 100644 --- a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java @@ -1,7 +1,6 @@ //#if MC>=11600 package com.replaymod.render.capturer; -import com.mojang.blaze3d.platform.GlStateManager; import com.replaymod.render.RenderSettings; import com.replaymod.render.frame.CubicOpenGlFrame; import com.replaymod.render.frame.ODSOpenGlFrame; @@ -16,18 +15,13 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static com.replaymod.core.versions.MCVer.popMatrix; -import static com.replaymod.core.versions.MCVer.pushMatrix; -import static com.replaymod.core.versions.MCVer.resizeMainWindow; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; - public class IrisODSFrameCapturer implements FrameCapturer { public static final String SHADER_PACK_NAME = "assets/replaymod/iris/ods"; public static IrisODSFrameCapturer INSTANCE; private final CubicPboOpenGlFrameCapturer left, right; private final String prevShaderPack; + private final boolean prevShadersEnabled; private int direction; private boolean isLeftEye; @@ -67,13 +61,16 @@ public class IrisODSFrameCapturer implements FrameCapturer { right = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize); INSTANCE = this; - prevShaderPack = Iris.getIrisConfig().getShaderPackName().orElse(null); - setShaderPack(SHADER_PACK_NAME); + IrisConfig irisConfig = Iris.getIrisConfig(); + prevShaderPack = irisConfig.getShaderPackName().orElse(null); + prevShadersEnabled = irisConfig.areShadersEnabled(); + setShaderPack(SHADER_PACK_NAME, true); } - private static void setShaderPack(String name) { + private static void setShaderPack(String name, boolean enabled) { IrisConfig irisConfig = Iris.getIrisConfig(); irisConfig.setShaderPackName(name); + irisConfig.setShadersEnabled(enabled); try { irisConfig.save(); Iris.reload(); @@ -121,7 +118,7 @@ public class IrisODSFrameCapturer implements FrameCapturer { left.close(); right.close(); INSTANCE = null; - setShaderPack(prevShaderPack); + setShaderPack(prevShaderPack, prevShadersEnabled); } private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer { @@ -131,25 +128,8 @@ public class IrisODSFrameCapturer implements FrameCapturer { @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { - resizeMainWindow(mc, getFrameWidth(), getFrameHeight()); - - pushMatrix(); - frameBuffer().beginWrite(true); - - GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT - //#if MC>=11400 - , false - //#endif - ); - GlStateManager.enableTexture(); - direction = captureData.ordinal(); - worldRenderer.renderWorld(partialTicks, null); - - frameBuffer().endWrite(); - popMatrix(); - - return captureFrame(frameId, captureData); + return super.renderFrame(frameId, partialTicks, captureData); } } } diff --git a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java index 4df2fa18..925257fe 100644 --- a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java @@ -1,6 +1,5 @@ package com.replaymod.render.capturer; -import com.mojang.blaze3d.platform.GlStateManager; import com.replaymod.render.rendering.Channel; import de.johni0702.minecraft.gui.utils.EventRegistrations; import com.replaymod.render.RenderSettings; @@ -20,12 +19,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static com.replaymod.core.versions.MCVer.popMatrix; -import static com.replaymod.core.versions.MCVer.pushMatrix; -import static com.replaymod.core.versions.MCVer.resizeMainWindow; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; - public class ODSFrameCapturer implements FrameCapturer { private static final Identifier vertexResource = new Identifier("replaymod", "shader/ods.vert"); private static final Identifier fragmentResource = new Identifier("replaymod", "shader/ods.frag"); @@ -166,25 +159,8 @@ public class ODSFrameCapturer implements FrameCapturer { @Override protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { - resizeMainWindow(mc, getFrameWidth(), getFrameHeight()); - - pushMatrix(); - frameBuffer().beginWrite(true); - - GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT - //#if MC>=11400 - , false - //#endif - ); - GlStateManager.enableTexture(); - directionVariable.set(captureData.ordinal()); - worldRenderer.renderWorld(partialTicks, null); - - frameBuffer().endWrite(); - popMatrix(); - - return captureFrame(frameId, captureData); + return super.renderFrame(frameId, partialTicks, captureData); } } } diff --git a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java index de5e9c0e..56f9229f 100644 --- a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java +++ b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java @@ -90,6 +90,7 @@ public class GuiExportFailed extends GuiScreen { oldSettings.getBitRate(), oldSettings.getOutputFile(), oldSettings.isRenderNameTags(), + oldSettings.isIncludeAlphaChannel(), oldSettings.isStabilizeYaw(), oldSettings.isStabilizePitch(), oldSettings.isStabilizeRoll(), diff --git a/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java b/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java new file mode 100644 index 00000000..6261f861 --- /dev/null +++ b/src/main/java/com/replaymod/render/gui/GuiNoFfmpeg.java @@ -0,0 +1,48 @@ +package com.replaymod.render.gui; + +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; + +import java.net.URI; + +import static com.replaymod.core.versions.MCVer.openURL; +import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString; + +public class GuiNoFfmpeg extends GuiScreen { + + private static final String LINK = "https://www.replaymod.com/docs/#installing-ffmpeg"; + + private final GuiLabel message = new GuiLabel() + .setI18nText("replaymod.gui.rendering.error.message"); + private final GuiLabel link = new GuiLabel() + .setText(LINK); + private final GuiButton openLinkButton = new GuiButton() + .setI18nLabel("chat.link.open") + .setSize(100, 20) + .onClick(() -> openURL(URI.create(LINK))); + private final GuiButton copyToClipboardButton = new GuiButton() + .setI18nLabel("chat.copy") + .setSize(100, 20) + .onClick(() -> setClipboardString(LINK)); + private final GuiButton backButton = new GuiButton() + .setI18nLabel("gui.back") + .setSize(100, 20); + private final GuiPanel buttons = new GuiPanel() + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(4)) + .addElements(null, openLinkButton, copyToClipboardButton, backButton); + + { + setBackground(Background.DIRT); + setTitle(new GuiLabel().setI18nText("replaymod.gui.rendering.error.title")); + setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(30)); + addElements(new VerticalLayout.Data(0.5), message, link, buttons); + } + + public GuiNoFfmpeg(Runnable goBack) { + backButton.onClick(goBack); + } +} diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index db37e673..86ca19dc 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -37,7 +37,6 @@ import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.NoticeScreen; import net.minecraft.util.crash.CrashReport; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; @@ -181,17 +180,7 @@ public class GuiRenderQueue extends AbstractGuiPopup implements videoRenderer.renderVideo(); } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); - NoticeScreen errorScreen = new NoticeScreen( - //#if MC>=11400 - container::display, - new TranslatableText("replaymod.gui.rendering.error.title"), - new TranslatableText("replaymod.gui.rendering.error.message") - //#else - //$$ I18n.format("replaymod.gui.rendering.error.title"), - //$$ I18n.format("replaymod.gui.rendering.error.message") - //#endif - ); - mc.openScreen(errorScreen); + mc.openScreen(new GuiNoFfmpeg(container::display).toMinecraft()); return; } catch (FFmpegWriter.FFmpegStartupException e) { int jobsToSkip = jobsDone; @@ -229,7 +218,7 @@ public class GuiRenderQueue extends AbstractGuiPopup implements ReplayFile replayFile = null; try { replayFile = mod.getCore().files.open(next.getKey().toPath()); - replayHandler = mod.startReplay(replayFile, true, false); + replayHandler = mod.startReplay(replayFile, false, false); } catch (IOException e) { Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {}); container.display(); // Re-show the queue popup and the new error popup diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 38d80a27..170aa21a 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -30,7 +30,6 @@ import de.johni0702.minecraft.gui.utils.Utils; import de.johni0702.minecraft.gui.utils.lwjgl.Color; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; -import net.minecraft.client.gui.screen.NoticeScreen; import net.minecraft.client.resource.language.I18n; import net.minecraft.util.crash.CrashReport; @@ -149,6 +148,9 @@ public class GuiRenderSettings extends AbstractGuiPopup { public final GuiCheckbox nametagCheckbox = new GuiCheckbox() .setI18nLabel("replaymod.gui.rendersettings.nametags"); + public final GuiCheckbox alphaCheckbox = new GuiCheckbox() + .setI18nLabel("replaymod.gui.rendersettings.includealpha"); + public final GuiPanel stabilizePanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(10)); public final GuiCheckbox stabilizeYaw = new GuiCheckbox(stabilizePanel) .setI18nLabel("replaymod.gui.yaw"); @@ -188,7 +190,7 @@ public class GuiRenderSettings extends AbstractGuiPopup { .setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE); public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15)) - .addElements(null, nametagCheckbox, new GuiPanel().setLayout( + .addElements(null, nametagCheckbox, alphaCheckbox, new GuiPanel().setLayout( new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15)) .addElements(new GridLayout.Data(0, 0.5), new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, @@ -240,17 +242,7 @@ public class GuiRenderSettings extends AbstractGuiPopup { videoRenderer.renderVideo(); } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); - NoticeScreen errorScreen = new NoticeScreen( - //#if MC>=11400 - getScreen()::display, - new TranslatableText("replaymod.gui.rendering.error.title"), - new TranslatableText("replaymod.gui.rendering.error.message") - //#else - //$$ I18n.format("replaymod.gui.rendering.error.title"), - //$$ I18n.format("replaymod.gui.rendering.error.message") - //#endif - ); - getMinecraft().openScreen(errorScreen); + getMinecraft().openScreen(new GuiNoFfmpeg(getScreen()::display).toMinecraft()); } catch (FFmpegWriter.FFmpegStartupException e) { GuiExportFailed.tryToRecover(e, newSettings -> { // Update settings with fixed ffmpeg arguments @@ -533,6 +525,7 @@ public class GuiRenderSettings extends AbstractGuiPopup { } outputFileButton.setLabel(this.outputFile.getName()); nametagCheckbox.setChecked(settings.isRenderNameTags()); + alphaCheckbox.setChecked(settings.isIncludeAlphaChannel()); stabilizeYaw.setChecked(settings.isStabilizeYaw()); stabilizePitch.setChecked(settings.isStabilizePitch()); stabilizeRoll.setChecked(settings.isStabilizeRoll()); @@ -571,6 +564,7 @@ public class GuiRenderSettings extends AbstractGuiPopup { bitRateField.getInteger() << (10 * bitRateUnit.getSelected()), serialize && !userDefinedOutputFileName ? getParentFile(outputFile) : outputFile, nametagCheckbox.isChecked(), + alphaCheckbox.isChecked(), stabilizeYaw.isChecked() && (serialize || stabilizeYaw.isEnabled()), stabilizePitch.isChecked() && (serialize || stabilizePitch.isEnabled()), stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()), diff --git a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java index bc69f061..cd9c6268 100644 --- a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java +++ b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java @@ -141,7 +141,7 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable { @Override public void tick() { - long current = System.currentTimeMillis(); + long current = System.nanoTime() / 1_000_000; //first, update the total render time (only if rendering is not paused and has already started) if(!renderer.isPaused() && renderer.getFramesDone() > 0 && prevTime > -1) { diff --git a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java index 8dbd50f7..ee68b87e 100644 --- a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java +++ b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java @@ -8,6 +8,7 @@ import com.replaymod.render.Setting; import com.replaymod.render.capturer.CaptureData; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.capturer.WorldRenderer; +import com.replaymod.render.mixin.GameRendererAccessor; import com.replaymod.replay.ReplayModReplay; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.client.MinecraftClient; @@ -81,11 +82,16 @@ public class EntityRendererHandler extends EventRegistrations implements WorldRe //#endif if (mc.world != null && mc.player != null) { + GameRendererAccessor gameRenderer = (GameRendererAccessor) mc.gameRenderer; Screen orgScreen = mc.currentScreen; boolean orgPauseOnLostFocus = mc.options.pauseOnLostFocus; + boolean orgRenderHand = gameRenderer.getRenderHand(); try { mc.currentScreen = null; // do not want to render the current screen (that'd just be the progress gui) mc.options.pauseOnLostFocus = false; // do not want the pause menu to open if the window is unfocused + if (omnidirectional) { + gameRenderer.setRenderHand(false); // makes no sense, we wouldn't even know where to put it + } //#if MC>=11400 mc.gameRenderer.render(partialTicks, finishTimeNano, true); @@ -100,6 +106,7 @@ public class EntityRendererHandler extends EventRegistrations implements WorldRe } finally { mc.currentScreen = orgScreen; mc.options.pauseOnLostFocus = orgPauseOnLostFocus; + gameRenderer.setRenderHand(orgRenderHand); } } diff --git a/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java b/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java new file mode 100644 index 00000000..e74e16bc --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/GameRendererAccessor.java @@ -0,0 +1,13 @@ +package com.replaymod.render.mixin; + +import net.minecraft.client.render.GameRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(GameRenderer.class) +public interface GameRendererAccessor { + @Accessor + boolean getRenderHand(); + @Accessor + void setRenderHand(boolean value); +} diff --git a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index 335c9908..a54dd178 100644 --- a/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -3,6 +3,7 @@ package com.replaymod.render.mixin; import net.minecraft.client.util.Window; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Window.class) public interface MainWindowAccessor { @@ -14,4 +15,12 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor void setFramebufferHeight(int value); + // FIXME preprocessor should be able to infer this mapping + // FIXME preprocessor should be able to remap this one when the mapping is given manually + //#if MC>=11500 + @Invoker + //#else + //$$ @Invoker("method_4483") + //#endif + void invokeUpdateFramebufferSize(); } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java index bb60ab4e..28872496 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_ChromaKeyColorSky.java @@ -21,7 +21,12 @@ public abstract class Mixin_ChromaKeyColorSky { @Shadow @Final private MinecraftClient client; //#if MC>=11800 - //$$ @Inject(method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V", + //$$ @Inject( + //#if MC>=11802 + //$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLnet/minecraft/client/render/Camera;ZLjava/lang/Runnable;)V", + //#else + //$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V", + //#endif //$$ at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", remap = false, shift = At.Shift.AFTER), //$$ cancellable = true) //#elseif MC>=11400 || 10710>=MC diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java index d8756e7b..56d4564b 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java @@ -4,7 +4,6 @@ package com.replaymod.render.mixin; import com.replaymod.render.capturer.IrisODSFrameCapturer; import net.coderbot.iris.Iris; import net.fabricmc.loader.api.FabricLoader; -import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.injection.At; @@ -15,14 +14,14 @@ import java.nio.file.Path; @Pseudo @Mixin(value = Iris.class, remap = false) public class Mixin_LoadIrisOdsShaderPack { - @Redirect(method = "loadExternalShaderpack", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/coderbot/iris/Iris;SHADERPACKS_DIRECTORY:Ljava/nio/file/Path;")) + @Redirect(method = "loadExternalShaderpack", at = @At(value = "INVOKE", target = "Lnet/coderbot/iris/Iris;getShaderpacksDirectory()Ljava/nio/file/Path;")) private static Path loadReplayModOdsPack(String name) { if (IrisODSFrameCapturer.INSTANCE != null && IrisODSFrameCapturer.SHADER_PACK_NAME.equals(name)) { return FabricLoader.getInstance().getModContainer("replaymod") .orElseThrow(() -> new RuntimeException("Failed to get mod container for ReplayMod")) .getRootPath(); } else { - return Iris.SHADERPACKS_DIRECTORY; + return Iris.getShaderpacksDirectory(); } } } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java index fb58ab87..8fa79479 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_Camera.java @@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.injection.Redirect; @Mixin(GameRenderer.class) public abstract class Mixin_Omnidirectional_Camera implements EntityRendererHandler.IEntityRenderer { - @Redirect(method = "method_22973", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;")) + @Redirect(method = "getBasicProjectionMatrix", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;")) private Matrix4f replayModRender_perspective$0(double fovY, float aspect, float zNear, float zFar) { return replayModRender_perspective((float) fovY, aspect, zNear, zFar); } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java deleted file mode 100644 index f174e0a3..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_DisableFrustumCulling.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.replaymod.render.mixin; - -import com.replaymod.core.versions.MCVer; -import com.replaymod.render.hooks.EntityRendererHandler; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -//#if MC>=10800 -import net.minecraft.client.render.Frustum; -//#else -//$$ import net.minecraft.client.renderer.culling.Frustrum; -//#endif - -//#if MC>=10800 -@Mixin(Frustum.class) -//#else -//$$ @Mixin(Frustrum.class) -//#endif -public abstract class Mixin_Omnidirectional_DisableFrustumCulling { - //#if MC>=11500 - @Inject(method = "isAnyCornerVisible", at = @At("HEAD"), cancellable = true) - //#else - //$$ @Inject(method = "intersects", at = @At("HEAD"), cancellable = true) - //#endif - public void intersects(CallbackInfoReturnable ci) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().gameRenderer).replayModRender_getHandler(); - if (handler != null && handler.omnidirectional) { - // Note the following used to be true but for simplicity non-ODS omnidirectional is the same now too. - // Normally the camera is always facing the direction of the omnidirectional image face that is currently - // getting rendered. With ODS however, the camera is always facing forwards and the turning happens in the - // vertex shader (non-trivial due to stereo). As such, all chunks need to be rendered all the time for ODS. - ci.setReturnValue(true); - } - } -} diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java b/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java deleted file mode 100644 index 939f6ec8..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Omnidirectional_SkipHand.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.replaymod.render.mixin; - -import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.GameRenderer; -import net.minecraft.client.util.math.MatrixStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(GameRenderer.class) -public abstract class Mixin_Omnidirectional_SkipHand implements EntityRendererHandler.IEntityRenderer { - @Inject(method = "renderHand", at = @At("HEAD"), cancellable = true) - private void replayModRender_renderSpectatorHand( - //#if MC>=11500 - MatrixStack matrixStack, - //#endif - //#if MC>=11400 - Camera camera, - //#endif - float partialTicks, - //#if MC<11400 - //$$ int renderPass, - //#endif - CallbackInfo ci - ) { - EntityRendererHandler handler = replayModRender_getHandler(); - if (handler != null && handler.omnidirectional) { - // No spectator hands during 360° view, we wouldn't even know where to put it - ci.cancel(); - } - } -} diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java b/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java index 945ac107..885608e3 100644 --- a/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java +++ b/src/main/java/com/replaymod/render/mixin/Mixin_Stereoscopic_Camera.java @@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(GameRenderer.class) public abstract class Mixin_Stereoscopic_Camera implements EntityRendererHandler.IEntityRenderer { - @Inject(method = "method_22973", at = @At("RETURN"), cancellable = true) + @Inject(method = "getBasicProjectionMatrix", at = @At("RETURN"), cancellable = true) private void replayModRender_setupStereoscopicProjection(CallbackInfoReturnable ci) { if (replayModRender_getHandler() != null) { Matrix4f offset; diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index dfa7e096..56d1daf6 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -120,6 +120,7 @@ public class VideoRenderer implements RenderInfo { private Framebuffer guiFramebuffer; private int displayWidth, displayHeight; + private int framebufferWidth, framebufferHeight; public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException { this.settings = settings; @@ -135,12 +136,12 @@ public class VideoRenderer implements RenderInfo { FrameConsumer frameConsumer; if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) { //#if MC>=11400 - frameConsumer = new EXRWriter(settings.getOutputFile().toPath()); + frameConsumer = new EXRWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); //#else //$$ throw new UnsupportedOperationException("EXR requires LWJGL3"); //#endif } else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) { - frameConsumer = new PNGWriter(settings.getOutputFile().toPath()); + frameConsumer = new PNGWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else { frameConsumer = new FFmpegWriter(this); } @@ -245,10 +246,10 @@ public class VideoRenderer implements RenderInfo { public float updateForNextFrame() { // because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily MainWindowAccessor acc = (MainWindowAccessor) (Object) mc.getWindow(); - int displayWidthBefore = acc.getFramebufferWidth(); - int displayHeightBefore = acc.getFramebufferHeight(); - acc.setFramebufferWidth(displayWidth); - acc.setFramebufferHeight(displayHeight); + int framebufferWidthBefore = acc.getFramebufferWidth(); + int framebufferHeightBefore = acc.getFramebufferHeight(); + acc.setFramebufferWidth(framebufferWidth); + acc.setFramebufferHeight(framebufferHeight); if (!settings.isHighPerformance() || framesDone % fps == 0) { while (drawGui() && paused) { @@ -289,8 +290,8 @@ public class VideoRenderer implements RenderInfo { } // change Minecraft's display size back - acc.setFramebufferWidth(displayWidthBefore); - acc.setFramebufferHeight(displayHeightBefore); + acc.setFramebufferWidth(framebufferWidthBefore); + acc.setFramebufferHeight(framebufferHeightBefore); if (cameraPathExporter != null) { cameraPathExporter.recordFrame(timer.tickDelta); @@ -362,6 +363,7 @@ public class VideoRenderer implements RenderInfo { } updateDisplaySize(); + updateFramebufferSize(); gui.toMinecraft().init(mc, mc.getWindow().getScaledWidth(), mc.getWindow().getScaledHeight()); @@ -369,9 +371,9 @@ public class VideoRenderer implements RenderInfo { // Set up our own framebuffer to render the GUI to //#if MC>=11700 - //$$ guiFramebuffer = new WindowFramebuffer(displayWidth, displayHeight); + //$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight); //#else - guiFramebuffer = new Framebuffer(displayWidth, displayHeight, true + guiFramebuffer = new Framebuffer(framebufferWidth, framebufferHeight, true //#if MC>=11400 , false //#endif @@ -425,7 +427,7 @@ public class VideoRenderer implements RenderInfo { } // Finally, resize the Minecraft framebuffer to the actual width/height of the window - resizeMainWindow(mc, displayWidth, displayHeight); + resizeMainWindow(mc, framebufferWidth, framebufferHeight); } private void executeTaskQueue() { @@ -483,17 +485,23 @@ public class VideoRenderer implements RenderInfo { return false; } - // Resize the GUI framebuffer if the display size changed + // Check if display size has changes and force recalculate GUI framebuffer size. if (displaySizeChanged()) { updateDisplaySize(); + ((MainWindowAccessor) (Object) window).invokeUpdateFramebufferSize(); + } + + // Resize the GUI framebuffer if the display size changed + if (framebufferSizeChanged()) { + updateFramebufferSize(); //#if MC>=11400 - guiFramebuffer.resize(displayWidth, displayHeight + guiFramebuffer.resize(framebufferWidth, framebufferHeight //#if MC>=11400 , false //#endif ); //#else - //$$ guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight); + //$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight); //#endif } @@ -594,7 +602,7 @@ public class VideoRenderer implements RenderInfo { guiFramebuffer.endWrite(); popMatrix(); pushMatrix(); - guiFramebuffer.draw(displayWidth, displayHeight); + guiFramebuffer.draw(framebufferWidth, framebufferHeight); popMatrix(); //#if MC>=11500 @@ -633,12 +641,18 @@ public class VideoRenderer implements RenderInfo { private boolean displaySizeChanged() { int realWidth = mc.getWindow().getWidth(); int realHeight = mc.getWindow().getHeight(); + return displayWidth != realWidth || displayHeight != realHeight; + } + + private boolean framebufferSizeChanged() { + int realWidth = mc.getWindow().getFramebufferWidth(); + int realHeight = mc.getWindow().getFramebufferHeight(); if (realWidth == 0 || realHeight == 0) { // These can be zero on Windows if minimized. // Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values. return false; } - return displayWidth != realWidth || displayHeight != realHeight; + return framebufferWidth != realWidth || framebufferHeight != realHeight; } private void updateDisplaySize() { @@ -646,6 +660,11 @@ public class VideoRenderer implements RenderInfo { displayHeight = mc.getWindow().getHeight(); } + private void updateFramebufferSize() { + framebufferWidth = mc.getWindow().getFramebufferWidth(); + framebufferHeight = mc.getWindow().getFramebufferHeight(); + } + public int getFramesDone() { return framesDone; } diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 95b944a5..b44febe9 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -125,6 +125,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.replaystudio.util.Utils.readInt; @@ -349,6 +352,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend return; } terminate = true; + syncSender.shutdown(); events.unregister(); try { channelInactive(ctx); @@ -403,53 +407,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend super.channelRead(ctx, p); } - // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world - // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. - // We do this after every SpawnX packet and after the destroy entities packet. - if (!asyncMode && mc.world != null) { - if (p instanceof PlayerSpawnS2CPacket - || p instanceof EntitySpawnS2CPacket - || p instanceof MobSpawnS2CPacket - //#if MC<11600 - //$$ || p instanceof EntitySpawnGlobalS2CPacket - //#endif - || p instanceof PaintingSpawnS2CPacket - || p instanceof ExperienceOrbSpawnS2CPacket - || p instanceof EntitiesDestroyS2CPacket) { - ClientWorld world = mc.world; - //#if MC>=11700 - //$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener) - //#elseif MC>=11400 - // Note: Not sure if it's still required but there's this really handy method anyway - world.finishRemovingEntities(); - //#else - //$$ Iterator iter = world.loadedEntityList.iterator(); - //$$ while (iter.hasNext()) { - //$$ Entity entity = iter.next(); - //$$ if (entity.isDead) { - //$$ int chunkX = entity.chunkCoordX; - //$$ int chunkY = entity.chunkCoordZ; - //$$ - //#if MC>=11400 - //$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) { - //#else - //#if MC>=10904 - //$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { - //#else - //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { - //#endif - //#endif - //$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); - //$$ } - //$$ - //$$ iter.remove(); - //$$ world.onEntityRemoved(entity); - //$$ } - //$$ - //$$ } - //#endif - } - } + maybeRemoveDeadEntities(p); //#if MC>=11400 if (p instanceof ChunkDataS2CPacket) { @@ -504,6 +462,69 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend return p; } + // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world + // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. + // We do this after every SpawnX packet and after the destroy entities packet. + private void maybeRemoveDeadEntities(Packet packet) { + if (asyncMode) { + return; // MC should have enough time to tick + } + + boolean relevantPacket = packet instanceof PlayerSpawnS2CPacket + || packet instanceof EntitySpawnS2CPacket + || packet instanceof MobSpawnS2CPacket + //#if MC<11600 + //$$ || packet instanceof EntitySpawnGlobalS2CPacket + //#endif + || packet instanceof PaintingSpawnS2CPacket + || packet instanceof ExperienceOrbSpawnS2CPacket + || packet instanceof EntitiesDestroyS2CPacket; + if (!relevantPacket) { + return; // don't want to do it too often, only when there's likely to be a dead entity + } + + mc.send(() -> { + ClientWorld world = mc.world; + if (world != null) { + removeDeadEntities(world); + } + }); + } + + private void removeDeadEntities(ClientWorld world) { + //#if MC>=11700 + //$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener) + //#elseif MC>=11400 + // Note: Not sure if it's still required but there's this really handy method anyway + world.finishRemovingEntities(); + //#else + //$$ Iterator iter = world.loadedEntityList.iterator(); + //$$ while (iter.hasNext()) { + //$$ Entity entity = iter.next(); + //$$ if (entity.isDead) { + //$$ int chunkX = entity.chunkCoordX; + //$$ int chunkY = entity.chunkCoordZ; + //$$ + //#if MC>=11400 + //$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) { + //#else + //#if MC>=10904 + //$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { + //#else + //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { + //#endif + //#endif + //$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); + //$$ } + //$$ + //$$ iter.remove(); + //$$ world.onEntityRemoved(entity); + //$$ } + //$$ + //$$ } + //#endif + } + /** * Process a packet and return the result. * @param p The packet to process @@ -627,7 +648,11 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend //#if MC>=11600 //#if MC>=11603 packet.getDimensionIds(), + //#if MC>=11800 + //$$ packet.registryManager(), + //#else (net.minecraft.util.registry.DynamicRegistryManager.Impl) packet.getRegistryManager(), + //#endif packet.getDimensionType(), //#else //$$ packet.method_29443(), @@ -824,7 +849,13 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend } } - return asyncMode ? processPacketAsync(p) : processPacketSync(p); + if (asyncMode) { + return processPacketAsync(p); + } else { + Packet fp = p; + mc.send(() -> processPacketSync(fp)); + return p; + } } @Override @@ -1085,6 +1116,10 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend // Synchronous packet processing // ///////////////////////////////////////////////////////// + // Even in sync mode, we send from another thread because mods may rely on that + private final ExecutorService syncSender = Executors.newSingleThreadExecutor(runnable -> + new Thread(runnable, "replaymod-sync-sender")); + /** * Sends all packets until the specified timestamp is reached (inclusive). * If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning. @@ -1093,6 +1128,36 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend @Override public void sendPacketsTill(int timestamp) { Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead."); + + // Submit our target to the sender thread and track its progress + AtomicBoolean doneSending = new AtomicBoolean(); + syncSender.submit(() -> { + try { + doSendPacketsTill(timestamp); + } finally { + doneSending.set(true); + } + }); + + // Drain the task queue while we are sending (in case a mod blocks the io thread waiting for the main thread) + while (!doneSending.get()) { + executeTaskQueue(); + + // Wait until the sender thread has made progress + try { + //noinspection BusyWait + Thread.sleep(0, 100_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + + // Everything has been sent, drain the queue one last time + executeTaskQueue(); + } + + private void doSendPacketsTill(int timestamp) { try { while (ctx == null && !terminate) { // Make sure channel is ready Thread.sleep(10); @@ -1112,7 +1177,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend loginPhase = true; startFromBeginning = false; nextPacket = null; - replayHandler.restartedReplay(); + ReplayMod.instance.runSync(replayHandler::restartedReplay); } if (replayIn == null) { @@ -1159,7 +1224,30 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend } } - protected Packet processPacketSync(Packet p) { + private void executeTaskQueue() { + //#if MC>=11400 + ((MCVer.MinecraftMethodAccessor) mc).replayModExecuteTaskQueue(); + //#else + //$$ java.util.Queue> scheduledTasks = ((MinecraftAccessor) mc).getScheduledTasks(); + //$$ + //$$ // Live-lock detection: if we already hold the lock, then the sender thread will never be able to queue its + //$$ // tasks + //$$ if (Thread.holdsLock(scheduledTasks)) { + //$$ throw new IllegalStateException("Task queue already locked. " + + //$$ "You may want to use `Scheduler.runLaterWithoutLock` to run while the lock is not taken."); + //$$ } + //$$ + //$$ //noinspection SynchronizationOnLocalVariableOrMethodParameter + //$$ synchronized (scheduledTasks) { + //$$ while (!scheduledTasks.isEmpty()) { + //$$ scheduledTasks.poll().run(); + //$$ } + //$$ } + //#endif + ReplayMod.instance.runTasks(); + } + + protected void processPacketSync(Packet p) { //#if MC>=10904 if (p instanceof UnloadChunkS2CPacket) { UnloadChunkS2CPacket packet = (UnloadChunkS2CPacket) p; @@ -1280,7 +1368,6 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend } } } - return p; // During synchronous playback everything is sent normally } private void forcePositionForVehicleAndSelf(Entity entity) { diff --git a/src/main/java/com/replaymod/replay/InputReplayTimer.java b/src/main/java/com/replaymod/replay/InputReplayTimer.java index 394fdd50..0b3e8b4a 100644 --- a/src/main/java/com/replaymod/replay/InputReplayTimer.java +++ b/src/main/java/com/replaymod/replay/InputReplayTimer.java @@ -8,6 +8,10 @@ import com.replaymod.replay.camera.CameraEntity; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.RenderTickCounter; +//#if MC>=11802 +//$$ import net.minecraft.client.gui.screen.DownloadingTerrainScreen; +//#endif + //#if MC>=11400 import org.lwjgl.glfw.GLFW; //#else @@ -111,6 +115,15 @@ public class InputReplayTimer extends WrappedTimer { //#endif //$$ } //#endif + + //#if MC>=11802 + //$$ // As of 1.18.2, this screen always stays open for at least two seconds, and requires ticking to close. + //$$ // Thanks, but we'll have none of that (at least while in a replay). + //$$ if (mc.currentScreen instanceof DownloadingTerrainScreen) { + //$$ mc.currentScreen.close(); + //$$ } + //#endif + } //#if MC>=11600 return ticksThisFrame; diff --git a/src/main/java/com/replaymod/replay/QuickReplaySender.java b/src/main/java/com/replaymod/replay/QuickReplaySender.java index 5f5d7462..6592b47d 100644 --- a/src/main/java/com/replaymod/replay/QuickReplaySender.java +++ b/src/main/java/com/replaymod/replay/QuickReplaySender.java @@ -1,4 +1,4 @@ -//#if MC>=10904 +//#if MC>=10800 package com.replaymod.replay; import com.google.common.util.concurrent.FutureCallback; @@ -78,7 +78,11 @@ public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySe wrappedBuf.writerIndex(size); PacketByteBuf packetByteBuf = new PacketByteBuf(wrappedBuf); + //#if MC>=10809 Packet mcPacket; + //#else + //$$ Packet mcPacket; + //#endif //#if MC>=11700 //$$ mcPacket = NetworkState.PLAY.getPacketHandler(NetworkSide.CLIENTBOUND, packet.getId(), packetByteBuf); //#elseif MC>=11500 @@ -137,13 +141,13 @@ public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySe LOGGER.info("Initialized quick replay sender in " + (System.currentTimeMillis() - start) + "ms"); } catch (Throwable e) { LOGGER.error("Initializing quick replay sender:", e); - mod.getCore().runLater(() -> { + mod.getCore().runLaterWithoutLock(() -> { mod.getCore().printWarningToChat("Error initializing quick replay sender: %s", e.getLocalizedMessage()); promise.setException(e); }); return; } - mod.getCore().runLater(() -> promise.set(null)); + mod.getCore().runLaterWithoutLock(() -> promise.set(null)); }).start(); return promise; } diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index adac3806..99e90c36 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -69,7 +69,7 @@ import net.minecraft.entity.LivingEntity; //$$ import io.netty.channel.ChannelOutboundHandlerAdapter; //#endif -//#if MC<10904 +//#if MC<10800 //$$ import de.johni0702.minecraft.gui.element.GuiLabel; //$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup; //$$ import de.johni0702.minecraft.gui.utils.Colors; @@ -119,11 +119,11 @@ public class ReplayHandler { * Decodes and sends packets into channel. */ private final FullReplaySender fullReplaySender; - //#if MC>=10904 + //#if MC>=10800 private final QuickReplaySender quickReplaySender; private boolean quickMode = false; //#else - //$$ private static final String QUICK_MODE_MIN_MC = "1.9.4"; + //$$ private static final String QUICK_MODE_MIN_MC = "1.8"; //#endif /** @@ -160,7 +160,7 @@ public class ReplayHandler { markers = replayFile.getMarkers().or(Collections.emptySet()); fullReplaySender = new FullReplaySender(this, replayFile, false); - //#if MC>=10904 + //#if MC>=10800 quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile); //#endif @@ -206,7 +206,7 @@ public class ReplayHandler { ReplayClosingCallback.EVENT.invoker().replayClosing(this); fullReplaySender.terminateReplay(); - //#if MC>=10904 + //#if MC>=10800 if (quickMode) { quickReplaySender.unregister(); } @@ -308,7 +308,7 @@ public class ReplayHandler { //$$ channel = new EmbeddedChannel(dummyHandler); //$$ channel.pipeline().remove(dummyHandler); //#endif - //#if MC>=10904 + //#if MC>=10800 channel.pipeline().addLast("ReplayModReplay_quickReplaySender", quickReplaySender); //#endif channel.pipeline().addLast("ReplayModReplay_replaySender", fullReplaySender); @@ -329,7 +329,7 @@ public class ReplayHandler { } public ReplaySender getReplaySender() { - //#if MC>=10904 + //#if MC>=10800 return quickMode ? quickReplaySender : fullReplaySender; //#else //$$ return fullReplaySender; @@ -340,7 +340,7 @@ public class ReplayHandler { return overlay; } - //#if MC>=10904 + //#if MC>=10800 public void ensureQuickModeInitialized(Runnable andThen) { if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return; ListenableFuture future = quickReplaySender.getInitializationPromise(); @@ -534,7 +534,11 @@ public class ReplayHandler { } public void doJump(int targetTime, boolean retainCameraPosition) { - //#if MC>=10904 + if (!getReplaySender().isAsyncMode()) { + return; // path playback, rendering, etc. -> no jumping allowed + } + + //#if MC>=10800 if (getReplaySender() == quickReplaySender) { // Always round to full tick targetTime = targetTime + targetTime % 50; @@ -603,6 +607,7 @@ public class ReplayHandler { // Render our please-wait-screen GuiScreen guiScreen = new GuiScreen(); guiScreen.setBackground(AbstractGuiScreen.Background.DIRT); + guiScreen.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER)); guiScreen.addElements(new HorizontalLayout.Data(0.5), new GuiLabel().setI18nText("replaymod.gui.pleasewait")); diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 5ae3fcec..35bd016c 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -31,6 +31,10 @@ import net.minecraft.stat.StatHandler; import net.minecraft.util.Identifier; import net.minecraft.util.math.Box; +//#if MC>=11802 +//$$ import net.minecraft.tag.TagKey; +//#endif + //#if FABRIC>=1 //#else //$$ import net.minecraftforge.client.event.EntityViewRenderEvent; @@ -354,7 +358,13 @@ public class CameraEntity //#if MC>=11400 @Override - public boolean isSubmergedIn(Tag fluid) { + public boolean isSubmergedIn( + //#if MC>=11802 + //$$ TagKey fluid + //#else + Tag fluid + //#endif + ) { return falseUnlessSpectating(entity -> entity.isSubmergedIn(fluid)); } diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index de4279aa..9a2dec3c 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -49,6 +49,8 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -95,8 +97,10 @@ public class GuiReplayViewer extends GuiScreen { List selected = list.getSelected(); if (selected.size() == 1) { + File file = selected.get(0).file; + LOGGER.info("Opening replay in viewer: " + file); try { - mod.startReplay(selected.get(0).file); + mod.startReplay(file); } catch (IOException e) { e.printStackTrace(); } @@ -130,8 +134,8 @@ public class GuiReplayViewer extends GuiScreen { public final GuiButton renameButton = new GuiButton().onClick(new Runnable() { @Override public void run() { - final File file = list.getSelected().get(0).file; - String name = Utils.fileNameToReplayName(file.getName()); + final Path path = list.getSelected().get(0).file.toPath(); + String name = Utils.fileNameToReplayName(path.getFileName().toString()); final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(name); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this, new GuiLabel().setI18nText("replaymod.gui.viewer.rename.name").setColor(Colors.BLACK), @@ -147,16 +151,16 @@ public class GuiReplayViewer extends GuiScreen { } }).onTextChanged(obj -> { popup.getYesButton().setEnabled(!nameField.getText().isEmpty() - && !new File(file.getParentFile(), Utils.replayNameToFileName(nameField.getText())).exists()); + && Files.notExists(Utils.replayNameToPath(path.getParent(), nameField.getText()))); }); popup.onAccept(() -> { // Sanitize their input String newName = nameField.getText().trim(); // This file is what they want - File targetFile = new File(file.getParentFile(), Utils.replayNameToFileName(newName)); + Path targetPath = Utils.replayNameToPath(path.getParent(), newName); try { // Finally, try to move it - FileUtils.moveFile(file, targetFile); + Files.move(path, targetPath); } catch (IOException e) { // We failed (might also be their OS) e.printStackTrace(); diff --git a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java index 9d938f7e..2257eceb 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java @@ -18,7 +18,7 @@ public abstract class MixinGuiSpectator { //$$ @Inject(method = "func_175260_a", at = @At("HEAD"), cancellable = true) //#endif public void isInReplay( - //#if MC>=11400 + //#if MC>=11400 && MC<11802 double i, //#else //$$ int i, diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java b/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java new file mode 100644 index 00000000..48e8c8a3 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_ShowSpectatedHand_Iris.java @@ -0,0 +1,35 @@ +//#if MC>=11400 +package com.replaymod.replay.mixin; + +import com.replaymod.replay.camera.CameraEntity; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.world.GameMode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import static com.replaymod.core.versions.MCVer.getMinecraft; + +@Pseudo +@Mixin(targets = "net.coderbot.iris.pipeline.HandRenderer", remap = false) +public abstract class Mixin_ShowSpectatedHand_Iris { + @Redirect( + method = "*", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;getCurrentGameMode()Lnet/minecraft/world/GameMode;", + remap = true + ) + ) + private GameMode getGameMode(ClientPlayerInteractionManager interactionManager) { + ClientPlayerEntity camera = getMinecraft().player; + if (camera instanceof CameraEntity) { + // alternative doesn't really matter, the caller only checks for equality to SPECTATOR + return camera.isSpectator() ? GameMode.SPECTATOR : GameMode.SURVIVAL; + } + return interactionManager.getCurrentGameMode(); + } +} +//#endif diff --git a/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java new file mode 100644 index 00000000..1e8f9e2b --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForMovement.java @@ -0,0 +1,38 @@ +package com.replaymod.replay.mixin.world_border; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Normally Minecraft's world border movement is based off real time; + * this redirect ensures that it is synced with the time in the Replay instead. + */ +//#if MC>=11400 +// FIXME: preprocessor should be able to remap between fabric and forge +//#if FABRIC +@Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingArea") +//#else +//$$ @Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingBorderInfo") +//#endif +//#else +//$$ @Mixin(net.minecraft.world.border.WorldBorder.class) +//#endif +public class Mixin_UseReplayTime_ForMovement { + + //#if MC>=11400 + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J")) + //#else + //$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Ljava/lang/System;currentTimeMillis()J")) + //#endif + private long getWorldBorderTime() { + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + if (replayHandler != null) { + return replayHandler.getReplaySender().currentTimeStamp(); + } + return MCVer.milliTime(); + } +} diff --git a/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java new file mode 100644 index 00000000..9abf28c4 --- /dev/null +++ b/src/main/java/com/replaymod/replay/mixin/world_border/Mixin_UseReplayTime_ForTexture.java @@ -0,0 +1,29 @@ +package com.replaymod.replay.mixin.world_border; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.client.render.WorldRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Normally Minecraft's world border texture animation is based off real time; + * this redirect ensures that it is synced with the time in the Replay instead. + */ +@Mixin(WorldRenderer.class) +public class Mixin_UseReplayTime_ForTexture { + //#if MC>=11400 + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J")) + //#else + //$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) + //#endif + private long getWorldBorderTime() { + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + if (replayHandler != null) { + return replayHandler.getReplaySender().currentTimeStamp(); + } + return MCVer.milliTime(); + } +} diff --git a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh index 3b53c589..68e7e0fb 100644 --- a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh +++ b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh @@ -9,10 +9,49 @@ uniform int direction; const float eyeDistance = 0.14; +void orient(vec4 position, int orientation) { + float z; + if (orientation == 0) { // LEFT + z = position.z; + position.z = position.x; + position.x = -z; + } else if (orientation == 1) { // RIGHT + z = position.z; + position.z = -position.x; + position.x = z; + } else if (orientation == 2) { // FRONT + // No changes required + } else if (orientation == 3) { // BACK + position.x = -position.x; + position.z = -position.z; + } else if (orientation == 4) { // TOP + z = position.z; + position.z = -position.y; + position.y = z; + } else if (orientation == 5) { // BOTTOM + z = position.z; + position.z = position.y; + position.y = -z; + } +} + +void orientInverse(vec4 position, int orientation) { + if (orientation < 2) { + orient(position, 1 - orientation); // LEFT and RIGHT flip + } else if (orientation < 4) { + orient(position, orientation); // FRONT and BACK are their own inverses + } else { + orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip + } +} + void main() { // Transform to view space vec4 position = gl_ModelViewMatrix * gl_Vertex; + // Undo the camera rotation, so we always apply our stereo effect looking in the same direction + orientInverse(position, direction); + // Distort for ODS // O := The origin // P := The current vertex/point @@ -34,30 +73,8 @@ void main() { // Calculate the vector between O and T and finally move the vertex by that vector position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0); - // Rotate for different cubic views - float z; - if (direction == 0) { // LEFT - z = position.z; - position.z = position.x; - position.x = -z; - } else if (direction == 1) { // RIGHT - z = position.z; - position.z = -position.x; - position.x = z; - } else if (direction == 2) { // FRONT - // No changes required - } else if (direction == 3) { // BACK - position.x = -position.x; - position.z = -position.z; - } else if (direction == 4) { // TOP - z = position.z; - position.z = -position.y; - position.y = z; - } else if (direction == 5) { // BOTTOM - z = position.z; - position.z = position.y; - position.y = -z; - } + // Rotate back into the correct cubic view + orient(position, direction); // Transform to screen space gl_Position = gl_ProjectionMatrix * position; diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 54ab62bd..7614b69e 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 54ab62bdbb646c6b4f4b6d61a26183258e5a017d +Subproject commit 7614b69e249eb728d219c02c3c71effcc5fc00a2 diff --git a/src/main/resources/assets/replaymod/shader/ods.vert b/src/main/resources/assets/replaymod/shader/ods.vert index 43160450..6a654a6b 100644 --- a/src/main/resources/assets/replaymod/shader/ods.vert +++ b/src/main/resources/assets/replaymod/shader/ods.vert @@ -14,10 +14,49 @@ uniform int direction; const float eyeDistance = 0.14; +void orient(vec4 position, int orientation) { + float z; + if (orientation == 0) { // LEFT + z = position.z; + position.z = position.x; + position.x = -z; + } else if (orientation == 1) { // RIGHT + z = position.z; + position.z = -position.x; + position.x = z; + } else if (orientation == 2) { // FRONT + // No changes required + } else if (orientation == 3) { // BACK + position.x = -position.x; + position.z = -position.z; + } else if (orientation == 4) { // TOP + z = position.z; + position.z = -position.y; + position.y = z; + } else if (orientation == 5) { // BOTTOM + z = position.z; + position.z = position.y; + position.y = -z; + } +} + +void orientInverse(vec4 position, int orientation) { + if (orientation < 2) { + orient(position, 1 - orientation); // LEFT and RIGHT flip + } else if (orientation < 4) { + orient(position, orientation); // FRONT and BACK are their own inverses + } else { + orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip + } +} + void main() { // Transform to view space vec4 position = gl_ModelViewMatrix * gl_Vertex; + // Undo the camera rotation, so we always apply our stereo effect looking in the same direction + orientInverse(position, direction); + // Distort for ODS // O := The origin // P := The current vertex/point @@ -39,30 +78,8 @@ void main() { // Calculate the vector between O and T and finally move the vertex by that vector position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0); - // Rotate for different cubic views - float z; - if (direction == 0) { // LEFT - z = position.z; - position.z = position.x; - position.x = -z; - } else if (direction == 1) { // RIGHT - z = position.z; - position.z = -position.x; - position.x = z; - } else if (direction == 2) { // FRONT - // No changes required - } else if (direction == 3) { // BACK - position.x = -position.x; - position.z = -position.z; - } else if (direction == 4) { // TOP - z = position.z; - position.z = -position.y; - position.y = z; - } else if (direction == 5) { // BOTTOM - z = position.z; - position.z = position.y; - position.y = -z; - } + // Rotate back into the correct cubic view + orient(position, direction); // Transform to screen space gl_Position = gl_ProjectionMatrix * position; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6eb336ce..73fe9d6e 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -47,6 +47,10 @@ "fabric-resource-loader-v0": "*" }, + "conflicts": { + "iris": "<1.1.3" + }, + "custom": { "mm:early_risers": [ "com.replaymod.core.ReplayModMMLauncher" diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index e6c9ee41..09545afc 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -11,8 +11,10 @@ "SPacketSpawnMobAccessor", "SPacketSpawnPlayerAccessor", "MixinServerInfo", - //#if MC>=11400 + //#if MC>=10800 "MixinDownloadingPackFinder", + //#endif + //#if MC>=11400 "MixinMouseHelper", //#endif //#if MC>=10904 diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 108c1484..092aa958 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -14,9 +14,7 @@ "Mixin_HideNameTags", "Mixin_HideNameTags_LivingEntity", "Mixin_Omnidirectional_Camera", - "Mixin_Omnidirectional_DisableFrustumCulling", "Mixin_Omnidirectional_Rotation", - "Mixin_Omnidirectional_SkipHand", "Mixin_PreserveDepthDuringGuiRendering", "Mixin_SkipBlockOutlinesDuringRender", "Mixin_SkipHudDuringRender", @@ -39,6 +37,7 @@ "Mixin_PreserveDepthDuringHandRendering", "Mixin_WindowsWorkaroundForTinyEXRNatives", //#endif + "GameRendererAccessor", "MainWindowAccessor", "WorldRendererAccessor", //#if MC>=10904 diff --git a/src/main/resources/mixins.replay.replaymod.json b/src/main/resources/mixins.replay.replaymod.json index 356d50cc..02e6fe65 100644 --- a/src/main/resources/mixins.replay.replaymod.json +++ b/src/main/resources/mixins.replay.replaymod.json @@ -7,6 +7,8 @@ "client": [ "entity_tracking.Mixin_EntityExt", "entity_tracking.Mixin_FixPartialUpdates", + "world_border.Mixin_UseReplayTime_ForMovement", + "world_border.Mixin_UseReplayTime_ForTexture", "Mixin_FixNPCSkinCaching", //#if MC>=11800 //$$ "Mixin_FixEntityNotTracking", @@ -23,6 +25,7 @@ "ClientWorldAccessor", "EntityLivingBaseAccessor", //#if MC>=11400 + "Mixin_ShowSpectatedHand_Iris", "Mixin_ShowSpectatedHand_NoOF", "Mixin_ShowSpectatedHand_OF", //#else diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index e491ce51..6afaebbc 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,14 +1,6 @@ { "pack": { "description": "ReplayMod resources", - //#if MC>=11400 "pack_format": 4 - //#else - //#if MC>=11002 - //$$ "pack_format": 2 - //#else - //$$ "pack_format": 1 - //#endif - //#endif } } diff --git a/version.txt b/version.txt index ec1cf33c..2714f531 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.6.3 +2.6.4 diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java index fdcadef3..26d697d2 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java +++ b/versions/1.12.2/src/main/java/com/replaymod/core/versions/Window.java @@ -34,6 +34,11 @@ public class Window implements MainWindowAccessor { mc.displayHeight = value; } + @Override + public void invokeUpdateFramebufferSize() { + // no-op, pre-LWJGL3 MC doesn't differentiate between window and framebuffer size + } + public long getHandle() { return 0; } diff --git a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java index 2d91cb58..a6166095 100644 --- a/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java +++ b/versions/1.12.2/src/main/java/com/replaymod/render/mixin/MainWindowAccessor.java @@ -3,6 +3,7 @@ package com.replaymod.render.mixin; import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Minecraft.class) public interface MainWindowAccessor { @@ -14,4 +15,6 @@ public interface MainWindowAccessor { int getFramebufferHeight(); @Accessor("displayHeight") void setFramebufferHeight(int value); + @Invoker + void invokeUpdateFramebufferSize(); } diff --git a/versions/1.12.2/src/main/resources/pack.mcmeta b/versions/1.12.2/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..5259b825 --- /dev/null +++ b/versions/1.12.2/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "ReplayMod resources", + "pack_format": 2 + } +} diff --git a/versions/1.18.2/.gitkeep b/versions/1.18.2/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/1.9.4/src/main/resources/pack.mcmeta b/versions/1.9.4/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..9a0e3411 --- /dev/null +++ b/versions/1.9.4/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "ReplayMod resources", + "pack_format": 1 + } +} diff --git a/versions/mapping-fabric-1.16.1-1.15.2.txt b/versions/mapping-fabric-1.16.1-1.15.2.txt index 1f8df9e4..25f2e2db 100644 --- a/versions/mapping-fabric-1.16.1-1.15.2.txt +++ b/versions/mapping-fabric-1.16.1-1.15.2.txt @@ -1 +1,2 @@ net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140() +net.minecraft.client.render.GameRenderer getBasicProjectionMatrix() method_22973()