diff --git a/build.gradle.kts b/build.gradle.kts index 11134609..e91e2915 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -178,7 +178,7 @@ val doRelease by tasks.registering { defaultTasks("shadowJar") preprocess { - "1.16.3"(11603, "yarn") { + "1.16.4"(11604, "yarn") { "1.16.1"(11601, "yarn") { "1.15.2"(11502, "yarn") { "1.14.4"(11404, "yarn", file("versions/mapping-fabric-1.15.2-1.14.4.txt")) { diff --git a/docs/content.md b/docs/content.md index 41aa76f0..139c7f5b 100755 --- a/docs/content.md +++ b/docs/content.md @@ -22,14 +22,17 @@ To render your creations with **Replay Mod** you will need to have FFmpeg instal ### Windows [windows] Download the latest FFmpeg release from https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip. -In your `.minecraft` folder, create a `ffmpeg` folder. Extract the downloaded .zip file into this folder. The FFmpeg executable should end up at `.minecraft\ffmpeg\bin\ffmpeg.exe`. +Extract the downloaded .zip file into your `.minecraft` folder. + +Historically the exact location inside the `.minecraft` folder was important, this is no longer the case. +It is sufficient for the `ffmpeg.exe` to be anywhere inside your `.minecraft` folder. **Notes for alternative launchers** - Twitch launcher by default installs Minecraft instances in C:\\Users\\*username*\\Twitch\\Minecraft\\Instances\\*instancename*\\ - GD Launcher by default installs Minecraft instances in C:\\Users\\*username*\\AppData\\Roaming\\gdlauncher_next\\instances\\*instancename*\\ - MultiMC by default installs Minecraft instances in C:\\Program Files (x86)\\MultiMC\\instances\\*Instancename*\\.minecraft\\ -For these launchers, make sure FFmpeg exists in `instancename\ffmpeg\bin\ffmpeg.exe` (Twitch / GD) or `Instancename\.minecraft\ffmpeg\bin\ffmpeg.exe` (MultiMC) +For these launchers, make sure FFmpeg exists inside the corresponding folder. ### Mac OSX [mac] On OSX, you can install **FFmpeg** with **[Homebrew](http://brew.sh/)** using `brew install ffmpeg`. @@ -144,6 +147,8 @@ If two Keyframes are 5 seconds apart, the Camera Path will take 5 seconds to int You can drag Keyframes on the **Keyframe Timeline** by left-clicking them, holding the mouse button and moving your mouse. +> **Hint:** Made a mistake? You can use `Ctrl+Z` to undo your changes to the keyframe timeline. Changed you mind? Press `Ctrl+Y` (or `Ctrl+Shift+Z`) to redo them. + ### Position Keyframes [place] The basic components of a Camera Path are **Position Keyframes**. A Position Keyframe stores a **Camera Position** (x, y, z, yaw, pitch, roll). @@ -354,6 +359,12 @@ There are **7 Encoding Presets** you can choose from: - **PNG Sequence** Exports the sequence as individual frames in the **PNG Format**. **Warning:** This can create a huge amount of files, so make sure to save them in a separate folder. +- **OpenEXR Sequence** (Minecraft 1.14 and above) + Exports the sequence as individual frames in the **OpenEXR Format**. + The images are of perfect quality and may contain + additional image layers beyond the visible one (e.g. [depth](#replaying-render-advanced-depth-map)). + As such, this format is a good choice if you have plenty of disk space to spare and want to perform additional editing + on the video in a third-party video editor. ### Advanced Settings [advanced] ![](img/rendersettings-advanced.jpg) @@ -380,6 +391,21 @@ Using a **Video Editing Software** like **Adobe After Effects** or **Sony Vegas* > **Note:** For best results, you should **disable clouds** before rendering, as they are transparent. +#### Depth Map [depth-map] +![](img/depthmapsuzanne.jpg) +"Suzanne" model added into a scene using Blender + +This setting causes depth information to be recorded and included in video formats which support it +(currently only [OpenEXR](#replaying-render-settings-quality)). + +Using a video editor which supports it (e.g. [Blender](https://www.blender.org/)), you can then for example (among many +other effects) add arbitrary geometry onto your rendered video in such a way that foreground objects in the +video will still occlude background geometry as if it actually was part of the world. + +> **Note:** For best results, avoid semi-transparent blocks in front of your geometry, as they cannot be accurately represented in the depth map. + +> **Note:** Anti-aliasing is not supported when exporting depth. Instead simply render and edit your video at a higher resolution and scale it down at the very end. + ### Command Line Settings [commandline] ![](img/rendersettings-commandline.jpg) The **Command Line Render Settings** diff --git a/docs/img/depthmapsuzanne.jpg b/docs/img/depthmapsuzanne.jpg new file mode 100644 index 00000000..3a87444b Binary files /dev/null and b/docs/img/depthmapsuzanne.jpg differ diff --git a/docs/img/depthmapsuzanne.png b/docs/img/depthmapsuzanne.png new file mode 100644 index 00000000..46c4fa52 Binary files /dev/null and b/docs/img/depthmapsuzanne.png differ diff --git a/jGui b/jGui index 6602aac4..6b16892d 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 6602aac42a5af9d7ab8c420a01bbf50476037a0b +Subproject commit 6b16892d6489bf28cbf6945329f71c2effb52bc2 diff --git a/settings.gradle.kts b/settings.gradle.kts index c5b02983..355bcbc5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,7 +29,7 @@ val jGuiVersions = listOf( "1.14.4", "1.15.2", "1.16.1", - "1.16.3" + "1.16.4" ) val replayModVersions = listOf( "1.7.10", @@ -46,7 +46,7 @@ val replayModVersions = listOf( "1.14.4", "1.15.2", "1.16.1", - "1.16.3" + "1.16.4" ) include(":jGui") diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java index addb407e..3c6d2d14 100644 --- a/src/main/java/com/replaymod/core/KeyBindingRegistry.java +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -2,7 +2,10 @@ package com.replaymod.core; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; +import com.replaymod.core.mixin.KeyBindingAccessor; import de.johni0702.minecraft.gui.utils.EventRegistrations; +import com.replaymod.core.events.KeyBindingEventCallback; +import com.replaymod.core.events.KeyEventCallback; import com.replaymod.core.versions.MCVer; import net.minecraft.client.options.KeyBinding; import net.minecraft.util.crash.CrashReport; @@ -20,24 +23,24 @@ import static com.replaymod.core.ReplayMod.MOD_ID; //#endif //#if MC>=11400 -import com.replaymod.core.events.KeyBindingEventCallback; -import com.replaymod.core.events.KeyEventCallback; import com.replaymod.core.events.PreRenderCallback; -import org.lwjgl.glfw.GLFW; //#else //$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -//$$ import net.minecraftforge.fml.common.gameevent.InputEvent; //$$ import net.minecraftforge.fml.common.gameevent.TickEvent; //$$ import org.lwjgl.input.Keyboard; //$$ import static com.replaymod.core.versions.MCVer.FML_BUS; //#endif +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; public class KeyBindingRegistry extends EventRegistrations { private static final String CATEGORY = "replaymod.title"; @@ -45,23 +48,25 @@ public class KeyBindingRegistry extends EventRegistrations { static { net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.addCategory(CATEGORY); } //#endif - private Map keyBindings = new HashMap(); + private final Map bindings = new HashMap<>(); private Set onlyInReplay = new HashSet<>(); - private Multimap keyBindingHandlers = ArrayListMultimap.create(); - private Multimap repeatedKeyBindingHandlers = ArrayListMultimap.create(); - private Multimap rawHandlers = ArrayListMultimap.create(); + private Multimap> rawHandlers = ArrayListMultimap.create(); - public void registerKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) { - keyBindingHandlers.put(registerKeyBinding(name, keyCode, onlyInRepay), whenPressed); + public Binding registerKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) { + Binding binding = registerKeyBinding(name, keyCode, onlyInRepay); + binding.handlers.add(whenPressed); + return binding; } - public void registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) { - repeatedKeyBindingHandlers.put(registerKeyBinding(name, keyCode, onlyInRepay), whenPressed); + public Binding registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) { + Binding binding = registerKeyBinding(name, keyCode, onlyInRepay); + binding.repeatedHandlers.add(whenPressed); + return binding; } - private KeyBinding registerKeyBinding(String name, int keyCode, boolean onlyInRepay) { - KeyBinding keyBinding = keyBindings.get(name); - if (keyBinding == null) { + private Binding registerKeyBinding(String name, int keyCode, boolean onlyInRepay) { + Binding binding = bindings.get(name); + if (binding == null) { //#if FABRIC>=1 if (keyCode == 0) { keyCode = -1; @@ -69,27 +74,28 @@ public class KeyBindingRegistry extends EventRegistrations { Identifier id = new Identifier(MOD_ID, name.substring(LangResourcePack.LEGACY_KEY_PREFIX.length())); FabricKeyBinding fabricKeyBinding = FabricKeyBinding.Builder.create(id, InputUtil.Type.KEYSYM, keyCode, CATEGORY).build(); net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.register(fabricKeyBinding); - keyBinding = fabricKeyBinding; + KeyBinding keyBinding = fabricKeyBinding; //#else - //$$ keyBinding = new KeyBinding(name, keyCode, CATEGORY); + //$$ KeyBinding keyBinding = new KeyBinding(name, keyCode, CATEGORY); //$$ ClientRegistry.registerKeyBinding(keyBinding); //#endif - keyBindings.put(name, keyBinding); + binding = new Binding(name, keyBinding); + bindings.put(name, binding); if (onlyInRepay) { this.onlyInReplay.add(keyBinding); } } else if (!onlyInRepay) { - this.onlyInReplay.remove(keyBinding); + this.onlyInReplay.remove(binding.keyBinding); } - return keyBinding; + return binding; } - public void registerRaw(int keyCode, Runnable whenPressed) { + public void registerRaw(int keyCode, Supplier whenPressed) { rawHandlers.put(keyCode, whenPressed); } - public Map getKeyBindings() { - return Collections.unmodifiableMap(keyBindings); + public Map getBindings() { + return Collections.unmodifiableMap(bindings); } public Set getOnlyInReplay() { @@ -97,17 +103,9 @@ public class KeyBindingRegistry extends EventRegistrations { } //#if MC>=11400 - { on(KeyBindingEventCallback.EVENT, this::handleKeyBindings); } - { on(KeyEventCallback.EVENT, (keyCode, scanCode, action, modifiers) -> handleRaw(keyCode, action)); } { on(PreRenderCallback.EVENT, this::handleRepeatedKeyBindings); } //#else //$$ @SubscribeEvent - //$$ public void onKeyInput(InputEvent.KeyInputEvent event) { - //$$ handleKeyBindings(); - //$$ handleRaw(); - //$$ } - //$$ - //$$ @SubscribeEvent //$$ public void onTick(TickEvent.RenderTickEvent event) { //$$ if (event.phase != TickEvent.Phase.START) return; //$$ handleRepeatedKeyBindings(); @@ -115,52 +113,106 @@ public class KeyBindingRegistry extends EventRegistrations { //#endif public void handleRepeatedKeyBindings() { - for (Map.Entry> entry : repeatedKeyBindingHandlers.asMap().entrySet()) { - if (entry.getKey().isPressed()) { - invokeKeyBindingHandlers(entry.getKey(), entry.getValue()); + for (Binding binding : bindings.values()) { + if (binding.keyBinding.isPressed()) { + invokeKeyBindingHandlers(binding, binding.repeatedHandlers); } } } - public void handleKeyBindings() { - for (Map.Entry> entry : keyBindingHandlers.asMap().entrySet()) { - while (entry.getKey().wasPressed()) { - invokeKeyBindingHandlers(entry.getKey(), entry.getValue()); + { on(KeyBindingEventCallback.EVENT, this::handleKeyBindings); } + private void handleKeyBindings() { + for (Binding binding : bindings.values()) { + while (binding.keyBinding.wasPressed()) { + invokeKeyBindingHandlers(binding, binding.handlers); + invokeKeyBindingHandlers(binding, binding.repeatedHandlers); } } } - private void invokeKeyBindingHandlers(KeyBinding keyBinding, Collection handlers) { + private void invokeKeyBindingHandlers(Binding binding, Collection handlers) { for (final Runnable runnable : handlers) { try { runnable.run(); } catch (Throwable cause) { CrashReport crashReport = CrashReport.create(cause, "Handling Key Binding"); CrashReportSection category = crashReport.addElement("Key Binding"); - MCVer.addDetail(category, "Key Binding", keyBinding::toString); + MCVer.addDetail(category, "Key Binding", () -> binding.name); MCVer.addDetail(category, "Handler", runnable::toString); throw new CrashException(crashReport); } } } - //#if MC>=11400 - private void handleRaw(int keyCode, int action) { - if (action != GLFW.GLFW_PRESS) return; - //#else - //$$ private void handleRaw() { - //$$ int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); - //#endif - for (final Runnable runnable : rawHandlers.get(keyCode)) { + { on(KeyEventCallback.EVENT, (keyCode, scanCode, action, modifiers) -> handleRaw(keyCode, action)); } + private boolean handleRaw(int keyCode, int action) { + if (action != KeyEventCallback.ACTION_PRESS) return false; + for (final Supplier handler : rawHandlers.get(keyCode)) { try { - runnable.run(); + if (handler.get()) { + return true; + } } catch (Throwable cause) { CrashReport crashReport = CrashReport.create(cause, "Handling Raw Key Binding"); CrashReportSection category = crashReport.addElement("Key Binding"); MCVer.addDetail(category, "Key Code", () -> "" + keyCode); - MCVer.addDetail(category, "Handler", runnable::toString); + MCVer.addDetail(category, "Handler", handler::toString); throw new CrashException(crashReport); } } + return false; + } + + public class Binding { + public final String name; + public final KeyBinding keyBinding; + private final List handlers = new ArrayList<>(); + private final List repeatedHandlers = new ArrayList<>(); + private boolean autoActivation; + private Consumer autoActivationUpdate; + + public Binding(String name, KeyBinding keyBinding) { + this.name = name; + this.keyBinding = keyBinding; + } + + public String getBoundKey() { + return MCVer.getBoundKey(keyBinding); + } + + public boolean isBound() { + //#if MC>=11400 + return !keyBinding.isUnbound(); + //#else + //$$ return keyBinding.getKeyCode() != 0; + //#endif + } + + public void trigger() { + KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding; + acc.setPressTime(acc.getPressTime() + 1); + handleKeyBindings(); + } + + public void registerAutoActivationSupport(boolean active, Consumer update) { + this.autoActivation = active; + this.autoActivationUpdate = update; + } + + public boolean supportsAutoActivation() { + return autoActivationUpdate != null; + } + + public boolean isAutoActivating() { + return supportsAutoActivation() && autoActivation; + } + + public void setAutoActivating(boolean active) { + if (this.autoActivation == active) { + return; + } + this.autoActivation = active; + this.autoActivationUpdate.accept(active); + } } } diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index e9d4dece..9971ba6e 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -1,11 +1,10 @@ package com.replaymod.core; -import com.google.common.io.Files; +import com.google.common.net.PercentEscaper; import com.replaymod.compat.ReplayModCompat; import com.replaymod.core.gui.GuiBackgroundProcesses; import com.replaymod.core.gui.GuiReplaySettings; import com.replaymod.core.gui.RestoreReplayGui; -import com.replaymod.core.handler.MainMenuHandler; import com.replaymod.core.mixin.MinecraftAccessor; import com.replaymod.core.versions.MCVer; import com.replaymod.editor.ReplayModEditor; @@ -13,6 +12,8 @@ import com.replaymod.extras.ReplayModExtras; import com.replaymod.recording.ReplayModRecording; import com.replaymod.render.ReplayModRender; import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.studio.ReplayStudio; import com.replaymod.replaystudio.us.myles.ViaVersion.api.protocol.ProtocolVersion; import com.replaymod.replaystudio.util.I18n; @@ -28,6 +29,7 @@ import net.minecraft.text.LiteralText; import net.minecraft.text.TranslatableText; import net.minecraft.util.Formatting; import net.minecraft.util.crash.CrashException; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FileUtils; //#if MC>=11400 @@ -89,10 +91,16 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -136,6 +144,7 @@ public class ReplayMod implements public static final Identifier TEXTURE = new Identifier("replaymod", "replay_gui.png"); public static final int TEXTURE_SIZE = 256; + public static final Identifier LOGO_FAVICON = new Identifier("replaymod", "favicon_logo.png"); private static final MinecraftClient mc = MCVer.getMinecraft(); @@ -216,11 +225,54 @@ public class ReplayMod implements return settingsRegistry; } - public File getReplayFolder() throws IOException { - String path = getSettingsRegistry().get(Setting.RECORDING_PATH); - File folder = new File(path.startsWith("./") ? getMinecraft().runDirectory : null, path); - FileUtils.forceMkdir(folder); - return folder; + public Path getReplayFolder() throws IOException { + String str = getSettingsRegistry().get(Setting.RECORDING_PATH); + return Files.createDirectories(getMinecraft().runDirectory.toPath().resolve(str)); + } + + /** + * Folder into which replay backups are saved before the MarkerProcessor is unleashed. + */ + public Path getRawReplayFolder() throws IOException { + return Files.createDirectories(getReplayFolder().resolve("raw")); + } + + /** + * Folder into which replays are recorded. + * Distinct from the main folder, so they cannot be opened while they are still saving. + */ + public Path getRecordingFolder() throws IOException { + return Files.createDirectories(getReplayFolder().resolve("recording")); + } + + /** + * Folder in which replay cache files are stored. + * Distinct from the recording folder cause people kept confusing them with recordings. + */ + public Path getCacheFolder() throws IOException { + String str = getSettingsRegistry().get(Setting.CACHE_PATH); + Path path = getMinecraft().runDirectory.toPath().resolve(str); + Files.createDirectories(path); + try { + Files.setAttribute(path, "dos:hidden", true); + } catch (UnsupportedOperationException ignored) { + } + return path; + } + + private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false); + + public Path getCachePathForReplay(Path replay) throws IOException { + Path replayFolder = getReplayFolder(); + Path cacheFolder = getCacheFolder(); + Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath()); + return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString())); + } + + public Path getReplayPathForCache(Path cache) throws IOException { + String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8"); + Path replayFolder = getReplayFolder(); + return replayFolder.resolve(relative); } public static final DirectoryResourcePack jGuiResourcePack; @@ -323,8 +375,6 @@ public class ReplayMod implements @Override public void initClient() { - new MainMenuHandler().register(); - //#if MC<=10710 //$$ FML_BUS.register(this); // For runLater(Runnable) //#endif @@ -348,31 +398,56 @@ public class ReplayMod implements runPostStartup(() -> { final long DAYS = 24 * 60 * 60 * 1000; - // Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging) + // Cleanup any cache folders still remaining in the recording folder (we once used to put them there) try { - File[] files = new File(getReplayFolder(), "raw").listFiles(); - if (files != null) { - for (File file : files) { - if (file.lastModified() + 21 * DAYS < System.currentTimeMillis()) { - FileUtils.forceDelete(file); + Files.walkFileTree(getReplayFolder(), new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String name = dir.getFileName().toString(); + if (name.endsWith(".mcpr.cache")) { + FileUtils.deleteDirectory(dir.toFile()); + return FileVisitResult.SKIP_SUBTREE; } + return super.preVisitDirectory(dir, attrs); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + + // Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging) + try (DirectoryStream paths = Files.newDirectoryStream(getRawReplayFolder())) { + for (Path path : paths) { + if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) { + Files.delete(path); } } } catch (IOException e) { e.printStackTrace(); } + // Move anything which is still in the recording folder into the regular replay folder + // so it can be opened and/or recovered + try (DirectoryStream paths = Files.newDirectoryStream(getRecordingFolder())) { + for (Path path : paths) { + Path destination = getReplayFolder().resolve(path.getFileName()); + if (Files.exists(destination)) { + continue; // better play it save + } + Files.move(path, destination); + } + } catch (IOException e) { + e.printStackTrace(); + } + // Cleanup cache folders 7 days after last modification or when its replay is gone - try { - File[] files = getReplayFolder().listFiles(); - if (files != null) { - for (File file : files) { - String name = file.getName(); - if (file.isDirectory() && name.endsWith(".mcpr.cache")) { - File replay = new File(getReplayFolder(), name.substring(0, name.length() - ".cache".length())); - if (file.lastModified() + 7 * DAYS < System.currentTimeMillis() || !replay.exists()) { - FileUtils.deleteDirectory(file); - } + try (DirectoryStream paths = Files.newDirectoryStream(getCacheFolder())) { + for (Path path : paths) { + if (Files.isDirectory(path)) { + Path replay = getReplayPathForCache(path); + long lastModified = Files.getLastModifiedTime(path).toMillis(); + if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) { + FileUtils.deleteDirectory(path.toFile()); } } } @@ -381,14 +456,13 @@ public class ReplayMod implements } // Cleanup deleted corrupted replays - try { - File[] files = getReplayFolder().listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory() && file.getName().endsWith(".mcpr.del")) { - if (file.lastModified() + 2 * DAYS < System.currentTimeMillis()) { - FileUtils.deleteDirectory(file); - } + try (DirectoryStream paths = Files.newDirectoryStream(getReplayFolder())) { + for (Path path : paths) { + String name = path.getFileName().toString(); + if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) { + long lastModified = Files.getLastModifiedTime(path).toMillis(); + if (lastModified + 2 * DAYS < System.currentTimeMillis()) { + FileUtils.deleteDirectory(path.toFile()); } } } @@ -397,14 +471,12 @@ public class ReplayMod implements } // Restore corrupted replays - try { - File[] files = getReplayFolder().listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory() && file.getName().endsWith(".mcpr.tmp")) { - File origFile = new File(file.getParentFile(), Files.getNameWithoutExtension(file.getName())); - new RestoreReplayGui(GuiScreen.wrap(mc.currentScreen), origFile).display(); - } + try (DirectoryStream paths = Files.newDirectoryStream(getReplayFolder())) { + for (Path path : paths) { + String name = path.getFileName().toString(); + if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) { + Path original = path.resolveSibling(FilenameUtils.getBaseName(name)); + new RestoreReplayGui(this, GuiScreen.wrap(mc.currentScreen), original.toFile()).display(); } } } catch (IOException e) { @@ -700,4 +772,17 @@ public class ReplayMod implements return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion, MCVer.getProtocolVersion()); } } + + public ReplayFile openReplay(Path path) throws IOException { + return openReplay(path, path); + } + + public ReplayFile openReplay(Path input, Path output) throws IOException { + return new ZipReplayFile( + new ReplayStudio(), + input != null ? input.toFile() : null, + output.toFile(), + getCachePathForReplay(output).toFile() + ); + } } diff --git a/src/main/java/com/replaymod/core/Setting.java b/src/main/java/com/replaymod/core/Setting.java index 0b6422c2..a5b263b0 100644 --- a/src/main/java/com/replaymod/core/Setting.java +++ b/src/main/java/com/replaymod/core/Setting.java @@ -3,6 +3,7 @@ package com.replaymod.core; public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting NOTIFICATIONS = make("notifications", "notifications", true); public static final Setting RECORDING_PATH = advanced("recordingPath", null, "./replay_recordings/"); + public static final Setting CACHE_PATH = advanced("cachePath", null, "./.replay_cache/"); private static Setting make(String key, String displayName, T defaultValue) { return new Setting<>("core", key, displayName, defaultValue); diff --git a/src/main/java/com/replaymod/core/SettingsRegistry.java b/src/main/java/com/replaymod/core/SettingsRegistry.java index 0dc0e50c..ff911a1d 100644 --- a/src/main/java/com/replaymod/core/SettingsRegistry.java +++ b/src/main/java/com/replaymod/core/SettingsRegistry.java @@ -72,7 +72,7 @@ public class SettingsRegistry { JsonElement category = root.get(key.getCategory()); if (category != null && category.isJsonObject()) { JsonElement valueElem = category.getAsJsonObject().get(key.getKey()); - if (!valueElem.isJsonPrimitive()) continue; + if (valueElem == null || !valueElem.isJsonPrimitive()) continue; JsonPrimitive value = valueElem.getAsJsonPrimitive(); if (key.getDefault() instanceof Boolean && value.isBoolean()) { entry.setValue(value.getAsBoolean()); diff --git a/src/main/java/com/replaymod/core/events/KeyBindingEventCallback.java b/src/main/java/com/replaymod/core/events/KeyBindingEventCallback.java index 942783ca..c9784d02 100644 --- a/src/main/java/com/replaymod/core/events/KeyBindingEventCallback.java +++ b/src/main/java/com/replaymod/core/events/KeyBindingEventCallback.java @@ -1,4 +1,3 @@ -//#if MC>=11400 package com.replaymod.core.events; import de.johni0702.minecraft.gui.utils.Event; @@ -14,4 +13,3 @@ public interface KeyBindingEventCallback { void onKeybindingEvent(); } -//#endif diff --git a/src/main/java/com/replaymod/core/events/KeyEventCallback.java b/src/main/java/com/replaymod/core/events/KeyEventCallback.java index 8403a1ba..894f822a 100644 --- a/src/main/java/com/replaymod/core/events/KeyEventCallback.java +++ b/src/main/java/com/replaymod/core/events/KeyEventCallback.java @@ -1,4 +1,3 @@ -//#if MC>=11400 package com.replaymod.core.events; import de.johni0702.minecraft.gui.utils.Event; @@ -7,11 +6,21 @@ public interface KeyEventCallback { Event EVENT = Event.create((listeners) -> (key, scanCode, action, modifiers) -> { for (KeyEventCallback listener : listeners) { - listener.onKeyEvent(key, scanCode, action, modifiers); + if (listener.onKeyEvent(key, scanCode, action, modifiers)) { + return true; + } } + return false; } ); - void onKeyEvent(int key, int scanCode, int action, int modifiers); + //#if MC>=11400 + int ACTION_RELEASE = org.lwjgl.glfw.GLFW.GLFW_RELEASE; + int ACTION_PRESS = org.lwjgl.glfw.GLFW.GLFW_PRESS; + //#else + //$$ int ACTION_RELEASE = 0; + //$$ int ACTION_PRESS = 1; + //#endif + + boolean onKeyEvent(int key, int scanCode, int action, int modifiers); } -//#endif diff --git a/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java b/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java index 3aabaa04..39b71973 100644 --- a/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java +++ b/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java @@ -38,7 +38,8 @@ public class GuiBackgroundProcesses extends EventRegistrations { //#endif if (guiScreen != getMinecraft().currentScreen) return; // people tend to construct GuiScreens without opening them - VanillaGuiScreen.setup(guiScreen).setLayout(new CustomLayout() { + VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(guiScreen); + vanillaGui.setLayout(new CustomLayout(vanillaGui.getLayout()) { @Override protected void layout(GuiScreen container, int width, int height) { pos(panel, width - 5 - width(panel), 5); @@ -76,6 +77,11 @@ public class GuiBackgroundProcesses extends EventRegistrations { }); } + @Override + public int getLayer() { + return 1; + } + @Override public ReadableDimension getMinSize() { ReadableDimension minSize = inner.getMinSize(); diff --git a/src/main/java/com/replaymod/core/gui/GuiReplayButton.java b/src/main/java/com/replaymod/core/gui/GuiReplayButton.java new file mode 100644 index 00000000..9bd3476f --- /dev/null +++ b/src/main/java/com/replaymod/core/gui/GuiReplayButton.java @@ -0,0 +1,17 @@ +package com.replaymod.core.gui; + +import com.replaymod.core.ReplayMod; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; + +public class GuiReplayButton extends GuiButton { + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + super.draw(renderer, size, renderInfo); + + renderer.bindTexture(ReplayMod.LOGO_FAVICON); + renderer.drawTexturedRect(3, 3, 0, 0, size.getWidth() - 6, size.getHeight() - 6, 1, 1, 1, 1); + } +} diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index 73b56e12..4d9ba137 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -2,32 +2,40 @@ package com.replaymod.core.gui; import com.google.common.io.Files; import com.google.gson.Gson; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.core.versions.MCVer; import com.replaymod.replaystudio.io.ReplayInputStream; import com.replaymod.replaystudio.io.ReplayOutputStream; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; -import com.replaymod.replaystudio.replay.ZipReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.container.VanillaGuiScreen; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.utils.Colors; +import net.minecraft.util.crash.CrashReport; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.function.Consumer; import static com.replaymod.replaystudio.util.Utils.readInt; import static com.replaymod.replaystudio.util.Utils.writeInt; public class RestoreReplayGui extends AbstractGuiScreen { + private static Logger LOGGER = LogManager.getLogger(); public final GuiScreen parent; public final File file; @@ -37,8 +45,10 @@ public class RestoreReplayGui extends AbstractGuiScreen { textPanel, buttonPanel).setLayout(new VerticalLayout().setSpacing(20)); public final GuiButton yesButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.yes"); public final GuiButton noButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.no"); + private final ReplayMod core; - public RestoreReplayGui(GuiScreen parent, File file) { + public RestoreReplayGui(ReplayMod core, GuiScreen parent, File file) { + this.core = core; this.parent = parent; this.file = file; @@ -47,52 +57,7 @@ public class RestoreReplayGui extends AbstractGuiScreen { new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())), new GuiLabel().setI18nText("replaymod.gui.restorereplay3")); yesButton.onClick(() -> { - try { - ReplayStudio studio = new ReplayStudio(); - ReplayFile replayFile = new ZipReplayFile(studio, null, file); - // Commit all not-yet-committed files into the main zip file. - // If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data! - replayFile.save(); - ReplayMetaData metaData = replayFile.getMetaData(); - if (metaData != null && metaData.getDuration() == 0) { - // Try to restore replay duration - // We need to re-write the packet data in case there are any incomplete packets dangling at the end - try (ReplayInputStream in = replayFile.getPacketData(MCVer.getPacketTypeRegistry(true)); - ReplayOutputStream out = replayFile.writePacketData()) { - while (true) { - // To prevent failing at un-parsable packets and to support recovery in minimal mode, - // we do not use the ReplayIn/OutputStream methods but instead parse the packets ourselves. - int time = readInt(in); - int length = readInt(in); - if (time == -1 || length == -1) { - break; - } - byte[] buf = new byte[length]; - IOUtils.readFully(in, buf); - - // Fully read, update replay duration - metaData.setDuration(time); - - // Write packet back into recovered replay - writeInt(out, time); - writeInt(out, length); - out.write(buf); - } - } catch (Throwable t) { - t.printStackTrace(); - } - // Write back the actual duration - try (OutputStream out = replayFile.write("metaData.json")) { - metaData.setGenerator(metaData.getGenerator() + "(+ ReplayMod Replay Recovery)"); - String json = (new Gson()).toJson(metaData); - out.write(json.getBytes()); - } - } - replayFile.save(); - replayFile.close(); - } catch (IOException e) { - e.printStackTrace(); - } + recoverInBackground(); parent.display(); }); noButton.onClick(() -> { @@ -121,4 +86,72 @@ public class RestoreReplayGui extends AbstractGuiScreen { protected RestoreReplayGui getThis() { return this; } + + private void recoverInBackground() { + GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK); + GuiProgressBar progressBar = new GuiProgressBar().setHeight(14); + GuiPanel savingProcess = new GuiPanel() + .setLayout(new VerticalLayout()) + .addElements(new VerticalLayout.Data(0.5), label, progressBar); + new Thread(() -> { + core.runLater(() -> core.getBackgroundProcesses().addProcess(savingProcess)); + try { + tryRecover(progressBar::setProgress); + } catch (IOException e) { + LOGGER.error("Recovering replay file:", e); + CrashReport crashReport = CrashReport.create(e, "Recovering replay file"); + core.runLater(() -> Utils.error(LOGGER, VanillaGuiScreen.wrap(getMinecraft().currentScreen), crashReport, () -> {})); + } finally { + core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingProcess)); + } + }).start(); + } + + private void tryRecover(Consumer progress) throws IOException { + ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath()); + // Commit all not-yet-committed files into the main zip file. + // If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data! + replayFile.save(); + progress.accept(0.4f); + ReplayMetaData metaData = replayFile.getMetaData(); + if (metaData != null && metaData.getDuration() == 0) { + // Try to restore replay duration + // We need to re-write the packet data in case there are any incomplete packets dangling at the end + try (ReplayInputStream in = replayFile.getPacketData(MCVer.getPacketTypeRegistry(true)); + ReplayOutputStream out = replayFile.writePacketData()) { + while (true) { + // To prevent failing at un-parsable packets and to support recovery in minimal mode, + // we do not use the ReplayIn/OutputStream methods but instead parse the packets ourselves. + int time = readInt(in); + int length = readInt(in); + if (time == -1 || length == -1) { + break; + } + byte[] buf = new byte[length]; + IOUtils.readFully(in, buf); + + // Fully read, update replay duration + metaData.setDuration(time); + + // Write packet back into recovered replay + writeInt(out, time); + writeInt(out, length); + out.write(buf); + } + } catch (Throwable t) { + t.printStackTrace(); + } + // Write back the actual duration + try (OutputStream out = replayFile.write("metaData.json")) { + metaData.setGenerator(metaData.getGenerator() + "(+ ReplayMod Replay Recovery)"); + String json = (new Gson()).toJson(metaData); + out.write(json.getBytes()); + } + } + progress.accept(0.6f); + replayFile.save(); + progress.accept(0.9f); + replayFile.close(); + progress.accept(1f); + } } diff --git a/src/main/java/com/replaymod/core/handler/MainMenuHandler.java b/src/main/java/com/replaymod/core/handler/MainMenuHandler.java deleted file mode 100644 index 905b2761..00000000 --- a/src/main/java/com/replaymod/core/handler/MainMenuHandler.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.replaymod.core.handler; - -import com.replaymod.core.mixin.GuiMainMenuAccessor; -import de.johni0702.minecraft.gui.utils.EventRegistrations; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.screen.Screen; -import org.lwjgl.opengl.GL11; - -import java.util.List; - -import static com.replaymod.core.versions.MCVer.*; - -//#if MC>=11400 && MC<11600 -//$$ import net.minecraft.realms.RealmsScreenProxy; -//#endif - -//#if FABRIC>=1 -import de.johni0702.minecraft.gui.versions.callbacks.InitScreenCallback; -//#else -//$$ import net.minecraftforge.client.event.GuiScreenEvent; -//$$ import net.minecraftforge.common.MinecraftForge; -//$$ import net.minecraftforge.eventbus.api.SubscribeEvent; -//#endif - -//#if MC>=11400 -import net.minecraft.client.gui.widget.AbstractButtonWidget; -import net.minecraft.client.resource.language.I18n; -//#else -//$$ import net.minecraft.client.gui.GuiButton; -//#endif - -/** - * Moves certain buttons on the main menu upwards so we can inject our own. - */ -public class MainMenuHandler extends EventRegistrations { - //#if FABRIC>=1 - { on(InitScreenCallback.EVENT, this::onInit); } - public void onInit(Screen guiScreen, List buttonList) { - //#else - //$$ @SubscribeEvent - //$$ public void onInit(GuiScreenEvent.InitGuiEvent.Post event) { - //$$ Screen guiScreen = getGui(event); - //$$ List buttonList = getButtonList(event); - //#endif - if (guiScreen instanceof TitleScreen) { - TitleScreen gui = (TitleScreen) guiScreen; - int realmsOffset = 0; - //#if MC>=11400 - final String BUTTON_REALMS = I18n.translate("menu.online"); - for (AbstractButtonWidget button : buttonList) { - //#else - //$$ for (GuiButton button : buttonList) { - //#endif - // Buttons that aren't in a rectangle directly above our space don't need moving - if (button.x + width(button) < gui.width / 2 - 100 - || button.x > gui.width / 2 + 100 - || button.y > gui.height / 4 + 10 + 4 * 24 + 20) continue; - // Move button up to make space for one rows of buttons - // and then move back down by 10 to compensate for the space to the exit button that was already there - int offset = -1 * 24 + 10; - button.y += offset; - - //#if MC>=11400 - //#if MC>=11400 - if (BUTTON_REALMS.equals(button.getMessage())) { - //#else - //$$ if (button.id == 14) { - //#endif - realmsOffset = offset; - } - //#endif - } - //#if MC>=11400 && MC<11600 - //$$ GuiMainMenuAccessor guiA = (GuiMainMenuAccessor) gui; - //$$ if (realmsOffset != 0 && guiA.getRealmsNotification() instanceof RealmsScreenProxy) { - //$$ guiA.setRealmsNotification(new RealmsNotificationProxy((RealmsScreenProxy) guiA.getRealmsNotification(), realmsOffset)); - //$$ } - //#endif - } - } - - //#if MC>=11400 && MC<11600 - //$$ private static class RealmsNotificationProxy extends Screen { - //$$ private final RealmsScreenProxy proxy; - //$$ private final int offset; - //$$ - //$$ private RealmsNotificationProxy(RealmsScreenProxy proxy, int offset) { - //#if MC>=11400 - //$$ super(null); - //#endif - //$$ this.proxy = proxy; - //$$ this.offset = offset; - //$$ } - //$$ - //$$ @Override - //$$ public void init(MinecraftClient mc, int width, int height) { - //$$ proxy.init(mc, width, height); - //$$ } - //$$ - //$$ @Override - //$$ public void tick() { - //$$ proxy.tick(); - //$$ } - //$$ - //$$ @Override - //$$ public void render(int mouseX, int mouseY, float partialTicks) { - //$$ GL11.glTranslated(0, offset, 0); - //$$ proxy.render(mouseX, mouseY - offset, partialTicks); - //$$ GL11.glTranslated(0, -offset, 0); - //$$ } - //$$ - //$$ @Override - //$$ public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { - //$$ return proxy.mouseClicked(mouseX, mouseY - offset, mouseButton); - //$$ } - //$$ - //$$ @Override - //$$ public void removed() { - //$$ proxy.removed(); - //$$ } - //$$ } - //#endif -} diff --git a/src/main/java/com/replaymod/core/mixin/MixinKeyboardListener.java b/src/main/java/com/replaymod/core/mixin/MixinKeyboardListener.java index fb73d164..3a7c680c 100644 --- a/src/main/java/com/replaymod/core/mixin/MixinKeyboardListener.java +++ b/src/main/java/com/replaymod/core/mixin/MixinKeyboardListener.java @@ -1,20 +1,51 @@ -//#if MC>=11400 package com.replaymod.core.mixin; import com.replaymod.core.events.KeyBindingEventCallback; import com.replaymod.core.events.KeyEventCallback; -import net.minecraft.client.Keyboard; 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; +//#if MC>=11400 +import net.minecraft.client.Keyboard; +//#else +//$$ import net.minecraft.client.settings.KeyBinding; +//$$ import org.lwjgl.input.Keyboard; +//#endif + +//#if MC>=11400 @Mixin(Keyboard.class) public class MixinKeyboardListener { - @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/options/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V", shift = At.Shift.AFTER)) + private static final String ON_KEY_PRESSED = "Lnet/minecraft/client/options/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"; + + @Inject(method = "onKey", at = @At(value = "INVOKE", target = ON_KEY_PRESSED), cancellable = true) + private void beforeKeyBindingTick(long windowPointer, int key, int scanCode, int action, int modifiers, CallbackInfo ci) { + if (KeyEventCallback.EVENT.invoker().onKeyEvent(key, scanCode, action, modifiers)) { + ci.cancel(); + } + } + + @Inject(method = "onKey", at = @At(value = "INVOKE", target = ON_KEY_PRESSED, shift = At.Shift.AFTER)) private void afterKeyBindingTick(long windowPointer, int key, int scanCode, int action, int modifiers, CallbackInfo ci) { KeyBindingEventCallback.EVENT.invoker().onKeybindingEvent(); - KeyEventCallback.EVENT.invoker().onKeyEvent(key, scanCode, action, modifiers); } } +//#else +//$$ @Mixin(KeyBinding.class) +//$$ public class MixinKeyboardListener { +//$$ @Inject(method = "onTick", at = @At("HEAD"), cancellable = true) +//$$ private static void beforeKeyBindingTick(CallbackInfo ci) { +//$$ int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); +//$$ int action = Keyboard.getEventKeyState() ? KeyEventCallback.ACTION_PRESS : KeyEventCallback.ACTION_RELEASE; +//$$ if (KeyEventCallback.EVENT.invoker().onKeyEvent(keyCode, 0, action, 0)) { +//$$ ci.cancel(); +//$$ } +//$$ } +//$$ +//$$ @Inject(method = "onTick", at = @At("RETURN")) +//$$ private static void afterKeyBindingTick(CallbackInfo ci) { +//$$ KeyBindingEventCallback.EVENT.invoker().onKeybindingEvent(); +//$$ } +//$$ } //#endif diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index f15ec91f..a318c793 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -367,4 +367,9 @@ public class Utils { open(); } } + + public static T configure(T instance, Consumer configure) { + configure.accept(instance); + return instance; + } } diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index f6295519..f0c99a10 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -7,6 +7,7 @@ import com.replaymod.replaystudio.us.myles.ViaVersion.api.protocol.ProtocolVersi import com.replaymod.replaystudio.us.myles.ViaVersion.packets.State; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.options.KeyBinding; import net.minecraft.client.world.ClientWorld; import net.minecraft.client.render.Tessellator; import net.minecraft.client.render.entity.EntityRenderDispatcher; @@ -37,7 +38,6 @@ import java.util.ArrayList; //#if MC>=11400 import com.replaymod.core.mixin.AbstractButtonWidgetAccessor; import net.minecraft.SharedConstants; -import net.minecraft.client.gui.screen.TitleScreen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.AbstractButtonWidget; @@ -597,13 +597,6 @@ public class MCVer { //#endif ) { GuiScreenAccessor acc = (GuiScreenAccessor) screen; - //#if MC>=11400 - if (screen instanceof TitleScreen && isModLoaded("modmenu")) { - // Since we bypass the usual addButton, we need to manually move our buttons down - // https://github.com/Prospector/ModMenu/blob/eea70ec37581a7229142cf21df795fdacc6a7b4c/src/main/java/io/github/prospector/modmenu/mixin/TitleScreenMixin.java#L42 - button.y += 12; - } - //#endif acc.getButtons().add(button); //#if MC>=11400 acc.getChildren().add(button); @@ -748,6 +741,23 @@ public class MCVer { //#endif } + public static String getBoundKey(KeyBinding keyBinding) { + try { + //#if MC>=11600 + return keyBinding.getBoundKeyLocalizedText().getString(); + //#else + //#if MC>=11400 + //$$ return keyBinding.getLocalizedName(); + //#else + //$$ return Keyboard.getKeyName(keyBinding.getKeyCode()); + //#endif + //#endif + } catch (ArrayIndexOutOfBoundsException e) { + // Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55 + return "Unknown"; + } + } + //#if MC>=11400 private static Boolean hasOptifine; public static boolean hasOptifine() { @@ -869,6 +879,10 @@ public class MCVer { //$$ public static final int KEY_Z = org.lwjgl.input.Keyboard.KEY_Z; //#endif + public static boolean hasControlDown() { + return Screen.hasControlDown(); + } + public static boolean isKeyDown(int keyCode) { //#if MC>=11500 return InputUtil.isKeyPressed(getMinecraft().getWindow().getHandle(), keyCode); @@ -893,6 +907,10 @@ public class MCVer { //$$ public static boolean getEventKeyState() { //$$ return org.lwjgl.input.Keyboard.getEventKeyState(); //$$ } + //$$ + //$$ public static String getKeyName(int code) { + //$$ return org.lwjgl.input.Keyboard.getKeyName(code); + //$$ } //#endif } } diff --git a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java index cb66ac33..1ee86995 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java +++ b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java @@ -5,14 +5,12 @@ import com.replaymod.core.utils.Utils; import com.replaymod.editor.ReplayModEditor; import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replaystudio.data.Marker; -import com.replaymod.replaystudio.replay.ZipReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; +import com.replaymod.replaystudio.replay.ReplayFile; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar; -import de.johni0702.minecraft.gui.element.GuiTexturedButton; import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; @@ -42,14 +40,14 @@ public class GuiEditReplay extends AbstractGuiPopup { private final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(300, 9); - private final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9) + private final GuiButton zoomInButton = new GuiButton().setSize(9, 9) .onClick(() -> zoomTimeline(2d / 3d)) - .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 20) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin")); - private final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9) + private final GuiButton zoomOutButton = new GuiButton().setSize(9, 9) .onClick(() -> zoomTimeline(3d / 2d)) - .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 30) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout")); private final GuiPanel zoomButtonPanel = new GuiPanel() @@ -62,7 +60,7 @@ public class GuiEditReplay extends AbstractGuiPopup { super(container); this.inputPath = inputPath; - try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) { + try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) { markers = replayFile.getMarkers().or(HashSet::new); timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers); timeline.setSize(300, 20) @@ -149,7 +147,7 @@ public class GuiEditReplay extends AbstractGuiPopup { ProgressPopup progressPopup = new ProgressPopup(this); new Thread(() -> { - try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) { + try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) { replayFile.writeMarkers(markers); replayFile.save(); } catch (IOException e) { diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java index 5f5703a8..eca8f74f 100644 --- a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -1,8 +1,8 @@ package com.replaymod.editor.gui; +import com.replaymod.core.ReplayMod; import com.replaymod.core.versions.MCVer; import com.replaymod.replaystudio.PacketData; -import com.replaymod.replaystudio.Studio; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.filter.SquashFilter; import com.replaymod.replaystudio.filter.StreamFilter; @@ -11,13 +11,11 @@ import com.replaymod.replaystudio.io.ReplayOutputStream; import com.replaymod.replaystudio.protocol.PacketTypeRegistry; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; -import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.stream.IteratorStream; -import com.replaymod.replaystudio.studio.ReplayStudio; +import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair; import com.replaymod.replaystudio.util.Utils; import org.apache.commons.io.FilenameUtils; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -50,7 +48,7 @@ public class MarkerProcessor { public static final String MARKER_NAME_SPLIT = "_RM_SPLIT"; private static boolean hasWork(Path path) throws IOException { - try (ZipReplayFile inputReplayFile = new ZipReplayFile(new ReplayStudio(), path.toFile())) { + try (ReplayFile inputReplayFile = ReplayMod.instance.openReplay(path)) { return inputReplayFile.getMarkers().or(HashSet::new).stream().anyMatch(m -> m.getName() != null && m.getName().startsWith("_RM_")); } } @@ -106,27 +104,33 @@ public class MarkerProcessor { return suffixes; } - public static void apply(Path path, Consumer progress) throws IOException { + public static List> apply(Path path, Consumer progress) throws IOException { + ReplayMod mod = ReplayMod.instance; if (!hasWork(path)) { - return; + ReplayMetaData metaData; + try (ReplayFile inputReplayFile = mod.openReplay(path)) { + metaData = inputReplayFile.getMetaData(); + } + return Collections.singletonList(new Pair<>(path, metaData)); } String replayName = FilenameUtils.getBaseName(path.getFileName().toString()); int splitCounter = 0; PacketTypeRegistry registry = MCVer.getPacketTypeRegistry(true); - Studio studio = new ReplayStudio(); SquashFilter squashFilter = new SquashFilter(); - squashFilter.init(studio, null); - Path inputPath = path.resolveSibling("raw").resolve(path.getFileName()); + List> outputPaths = new ArrayList<>(); + + Path rawFolder = ReplayMod.instance.getRawReplayFolder(); + Path inputPath = rawFolder.resolve(path.getFileName()); for (int i = 1; Files.exists(inputPath); i++) { inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr"); } Files.createDirectories(inputPath.getParent()); Files.move(path, inputPath); - try (ZipReplayFile inputReplayFile = new ZipReplayFile(studio, inputPath.toFile())) { + try (ReplayFile inputReplayFile = mod.openReplay(inputPath)) { List markers = inputReplayFile.getMarkers().or(HashSet::new) .stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList()); Iterator markerIterator = markers.iterator(); @@ -142,8 +146,8 @@ public class MarkerProcessor { Marker nextMarker = markerIterator.next(); while (nextPacket != null && outputFileSuffixes.hasNext()) { - File outputFile = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr").toFile(); - try (ZipReplayFile outputReplayFile = new ZipReplayFile(studio, null, outputFile)) { + Path outputPath = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr"); + try (ReplayFile outputReplayFile = mod.openReplay(null, outputPath)) { long duration = 0; Set outputMarkers = new HashSet<>(); ReplayMetaData metaData = inputReplayFile.getMetaData(); @@ -240,6 +244,8 @@ public class MarkerProcessor { } outputReplayFile.save(); + + outputPaths.add(new Pair(outputPath, metaData)); } } @@ -248,5 +254,7 @@ public class MarkerProcessor { cutFilter.release(); } } + + return outputPaths; } } diff --git a/src/main/java/com/replaymod/editor/handler/GuiHandler.java b/src/main/java/com/replaymod/editor/handler/GuiHandler.java index 599da527..7f5f4727 100644 --- a/src/main/java/com/replaymod/editor/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/editor/handler/GuiHandler.java @@ -43,7 +43,7 @@ public class GuiHandler extends EventRegistrations { replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> { if (Utils.ifMinimalModeDoPopup(replayViewer, () -> {})) return; try { - new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) { + new GuiEditReplay(replayViewer, replayViewer.list.getSelected().get(0).file.toPath()) { @Override protected void close() { super.close(); diff --git a/src/main/java/com/replaymod/extras/FullBrightness.java b/src/main/java/com/replaymod/extras/FullBrightness.java index 4f1fa7c5..63fe5c16 100644 --- a/src/main/java/com/replaymod/extras/FullBrightness.java +++ b/src/main/java/com/replaymod/extras/FullBrightness.java @@ -11,6 +11,8 @@ import de.johni0702.minecraft.gui.element.IGuiImage; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; //#if FABRIC>=1 import com.replaymod.core.events.PreRenderCallback; @@ -21,6 +23,7 @@ import com.replaymod.core.events.PostRenderCallback; //#endif public class FullBrightness extends EventRegistrations implements Extra { + private ReplayMod core; private ReplayModReplay module; private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 16).setSize(19, 16); @@ -35,6 +38,7 @@ public class FullBrightness extends EventRegistrations implements Extra { @Override public void register(final ReplayMod mod) throws Exception { + this.core = mod; this.module = ReplayModReplay.instance; this.mc = mod.getMinecraft(); @@ -58,6 +62,16 @@ public class FullBrightness extends EventRegistrations implements Extra { register(); } + public Type getType() { + String str = core.getSettingsRegistry().get(Setting.FULL_BRIGHTNESS); + for (Type type : Type.values()) { + if (type.toString().equals(str)) { + return type; + } + } + return Type.Gamma; + } + //#if FABRIC>=1 { on(PreRenderCallback.EVENT, this::preRender); } private void preRender() { @@ -67,8 +81,20 @@ public class FullBrightness extends EventRegistrations implements Extra { //$$ if (event.phase != TickEvent.Phase.START) return; //#endif if (active && module.getReplayHandler() != null) { - originalGamma = mc.options.gamma; - mc.options.gamma = 1000; + Type type = getType(); + if (type == Type.Gamma || type == Type.Both) { + originalGamma = mc.options.gamma; + mc.options.gamma = 1000; + } + if (type == Type.NightVision || type == Type.Both) { + if (mc.player != null) { + mc.player.addStatusEffect(new StatusEffectInstance(StatusEffects.NIGHT_VISION + //#if MC<=10809 + //$$ .id + //#endif + , Integer.MAX_VALUE)); + } + } } } @@ -81,7 +107,19 @@ public class FullBrightness extends EventRegistrations implements Extra { //$$ if (event.phase != TickEvent.Phase.END) return; //#endif if (active && module.getReplayHandler() != null) { - mc.options.gamma = originalGamma; + Type type = getType(); + if (type == Type.Gamma || type == Type.Both) { + mc.options.gamma = originalGamma; + } + if (type == Type.NightVision || type == Type.Both) { + if (mc.player != null) { + mc.player.removeStatusEffect(StatusEffects.NIGHT_VISION + //#if MC<=10809 + //$$ .id + //#endif + ); + } + } } } @@ -93,4 +131,16 @@ public class FullBrightness extends EventRegistrations implements Extra { overlay.statusIndicatorPanel.removeElement(indicator); } } + + enum Type { + Gamma, + NightVision, + Both, + ; + + @Override + public String toString() { + return "replaymod.gui.settings.fullbrightness." + name().toLowerCase(); + } + } } diff --git a/src/main/java/com/replaymod/extras/HotkeyButtons.java b/src/main/java/com/replaymod/extras/HotkeyButtons.java index 08d37b19..41829207 100644 --- a/src/main/java/com/replaymod/extras/HotkeyButtons.java +++ b/src/main/java/com/replaymod/extras/HotkeyButtons.java @@ -2,31 +2,23 @@ package com.replaymod.extras; import com.replaymod.core.KeyBindingRegistry; import com.replaymod.core.ReplayMod; -import com.replaymod.core.mixin.KeyBindingAccessor; import com.replaymod.replay.events.ReplayOpenedCallback; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; -import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; -import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.LayoutData; import de.johni0702.minecraft.gui.utils.EventRegistrations; -import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.resource.language.I18n; -//#if MC>=11400 -//#else -//$$ import org.lwjgl.input.Keyboard; -//#endif - import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -44,15 +36,15 @@ public class HotkeyButtons extends EventRegistrations implements Extra { { on(ReplayOpenedCallback.EVENT, replayHandler -> new Gui(mod, replayHandler.getOverlay())); } public static final class Gui { - private final GuiTexturedButton toggleButton; + private final GuiButton toggleButton; private final GridLayout panelLayout; private final GuiPanel panel; private boolean open; public Gui(ReplayMod mod, GuiReplayOverlay overlay) { - toggleButton = new GuiTexturedButton(overlay).setSize(20, 20) - .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(0, 120) + toggleButton = new GuiButton(overlay).setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(0, 120) .onClick(new Runnable() { @Override public void run() { @@ -73,50 +65,51 @@ public class HotkeyButtons extends EventRegistrations implements Extra { }.setLayout(panelLayout = new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(1)); final KeyBindingRegistry keyBindingRegistry = mod.getKeyBindingRegistry(); - keyBindingRegistry.getKeyBindings().values().stream() - .sorted(Comparator.comparing(it -> I18n.translate(it.getTranslationKey()))) + keyBindingRegistry.getBindings().values().stream() + .sorted(Comparator.comparing(it -> I18n.translate(it.name))) .forEachOrdered(keyBinding -> { GuiButton button = new GuiButton(){ @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { // There doesn't seem to be an KeyBindingUpdate event, so we'll just update it every time - String keyName = "???"; - try { - //#if MC>=11600 - keyName = keyBinding.getBoundKeyLocalizedText().asString(); - //#else - //#if MC>=11400 - //$$ keyName = keyBinding.getLocalizedName(); - //#else - //$$ keyName = Keyboard.getKeyName(keyBinding.getKeyCode()); - //#endif - //#endif - } catch (ArrayIndexOutOfBoundsException e) { - // Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55 + setLabel(keyBinding.isBound() ? keyBinding.getBoundKey() : ""); + + if (keyBinding.supportsAutoActivation()) { + setTooltip(new GuiTooltip().setText(new String[]{ + I18n.translate("replaymod.gui.ingame.autoactivating"), + I18n.translate("replaymod.gui.ingame.autoactivating." + + (keyBinding.isAutoActivating() ? "disable" : "enable")), + })); + setLabelColor(keyBinding.isAutoActivating() ? 0x00ff00 : 0xe0e0e0); } - setLabel(keyName); + super.draw(renderer, size, renderInfo); } }.onClick(() -> { - KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding; - acc.setPressTime(acc.getPressTime() + 1); - keyBindingRegistry.handleKeyBindings(); + if (keyBinding.supportsAutoActivation() && Screen.hasControlDown()) { + keyBinding.setAutoActivating(!keyBinding.isAutoActivating()); + } else { + keyBinding.trigger(); + } }); - panel.addElements(null, new GuiPanel().setSize(150, 20).setLayout(new HorizontalLayout().setSpacing(2)) - .addElements(new HorizontalLayout.Data(0.5), - new GuiPanel().setLayout(new CustomLayout() { - @Override - protected void layout(GuiPanel container, int width, int height) { - size(button, width, height); - } + GuiLabel label = new GuiLabel().setI18nText(keyBinding.name); + panel.addElements(null, new GuiPanel().setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + width(button, Math.max(10 /* consistent min width */, width(button)) + 10 /* padding */); + height(button, 20); - @Override - public ReadableDimension calcMinSize(GuiContainer container) { - return new Dimension(Math.max(10, button.getMinSize().getWidth()) + 10, 20); - } - }).addElements(null, button), - new GuiLabel().setI18nText(keyBinding.getTranslationKey()) - )); + int textWidth = width(label); + + x(label, width(button) + 4); + width(label, width - x(label)); + + if (textWidth > width - x(label)) { + height(label, height(label) * 2); // split over two lines + } + y(label, (height - height(label)) / 2); + } + }).addElements(null, button, label).setSize(150, 20)); }); overlay.setLayout(new CustomLayout(overlay.getLayout()) { diff --git a/src/main/java/com/replaymod/extras/Setting.java b/src/main/java/com/replaymod/extras/Setting.java index ae6a359c..278ff94f 100644 --- a/src/main/java/com/replaymod/extras/Setting.java +++ b/src/main/java/com/replaymod/extras/Setting.java @@ -2,9 +2,18 @@ package com.replaymod.extras; import com.replaymod.core.SettingsRegistry; +import java.util.Arrays; +import java.util.stream.Collectors; + public final class Setting { public static final SettingsRegistry.SettingKey ASK_FOR_OPEN_EYE = new SettingsRegistry.SettingKeys<>("advanced", "askForOpenEye", null, true); public static final SettingsRegistry.SettingKey SKIP_POST_SCREENSHOT_GUI = new SettingsRegistry.SettingKeys<>("advanced", "skipPostScreenshotGui", null, false); + public static final SettingsRegistry.MultipleChoiceSettingKeys FULL_BRIGHTNESS = new SettingsRegistry.MultipleChoiceSettingKeys<>( + "advanced", "fullBrightness", "replaymod.gui.settings.fullbrightness", + FullBrightness.Type.Gamma.toString()); + static { + FULL_BRIGHTNESS.setChoices(Arrays.stream(FullBrightness.Type.values()).map(Object::toString).collect(Collectors.toList())); + } } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java b/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java index dd18f384..70aca0d2 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java @@ -20,6 +20,6 @@ public class AdvancedScreenshots implements Extra { } private void takeScreenshot() { - ReplayMod.instance.runLater(() -> new GuiCreateScreenshot(mod).display()); + ReplayMod.instance.runLater(() -> new GuiCreateScreenshot(mod).open()); } } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java index 71d13f6a..5e831ebb 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java @@ -27,7 +27,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable { private final ReplayMod mod; public GuiCreateScreenshot(ReplayMod mod) { - super(null, null); + super(GuiRenderSettings.createBaseScreen(), null, null); this.mod = mod; @@ -64,7 +64,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable { } catch (Throwable t) { error(LOGGER, GuiCreateScreenshot.this, CrashReport.create(t, "Rendering video"), () -> {}); - display(); // Re-show the render settings gui and the new error popup + getScreen().display(); // Re-show the render settings gui and the new error popup } }); }); @@ -75,6 +75,18 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable { return container; } + @Override + public void open() { + super.open(); + getScreen().display(); + } + + @Override + public void close() { + super.close(); + getMinecraft().openScreen(null); + } + @Override public void load() { // pause replay when opening this gui diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java index b79dfe9c..70522b22 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java @@ -4,7 +4,8 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; import com.replaymod.core.versions.MCVer; import com.replaymod.extras.ReplayModExtras; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.rendering.Channel; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.replay.ReplayModReplay; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; @@ -13,8 +14,9 @@ import net.minecraft.util.crash.CrashReport; import java.io.File; import java.io.IOException; +import java.util.Map; -public class ScreenshotWriter implements FrameConsumer { +public class ScreenshotWriter implements FrameConsumer { private final File outputFile; @@ -23,7 +25,9 @@ public class ScreenshotWriter implements FrameConsumer { } @Override - public void consume(RGBFrame frame) { + public void consume(Map channels) { + BitmapFrame frame = channels.get(Channel.BRGA); + // skip the first frame, in which not all chunks are properly loaded if (frame.getFrameId() == 0) return; diff --git a/src/main/java/com/replaymod/extras/modcore/ModCoreInstaller.java b/src/main/java/com/replaymod/extras/modcore/ModCoreInstaller.java index 6fd0c147..40615608 100644 --- a/src/main/java/com/replaymod/extras/modcore/ModCoreInstaller.java +++ b/src/main/java/com/replaymod/extras/modcore/ModCoreInstaller.java @@ -122,6 +122,11 @@ public class ModCoreInstaller { public static int initialize(File gameDir, String minecraftVersion) { if (isInitalized()) return -1; + JsonHolder jsonHolder = fetchJSON(VERSION_URL); + if (!jsonHolder.has(minecraftVersion)) { + System.out.println("No ModCore target for " + minecraftVersion + ". This in fine, unless you're specifically looking for ModCore."); + return -2; + } dataDir = new File(gameDir, "modcore"); if (!dataDir.exists()) { if (!dataDir.mkdirs()) { @@ -129,11 +134,6 @@ public class ModCoreInstaller { return 1; } } - JsonHolder jsonHolder = fetchJSON(VERSION_URL); - if (!jsonHolder.has(minecraftVersion)) { - System.out.println("No ModCore target for " + minecraftVersion + ". This in fine, unless you're specifically looking for ModCore."); - return -2; - } String latestRemote = jsonHolder.optString(minecraftVersion); boolean failed = jsonHolder.getKeys().size() == 0 || (jsonHolder.has("success") && !jsonHolder.optBoolean("success")); diff --git a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java index 5f94852f..34760873 100644 --- a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java +++ b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java @@ -5,10 +5,15 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.gui.GuiRenderQueue; +import com.replaymod.render.gui.GuiRenderSettings; +import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; import com.replaymod.replaystudio.replay.ReplayFile; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; @@ -18,32 +23,46 @@ import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiVerticalList; import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiTextField; -import de.johni0702.minecraft.gui.element.IGuiButton; import de.johni0702.minecraft.gui.function.Closeable; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.CustomLayout; -import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; 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.util.crash.CrashReport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.IOException; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static de.johni0702.minecraft.gui.versions.MCVer.getClipboardString; +import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString; /** * Gui for loading and saving {@link Timeline Timelines}. */ -public class GuiKeyframeRepository extends GuiScreen implements Closeable { +public class GuiKeyframeRepository extends GuiScreen implements Closeable, Typeable { + private static final Logger LOGGER = LogManager.getLogger(); + public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT); public final GuiLabel title = new GuiLabel(contentPanel).setI18nText("replaymod.gui.keyframerepository.title"); public final GuiVerticalList list = new GuiVerticalList(contentPanel).setDrawShadow(true).setDrawSlider(true); - public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(5)); - public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() { + public final GuiButton overwriteButton = new GuiButton().onClick(new Runnable() { @Override public void run() { GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, @@ -51,14 +70,16 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no"); Utils.addCallback(popup.getFuture(), doIt -> { if (doIt) { - timelines.put(selectedEntry.name, currentTimeline); + for (Entry entry : selectedEntries) { + timelines.put(entry.name, currentTimeline); + } overwriteButton.setDisabled(); save(); } }, Throwable::printStackTrace); } }).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled(); - public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() { + public final GuiButton saveAsButton = new GuiButton().onClick(new Runnable() { @Override public void run() { final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true); @@ -100,12 +121,12 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.saveas"); - public final GuiButton loadButton = new GuiButton(buttonPanel).onClick(new Runnable() { + public final GuiButton loadButton = new GuiButton().onClick(new Runnable() { @Override public void run() { getMinecraft().openScreen(null); try { - Timeline timeline = timelines.get(selectedEntry.name); + Timeline timeline = timelines.get(selectedEntries.iterator().next().name); for (Path path : timeline.getPaths()) { path.updateAll(); } @@ -115,9 +136,10 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { } } }).setSize(75, 20).setI18nLabel("replaymod.gui.load").setDisabled(); - public final GuiButton renameButton = new GuiButton(buttonPanel).onClick(new Runnable() { + public final GuiButton renameButton = new GuiButton().onClick(new Runnable() { @Override public void run() { + Entry selectedEntry = selectedEntries.iterator().next(); final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(selectedEntry.name); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, new GuiLabel().setI18nText("replaymod.gui.rename").setColor(Colors.BLACK), @@ -158,7 +180,7 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { }); } }).setSize(75, 20).setI18nLabel("replaymod.gui.rename").setDisabled(); - public final GuiButton removeButton = new GuiButton(buttonPanel).onClick(new Runnable() { + public final GuiButton removeButton = new GuiButton().onClick(new Runnable() { @Override public void run() { GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, @@ -168,14 +190,13 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { @Override public void onSuccess(Boolean delete) { if (delete) { - timelines.remove(selectedEntry.name); - list.getListPanel().removeElement(selectedEntry); + for (Entry entry : selectedEntries) { + timelines.remove(entry.name); + list.getListPanel().removeElement(entry); + } - selectedEntry = null; - overwriteButton.setDisabled(); - loadButton.setDisabled(); - renameButton.setDisabled(); - removeButton.setDisabled(); + selectedEntries.clear(); + updateButtons(); save(); } } @@ -188,13 +209,102 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { } }).setSize(75, 20).setI18nLabel("replaymod.gui.remove").setDisabled(); + public final GuiButton copyButton = new GuiButton().onClick(new Runnable() { + @Override + public void run() { + Map toBeSerialized = new HashMap<>(); + for (Entry entry : selectedEntries) { + toBeSerialized.put(entry.name, timelines.get(entry.name)); + } + try { + TimelineSerialization serialization = new TimelineSerialization(registry, null); + setClipboardString(serialization.serialize(toBeSerialized)); + } catch (Throwable t) { + t.printStackTrace(); + CrashReport report = CrashReport.create(t, "Copying timeline(s)"); + Utils.error(LOGGER, GuiKeyframeRepository.this, report, () -> {}); + } + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.copy").setDisabled(); + + public final GuiButton pasteButton = new GuiButton().onClick(new Runnable() { + @Override + public void run() { + try { + TimelineSerialization serialization = new TimelineSerialization(registry, null); + for (Map.Entry entry : serialization.deserialize(getClipboardString()).entrySet()) { + String name = entry.getKey(); + while (timelines.containsKey(name)) { + name += " (Copy)"; + } + timelines.put(name, entry.getValue()); + list.getListPanel().addElements(null, new Entry(name)); + } + save(); + } catch (Throwable t) { + // Intentionally not making a fuzz about it cause this will likely happen when they have anything + // else in their clipboard. + // If it's actually not working, they'll go complain and we'll just check the log. + t.printStackTrace(); + } + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.paste"); + + public final GuiButton addToQueueButton = new GuiButton().onClick(new Runnable() { + @Override + public void run() { + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); + GuiRenderQueue queue = new GuiRenderQueue(GuiKeyframeRepository.this, replayHandler, null); + queue.open(); + + Set selected = selectedEntries.stream().map(e -> e.name).collect(Collectors.toSet()); + ArrayDeque> toBeAdded = new ArrayDeque<>(); + // Iterating over timelines to get consistent ordering (cause selectedEntries is an unordered set) + // and we need to get the Timelines anyway. + for (Map.Entry entry : timelines.entrySet()) { + if (selected.contains(entry.getKey())) { + toBeAdded.offerLast(entry); + } + } + new Runnable() { + @Override + public void run() { + Map.Entry entry = toBeAdded.pollFirst(); + if (entry == null) { + return; + } + String name = entry.getKey(); + Timeline timeline = entry.getValue(); + GuiRenderSettings settingsGui = queue.addJob(timeline); + settingsGui.buttonPanel.removeElement(settingsGui.renderButton); + settingsGui.setOutputFileBaseName(name); + Runnable orgOnClick = settingsGui.queueButton.getOnClick(); + settingsGui.queueButton.onClick(() -> { + orgOnClick.run(); + this.run(); + }); + settingsGui.open(); + } + }.run(); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.rendersettings.addtoqueue"); + + public final GuiPanel buttonPanel = new GuiPanel(contentPanel) + .setLayout(new GridLayout() + .setColumns(4) + .setSpacingX(5) + .setSpacingY(5)) + .addElements(null, + overwriteButton, saveAsButton, renameButton, removeButton, + loadButton, addToQueueButton, copyButton, pasteButton); + private final Map timelines = new LinkedHashMap<>(); private final Timeline currentTimeline; private final SettableFuture future = SettableFuture.create(); private final PathingRegistry registry; private final ReplayFile replayFile; - private Entry selectedEntry; + private final Set selectedEntries = new HashSet<>(); { setBackground(Background.NONE); @@ -232,6 +342,19 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { if (entry.getKey().isEmpty()) continue; // don't show auto-save slot list.getListPanel().addElements(null, new Entry(entry.getKey())); } + + updateButtons(); + } + + private void updateButtons() { + int selected = selectedEntries.size(); + + overwriteButton.setEnabled(selected >= 1); + loadButton.setEnabled(selected == 1); + renameButton.setEnabled(selected == 1); + removeButton.setEnabled(selected >= 1); + copyButton.setEnabled(selected >= 1); + addToQueueButton.setEnabled(selected >= 1); } @Override @@ -258,6 +381,33 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { } } + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (MCVer.Keyboard.hasControlDown()) { + switch (keyCode) { + case MCVer.Keyboard.KEY_A: + if (selectedEntries.size() < timelines.size()) { + for (GuiElement child : list.getListPanel().getChildren()) { + if (child instanceof Entry) { + selectedEntries.add((Entry) child); + } + } + } else { + selectedEntries.clear(); + } + updateButtons(); + return true; + case MCVer.Keyboard.KEY_C: + copyButton.onClick(); + return true; + case MCVer.Keyboard.KEY_V: + pasteButton.onClick(); + return true; + } + } + return false; + } + public class Entry extends AbstractGuiClickableContainer { public final GuiLabel label = new GuiLabel(this); private String name; @@ -281,13 +431,20 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { @Override protected void onClick() { - selectedEntry = this; - buttonPanel.forEach(IGuiButton.class).setEnabled(); + if (!MCVer.Keyboard.hasControlDown()) { + selectedEntries.clear(); + } + if (selectedEntries.contains(this)) { + selectedEntries.remove(this); + } else { + selectedEntries.add(this); + } + updateButtons(); } @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - if (selectedEntry == this) { + if (selectedEntries.contains(this)) { renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK); renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE); } diff --git a/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java index 57896bf9..a495399e 100644 --- a/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java +++ b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java @@ -30,6 +30,7 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations { private final ReplayHandler replayHandler; private Timeline timeline; protected long startOffset; + private boolean wasAsyncMode; private long lastTime; private long lastTimestamp; private ListenableFuture future; @@ -69,6 +70,7 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations { }.max(iter).getTime(); } + wasAsyncMode = replayHandler.getReplaySender().isAsyncMode(); replayHandler.getReplaySender().setSyncModeAndWait(); register(); lastTime = 0; @@ -103,7 +105,9 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations { MinecraftAccessor mcA = (MinecraftAccessor) mc; mcA.setTimer(((ReplayTimer) mcA.getTimer()).getWrapped()); replayHandler.getReplaySender().setReplaySpeed(0); - replayHandler.getReplaySender().setAsyncMode(true); + if (wasAsyncMode) { + replayHandler.getReplaySender().setAsyncMode(true); + } unregister(); return; } diff --git a/src/main/java/com/replaymod/recording/ServerInfoExt.java b/src/main/java/com/replaymod/recording/ServerInfoExt.java new file mode 100644 index 00000000..a0f26756 --- /dev/null +++ b/src/main/java/com/replaymod/recording/ServerInfoExt.java @@ -0,0 +1,17 @@ +package com.replaymod.recording; + +import net.minecraft.client.network.ServerInfo; + +/** Extension interface for {@link net.minecraft.client.network.ServerInfo}. */ +public interface ServerInfoExt { + + static ServerInfoExt from(ServerInfo base) { + return (ServerInfoExt) base; + } + + /** Per-server optional overwrite for {@link Setting#AUTO_START_RECORDING}. */ + Boolean getAutoRecording(); + + /** Per-server optional overwrite for {@link Setting#AUTO_START_RECORDING}. */ + void setAutoRecording(Boolean autoRecording); +} diff --git a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java index c34c21e2..df97260f 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java @@ -69,11 +69,11 @@ public class GuiRecordingControls extends EventRegistrations { updateState(); }).setSize(98, 20); - public GuiRecordingControls(ReplayMod core, PacketListener packetListener) { + public GuiRecordingControls(ReplayMod core, PacketListener packetListener, boolean autoStart) { this.core = core; this.packetListener = packetListener; - paused = stopped = !core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING); + paused = stopped = !autoStart; updateState(); } @@ -107,7 +107,8 @@ public class GuiRecordingControls extends EventRegistrations { .orElse(MCVer.findButton(buttonList, "menu.disconnect", 1)) .>map(it -> (height) -> it.y) .orElse((height) -> height / 4 + 120 - 16); - VanillaGuiScreen.setup(guiScreen).setLayout(new CustomLayout() { + VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(guiScreen); + vanillaGui.setLayout(new CustomLayout(vanillaGui.getLayout()) { @Override protected void layout(de.johni0702.minecraft.gui.container.GuiScreen container, int width, int height) { pos(panel, width / 2 - 100, yPos.apply(height) + 16 + 8); diff --git a/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java new file mode 100644 index 00000000..e42b6e91 --- /dev/null +++ b/src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java @@ -0,0 +1,161 @@ +package com.replaymod.recording.gui; + +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; +import com.replaymod.replay.gui.screen.GuiReplayViewer; +import com.replaymod.replaystudio.replay.ReplayMetaData; +import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair; +import de.johni0702.minecraft.gui.container.AbstractGuiScreen; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.container.VanillaGuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiTextField; +import de.johni0702.minecraft.gui.element.GuiTooltip; +import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; +import de.johni0702.minecraft.gui.function.Focusable; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraft.client.MinecraftClient; +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.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static com.replaymod.core.versions.MCVer.getMinecraft; +import static de.johni0702.minecraft.gui.utils.Utils.link; + +public class GuiSavingReplay { + + private static final MinecraftClient mc = getMinecraft(); + private static final Logger logger = LogManager.getLogger(); + + private final GuiLabel label = new GuiLabel() + .setI18nText("replaymod.gui.replaysaving.title") + .setColor(Colors.BLACK); + + private final GuiProgressBar progressBar = new GuiProgressBar() + .setHeight(14); + + private final GuiPanel panel = new GuiPanel() + .setLayout(new VerticalLayout().setSpacing(2)) + .addElements(new VerticalLayout.Data(0.5), label, progressBar); + + private final ReplayMod core; + private final List apply = new ArrayList<>(); + + public GuiSavingReplay(ReplayMod core) { + this.core = core; + } + + public void open() { + core.getBackgroundProcesses().addProcess(panel); + } + + public void close() { + core.getBackgroundProcesses().removeProcess(panel); + AbstractGuiScreen currentScreen = GuiScreen.from(mc.currentScreen); + if (currentScreen instanceof GuiReplayViewer) { + ((GuiReplayViewer) currentScreen).list.load(); + } + } + + public GuiProgressBar getProgressBar() { + return progressBar; + } + + public void presentRenameDialog(List> outputPaths) { + panel.removeElement(progressBar); + + link(outputPaths.stream().map(it -> addOutput(it.getKey(), it.getValue())).toArray(Focusable[]::new)); + + GuiButton applyButton = new GuiButton() + .setSize(150, 20) + .setI18nLabel("replaymod.gui.done") + .onClick(() -> { + apply.forEach(Runnable::run); + close(); + }); + + panel.addElements(new VerticalLayout.Data(0.5), applyButton); + } + + private GuiTextField addOutput(Path path, ReplayMetaData metaData) { + String originalName = Utils.fileNameToReplayName(path.getFileName().toString()); + GuiTextField textField = new GuiTextField() + .setSize(130, 20) + .setText(originalName) + .setI18nHint("replaymod.gui.delete") + .setTextColorDisabled(Colors.RED) + .setTooltip(createTooltip(path, metaData)); + GuiButton clearButton = new GuiButton() + .setSize(20, 20) + .setLabel("X") + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.delete")) + .onClick(() -> textField.setText("")); + GuiPanel row = new GuiPanel() + .setLayout(new HorizontalLayout()) + .addElements(null, textField, clearButton); + panel.addElements(new VerticalLayout.Data(0.5), row); + + apply.add(() -> applyOutput(path, textField.getText())); + + return textField; + } + + private GuiPanel createTooltip(Path path, ReplayMetaData metaData) { + GuiTooltip tooltip = new GuiTooltip(); + GuiReplayViewer.GuiReplayEntry entry = new GuiReplayViewer.GuiReplayEntry(path.toFile(), metaData, null, new ArrayList<>()); + return new GuiPanel().setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(entry, 4, 4); + size(entry, width - 8, height - 8); + size(tooltip, width, height); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + ReadableDimension size = entry.calcMinSize(); + return new Dimension(size.getWidth() + 8, size.getHeight() + 8); + } + }).addElements(null, tooltip, entry); + } + + private void applyOutput(Path path, String newName) { + if (newName.isEmpty()) { + try { + Files.delete(path); + } catch (IOException e) { + logger.error("Deleting replay file:", e); + CrashReport crashReport = CrashReport.create(e, "Deleting replay file"); + core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, () -> {})); + } + return; + } + + try { + Path replaysFolder = core.getReplayFolder(); + Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName)); + for (int i = 1; Files.exists(newPath); i++) { + newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")")); + } + Files.move(path, newPath); + } catch (IOException e) { + logger.error("Renaming replay file:", e); + CrashReport crashReport = CrashReport.create(e, "Renaming replay file"); + core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, () -> {})); + } + } +} diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index cd09a29b..9707020a 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -4,6 +4,7 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.Utils; import com.replaymod.editor.gui.MarkerProcessor; +import com.replaymod.recording.ServerInfoExt; import com.replaymod.recording.Setting; import com.replaymod.recording.gui.GuiRecordingControls; import com.replaymod.recording.gui.GuiRecordingOverlay; @@ -11,10 +12,10 @@ import com.replaymod.recording.mixin.NetworkManagerAccessor; import com.replaymod.recording.packet.PacketListener; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; -import com.replaymod.replaystudio.replay.ZipReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; import io.netty.channel.Channel; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.client.resource.language.I18n; import net.minecraft.network.ClientConnection; import org.apache.logging.log4j.Logger; @@ -30,7 +31,7 @@ import net.minecraft.world.World; //#endif //#endif -import java.io.File; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -89,14 +90,26 @@ public class ConnectionEventHandler { } String worldName; + String serverName = null; + boolean autoStart = core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING); if (local) { //#if MC>=11600 worldName = mc.getServer().getSaveProperties().getLevelName(); //#else //$$ worldName = mc.getServer().getLevelName(); //#endif + serverName = worldName; } else if (mc.getCurrentServerEntry() != null) { - worldName = mc.getCurrentServerEntry().address; + ServerInfo serverInfo = mc.getCurrentServerEntry(); + worldName = serverInfo.address; + if (!I18n.translate("selectServer.defaultName").equals(serverInfo.name)) { + serverName = serverInfo.name; + } + + Boolean autoStartServer = ServerInfoExt.from(serverInfo).getAutoRecording(); + if (autoStartServer != null) { + autoStart = autoStartServer; + } //#if MC>=11100 } else if (mc.isConnectedToRealms()) { // we can't access the server name without tapping too deep in the Realms Library @@ -107,34 +120,38 @@ public class ConnectionEventHandler { return; } - File folder = core.getReplayFolder(); + if (ReplayMod.isMinimalMode()) { + // Recording controls are not supported in minimal mode, so always auto-start + autoStart = true; + } String name = sdf.format(Calendar.getInstance().getTime()); - File currentFile = new File(folder, Utils.replayNameToFileName(name)); - ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile); + Path outputPath = core.getRecordingFolder().resolve(Utils.replayNameToFileName(name)); + ReplayFile replayFile = core.openReplay(outputPath); replayFile.writeModInfo(ModCompat.getInstalledNetworkMods()); ReplayMetaData metaData = new ReplayMetaData(); metaData.setSingleplayer(local); metaData.setServerName(worldName); + metaData.setCustomServerName(serverName); metaData.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion()); metaData.setDate(System.currentTimeMillis()); metaData.setMcVersion(ReplayMod.getMinecraftVersion()); - packetListener = new PacketListener(core, currentFile.toPath(), replayFile, metaData); + packetListener = new PacketListener(core, outputPath, replayFile, metaData); Channel channel = ((NetworkManagerAccessor) networkManager).getChannel(); channel.pipeline().addBefore(packetHandlerKey, "replay_recorder", packetListener); recordingEventHandler = new RecordingEventHandler(packetListener); recordingEventHandler.register(); - guiControls = new GuiRecordingControls(core, packetListener); + guiControls = new GuiRecordingControls(core, packetListener, autoStart); guiControls.register(); guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls); guiOverlay.register(); - if (core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING) || ReplayMod.isMinimalMode()) { + if (autoStart) { core.printInfoToChat("replaymod.chat.recordingstarted"); } else { packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0); diff --git a/src/main/java/com/replaymod/recording/handler/GuiHandler.java b/src/main/java/com/replaymod/recording/handler/GuiHandler.java index 074786c3..ffeb1e87 100644 --- a/src/main/java/com/replaymod/recording/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/recording/handler/GuiHandler.java @@ -2,14 +2,23 @@ package com.replaymod.recording.handler; import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; +import com.replaymod.core.gui.GuiReplayButton; +import com.replaymod.recording.ServerInfoExt; import com.replaymod.recording.Setting; +import com.replaymod.recording.mixin.AddServerScreenAccessor; import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.VanillaGuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiCheckbox; +import de.johni0702.minecraft.gui.element.GuiToggleButton; import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.utils.EventRegistrations; +import net.minecraft.client.gui.screen.AddServerScreen; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.gui.screen.world.SelectWorldScreen; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.client.resource.language.I18n; //#if FABRIC>=1 import de.johni0702.minecraft.gui.versions.callbacks.InitScreenCallback; @@ -51,7 +60,8 @@ public class GuiHandler extends EventRegistrations { settingsRegistry.save(); }); - VanillaGuiScreen.setup(gui).setLayout(new CustomLayout() { + VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(gui); + vanillaGui.setLayout(new CustomLayout(vanillaGui.getLayout()) { @Override protected void layout(GuiScreen container, int width, int height) { //size(recordingCheckbox, 200, 20); @@ -59,5 +69,34 @@ public class GuiHandler extends EventRegistrations { } }).addElements(null, recordingCheckbox); } + + if (gui instanceof AddServerScreen) { + VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(gui); + GuiButton replayButton = new GuiReplayButton().onClick(() -> { + ServerInfo serverInfo = ((AddServerScreenAccessor) gui).getServer(); + ServerInfoExt serverInfoExt = ServerInfoExt.from(serverInfo); + Boolean state = serverInfoExt.getAutoRecording(); + GuiToggleButton autoRecording = new GuiToggleButton() + .setI18nLabel("replaymod.gui.settings.autostartrecording") + .setValues( + I18n.translate("replaymod.gui.settings.default"), + I18n.translate("options.off"), + I18n.translate("options.on") + ) + .setSelected(state == null ? 0 : state ? 2 : 1); + autoRecording.onClick(() -> { + int selected = autoRecording.getSelected(); + serverInfoExt.setAutoRecording(selected == 0 ? null : selected == 2); + }); + GuiInfoPopup.open(vanillaGui, autoRecording); + }); + vanillaGui.setLayout(new CustomLayout(vanillaGui.getLayout()) { + @Override + protected void layout(GuiScreen container, int width, int height) { + size(replayButton, 20, 20); + pos(replayButton, width - width(replayButton) - 5, 5); + } + }).addElements(null, replayButton); + } } } diff --git a/src/main/java/com/replaymod/recording/mixin/AddServerScreenAccessor.java b/src/main/java/com/replaymod/recording/mixin/AddServerScreenAccessor.java new file mode 100644 index 00000000..b8941123 --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/AddServerScreenAccessor.java @@ -0,0 +1,12 @@ +package com.replaymod.recording.mixin; + +import net.minecraft.client.gui.screen.AddServerScreen; +import net.minecraft.client.network.ServerInfo; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AddServerScreen.class) +public interface AddServerScreenAccessor { + @Accessor + ServerInfo getServer(); +} diff --git a/src/main/java/com/replaymod/recording/mixin/MixinServerInfo.java b/src/main/java/com/replaymod/recording/mixin/MixinServerInfo.java new file mode 100644 index 00000000..f9107228 --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinServerInfo.java @@ -0,0 +1,47 @@ +package com.replaymod.recording.mixin; + +import com.replaymod.recording.ServerInfoExt; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.nbt.CompoundTag; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerInfo.class) +public abstract class MixinServerInfo implements ServerInfoExt { + private Boolean autoRecording; + + @Override + public Boolean getAutoRecording() { + return autoRecording; + } + + @Override + public void setAutoRecording(Boolean autoRecording) { + this.autoRecording = autoRecording; + } + + @Inject(method = "serialize", at = @At("RETURN")) + private void serialize(CallbackInfoReturnable ci) { + CompoundTag tag = ci.getReturnValue(); + if (autoRecording != null) { + tag.putBoolean("autoRecording", autoRecording); + } + } + + @Inject(method = "deserialize", at = @At("RETURN")) + private static void deserialize(CompoundTag tag, CallbackInfoReturnable ci) { + ServerInfoExt serverInfo = ServerInfoExt.from(ci.getReturnValue()); + if (tag.contains("autoRecording")) { + serverInfo.setAutoRecording(tag.getBoolean("autoRecording")); + } + } + + @Inject(method = "copyFrom", at = @At("RETURN")) + public void copyFrom(ServerInfo serverInfo, CallbackInfo ci) { + ServerInfoExt from = ServerInfoExt.from(serverInfo); + this.autoRecording = from.getAutoRecording(); + } +} diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index ba1d5ad0..53b7de29 100644 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -11,6 +11,7 @@ import com.replaymod.core.versions.MCVer; import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.ReplayModRecording; import com.replaymod.recording.Setting; +import com.replaymod.recording.gui.GuiSavingReplay; import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.recording.mixin.SPacketSpawnMobAccessor; import com.replaymod.recording.mixin.SPacketSpawnPlayerAccessor; @@ -19,9 +20,8 @@ import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.io.ReplayOutputStream; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; +import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair; import de.johni0702.minecraft.gui.container.VanillaGuiScreen; -import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.utils.Colors; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -67,7 +67,9 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -245,9 +247,9 @@ public class PacketListener extends ChannelInboundHandlerAdapter { } }); - GuiLabel savingLabel = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK); + GuiSavingReplay guiSavingReplay = new GuiSavingReplay(core); new Thread(() -> { - core.runLater(() -> core.getBackgroundProcesses().addProcess(savingLabel)); + core.runLater(guiSavingReplay::open); saveService.shutdown(); try { @@ -261,22 +263,26 @@ public class PacketListener extends ChannelInboundHandlerAdapter { logger.error("Failed to close packet output stream:", e); } + List> outputPaths; synchronized (replayFile) { try { replayFile.save(); replayFile.close(); if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS) && !ReplayMod.isMinimalMode()) { - MarkerProcessor.apply(outputPath, progress -> {}); + outputPaths = MarkerProcessor.apply(outputPath, guiSavingReplay.getProgressBar()::setProgress); + } else { + outputPaths = Collections.singletonList(new Pair<>(outputPath, metaData)); } } catch (Exception e) { logger.error("Saving replay file:", e); CrashReport crashReport = CrashReport.create(e, "Saving replay file"); - core.runLater(() -> Utils.error(logger, VanillaGuiScreen.setup(mc.currentScreen), crashReport, () -> {})); + core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, guiSavingReplay::close)); + return; } } - core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingLabel)); + core.runLater(() -> guiSavingReplay.presentRenameDialog(outputPaths)); }).start(); } diff --git a/src/main/java/com/replaymod/render/CameraPathExporter.java b/src/main/java/com/replaymod/render/CameraPathExporter.java new file mode 100644 index 00000000..abb9e994 --- /dev/null +++ b/src/main/java/com/replaymod/render/CameraPathExporter.java @@ -0,0 +1,227 @@ +package com.replaymod.render; + +import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; +import com.replaymod.replay.camera.CameraEntity; +import de.javagl.jgltf.impl.v2.Accessor; +import de.javagl.jgltf.impl.v2.Animation; +import de.javagl.jgltf.impl.v2.AnimationChannel; +import de.javagl.jgltf.impl.v2.AnimationChannelTarget; +import de.javagl.jgltf.impl.v2.AnimationSampler; +import de.javagl.jgltf.impl.v2.Asset; +import de.javagl.jgltf.impl.v2.Buffer; +import de.javagl.jgltf.impl.v2.BufferView; +import de.javagl.jgltf.impl.v2.Camera; +import de.javagl.jgltf.impl.v2.CameraPerspective; +import de.javagl.jgltf.impl.v2.GlTF; +import de.javagl.jgltf.impl.v2.Node; +import de.javagl.jgltf.model.io.v2.GltfAssetV2; +import de.javagl.jgltf.model.io.v2.GltfAssetWriterV2; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Quaternion; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector4f; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.opengl.GL11; + +//#if MC>=11400 +import net.minecraft.util.math.Vec3d; +//#else +//#endif + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; + +import static com.replaymod.core.utils.Utils.configure; + +public class CameraPathExporter { + + private final MinecraftClient mc = MCVer.getMinecraft(); + private final RenderSettings settings; + private int framesDone; + private ByteBuffer timeBuffer; + private ByteBuffer cameraTranslationBuffer; + private ByteBuffer cameraRotationBuffer; + + public CameraPathExporter(RenderSettings settings) { + this.settings = settings; + } + + public void setup(int totalFrames) { + timeBuffer = ByteBuffer.allocate(4 * totalFrames).order(ByteOrder.LITTLE_ENDIAN); + cameraTranslationBuffer = ByteBuffer.allocate(4 * totalFrames * 3).order(ByteOrder.LITTLE_ENDIAN); + cameraRotationBuffer = ByteBuffer.allocate(4 * totalFrames * 4).order(ByteOrder.LITTLE_ENDIAN); + } + + public void recordFrame(float tickDelta) { + //#if MC>=10800 + Entity entity = mc.getCameraEntity() == null ? mc.player : mc.getCameraEntity(); + //#else + //$$ Entity entity = mc.renderViewEntity == null ? mc.thePlayer : mc.renderViewEntity; + //#endif + + //#if MC>=11400 + net.minecraft.client.render.Camera camera = mc.gameRenderer.getCamera(); + Vec3d vec = camera.getPos(); + float x = (float) vec.getX(); + float y = (float) vec.getY(); + float z = (float) vec.getZ(); + float yaw = camera.getYaw() + 180; + float pitch = camera.getPitch(); + //#else + //#if MC>=10800 + //$$ float eyeHeight = entity.getEyeHeight(); + //#else + //$$ float eyeHeight = 1.62f - entity.yOffset; + //#endif + //$$ float x = (float) (entity.prevPosX + (entity.posX - entity.prevPosX) * tickDelta); + //$$ float y = (float) (entity.prevPosY + (entity.posY - entity.prevPosY) * tickDelta + eyeHeight); + //$$ float z = (float) (entity.prevPosZ + (entity.posZ - entity.prevPosZ) * tickDelta); + //$$ float yaw = entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * tickDelta + 180; + //$$ float pitch = entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * tickDelta; + //#endif + float roll = entity instanceof CameraEntity ? ((CameraEntity) entity).roll : 0; + + Quaternion quatYaw = new Quaternion(); + Quaternion quatPitch = new Quaternion(); + Quaternion quatRoll = new Quaternion(); + + quatYaw.setFromAxisAngle(new Vector4f(0, -1, 0, (float) Math.toRadians(yaw))); + quatPitch.setFromAxisAngle(new Vector4f(-1, 0, 0, (float) Math.toRadians(pitch))); + quatRoll.setFromAxisAngle(new Vector4f(0, 0, 1, (float) Math.toRadians(roll))); + + Quaternion quaternion = new Quaternion(0, 0, 0, 1); + Quaternion.mul(quaternion, quatYaw, quaternion); + Quaternion.mul(quaternion, quatPitch, quaternion); + Quaternion.mul(quaternion, quatRoll, quaternion); + quaternion.normalise(quaternion); + + float[] translation = new float[] { x, y, z }; + float[] rotation = new float[] { quaternion.getX(), quaternion.getY(), quaternion.getZ(), quaternion.getW() }; + + timeBuffer.putFloat(framesDone / (float) settings.getFramesPerSecond()); + for (float f : translation) { + cameraTranslationBuffer.putFloat(f); + } + for (float f : rotation) { + cameraRotationBuffer.putFloat(f); + } + + framesDone++; + } + + public void finish() throws IOException { + int timeBufferSize = timeBuffer.rewind().remaining(); + int cameraTranslationBufferSize = cameraTranslationBuffer.rewind().remaining(); + int cameraRotationBufferSize = cameraRotationBuffer.rewind().remaining(); + + int binaryDataSize = 0; + binaryDataSize += timeBufferSize; + binaryDataSize += cameraTranslationBufferSize; + binaryDataSize += cameraRotationBufferSize; + + ByteBuffer binaryData = ByteBuffer.allocate(binaryDataSize); + int timeBufferOffset = binaryData.position(); + binaryData.put(timeBuffer); + int cameraTranslationBufferOffset = binaryData.position(); + binaryData.put(cameraTranslationBuffer); + int cameraRotationBufferOffset = binaryData.position(); + binaryData.put(cameraRotationBuffer); + binaryData.rewind(); + + GlTF glTF = new GlTF(); + glTF.setAsset(configure(new Asset(), asset -> { + asset.setVersion("2.0"); + asset.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion()); + })); + glTF.addAnimations(configure(new Animation(), animation -> { + animation.addChannels(configure(new AnimationChannel(), channel -> { + channel.setTarget(configure(new AnimationChannelTarget(), target -> { + target.setNode(0); + target.setPath("translation"); + })); + channel.setSampler(0); + })); + animation.addChannels(configure(new AnimationChannel(), channel -> { + channel.setTarget(configure(new AnimationChannelTarget(), target -> { + target.setNode(0); + target.setPath("rotation"); + })); + channel.setSampler(1); + })); + animation.addSamplers(configure(new AnimationSampler(), sampler -> { + sampler.setInput(0); + sampler.setOutput(1); + })); + animation.addSamplers(configure(new AnimationSampler(), sampler -> { + sampler.setInput(0); + sampler.setOutput(2); + })); + })); + glTF.addCameras(configure(new Camera(), camera -> { + camera.setType("perspective"); + camera.setPerspective(configure(new CameraPerspective(), perspective -> { + float aspectRatio = (float) settings.getVideoWidth() / (float) settings.getVideoHeight(); + perspective.setAspectRatio(aspectRatio); + perspective.setYfov((float) Math.toRadians(mc.options.fov)); + perspective.setZnear(0.05f); + perspective.setZfar((float) mc.options.viewDistance * 16 * 4); + })); + })); + glTF.addNodes(configure(new Node(), node -> node.setCamera(0))); + glTF.addBuffers(configure(new Buffer(), buffer -> buffer.setByteLength(binaryData.limit()))); + // Time + glTF.addBufferViews(configure(new BufferView(), bufferView -> { + bufferView.setBuffer(0); + bufferView.setByteOffset(timeBufferOffset); + bufferView.setByteLength(timeBufferSize); + })); + glTF.addAccessors(configure(new Accessor(), accessor -> { + accessor.setBufferView(0); + accessor.setType("SCALAR"); + accessor.setComponentType(GL11.GL_FLOAT); + accessor.setCount(framesDone); + })); + // Camera translation + glTF.addBufferViews(configure(new BufferView(), bufferView -> { + bufferView.setBuffer(0); + bufferView.setByteOffset(cameraTranslationBufferOffset); + bufferView.setByteLength(cameraTranslationBufferSize); + })); + glTF.addAccessors(configure(new Accessor(), accessor -> { + accessor.setBufferView(1); + accessor.setType("VEC3"); + accessor.setComponentType(GL11.GL_FLOAT); + accessor.setCount(framesDone); + })); + // Camera rotation + glTF.addBufferViews(configure(new BufferView(), bufferView -> { + bufferView.setBuffer(0); + bufferView.setByteOffset(cameraRotationBufferOffset); + bufferView.setByteLength(cameraRotationBufferSize); + })); + glTF.addAccessors(configure(new Accessor(), accessor -> { + accessor.setBufferView(2); + accessor.setType("VEC4"); + accessor.setComponentType(GL11.GL_FLOAT); + accessor.setCount(framesDone); + })); + + java.nio.file.Path videoPath = settings.getOutputFile().toPath(); + java.nio.file.Path glbBasePath = Files.isDirectory(videoPath) + ? videoPath.resolve("camera.glb") + : videoPath.resolveSibling(videoPath.getFileName() + ".glb"); + java.nio.file.Path glbPath = glbBasePath; + for (int i = 0; Files.exists(glbPath); i++) { + String baseName = FilenameUtils.getBaseName(glbBasePath.getFileName().toString()); + glbPath = glbBasePath.resolveSibling(baseName + "." + i + ".glb"); + } + try (OutputStream out = Files.newOutputStream(glbPath)) { + GltfAssetV2 asset = new GltfAssetV2(glTF, binaryData); + new GltfAssetWriterV2().writeBinary(asset, out); + } + } +} diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java new file mode 100644 index 00000000..1b2348fe --- /dev/null +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -0,0 +1,125 @@ +//#if MC>=11400 +package com.replaymod.render; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.rendering.Channel; +import com.replaymod.render.rendering.FrameConsumer; +import com.replaymod.render.utils.ByteBufferPool; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraft.util.crash.CrashReport; +import org.lwjgl.PointerBuffer; +import org.lwjgl.util.tinyexr.EXRChannelInfo; +import org.lwjgl.util.tinyexr.EXRHeader; +import org.lwjgl.util.tinyexr.EXRImage; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.lwjgl.system.MemoryStack.*; +import static org.lwjgl.system.MemoryUtil.*; +import static org.lwjgl.util.tinyexr.TinyEXR.*; + +public class EXRWriter implements FrameConsumer { + + private final Path outputFolder; + + public EXRWriter(Path outputFolder) throws IOException { + this.outputFolder = outputFolder; + + Files.createDirectories(outputFolder); + } + + @Override + public void consume(Map channels) { + BitmapFrame bgraFrame = channels.get(Channel.BRGA); + BitmapFrame depthFrame = channels.get(Channel.DEPTH); + + Path path = outputFolder.resolve(bgraFrame.getFrameId() + ".exr"); + ReadableDimension size = bgraFrame.getSize(); + ByteBuffer bgra = bgraFrame.getByteBuffer(); + int width = size.getWidth(); + int height = size.getHeight(); + int numChannels = 4 + (depthFrame != null ? 1 : 0); + + stackPush(); + EXRHeader header = EXRHeader.mallocStack(); InitEXRHeader(header); + EXRChannelInfo.Buffer channelInfos = EXRChannelInfo.mallocStack(numChannels); + IntBuffer pixelTypes = stackMallocInt(numChannels); + IntBuffer requestedPixelTypes = stackMallocInt(numChannels); + EXRImage image = EXRImage.mallocStack(); InitEXRImage(image); + PointerBuffer imagePointers = stackMallocPointer(numChannels); + FloatBuffer images = memAllocFloat(width * height * numChannels); + PointerBuffer err = stackMallocPointer(1); + try { + header.num_channels(numChannels); + header.channels(channelInfos); + header.pixel_types(pixelTypes); + header.requested_pixel_types(requestedPixelTypes); + + // Some readers ignore this, so we use the most expected order + memASCII("A", true, channelInfos.get(0).name()); + memASCII("B", true, channelInfos.get(1).name()); + memASCII("G", true, channelInfos.get(2).name()); + memASCII("R", true, channelInfos.get(3).name()); + for (int i = 0; i < numChannels; i++) { + pixelTypes.put(i, TINYEXR_PIXELTYPE_FLOAT); + requestedPixelTypes.put(i, TINYEXR_PIXELTYPE_HALF); + } + if (depthFrame != null) { + memASCII("Z", true, channelInfos.get(4).name()); + requestedPixelTypes.put(4, TINYEXR_PIXELTYPE_FLOAT); + } + + image.num_channels(numChannels); + image.width(width); + image.height(height); + image.images(imagePointers); + FloatBuffer[] bgrChannels = new FloatBuffer[4]; + FloatBuffer depthChannel = null; + for (int i = 0; i < numChannels; i++) { + FloatBuffer channel = images.slice(); + channel.position(width * height * i); + imagePointers.put(i, channel.slice()); + if (i == 4) { + depthChannel = channel; + } else { + bgrChannels[(i + 3) % 4] = channel; + } + } + 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); + } + } + } + if (depthFrame != null && depthChannel != null) { + depthChannel.put(depthFrame.getByteBuffer().asFloatBuffer()); + } + + int ret = SaveEXRImageToFile(image, header, path.toString(), err); + if (ret != TINYEXR_SUCCESS) { + String message = err.getStringASCII(0); + FreeEXRErrorMessage(err.getByteBuffer(0)); + throw new IOException(message); + } + } catch (Throwable t) { + MCVer.getMinecraft().setCrashReport(CrashReport.create(t, "Exporting EXR frame")); + } finally { + memFree(images); + stackPop(); + channels.values().forEach(it -> ByteBufferPool.release(it.getByteBuffer())); + } + } + + @Override + public void close() { + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/FFmpegWriter.java similarity index 93% rename from src/main/java/com/replaymod/render/VideoWriter.java rename to src/main/java/com/replaymod/render/FFmpegWriter.java index 1d306a36..c0067e3a 100644 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/FFmpegWriter.java @@ -1,7 +1,8 @@ package com.replaymod.render; import com.replaymod.core.versions.MCVer; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.rendering.Channel; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.render.utils.ByteBufferPool; @@ -21,12 +22,13 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import java.util.Map; import java.util.concurrent.TimeUnit; import static com.replaymod.render.ReplayModRender.LOGGER; import static org.apache.commons.lang3.Validate.isTrue; -public class VideoWriter implements FrameConsumer { +public class FFmpegWriter implements FrameConsumer { private final VideoRenderer renderer; private final RenderSettings settings; @@ -38,7 +40,7 @@ public class VideoWriter implements FrameConsumer { private ByteArrayOutputStream ffmpegLog = new ByteArrayOutputStream(4096); - public VideoWriter(final VideoRenderer renderer) throws IOException { + public FFmpegWriter(final VideoRenderer renderer) throws IOException { this.renderer = renderer; this.settings = renderer.getRenderSettings(); @@ -103,11 +105,11 @@ public class VideoWriter implements FrameConsumer { } @Override - public void consume(RGBFrame frame) { + public void consume(Map channels) { + BitmapFrame frame = channels.get(Channel.BRGA); try { checkSize(frame.getSize()); channel.write(frame.getByteBuffer()); - ByteBufferPool.release(frame.getByteBuffer()); } catch (Throwable t) { if (aborted) { return; @@ -127,6 +129,8 @@ public class VideoWriter implements FrameConsumer { MCVer.addDetail(exportDetails, "Export command", settings::getExportCommand); MCVer.addDetail(exportDetails, "Export args", commandArgs::toString); MCVer.getMinecraft().setCrashReport(report); + } finally { + channels.values().forEach(it -> ByteBufferPool.release(it.getByteBuffer())); } } diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index f696e862..1043792d 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -7,6 +7,12 @@ import net.minecraft.client.resource.language.I18n; import net.minecraft.util.Util; import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Comparator; import java.util.Optional; @@ -73,6 +79,8 @@ public class RenderSettings { BLEND(null, "blend"), + EXR(null, "exr"), + PNG("\"%FILENAME%-%06d.png\"", "png"); private final String preset; @@ -105,6 +113,13 @@ public class RenderSettings { public boolean isSupported() { if (this == BLEND) { return RenderMethod.BLEND.isSupported(); + } else if (this == EXR) { + // Need LJWGL 3 + //#if MC>=11400 + return true; + //#else + //$$ return false; + //#endif } else { return true; } @@ -150,6 +165,8 @@ public class RenderSettings { private final int sphericalFovX; private final int sphericalFovY; private final boolean injectSphericalMetadata; + private final boolean depthMap; + private final boolean cameraPathExport; private final AntiAliasing antiAliasing; private final String exportCommand; @@ -180,6 +197,8 @@ public class RenderSettings { int sphericalFovX, int sphericalFovY, boolean injectSphericalMetadata, + boolean depthMap, + boolean cameraPathExport, AntiAliasing antiAliasing, String exportCommand, String exportArguments, @@ -200,6 +219,8 @@ public class RenderSettings { this.sphericalFovX = sphericalFovX; this.sphericalFovY = sphericalFovY; this.injectSphericalMetadata = injectSphericalMetadata; + this.depthMap = depthMap; + this.cameraPathExport = cameraPathExport; this.antiAliasing = antiAliasing; this.exportCommand = exportCommand; this.exportArguments = exportArguments; @@ -223,6 +244,8 @@ public class RenderSettings { sphericalFovX, sphericalFovY, injectSphericalMetadata, + depthMap, + cameraPathExport, antiAliasing, exportCommand, exportArguments, @@ -277,11 +300,35 @@ public class RenderSettings { switch (Util.getOperatingSystem()) { case WINDOWS: // Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder - File inDotMinecraft = new File(MCVer.getMinecraft().runDirectory, "ffmpeg/bin/ffmpeg.exe"); + File dotMinecraft = MCVer.getMinecraft().runDirectory; + File inDotMinecraft = new File(dotMinecraft, "ffmpeg/bin/ffmpeg.exe"); if (inDotMinecraft.exists()) { LOGGER.debug("FFmpeg found in .minecraft/ffmpeg"); return inDotMinecraft.getAbsolutePath(); } + // But a significant amount of people are not even able to follow instruction to do that. + // Instead they'll regularly put it at `.minecraft/ffmpeg-version/bin/ffmpeg.exe` + // or `.minecraft/ffmpeg/ffmpeg-version/bin/ffmpeg.exe` + // So, for support's sake, let's just search the entire .minecraft folder for an `ffmpeg.exe` file. + // We retain above check to have a fast path for any old installations. + try { + Path[] result = new Path[1]; + Files.walkFileTree(dotMinecraft.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if ("ffmpeg.exe".equals(file.getFileName().toString())) { + result[0] = file; + return FileVisitResult.TERMINATE; + } + return super.visitFile(file, attrs); + } + }); + if (result[0] != null) { + return result[0].toAbsolutePath().toString(); + } + } catch (IOException e) { + LOGGER.debug("Error searching .minecraft for ffmpeg.exe:", e); + } break; case OSX: // The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves @@ -373,6 +420,14 @@ public class RenderSettings { return injectSphericalMetadata; } + public boolean isDepthMap() { + return depthMap; + } + + public boolean isCameraPathExport() { + return cameraPathExport; + } + public AntiAliasing getAntiAliasing() { return antiAliasing; } diff --git a/src/main/java/com/replaymod/render/ReplayModRender.java b/src/main/java/com/replaymod/render/ReplayModRender.java index 5c5c46ce..71e07b20 100644 --- a/src/main/java/com/replaymod/render/ReplayModRender.java +++ b/src/main/java/com/replaymod/render/ReplayModRender.java @@ -2,8 +2,13 @@ package com.replaymod.render; import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.render.utils.RenderJob; +import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.events.ReplayClosedCallback; +import com.replaymod.replay.events.ReplayOpenedCallback; +import com.replaymod.replaystudio.replay.ReplayFile; +import de.johni0702.minecraft.gui.container.VanillaGuiScreen; import de.johni0702.minecraft.gui.utils.EventRegistrations; import net.minecraft.util.crash.CrashReport; import net.minecraft.util.crash.CrashException; @@ -17,8 +22,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import static com.replaymod.core.versions.MCVer.*; - public class ReplayModRender extends EventRegistrations implements Module { { instance = this; } public static ReplayModRender instance; @@ -27,6 +30,7 @@ public class ReplayModRender extends EventRegistrations implements Module { public static Logger LOGGER = LogManager.getLogger(); + private ReplayFile replayFile; private final List renderQueue = new ArrayList<>(); public ReplayModRender(ReplayMod core) { @@ -41,8 +45,6 @@ public class ReplayModRender extends EventRegistrations implements Module { @Override public void initClient() { - on(ReplayClosedCallback.EVENT, replayHandler -> renderQueue.clear()); - register(); } @@ -64,4 +66,31 @@ public class ReplayModRender extends EventRegistrations implements Module { public List getRenderQueue() { return renderQueue; } + + { on(ReplayOpenedCallback.EVENT, this::onReplayOpened); } + private void onReplayOpened(ReplayHandler replayHandler) { + replayFile = replayHandler.getReplayFile(); + try { + renderQueue.addAll(RenderJob.readQueue(replayFile)); + } catch (IOException e) { + throw new CrashException(CrashReport.create(e, "Reading timeline")); + } + } + + { on(ReplayClosedCallback.EVENT, replayHandler -> onReplayClosed()); } + private void onReplayClosed() { + renderQueue.clear(); + replayFile = null; + } + + public void saveRenderQueue() { + try { + RenderJob.writeQueue(replayFile, renderQueue); + } catch (IOException e) { + e.printStackTrace(); + VanillaGuiScreen screen = VanillaGuiScreen.wrap(getCore().getMinecraft().currentScreen); + CrashReport report = CrashReport.create(e, "Reading timeline"); + Utils.error(LOGGER, screen, report, () -> {}); + } + } } diff --git a/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java b/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java index 3b31b127..1047a78b 100644 --- a/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java +++ b/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java @@ -3,14 +3,17 @@ package com.replaymod.render.blend; import com.replaymod.core.versions.MCVer; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.capturer.WorldRenderer; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.rendering.Channel; import com.replaymod.render.rendering.FrameCapturer; import com.replaymod.render.utils.ByteBufferPool; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import java.io.IOException; +import java.util.Collections; +import java.util.Map; -public class BlendFrameCapturer implements FrameCapturer { +public class BlendFrameCapturer implements FrameCapturer { protected final WorldRenderer worldRenderer; protected final RenderInfo renderInfo; protected int framesDone; @@ -26,7 +29,7 @@ public class BlendFrameCapturer implements FrameCapturer { } @Override - public RGBFrame process() { + public Map process() { if (framesDone == 0) { BlendState.getState().setup(); } @@ -37,7 +40,8 @@ public class BlendFrameCapturer implements FrameCapturer { worldRenderer.renderWorld(MCVer.getRenderPartialTicks(), null); BlendState.getState().postFrame(framesDone); - return new RGBFrame(framesDone++, new Dimension(0, 0), ByteBufferPool.allocate(0)); + BitmapFrame frame = new BitmapFrame(framesDone++, new Dimension(0, 0), 0, ByteBufferPool.allocate(0)); + return Collections.singletonMap(Channel.BRGA, frame); } @Override diff --git a/src/main/java/com/replaymod/render/capturer/CubicOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/CubicOpenGlFrameCapturer.java index 7631f30d..1beb9b92 100644 --- a/src/main/java/com/replaymod/render/capturer/CubicOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/CubicOpenGlFrameCapturer.java @@ -1,6 +1,10 @@ package com.replaymod.render.capturer; import com.replaymod.render.frame.CubicOpenGlFrame; +import com.replaymod.render.rendering.Channel; + +import java.util.Collections; +import java.util.Map; public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer { public enum Data implements CaptureData { @@ -25,14 +29,15 @@ public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer process() { float partialTicks = renderInfo.updateForNextFrame(); int frameId = framesDone++; - return new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT), + CubicOpenGlFrame frame = new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT), renderFrame(frameId, partialTicks, Data.RIGHT), renderFrame(frameId, partialTicks, Data.FRONT), renderFrame(frameId, partialTicks, Data.BACK), renderFrame(frameId, partialTicks, Data.TOP), renderFrame(frameId, partialTicks, Data.BOTTOM)); + return Collections.singletonMap(Channel.BRGA, frame); } } diff --git a/src/main/java/com/replaymod/render/capturer/CubicPboOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/CubicPboOpenGlFrameCapturer.java index cac970ee..1896b8c0 100644 --- a/src/main/java/com/replaymod/render/capturer/CubicPboOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/CubicPboOpenGlFrameCapturer.java @@ -4,7 +4,7 @@ import com.replaymod.render.frame.CubicOpenGlFrame; import com.replaymod.render.frame.OpenGlFrame; public class CubicPboOpenGlFrameCapturer extends - MultiFramePboOpenGlFrameCapturer { + PboOpenGlFrameCapturer { private final int frameSize; public CubicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, int frameSize) { diff --git a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java index e0a3f1d4..473bbf1d 100644 --- a/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/ODSFrameCapturer.java @@ -1,5 +1,6 @@ package com.replaymod.render.capturer; +import com.replaymod.render.rendering.Channel; import de.johni0702.minecraft.gui.utils.EventRegistrations; import com.replaymod.render.RenderSettings; import com.replaymod.render.frame.CubicOpenGlFrame; @@ -17,6 +18,8 @@ import net.minecraft.util.Identifier; import static com.mojang.blaze3d.platform.GlStateManager.*; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; @@ -125,16 +128,22 @@ public class ODSFrameCapturer implements FrameCapturer { } @Override - public ODSOpenGlFrame process() { + public Map process() { bindProgram(); leftEyeVariable.set(true); - CubicOpenGlFrame leftFrame = left.process(); + Map leftChannels = left.process(); leftEyeVariable.set(false); - CubicOpenGlFrame rightFrame = right.process(); + Map rightChannels = right.process(); unbindProgram(); - if (leftFrame != null && rightFrame != null) { - return new ODSOpenGlFrame(leftFrame, rightFrame); + if (leftChannels != null && rightChannels != null) { + Map result = new HashMap<>(); + for (Channel channel : Channel.values()) { + CubicOpenGlFrame leftFrame = leftChannels.get(channel); + CubicOpenGlFrame rightFrame = rightChannels.get(channel); + result.put(channel, new ODSOpenGlFrame(leftFrame, rightFrame)); + } + return result; } return null; } diff --git a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java index 9f296caf..571d5164 100644 --- a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java @@ -112,7 +112,7 @@ public abstract class OpenGlFrameCapturer & CaptureData> +public abstract class PboOpenGlFrameCapturer & CaptureData> extends OpenGlFrameCapturer { + private final boolean withDepth; private final D[] data; private PixelBufferObject pbo, otherPBO; - public MultiFramePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, Class type, int framePixels) { + public PboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, Class type, int framePixels) { super(worldRenderer, renderInfo); + withDepth = renderInfo.getRenderSettings().isDepthMap(); data = type.getEnumConstants(); - int bufferSize = framePixels * 4 * data.length; + int bufferSize = framePixels * (4 /* bgra */ + (withDepth ? 4 /* float */ : 0)) * data.length; pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ); otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ); } @@ -37,29 +43,36 @@ public abstract class MultiFramePboOpenGlFrameCapturer= renderInfo.getTotalFrames() + 2; } + private F readFromPbo(ByteBuffer pboBuffer, int bytesPerPixel) { + OpenGlFrame[] frames = new OpenGlFrame[data.length]; + int frameBufferSize = getFrameWidth() * getFrameHeight() * bytesPerPixel; + for (int i = 0; i < frames.length; i++) { + ByteBuffer frameBuffer = ByteBufferPool.allocate(frameBufferSize); + pboBuffer.limit(pboBuffer.position() + frameBufferSize); + frameBuffer.put(pboBuffer); + frameBuffer.rewind(); + frames[i] = new OpenGlFrame(framesDone - 2, frameSize, bytesPerPixel, frameBuffer); + } + return create(frames); + } + @Override - public F process() { - F frame = null; + public Map process() { + Map channels = null; if (framesDone > 1) { // Read pbo to memory pbo.bind(); ByteBuffer pboBuffer = pbo.mapReadOnly(); - OpenGlFrame[] frames = new OpenGlFrame[data.length]; - int frameBufferSize = getFrameWidth() * getFrameHeight() * 4; - for (int i = 0; i < frames.length; i++) { - ByteBuffer frameBuffer = ByteBufferPool.allocate(frameBufferSize); - pboBuffer.limit(pboBuffer.position() + frameBufferSize); - frameBuffer.put(pboBuffer); - frameBuffer.rewind(); - frames[i] = new OpenGlFrame(framesDone - 2, frameSize, frameBuffer); + channels = new HashMap<>(); + channels.put(Channel.BRGA, readFromPbo(pboBuffer, 4)); + if (withDepth) { + channels.put(Channel.DEPTH, readFromPbo(pboBuffer, 4)); } pbo.unmap(); pbo.unbind(); - - frame = create(frames); } if (framesDone < renderInfo.getTotalFrames()) { @@ -72,7 +85,7 @@ public abstract class MultiFramePboOpenGlFrameCapturer { @@ -9,8 +13,9 @@ public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer process() { float partialTicks = renderInfo.updateForNextFrame(); - return renderFrame(framesDone++, partialTicks); + OpenGlFrame frame = renderFrame(framesDone++, partialTicks); + return Collections.singletonMap(Channel.BRGA, frame); } } diff --git a/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java index 9bbdba57..2543c2ba 100644 --- a/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java @@ -1,81 +1,20 @@ package com.replaymod.render.capturer; import com.replaymod.render.frame.OpenGlFrame; -import com.replaymod.render.utils.ByteBufferPool; -import com.replaymod.render.utils.PixelBufferObject; -import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import java.io.IOException; -import java.nio.ByteBuffer; - -public class SimplePboOpenGlFrameCapturer extends OpenGlFrameCapturer { - private final int bufferSize; - private PixelBufferObject pbo, otherPBO; +public class SimplePboOpenGlFrameCapturer extends PboOpenGlFrameCapturer { public SimplePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { - super(worldRenderer, renderInfo); - - ReadableDimension size = renderInfo.getFrameSize(); - bufferSize = size.getHeight() * size.getWidth() * 4; - pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ); - otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ); - } - - private void swapPBOs() { - PixelBufferObject old = pbo; - pbo = otherPBO; - otherPBO = old; + super(worldRenderer, renderInfo, SinglePass.class, + renderInfo.getFrameSize().getWidth() * renderInfo.getFrameSize().getHeight()); } @Override - public boolean isDone() { - return framesDone >= renderInfo.getTotalFrames() + 2; + protected OpenGlFrame create(OpenGlFrame[] from) { + return from[0]; } - @Override - public OpenGlFrame process() { - OpenGlFrame frame = null; - - if (framesDone > 1) { - // Read pbo to memory - pbo.bind(); - ByteBuffer pboBuffer = pbo.mapReadOnly(); - ByteBuffer buffer = ByteBufferPool.allocate(bufferSize); - buffer.put(pboBuffer); - buffer.rewind(); - pbo.unmap(); - pbo.unbind(); - frame = new OpenGlFrame(framesDone - 2, frameSize, buffer); - } - - if (framesDone < renderInfo.getTotalFrames()) { - // Then fill it again - renderFrame(framesDone, renderInfo.updateForNextFrame()); - } - - framesDone++; - swapPBOs(); - return frame; - } - - @Override - protected OpenGlFrame captureFrame(int frameId, CaptureData data) { - pbo.bind(); - - frameBuffer().beginWrite(true); - GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE, 0); - frameBuffer().endWrite(); - - pbo.unbind(); - return null; - } - - @Override - public void close() throws IOException { - super.close(); - pbo.delete(); - otherPBO.delete(); + public enum SinglePass implements CaptureData { + SINGLE_PASS } } diff --git a/src/main/java/com/replaymod/render/capturer/StereoscopicOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/StereoscopicOpenGlFrameCapturer.java index 846a9274..ccf888e8 100644 --- a/src/main/java/com/replaymod/render/capturer/StereoscopicOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/StereoscopicOpenGlFrameCapturer.java @@ -2,6 +2,10 @@ package com.replaymod.render.capturer; import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.frame.StereoscopicOpenGlFrame; +import com.replaymod.render.rendering.Channel; + +import java.util.Collections; +import java.util.Map; public class StereoscopicOpenGlFrameCapturer extends OpenGlFrameCapturer { @@ -19,11 +23,12 @@ public class StereoscopicOpenGlFrameCapturer } @Override - public StereoscopicOpenGlFrame process() { + public Map process() { float partialTicks = renderInfo.updateForNextFrame(); int frameId = framesDone++; OpenGlFrame left = renderFrame(frameId, partialTicks, Data.LEFT_EYE); OpenGlFrame right = renderFrame(frameId, partialTicks, Data.RIGHT_EYE); - return new StereoscopicOpenGlFrame(left, right); + StereoscopicOpenGlFrame frame = new StereoscopicOpenGlFrame(left, right); + return Collections.singletonMap(Channel.BRGA, frame); } } diff --git a/src/main/java/com/replaymod/render/capturer/StereoscopicPboOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/StereoscopicPboOpenGlFrameCapturer.java index 95b36ef9..130936d7 100644 --- a/src/main/java/com/replaymod/render/capturer/StereoscopicPboOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/StereoscopicPboOpenGlFrameCapturer.java @@ -4,7 +4,7 @@ import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.frame.StereoscopicOpenGlFrame; public class StereoscopicPboOpenGlFrameCapturer - extends MultiFramePboOpenGlFrameCapturer { + extends PboOpenGlFrameCapturer { public StereoscopicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { super(worldRenderer, renderInfo, StereoscopicOpenGlFrameCapturer.Data.class, diff --git a/src/main/java/com/replaymod/render/frame/RGBFrame.java b/src/main/java/com/replaymod/render/frame/BitmapFrame.java similarity index 63% rename from src/main/java/com/replaymod/render/frame/RGBFrame.java rename to src/main/java/com/replaymod/render/frame/BitmapFrame.java index 9c6e284e..d310a873 100644 --- a/src/main/java/com/replaymod/render/frame/RGBFrame.java +++ b/src/main/java/com/replaymod/render/frame/BitmapFrame.java @@ -6,19 +6,19 @@ import org.apache.commons.lang3.Validate; import java.nio.ByteBuffer; -public class RGBFrame implements Frame { +public class BitmapFrame implements Frame { private final int frameId; - private final ReadableDimension size; - + private final int bytesPerPixel; private final ByteBuffer byteBuffer; - public RGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) { - Validate.isTrue(size.getWidth() * size.getHeight() * 4 == byteBuffer.remaining(), + public BitmapFrame(int frameId, ReadableDimension size, int bytesPerPixel, ByteBuffer byteBuffer) { + Validate.isTrue(size.getWidth() * size.getHeight() * bytesPerPixel == byteBuffer.remaining(), "Buffer size is %d (cap: %d) but should be %d", - byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 4); + byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * bytesPerPixel); this.frameId = frameId; this.size = size; + this.bytesPerPixel = bytesPerPixel; this.byteBuffer = byteBuffer; } @@ -30,6 +30,10 @@ public class RGBFrame implements Frame { return this.size; } + public int getBytesPerPixel() { + return bytesPerPixel; + } + public ByteBuffer getByteBuffer() { return this.byteBuffer; } diff --git a/src/main/java/com/replaymod/render/frame/OpenGlFrame.java b/src/main/java/com/replaymod/render/frame/OpenGlFrame.java index b2762eb7..15f4fafe 100644 --- a/src/main/java/com/replaymod/render/frame/OpenGlFrame.java +++ b/src/main/java/com/replaymod/render/frame/OpenGlFrame.java @@ -7,14 +7,14 @@ import java.nio.ByteBuffer; public class OpenGlFrame implements Frame { private final int frameId; - private final ReadableDimension size; - + private final int bytesPerPixel; private final ByteBuffer byteBuffer; - public OpenGlFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) { + public OpenGlFrame(int frameId, ReadableDimension size, int bytesPerPixel, ByteBuffer byteBuffer) { this.frameId = frameId; this.size = size; + this.bytesPerPixel = bytesPerPixel; this.byteBuffer = byteBuffer; } @@ -26,6 +26,10 @@ public class OpenGlFrame implements Frame { return this.size; } + public int getBytesPerPixel() { + return bytesPerPixel; + } + public ByteBuffer getByteBuffer() { return this.byteBuffer; } diff --git a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java index d6b9937c..b3a8a702 100644 --- a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java +++ b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java @@ -2,7 +2,7 @@ package com.replaymod.render.gui; import com.replaymod.core.ReplayMod; import com.replaymod.render.RenderSettings; -import com.replaymod.render.VideoWriter; +import com.replaymod.render.FFmpegWriter; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiVerticalList; @@ -23,7 +23,7 @@ import static com.replaymod.core.versions.MCVer.addDetail; import static com.replaymod.render.ReplayModRender.LOGGER; public class GuiExportFailed extends GuiScreen { - public static GuiExportFailed tryToRecover(VideoWriter.FFmpegStartupException e, Consumer doRestart) { + public static GuiExportFailed tryToRecover(FFmpegWriter.FFmpegStartupException e, Consumer doRestart) { // Always log the error first LOGGER.error("Rendering video:", e); @@ -74,7 +74,7 @@ public class GuiExportFailed extends GuiScreen { setBackground(Background.DIRT); } - public GuiExportFailed(VideoWriter.FFmpegStartupException e, Consumer doRestart) { + public GuiExportFailed(FFmpegWriter.FFmpegStartupException e, Consumer doRestart) { logList.getListPanel().addElements(null, Arrays.stream(e.getLog().replace("\t", " ").split("\n")) .map(l -> new GuiLabel().setText(l)) @@ -98,6 +98,8 @@ public class GuiExportFailed extends GuiScreen { oldSettings.getSphericalFovX(), oldSettings.getSphericalFovY(), oldSettings.isInjectSphericalMetadata(), + oldSettings.isDepthMap(), + oldSettings.isCameraPathExport(), oldSettings.getAntiAliasing(), oldSettings.getExportCommand(), oldSettings.getEncodingPreset().getValue(), diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index 31877d3d..33d1392a 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -1,17 +1,20 @@ package com.replaymod.render.gui; import com.google.common.collect.Iterables; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; -import com.replaymod.render.VideoWriter; +import com.replaymod.render.FFmpegWriter; import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.render.utils.RenderJob; import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replay.ReplaySender; import com.replaymod.replaystudio.pathing.path.Timeline; -import com.replaymod.replaystudio.util.I18n; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer; @@ -20,35 +23,47 @@ import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiVerticalList; import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiTextField; +import de.johni0702.minecraft.gui.element.GuiTooltip; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; -import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; 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 javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Set; +import java.util.function.Supplier; import static com.replaymod.render.ReplayModRender.LOGGER; //#if MC>=11400 import net.minecraft.text.TranslatableText; +//#else +//$$ import com.replaymod.replaystudio.util.I18n; //#endif -public class GuiRenderQueue extends AbstractGuiPopup { +public class GuiRenderQueue extends AbstractGuiPopup implements Typeable { private final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.renderqueue.title").setColor(Colors.BLACK); private final GuiVerticalList list = new GuiVerticalList().setDrawShadow(true).setDrawSlider(true); private final GuiButton addButton = new GuiButton().setI18nLabel("replaymod.gui.renderqueue.add").setSize(150, 20); - private final GuiButton renameButton = new GuiButton().setI18nLabel("replaymod.gui.rename").setSize(73, 20); + private final GuiButton editButton = new GuiButton().setI18nLabel("replaymod.gui.edit").setSize(73, 20); private final GuiButton removeButton = new GuiButton().setI18nLabel("replaymod.gui.remove").setSize(73, 20); - private final GuiButton renderButton = new GuiButton().setI18nLabel("replaymod.gui.render").setSize(150, 20); + private final GuiButton renderButton = new GuiButton().setSize(150, 20); private final GuiButton closeButton = new GuiButton().setI18nLabel("replaymod.gui.close").setSize(150, 20).onClick(this::close); /* @@ -56,7 +71,7 @@ public class GuiRenderQueue extends AbstractGuiPopup { |---------------------------------| | Add | Render | |---------------------------------| - | Rename | Remove | Close | + | Edit | Remove | Close | |---------------------------------| */ @@ -66,12 +81,14 @@ public class GuiRenderQueue extends AbstractGuiPopup { addButton, renderButton, new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)).addElements(null, - renameButton, removeButton), + editButton, removeButton), closeButton); - private final AbstractGuiScreen container; + private final AbstractGuiScreen container; private final ReplayHandler replayHandler; - private Entry selectedEntry; + private final Set selectedEntries = new HashSet<>(); + private final Supplier timelineSupplier; + private boolean opened; { popup.setLayout(new CustomLayout() { @@ -92,120 +109,59 @@ public class GuiRenderQueue extends AbstractGuiPopup { }).addElements(null, title, list, buttonPanel); } - public GuiRenderQueue(AbstractGuiScreen container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) { + private final ReplayModRender mod = ReplayModRender.instance; + private final List jobs = mod.getRenderQueue(); + + public GuiRenderQueue(AbstractGuiScreen container, ReplayHandler replayHandler, Supplier timelineSupplier) { super(container); this.container = container; this.replayHandler = replayHandler; + this.timelineSupplier = timelineSupplier; LOGGER.trace("Opening render queue popup"); setBackgroundColor(Colors.DARK_TRANSPARENT); - List queue = ReplayModRender.instance.getRenderQueue(); - - for (RenderJob renderJob : queue) { + for (RenderJob renderJob : jobs) { LOGGER.trace("Adding {} to job queue list", renderJob); list.getListPanel().addElements(null, new Entry(renderJob)); } - addButton.onClick(() -> { - LOGGER.trace("Add button clicked"); - // Open popup - GuiYesNoPopup popup = GuiYesNoPopup.open(container) - .setYesI18nLabel("replaymod.gui.add").setNoI18nLabel("replaymod.gui.cancel"); - popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)); - // Add content - GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK); - GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true); - popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField); - // Disable "Yes" button while name is empty - nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> { - if (popup.getYesButton().isEnabled()) { - popup.getYesButton().onClick(); - } - }); - popup.getYesButton().setDisabled(); - // Register callback - Futures.addCallback(popup.getFuture(), new FutureCallback() { - @Override - public void onSuccess(@Nullable Boolean result) { - if (result == Boolean.TRUE) { - RenderJob newJob = new RenderJob(); - newJob.setName(nameField.getText()); - newJob.setSettings(guiRenderSettings.save(false)); - newJob.setTimeline(timeline); - LOGGER.trace("Adding new job: {}", newJob); - queue.add(newJob); - list.getListPanel().addElements(null, new Entry(newJob)); - } else { - LOGGER.trace("Adding cancelled"); - } - } + addButton.onClick(this::addButtonClicked); - @Override - public void onFailure(Throwable t) { - LOGGER.error("Add Job popup:", t); - } - }); - }); - if (guiRenderSettings != null) { - addButton.setEnabled(guiRenderSettings.renderButton.isEnabled()); - } - - renameButton.onClick(() -> { - LOGGER.trace("Rename button clicked for {}", selectedEntry.job); - // Open popup - GuiYesNoPopup popup = GuiYesNoPopup.open(container) - .setYesI18nLabel("replaymod.gui.rename").setNoI18nLabel("replaymod.gui.cancel"); - popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)); - // Add content - GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK); - GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true) - .setText(selectedEntry.job.getName()); - popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField); - // Disable "Yes" button while name is empty - nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> { - if (popup.getYesButton().isEnabled()) { - popup.getYesButton().onClick(); - } - }); - // Register callback - Futures.addCallback(popup.getFuture(), new FutureCallback() { - @Override - public void onSuccess(@Nullable Boolean result) { - if (result == Boolean.TRUE) { - LOGGER.trace("Renaming {} to \"{}\"", selectedEntry.job, nameField.getText()); - selectedEntry.setName(nameField.getText()); - } else { - LOGGER.trace("Renaming cancelled"); - } - } - - @Override - public void onFailure(Throwable t) { - LOGGER.error("Rename Job popup:", t); - } - }); + editButton.onClick(() -> { + Entry job = selectedEntries.iterator().next(); + GuiRenderSettings gui = job.edit(); + gui.open(); }); removeButton.onClick(() -> { - LOGGER.trace("Remove button clicked for {}", selectedEntry.job); - list.getListPanel().removeElement(selectedEntry); - queue.remove(selectedEntry.job); - selectedEntry = null; + for (Entry entry : selectedEntries) { + LOGGER.trace("Remove button clicked for {}", entry.job); + list.getListPanel().removeElement(entry); + jobs.remove(entry.job); + } + selectedEntries.clear(); updateButtons(); + mod.saveRenderQueue(); }); renderButton.onClick(() -> { LOGGER.trace("Render button clicked"); - ReplayMod.instance.runLaterWithoutLock(() -> processQueue(queue)); + List renderQueue = new ArrayList<>(); + for (Entry entry : selectedEntries) { + renderQueue.add(entry.job); + } + ReplayMod.instance.runLaterWithoutLock(() -> processQueue(container, replayHandler, renderQueue, () -> {})); }); updateButtons(); } - private void processQueue(Iterable queue) { + private static void processQueue(AbstractGuiScreen container, ReplayHandler replayHandler, Iterable queue, Runnable done) { + MinecraftClient mc = MCVer.getMinecraft(); + // Close all GUIs (so settings in GuiRenderSettings are saved) - getMinecraft().openScreen(null); + mc.openScreen(null); // Start rendering int jobsDone = 0; for (RenderJob renderJob : queue) { @@ -213,7 +169,7 @@ public class GuiRenderQueue extends AbstractGuiPopup { try { VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline()); videoRenderer.renderVideo(); - } catch (VideoWriter.NoFFmpegException e) { + } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); NoticeScreen errorScreen = new NoticeScreen( //#if MC>=11400 @@ -225,29 +181,150 @@ public class GuiRenderQueue extends AbstractGuiPopup { //$$ I18n.format("replaymod.gui.rendering.error.message") //#endif ); - getMinecraft().openScreen(errorScreen); + mc.openScreen(errorScreen); return; - } catch (VideoWriter.FFmpegStartupException e) { + } catch (FFmpegWriter.FFmpegStartupException e) { int jobsToSkip = jobsDone; GuiExportFailed.tryToRecover(e, newSettings -> { // Update current job with fixed ffmpeg arguments renderJob.setSettings(newSettings); // Restart queue, skipping the already completed jobs - ReplayMod.instance.runLaterWithoutLock(() -> processQueue(Iterables.skip(queue, jobsToSkip))); + ReplayMod.instance.runLaterWithoutLock(() -> processQueue(container, replayHandler, Iterables.skip(queue, jobsToSkip), done)); }); return; } catch (Throwable t) { - Utils.error(LOGGER, this, CrashReport.create(t, "Rendering video"), () -> {}); + Utils.error(LOGGER, container, CrashReport.create(t, "Rendering video"), () -> {}); container.display(); // Re-show the queue popup and the new error popup return; } jobsDone++; } + done.run(); + } + + public static void processMultipleReplays( + AbstractGuiScreen container, + ReplayModReplay mod, + Iterator>> queue, + Runnable done + ) { + if (!queue.hasNext()) { + done.run(); + return; + } + Pair> next = queue.next(); + + LOGGER.info("Opening replay {} for {} render jobs", next.getKey(), next.getValue().size()); + ReplayHandler replayHandler; + ReplayFile replayFile = null; + try { + replayFile = mod.getCore().openReplay(next.getKey().toPath()); + replayHandler = mod.startReplay(replayFile, true, 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 + IOUtils.closeQuietly(replayFile); + return; + } + if (replayHandler == null) { + LOGGER.warn("Replay failed to open (missing mods?), skipping.."); + IOUtils.closeQuietly(replayFile); + processMultipleReplays(container, mod, queue, done); + return; + } + ReplaySender replaySender = replayHandler.getReplaySender(); + + MinecraftClient mc = mod.getCore().getMinecraft(); + int jumpTo = 1000; + while (mc.world == null && jumpTo < replayHandler.getReplayDuration()) { + replaySender.sendPacketsTill(jumpTo); + jumpTo += 1000; + } + if (mc.world == null) { + LOGGER.warn("Replay failed to load world (corrupted?), skipping.."); + IOUtils.closeQuietly(replayFile); + processMultipleReplays(container, mod, queue, done); + return; + } + + processQueue(container, replayHandler, next.getValue(), () -> { + try { + replayHandler.endReplay(); + } catch (IOException e) { + Utils.error(LOGGER, container, CrashReport.create(e, "Closing replay"), () -> {}); + container.display(); // Re-show the queue popup and the new error popup + return; + } + processMultipleReplays(container, mod, queue, done); + }); + } + + private GuiRenderSettings addButtonClicked() { + Timeline timeline = timelineSupplier.get(); + if (timeline != null) { + GuiRenderSettings popup = addJob(timeline); + popup.open(); + return popup; + } else { + return null; + } + } + + public GuiRenderSettings addJob(Timeline timeline) { + return new GuiRenderSettings(container, replayHandler, timeline) { + { + if (!jobs.isEmpty()) { + buttonPanel.removeElement(renderButton); + } + queueButton.onClick(() -> { + RenderSettings settings = save(false); + + RenderJob newJob = new RenderJob(); + newJob.setSettings(settings); + newJob.setTimeline(timeline); + LOGGER.trace("Adding new job: {}", newJob); + jobs.add(newJob); + list.getListPanel().addElements(null, new Entry(newJob)); + updateButtons(); + mod.saveRenderQueue(); + + // Need to close the inner popup before we can open the outer one + close(); + if (!opened) { + GuiRenderQueue.this.open(); + } + }); + } + + @Override + public void close() { + super.close(); + if (!opened && jobs.isEmpty()) { + GuiRenderQueue.this.close(); + } + } + }; } @Override public void open() { + if (jobs.isEmpty() && timelineSupplier != null) { + if (addButtonClicked() == null) { + close(); + } + return; + } + super.open(); + opened = true; + } + + @Override + protected void close() { + if (opened) { + super.close(); + } + opened = false; } @Override @@ -256,9 +333,35 @@ public class GuiRenderQueue extends AbstractGuiPopup { } public void updateButtons() { - renameButton.setEnabled(selectedEntry != null); - removeButton.setEnabled(selectedEntry != null); - renderButton.setEnabled(!list.getListPanel().getChildren().isEmpty()); + int selected = selectedEntries.size(); + addButton.setEnabled(timelineSupplier != null); + editButton.setEnabled(selected == 1); + removeButton.setEnabled(selected >= 1); + renderButton.setEnabled(selected > 0); + renderButton.setI18nLabel("replaymod.gui.renderqueue.render" + (selected > 0 ? "selected" : "all")); + + String[] compatError = VideoRenderer.checkCompat(); + if (compatError != null) { + renderButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError)); + } + } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (MCVer.Keyboard.hasControlDown() && keyCode == MCVer.Keyboard.KEY_A) { + if (selectedEntries.size() < list.getListPanel().getChildren().size()) { + for (GuiElement child : list.getListPanel().getChildren()) { + if (child instanceof Entry) { + selectedEntries.add((Entry) child); + } + } + } else { + selectedEntries.clear(); + } + updateButtons(); + return true; + } + return false; } public class Entry extends AbstractGuiClickableContainer { @@ -284,27 +387,42 @@ public class GuiRenderQueue extends AbstractGuiPopup { @Override protected void onClick() { - selectedEntry = this; + if (!MCVer.Keyboard.hasControlDown()) { + selectedEntries.clear(); + } + if (selectedEntries.contains(this)) { + selectedEntries.remove(this); + } else { + selectedEntries.add(this); + } updateButtons(); } @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - if (selectedEntry == this) { + if (selectedEntries.contains(this)) { renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK); renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE); } super.draw(renderer, size, renderInfo); } - public void setName(String name) { - job.setName(name); - label.setText(name); - } - @Override protected Entry getThis() { return this; } + + public GuiRenderSettings edit() { + GuiRenderSettings gui = new GuiRenderSettings(container, replayHandler, job.getTimeline()); + gui.buttonPanel.removeElement(gui.renderButton); + gui.queueButton.setI18nLabel("replaymod.gui.done").onClick(() -> { + job.setSettings(gui.save(false)); + label.setText(job.getName()); + mod.saveRenderQueue(); + gui.close(); + }); + gui.load(job.getSettings()); + return gui; + } } } diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index e6d43fa9..5a632c57 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -10,10 +10,11 @@ import com.google.gson.JsonSyntaxException; import com.replaymod.core.ReplayMod; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; -import com.replaymod.render.VideoWriter; +import com.replaymod.render.FFmpegWriter; import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.replay.ReplayHandler; import com.replaymod.replaystudio.pathing.path.Timeline; +import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; @@ -21,11 +22,11 @@ import de.johni0702.minecraft.gui.container.GuiVerticalList; import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.element.advanced.GuiColorPicker; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; -import de.johni0702.minecraft.gui.function.Closeable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiFileChooserPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; @@ -44,6 +45,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.text.SimpleDateFormat; @@ -57,8 +59,9 @@ import static com.replaymod.render.ReplayModRender.LOGGER; import net.minecraft.text.TranslatableText; //#endif -public class GuiRenderSettings extends GuiScreen implements Closeable { - public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT); +public class GuiRenderSettings extends AbstractGuiPopup { + { disablePopupBackground(); } + public final GuiPanel contentPanel = new GuiPanel(popup).setBackgroundColor(new Color(0, 0, 0, 230)); public final GuiVerticalList settingsList = new GuiVerticalList(contentPanel).setDrawSlider(true); public final GuiDropdownMenu renderMethodDropdown = @@ -193,6 +196,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { public final GuiCheckbox injectSphericalMetadata = new GuiCheckbox() .setI18nLabel("replaymod.gui.rendersettings.sphericalmetadata"); + public final GuiCheckbox depthMap = new GuiCheckbox() + .setI18nLabel("replaymod.gui.rendersettings.depthmap"); + + public final GuiCheckbox cameraPathExport = new GuiCheckbox() + .setI18nLabel("replaymod.gui.rendersettings.camerapath"); + public final GuiDropdownMenu antiAliasingDropdown = new GuiDropdownMenu() .setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE); @@ -203,16 +212,25 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, chromaKeyingCheckbox, chromaKeyingColor, injectSphericalMetadata, sphericalFovSlider, + depthMap, new GuiLabel(), + cameraPathExport, new GuiLabel(), new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown)); public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command") - .setSize(55, 20).setMaxLength(100); + .setSize(55, 20).setMaxLength(100).onTextChanged((old) -> updateInputs()); public final GuiTextField exportArguments = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.arguments") - .setMinSize(new Dimension(245, 20)).setMaxLength(500); + .setMinSize(new Dimension(245, 20)).setMaxLength(500).onTextChanged((old) -> updateInputs()); + public final GuiButton exportReset = new GuiButton().setLabel("X") + .setSize(20, 20) + .onClick(() -> { + exportCommand.setText(""); + exportArguments.setText(encodingPresetDropdown.getSelectedValue().getValue()); + updateInputs(); + }); public final GuiPanel commandlinePanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(10)) .addElements(null, - new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).addElements(null, exportCommand, exportArguments), + new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).addElements(null, exportCommand, exportArguments, exportReset), new GuiLabel(new GuiPanel().setLayout(new CustomLayout() { @Override protected void layout(GuiPanel container, int width, int height) { @@ -227,12 +245,9 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(4)); - public final GuiButton queueButton = new GuiButton(buttonPanel).onClick(new Runnable() { - @Override - public void run() { - new GuiRenderQueue(GuiRenderSettings.this, GuiRenderSettings.this, replayHandler, timeline).open(); - } - }).setSize(100, 20).setI18nLabel("replaymod.gui.renderqueue.open"); + public final GuiButton queueButton = new GuiButton(buttonPanel) + .setSize(100, 20) + .setI18nLabel("replaymod.gui.rendersettings.addtoqueue"); public final GuiButton renderButton = new GuiButton(buttonPanel).onClick(() -> ReplayMod.instance.runLaterWithoutLock(new Runnable() { @Override public void run() { @@ -241,11 +256,11 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { try { VideoRenderer videoRenderer = new VideoRenderer(save(false), replayHandler, timeline); videoRenderer.renderVideo(); - } catch (VideoWriter.NoFFmpegException e) { + } catch (FFmpegWriter.NoFFmpegException e) { LOGGER.error("Rendering video:", e); NoticeScreen errorScreen = new NoticeScreen( //#if MC>=11400 - GuiRenderSettings.this::display, + getScreen()::display, new TranslatableText("replaymod.gui.rendering.error.title"), new TranslatableText("replaymod.gui.rendering.error.message") //#else @@ -254,7 +269,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { //#endif ); getMinecraft().openScreen(errorScreen); - } catch (VideoWriter.FFmpegStartupException e) { + } catch (FFmpegWriter.FFmpegStartupException e) { GuiExportFailed.tryToRecover(e, newSettings -> { // Update settings with fixed ffmpeg arguments exportArguments.setText(newSettings.getExportArguments()); @@ -263,26 +278,17 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { }); } catch (Throwable t) { error(LOGGER, GuiRenderSettings.this, CrashReport.create(t, "Rendering video"), () -> {}); - display(); // Re-show the render settings gui and the new error popup + getScreen().display(); // Re-show the render settings gui and the new error popup } } })).setSize(100, 20).setI18nLabel("replaymod.gui.render"); - public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() { - @Override - public void run() { - getMinecraft().openScreen(null); - } - }).setSize(100, 20).setI18nLabel("replaymod.gui.cancel"); + public final GuiButton cancelButton = new GuiButton(buttonPanel) + .onClick(this::close) + .setSize(100, 20) + .setI18nLabel("replaymod.gui.cancel"); { - setBackground(Background.NONE); Utils.link(videoWidth, videoHeight, bitRateField); - setLayout(new CustomLayout() { - @Override - protected void layout(GuiScreen container, int width, int height) { - pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2); - } - }); contentPanel.setLayout(new CustomLayout() { @Override @@ -294,7 +300,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { @Override public ReadableDimension calcMinSize(GuiContainer container) { - ReadableDimension screenSize = getMinSize(); + ReadableDimension screenSize = getContainer().getMinSize(); return new Dimension(screenSize.getWidth() - 40, screenSize.getHeight() - 40); } }); @@ -319,12 +325,15 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { }); } + private final AbstractGuiScreen screen; private final ReplayHandler replayHandler; private final Timeline timeline; private File outputFile; private boolean userDefinedOutputFileName; - public GuiRenderSettings(ReplayHandler replayHandler, Timeline timeline) { + public GuiRenderSettings(AbstractGuiScreen container, ReplayHandler replayHandler, Timeline timeline) { + super(container); + this.screen = container; this.replayHandler = replayHandler; this.timeline = timeline; @@ -358,17 +367,26 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { videoWidth.setEnabled(!renderMethod.hasFixedAspectRatio()); // Validate video width and height - String error = updateResolution(); - if (error == null) { - renderButton.setEnabled().setTooltip(null); + String resolutionError = updateResolution(); + if (resolutionError == null) { + queueButton.setEnabled().setTooltip(null); videoWidth.setTextColor(Colors.WHITE); videoHeight.setTextColor(Colors.WHITE); } else { - renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(error)); + queueButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(resolutionError)); videoWidth.setTextColor(Colors.RED); videoHeight.setTextColor(Colors.RED); } + String[] compatError = VideoRenderer.checkCompat(); + if (resolutionError != null) { + renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(resolutionError)); + } else if (compatError != null) { + renderButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError)); + } else { + renderButton.setEnabled().setTooltip(null); + } + // Enable/Disable bitrate input field and dropdown if (encodingPresetDropdown.getSelectedValue().hasBitrateSetting()) { bitRateField.setEnabled(); @@ -402,15 +420,29 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } // Enable/Disable various options for blend export + boolean isEXR = encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.EXR; boolean isBlend = renderMethod == RenderSettings.RenderMethod.BLEND; + boolean isFFmpeg = !isBlend && !isEXR; if (isBlend) { videoWidth.setDisabled(); videoHeight.setDisabled(); } encodingPresetDropdown.setEnabled(!isBlend); - exportCommand.setEnabled(!isBlend); - exportArguments.setEnabled(!isBlend); - antiAliasingDropdown.setEnabled(!isBlend); + exportCommand.setEnabled(isFFmpeg); + exportArguments.setEnabled(isFFmpeg); + antiAliasingDropdown.setEnabled(isFFmpeg); + + if (isEXR) { + depthMap.setEnabled().setTooltip(null); + } else { + depthMap.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED) + .setI18nText("replaymod.gui.rendersettings.depthmap.onlyexr")); + } + + // Enable/Disable export args reset button + boolean commandChanged = !exportCommand.getText().isEmpty(); + boolean argsChanged = !encodingPresetDropdown.getSelectedValue().getValue().equals(exportArguments.getText()); + exportReset.setEnabled(commandChanged || argsChanged); } protected String updateResolution() { @@ -511,7 +543,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { userDefinedOutputFileName = false; } else if (savedOutputFile.exists()) { String name = generateOutputFile(encodingPreset).getName(); - this.outputFile = new File(savedOutputFile.isDirectory() ? savedOutputFile : savedOutputFile.getParentFile(), name); + boolean isFolder = savedOutputFile.isDirectory() && !savedOutputFile.getName().endsWith(".exr"); + this.outputFile = new File(isFolder ? savedOutputFile : savedOutputFile.getParentFile(), name); userDefinedOutputFileName = false; } else { this.outputFile = conformExtension(savedOutputFile, encodingPreset); @@ -531,6 +564,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } sphericalFovSlider.setValue((settings.getSphericalFovX() - MIN_SPHERICAL_FOV) / SPHERICAL_FOV_STEP_SIZE); injectSphericalMetadata.setChecked(settings.isInjectSphericalMetadata()); + depthMap.setChecked(settings.isDepthMap()); + cameraPathExport.setChecked(settings.isCameraPathExport()); antiAliasingDropdown.setSelected(settings.getAntiAliasing()); exportCommand.setText(settings.getExportCommand()); String exportArguments = settings.getExportArguments(); @@ -561,7 +596,9 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null, sphericalFov, Math.min(180, sphericalFov), injectSphericalMetadata.isChecked() && (serialize || injectSphericalMetadata.isEnabled()), - antiAliasingDropdown.getSelectedValue(), + depthMap.isChecked() && (serialize || depthMap.isEnabled()), + cameraPathExport.isChecked(), + serialize || antiAliasingDropdown.isEnabled() ? antiAliasingDropdown.getSelectedValue() : RenderSettings.AntiAliasing.NONE, exportCommand.getText(), exportArguments.getText(), net.minecraft.client.gui.screen.Screen.hasControlDown() @@ -574,6 +611,24 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { return new File(folder, fileName + "." + encodingPreset.getFileExtension()); } + public AbstractGuiScreen getScreen() { + return screen; + } + + public void setOutputFileBaseName(String base) { + RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue(); + File file = new File(outputFile.getParentFile(), base + "." + preset.getFileExtension()); + // Ensure the file name is valid + try { + //noinspection ResultOfMethodCallIgnored + file.toPath(); + outputFile = file; + outputFileButton.setLabel(file.getName()); + } catch (InvalidPathException ignored) { + setOutputFileBaseName("filename_invalid_" + base.hashCode()); + } + } + protected File conformExtension(File file, RenderSettings.EncodingPreset preset) { String name = file.getName(); if (name.contains(".")) { @@ -588,7 +643,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { private RenderSettings getDefaultRenderSettings() { return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null, - true, false, false, false, null, 360, 180, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); + true, false, false, false, null, 360, 180, false, false, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); + } + + @Override + public void open() { + super.open(); } @Override @@ -600,9 +660,21 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } catch (IOException e) { LOGGER.error("Saving render settings:", e); } + super.close(); } public ReplayHandler getReplayHandler() { return replayHandler; } + + @Override + protected GuiRenderSettings getThis() { + return this; + } + + public static GuiScreen createBaseScreen() { + GuiScreen screen = new GuiScreen(); + screen.setBackground(AbstractGuiScreen.Background.NONE); + return screen; + } } diff --git a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java index a916e893..bc69f061 100644 --- a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java +++ b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java @@ -1,7 +1,7 @@ package com.replaymod.render.gui; import com.replaymod.core.utils.Utils; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.rendering.VideoRenderer; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; @@ -265,9 +265,8 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable { guiRenderer.drawTexturedRect(x, y, 0, 0, width, height, videoWidth, videoHeight, videoWidth, videoHeight); } - public void updatePreview(RGBFrame frame) { + public void updatePreview(ByteBuffer buffer, ReadableDimension size) { if (previewCheckbox.isChecked() && previewTexture != null) { - ByteBuffer buffer = frame.getByteBuffer(); buffer.mark(); synchronized (this) { //#if MC>=11400 @@ -279,7 +278,6 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable { // Note: Optifine changes the texture data array to be three times as long (for use by shaders), // we only want to initialize the first third and since we use our frame size, not the array size, // we're good to go. - ReadableDimension size = frame.getSize(); int width = size.getWidth(); for (int y = 0; y < size.getHeight(); y++) { for (int x = 0; x < width; x++) { diff --git a/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java b/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java index 334f6c02..f15097cf 100644 --- a/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java +++ b/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java @@ -63,15 +63,6 @@ public class ChunkLoadingRenderGlobal { } private void install() { - //#if FABRIC>=1 - if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sodium")) { - throw new UnsupportedOperationException( - "Rendering is not currently supported while Sodium is installed.\n" + - "See https://github.com/ReplayMod/ReplayMod/issues/150\n" + - "For now, you need to uninstall Sodium before rendering!" - ); - } - //#endif try { //#if MC>=11400 Field hookField = WorldRenderer.class.getField("replayModRender_hook"); diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java new file mode 100644 index 00000000..5edd5e29 --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/Mixin_PreserveDepthDuringHandRendering.java @@ -0,0 +1,24 @@ +package com.replaymod.render.mixin; + +import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.render.GameRenderer; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(GameRenderer.class) +public abstract class Mixin_PreserveDepthDuringHandRendering { + @ModifyArg( + method = "renderWorld", + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"), + index = 0 + ) + private int replayModRender_skipClearWhenRecordingDepth(int mask) { + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this).replayModRender_getHandler(); + if (handler != null && handler.getSettings().isDepthMap()) { + mask = mask & ~GL11.GL_DEPTH_BUFFER_BIT; + } + return mask; + } +} diff --git a/src/main/java/com/replaymod/render/processor/CubicToBitmapProcessor.java b/src/main/java/com/replaymod/render/processor/CubicToBitmapProcessor.java new file mode 100644 index 00000000..259ccc0c --- /dev/null +++ b/src/main/java/com/replaymod/render/processor/CubicToBitmapProcessor.java @@ -0,0 +1,35 @@ +package com.replaymod.render.processor; + +import com.replaymod.render.frame.CubicOpenGlFrame; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.utils.ByteBufferPool; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; + +import java.nio.ByteBuffer; + +import static com.replaymod.render.utils.Utils.openGlBytesToBitmap; + +public class CubicToBitmapProcessor extends AbstractFrameProcessor { + + @Override + public BitmapFrame process(CubicOpenGlFrame rawFrame) { + int size = rawFrame.getLeft().getSize().getWidth(); + int bpp = rawFrame.getLeft().getBytesPerPixel(); + int width = size * 4; + int height = size * 3; + ByteBuffer result = ByteBufferPool.allocate(width * height * bpp); + openGlBytesToBitmap(rawFrame.getLeft(), 0, size, result, width); + openGlBytesToBitmap(rawFrame.getFront(), size, size, result, width); + openGlBytesToBitmap(rawFrame.getRight(), size * 2, size, result, width); + openGlBytesToBitmap(rawFrame.getBack(), size * 3, size, result, width); + openGlBytesToBitmap(rawFrame.getTop(), size, 0, result, width); + openGlBytesToBitmap(rawFrame.getBottom(), size, size * 2, result, width); + ByteBufferPool.release(rawFrame.getLeft().getByteBuffer()); + ByteBufferPool.release(rawFrame.getRight().getByteBuffer()); + ByteBufferPool.release(rawFrame.getFront().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBack().getByteBuffer()); + ByteBufferPool.release(rawFrame.getTop().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBottom().getByteBuffer()); + return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width, height), bpp, result); + } +} diff --git a/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java deleted file mode 100644 index b014f77b..00000000 --- a/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.replaymod.render.processor; - -import com.replaymod.render.frame.CubicOpenGlFrame; -import com.replaymod.render.frame.RGBFrame; -import com.replaymod.render.utils.ByteBufferPool; -import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; - -import java.nio.ByteBuffer; - -import static com.replaymod.render.utils.Utils.openGlBytesToRBG; - -public class CubicToRGBProcessor extends AbstractFrameProcessor { - @Override - public RGBFrame process(CubicOpenGlFrame rawFrame) { - int size = rawFrame.getLeft().getSize().getWidth(); - int width = size * 4; - int height = size * 3; - ByteBuffer result = ByteBufferPool.allocate(width * height * 4); - openGlBytesToRBG(rawFrame.getLeft().getByteBuffer(), size, 0, size, result, width); - openGlBytesToRBG(rawFrame.getFront().getByteBuffer(), size, size, size, result, width); - openGlBytesToRBG(rawFrame.getRight().getByteBuffer(), size, size * 2, size, result, width); - openGlBytesToRBG(rawFrame.getBack().getByteBuffer(), size, size * 3, size, result, width); - openGlBytesToRBG(rawFrame.getTop().getByteBuffer(), size, size, 0, result, width); - openGlBytesToRBG(rawFrame.getBottom().getByteBuffer(), size, size, size * 2, result, width); - ByteBufferPool.release(rawFrame.getLeft().getByteBuffer()); - ByteBufferPool.release(rawFrame.getRight().getByteBuffer()); - ByteBufferPool.release(rawFrame.getFront().getByteBuffer()); - ByteBufferPool.release(rawFrame.getBack().getByteBuffer()); - ByteBufferPool.release(rawFrame.getTop().getByteBuffer()); - ByteBufferPool.release(rawFrame.getBottom().getByteBuffer()); - return new RGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result); - } -} diff --git a/src/main/java/com/replaymod/render/processor/EquirectangularToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/EquirectangularToBitmapProcessor.java similarity index 90% rename from src/main/java/com/replaymod/render/processor/EquirectangularToRGBProcessor.java rename to src/main/java/com/replaymod/render/processor/EquirectangularToBitmapProcessor.java index b0e4cf2e..62b78e62 100644 --- a/src/main/java/com/replaymod/render/processor/EquirectangularToRGBProcessor.java +++ b/src/main/java/com/replaymod/render/processor/EquirectangularToBitmapProcessor.java @@ -1,7 +1,7 @@ package com.replaymod.render.processor; import com.replaymod.render.frame.CubicOpenGlFrame; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.utils.ByteBufferPool; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import org.apache.commons.lang3.Validate; @@ -10,7 +10,7 @@ import java.nio.ByteBuffer; import static java.lang.Math.PI; -public class EquirectangularToRGBProcessor extends AbstractFrameProcessor { +public class EquirectangularToBitmapProcessor extends AbstractFrameProcessor { private static final byte IMAGE_BACK = 0; private static final byte IMAGE_FRONT = 1; private static final byte IMAGE_LEFT = 2; @@ -26,7 +26,7 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor { + // absolute depth is 2 * near * far / (far + near - (far - near) * (2 * z - 1)) + // precomputed: [ a ] [ b ] [ c ] + private final float a; + private final float b; + private final float c; + + public GlToAbsoluteDepthProcessor(float near, float far) { + a = 2 * near * far; + b = far + near; + c = far - near; + } + + @Override + public BitmapFrame process(BitmapFrame frame) { + + FloatBuffer buffer = frame.getByteBuffer().asFloatBuffer(); + int len = buffer.remaining(); + for (int i = 0; i < len; i++) { + float z = buffer.get(i); + z = a / (b - c * (2 * z - 1)); + buffer.put(i, z); + } + + return frame; + } +} diff --git a/src/main/java/com/replaymod/render/processor/ODSToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/ODSToBitmapProcessor.java similarity index 55% rename from src/main/java/com/replaymod/render/processor/ODSToRGBProcessor.java rename to src/main/java/com/replaymod/render/processor/ODSToBitmapProcessor.java index bbc1d2f8..cff37fa9 100644 --- a/src/main/java/com/replaymod/render/processor/ODSToRGBProcessor.java +++ b/src/main/java/com/replaymod/render/processor/ODSToBitmapProcessor.java @@ -1,7 +1,7 @@ package com.replaymod.render.processor; import com.replaymod.render.frame.ODSOpenGlFrame; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.utils.ByteBufferPool; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; @@ -9,25 +9,26 @@ import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import java.io.IOException; import java.nio.ByteBuffer; -public class ODSToRGBProcessor extends AbstractFrameProcessor { - private final EquirectangularToRGBProcessor processor; +public class ODSToBitmapProcessor extends AbstractFrameProcessor { + private final EquirectangularToBitmapProcessor processor; - public ODSToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) { - processor = new EquirectangularToRGBProcessor(outputWidth, outputHeight / 2, sphericalFovX); + public ODSToBitmapProcessor(int outputWidth, int outputHeight, int sphericalFovX) { + processor = new EquirectangularToBitmapProcessor(outputWidth, outputHeight / 2, sphericalFovX); } @Override - public RGBFrame process(ODSOpenGlFrame rawFrame) { - RGBFrame leftFrame = processor.process(rawFrame.getLeft()); - RGBFrame rightFrame = processor.process(rawFrame.getRight()); + public BitmapFrame process(ODSOpenGlFrame rawFrame) { + BitmapFrame leftFrame = processor.process(rawFrame.getLeft()); + BitmapFrame rightFrame = processor.process(rawFrame.getRight()); ReadableDimension size = new Dimension(leftFrame.getSize().getWidth(), leftFrame.getSize().getHeight() * 2); - ByteBuffer result = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * 4); + int bpp = rawFrame.getLeft().getLeft().getBytesPerPixel(); + ByteBuffer result = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * bpp); result.put(leftFrame.getByteBuffer()); result.put(rightFrame.getByteBuffer()); result.rewind(); ByteBufferPool.release(leftFrame.getByteBuffer()); ByteBufferPool.release(rightFrame.getByteBuffer()); - return new RGBFrame(rawFrame.getFrameId(), size, result); + return new BitmapFrame(rawFrame.getFrameId(), size, bpp, result); } @Override diff --git a/src/main/java/com/replaymod/render/processor/OpenGlToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java similarity index 74% rename from src/main/java/com/replaymod/render/processor/OpenGlToRGBProcessor.java rename to src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java index cc32c5d8..6cb60f5e 100644 --- a/src/main/java/com/replaymod/render/processor/OpenGlToRGBProcessor.java +++ b/src/main/java/com/replaymod/render/processor/OpenGlToBitmapProcessor.java @@ -1,21 +1,22 @@ package com.replaymod.render.processor; import com.replaymod.render.frame.OpenGlFrame; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import java.nio.ByteBuffer; -public class OpenGlToRGBProcessor extends AbstractFrameProcessor { +public class OpenGlToBitmapProcessor extends AbstractFrameProcessor { private byte[] row, rowSwap; @Override - public RGBFrame process(OpenGlFrame rawFrame) { + public BitmapFrame process(OpenGlFrame rawFrame) { // Flip whole image in place ReadableDimension size = rawFrame.getSize(); - int rowSize = size.getWidth() * 4; + int bpp = rawFrame.getBytesPerPixel(); + int rowSize = size.getWidth() * bpp; if (row == null || row.length < rowSize) { row = new byte[rowSize]; rowSwap = new byte[rowSize]; @@ -37,6 +38,6 @@ public class OpenGlToRGBProcessor extends AbstractFrameProcessor { + @Override + public BitmapFrame process(StereoscopicOpenGlFrame rawFrame) { + ReadableDimension size = rawFrame.getLeft().getSize(); + int width = size.getWidth(); + int bpp = rawFrame.getLeft().getBytesPerPixel(); + ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * bpp); + openGlBytesToBitmap(rawFrame.getLeft(), 0, 0, result, width * 2); + openGlBytesToBitmap(rawFrame.getRight(), size.getWidth(), 0, result, width * 2); + ByteBufferPool.release(rawFrame.getLeft().getByteBuffer()); + ByteBufferPool.release(rawFrame.getRight().getByteBuffer()); + return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), bpp, result); + } +} diff --git a/src/main/java/com/replaymod/render/processor/StereoscopicToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/StereoscopicToRGBProcessor.java deleted file mode 100644 index 5e6a1f61..00000000 --- a/src/main/java/com/replaymod/render/processor/StereoscopicToRGBProcessor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.replaymod.render.processor; - -import com.replaymod.render.frame.RGBFrame; -import com.replaymod.render.frame.StereoscopicOpenGlFrame; -import com.replaymod.render.utils.ByteBufferPool; -import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; -import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; - -import java.nio.ByteBuffer; - -import static com.replaymod.render.utils.Utils.openGlBytesToRBG; - -public class StereoscopicToRGBProcessor extends AbstractFrameProcessor { - @Override - public RGBFrame process(StereoscopicOpenGlFrame rawFrame) { - ReadableDimension size = rawFrame.getLeft().getSize(); - int width = size.getWidth(); - ByteBuffer leftBuffer = rawFrame.getLeft().getByteBuffer(); - ByteBuffer rightBuffer = rawFrame.getRight().getByteBuffer(); - ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * 4); - openGlBytesToRBG(leftBuffer, width, 0, 0, result, width * 2); - openGlBytesToRBG(rightBuffer, width, size.getWidth(), 0, result, width * 2); - ByteBufferPool.release(leftBuffer); - ByteBufferPool.release(rightBuffer); - return new RGBFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), result); - } -} diff --git a/src/main/java/com/replaymod/render/rendering/Channel.java b/src/main/java/com/replaymod/render/rendering/Channel.java new file mode 100644 index 00000000..0ab81d57 --- /dev/null +++ b/src/main/java/com/replaymod/render/rendering/Channel.java @@ -0,0 +1,6 @@ +package com.replaymod.render.rendering; + +public enum Channel { + BRGA, + DEPTH, +} diff --git a/src/main/java/com/replaymod/render/rendering/FrameCapturer.java b/src/main/java/com/replaymod/render/rendering/FrameCapturer.java index a2826e7e..cfe429e3 100644 --- a/src/main/java/com/replaymod/render/rendering/FrameCapturer.java +++ b/src/main/java/com/replaymod/render/rendering/FrameCapturer.java @@ -1,11 +1,12 @@ package com.replaymod.render.rendering; import java.io.Closeable; +import java.util.Map; public interface FrameCapturer extends Closeable { boolean isDone(); - R process(); + Map process(); } diff --git a/src/main/java/com/replaymod/render/rendering/FrameConsumer.java b/src/main/java/com/replaymod/render/rendering/FrameConsumer.java index e2f0fd07..fd74586c 100644 --- a/src/main/java/com/replaymod/render/rendering/FrameConsumer.java +++ b/src/main/java/com/replaymod/render/rendering/FrameConsumer.java @@ -1,9 +1,10 @@ package com.replaymod.render.rendering; import java.io.Closeable; +import java.util.Map; public interface FrameConsumer

extends Closeable { - void consume(P frame); + void consume(Map channels); } diff --git a/src/main/java/com/replaymod/render/rendering/Pipeline.java b/src/main/java/com/replaymod/render/rendering/Pipeline.java index 42f0ad12..23c22895 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipeline.java +++ b/src/main/java/com/replaymod/render/rendering/Pipeline.java @@ -3,6 +3,8 @@ package com.replaymod.render.rendering; import com.replaymod.core.mixin.MinecraftAccessor; import com.replaymod.core.versions.MCVer; import com.replaymod.render.capturer.WorldRenderer; +import com.replaymod.render.frame.BitmapFrame; +import com.replaymod.render.processor.GlToAbsoluteDepthProcessor; import net.minecraft.client.MinecraftClient; import net.minecraft.util.crash.CrashReport; import net.minecraft.util.crash.CrashException; @@ -13,11 +15,14 @@ import org.lwjgl.glfw.GLFW; //$$ import org.lwjgl.opengl.Display; //#endif +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import static com.replaymod.core.versions.MCVer.getMinecraft; //#if MC>=11400 import static com.replaymod.core.versions.MCVer.getWindow; //#endif @@ -27,6 +32,7 @@ public class Pipeline implements Runnable { private final WorldRenderer worldRenderer; private final FrameCapturer capturer; private final FrameProcessor processor; + private final GlToAbsoluteDepthProcessor depthProcessor; private int consumerNextFrame; private final Object consumerLock = new Object(); private final FrameConsumer

consumer; @@ -38,6 +44,10 @@ public class Pipeline implements Runnable { this.capturer = capturer; this.processor = processor; this.consumer = consumer; + + float near = 0.05f; + float far = getMinecraft().options.viewDistance * 16 * 4; + this.depthProcessor = new GlToAbsoluteDepthProcessor(near, far); } @Override @@ -70,7 +80,7 @@ public class Pipeline implements Runnable { processService.shutdown(); return; } - R rawFrame = capturer.process(); + Map rawFrame = capturer.process(); if (rawFrame != null) { processService.submit(new ProcessTask(rawFrame)); } @@ -99,25 +109,37 @@ public class Pipeline implements Runnable { } private class ProcessTask implements Runnable { - private final R rawFrame; + private final Map rawChannels; - public ProcessTask(R rawFrame) { - this.rawFrame = rawFrame; + public ProcessTask(Map rawChannels) { + this.rawChannels = rawChannels; } @Override public void run() { try { - P processedFrame = processor.process(rawFrame); + Integer frameId = null; + Map processedChannels = new HashMap<>(); + for (Map.Entry entry : rawChannels.entrySet()) { + P processedFrame = processor.process(entry.getValue()); + if (entry.getKey() == Channel.DEPTH && processedFrame instanceof BitmapFrame) { + depthProcessor.process((BitmapFrame) processedFrame); + } + processedChannels.put(entry.getKey(), processedFrame); + frameId = processedFrame.getFrameId(); + } + if (frameId == null) { + return; + } synchronized (consumerLock) { - while (consumerNextFrame != processedFrame.getFrameId()) { + while (consumerNextFrame != frameId) { try { consumerLock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } - consumer.consume(processedFrame); + consumer.consume(processedChannels); consumerNextFrame++; consumerLock.notifyAll(); } diff --git a/src/main/java/com/replaymod/render/rendering/Pipelines.java b/src/main/java/com/replaymod/render/rendering/Pipelines.java index 51c4a2a8..62a2b502 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipelines.java +++ b/src/main/java/com/replaymod/render/rendering/Pipelines.java @@ -14,19 +14,21 @@ import com.replaymod.render.capturer.WorldRenderer; import com.replaymod.render.frame.CubicOpenGlFrame; import com.replaymod.render.frame.ODSOpenGlFrame; import com.replaymod.render.frame.OpenGlFrame; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.frame.StereoscopicOpenGlFrame; import com.replaymod.render.hooks.EntityRendererHandler; -import com.replaymod.render.processor.CubicToRGBProcessor; +import com.replaymod.render.processor.CubicToBitmapProcessor; import com.replaymod.render.processor.DummyProcessor; -import com.replaymod.render.processor.EquirectangularToRGBProcessor; -import com.replaymod.render.processor.ODSToRGBProcessor; -import com.replaymod.render.processor.OpenGlToRGBProcessor; -import com.replaymod.render.processor.StereoscopicToRGBProcessor; +import com.replaymod.render.processor.EquirectangularToBitmapProcessor; +import com.replaymod.render.processor.ODSToBitmapProcessor; +import com.replaymod.render.processor.OpenGlToBitmapProcessor; +import com.replaymod.render.processor.StereoscopicToBitmapProcessor; import com.replaymod.render.utils.PixelBufferObject; +import java.util.Map; + public class Pipelines { - public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer consumer) { switch (method) { case DEFAULT: return newDefaultPipeline(renderInfo, consumer); @@ -44,51 +46,51 @@ public class Pipelines { throw new UnsupportedOperationException("Unknown method: " + method); } - public static Pipeline newDefaultPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newDefaultPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); FrameCapturer capturer; - if (PixelBufferObject.SUPPORTED) { + if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) { capturer = new SimplePboOpenGlFrameCapturer(worldRenderer, renderInfo); } else { capturer = new SimpleOpenGlFrameCapturer(worldRenderer, renderInfo); } - return new Pipeline<>(worldRenderer, capturer, new OpenGlToRGBProcessor(), consumer); + return new Pipeline<>(worldRenderer, capturer, new OpenGlToBitmapProcessor(), consumer); } - public static Pipeline newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); FrameCapturer capturer; - if (PixelBufferObject.SUPPORTED) { + if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) { capturer = new StereoscopicPboOpenGlFrameCapturer(worldRenderer, renderInfo); } else { capturer = new StereoscopicOpenGlFrameCapturer(worldRenderer, renderInfo); } - return new Pipeline<>(worldRenderer, capturer, new StereoscopicToRGBProcessor(), consumer); + return new Pipeline<>(worldRenderer, capturer, new StereoscopicToBitmapProcessor(), consumer); } - public static Pipeline newCubicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newCubicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); FrameCapturer capturer; - if (PixelBufferObject.SUPPORTED) { + if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) { capturer = new CubicPboOpenGlFrameCapturer(worldRenderer, renderInfo, settings.getVideoWidth() / 4); } else { capturer = new CubicOpenGlFrameCapturer(worldRenderer, renderInfo, settings.getVideoWidth() / 4); } - return new Pipeline<>(worldRenderer, capturer, new CubicToRGBProcessor(), consumer); + return new Pipeline<>(worldRenderer, capturer, new CubicToBitmapProcessor(), consumer); } - public static Pipeline newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); - EquirectangularToRGBProcessor processor = new EquirectangularToRGBProcessor(settings.getVideoWidth(), + EquirectangularToBitmapProcessor processor = new EquirectangularToBitmapProcessor(settings.getVideoWidth(), settings.getVideoHeight(), settings.getSphericalFovX()); FrameCapturer capturer; - if (PixelBufferObject.SUPPORTED) { + if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) { capturer = new CubicPboOpenGlFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()); } else { capturer = new CubicOpenGlFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()); @@ -96,11 +98,11 @@ public class Pipelines { return new Pipeline<>(worldRenderer, capturer, processor, consumer); } - public static Pipeline newODSPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + public static Pipeline newODSPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); - ODSToRGBProcessor processor = new ODSToRGBProcessor(settings.getVideoWidth(), + ODSToBitmapProcessor processor = new ODSToBitmapProcessor(settings.getVideoWidth(), settings.getVideoHeight(), settings.getSphericalFovX()); FrameCapturer capturer = @@ -108,13 +110,13 @@ public class Pipelines { return new Pipeline<>(worldRenderer, capturer, processor, consumer); } - public static Pipeline newBlendPipeline(RenderInfo renderInfo) { + public static Pipeline newBlendPipeline(RenderInfo renderInfo) { RenderSettings settings = renderInfo.getRenderSettings(); WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo); - FrameCapturer capturer = new BlendFrameCapturer(worldRenderer, renderInfo); - FrameConsumer consumer = new FrameConsumer() { + FrameCapturer capturer = new BlendFrameCapturer(worldRenderer, renderInfo); + FrameConsumer consumer = new FrameConsumer() { @Override - public void consume(RGBFrame frame) { + public void consume(Map channels) { } @Override diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 1e1481a1..565b84eb 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -7,13 +7,14 @@ import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.player.AbstractTimelinePlayer; import com.replaymod.pathing.player.ReplayTimer; import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.render.CameraPathExporter; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; -import com.replaymod.render.VideoWriter; +import com.replaymod.render.FFmpegWriter; import com.replaymod.render.blend.BlendState; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.events.ReplayRenderCallback; -import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.gui.GuiRenderingDone; import com.replaymod.render.gui.GuiVideoRenderer; import com.replaymod.render.metadata.MetadataInjector; @@ -43,6 +44,7 @@ import org.lwjgl.opengl.GL11; //#endif //#if MC>=11400 +import com.replaymod.render.EXRWriter; import com.replaymod.render.mixin.MainWindowAccessor; import net.minecraft.client.gui.screen.Screen; import org.lwjgl.glfw.GLFW; @@ -83,7 +85,8 @@ public class VideoRenderer implements RenderInfo { private final ReplayHandler replayHandler; private final Timeline timeline; private final Pipeline renderingPipeline; - private final VideoWriter videoWriter; + private final FFmpegWriter ffmpegWriter; + private final CameraPathExporter cameraPathExporter; private int fps; private boolean mouseWasGrabbed; @@ -119,16 +122,41 @@ public class VideoRenderer implements RenderInfo { BlendState.setState(new BlendState(settings.getOutputFile())); this.renderingPipeline = Pipelines.newBlendPipeline(this); - this.videoWriter = null; + this.ffmpegWriter = null; } else { - this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, - videoWriter = new VideoWriter(this) { - @Override - public void consume(RGBFrame frame) { - gui.updatePreview(frame); - super.consume(frame); - } - }); + FrameConsumer frameConsumer; + if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) { + ffmpegWriter = null; + //#if MC>=11400 + frameConsumer = new EXRWriter(settings.getOutputFile().toPath()); + //#else + //$$ throw new UnsupportedOperationException("EXR requires LWJGL3"); + //#endif + } else { + frameConsumer = ffmpegWriter = new FFmpegWriter(this); + } + FrameConsumer previewingFrameConsumer = new FrameConsumer() { + @Override + public void consume(Map channels) { + BitmapFrame bgra = channels.get(Channel.BRGA); + if (bgra != null) { + gui.updatePreview(bgra.getByteBuffer(), bgra.getSize()); + } + frameConsumer.consume(channels); + } + + @Override + public void close() throws IOException { + frameConsumer.close(); + } + }; + this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, previewingFrameConsumer); + } + + if (settings.isCameraPathExport()) { + this.cameraPathExporter = new CameraPathExporter(settings); + } else { + this.cameraPathExporter = null; } } @@ -267,6 +295,10 @@ public class VideoRenderer implements RenderInfo { //$$ mc.displayHeight = displayHeightBefore; //#endif + if (cameraPathExporter != null) { + cameraPathExporter.recordFrame(timer.tickDelta); + } + framesDone++; return timer.tickDelta; } @@ -328,6 +360,10 @@ public class VideoRenderer implements RenderInfo { totalFrames = (int) (duration*fps/1000); + if (cameraPathExporter != null) { + cameraPathExporter.setup(totalFrames); + } + updateDisplaySize(); //#if MC<=10809 @@ -378,13 +414,21 @@ public class VideoRenderer implements RenderInfo { } //#endif + if (!hasFailed() && cameraPathExporter != null) { + try { + cameraPathExporter.finish(); + } catch (IOException e) { + setFailure(e); + } + } + new SoundHandler().playRenderSuccessSound(); try { - if (!hasFailed() && videoWriter != null) { - new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display(); + if (!hasFailed() && ffmpegWriter != null) { + new GuiRenderingDone(ReplayModRender.instance, ffmpegWriter.getVideoFile(), totalFrames, settings).display(); } - } catch (VideoWriter.FFmpegStartupException e) { + } catch (FFmpegWriter.FFmpegStartupException e) { setFailure(e); } @@ -652,8 +696,8 @@ public class VideoRenderer implements RenderInfo { } public void cancel() { - if (videoWriter != null) { - videoWriter.abort(); + if (ffmpegWriter != null) { + ffmpegWriter.abort(); } this.cancelled = true; renderingPipeline.cancel(); @@ -683,4 +727,17 @@ public class VideoRenderer implements RenderInfo { return getVideoTime(); } } + + public static String[] checkCompat() { + //#if FABRIC>=1 + if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sodium")) { + return new String[] { + "Rendering is not currently supported while Sodium is installed.", + "See https://github.com/ReplayMod/ReplayMod/issues/150", + "For now, you need to uninstall Sodium before rendering!" + }; + } + //#endif + return null; + } } diff --git a/src/main/java/com/replaymod/render/utils/RenderJob.java b/src/main/java/com/replaymod/render/utils/RenderJob.java index 381cc35e..5203d254 100644 --- a/src/main/java/com/replaymod/render/utils/RenderJob.java +++ b/src/main/java/com/replaymod/render/utils/RenderJob.java @@ -1,12 +1,30 @@ package com.replaymod.render.utils; +import com.google.common.base.Optional; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.replaymod.render.RenderSettings; +import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.simplepathing.SPTimeline; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; public class RenderJob { - private String name; private Timeline timeline; private RenderSettings settings; @@ -14,7 +32,7 @@ public class RenderJob { } public String getName() { - return this.name; + return settings.getOutputFile().getName(); } public Timeline getTimeline() { @@ -25,10 +43,6 @@ public class RenderJob { return this.settings; } - public void setName(String name) { - this.name = name; - } - public void setTimeline(Timeline timeline) { this.timeline = timeline; } @@ -42,22 +56,76 @@ public class RenderJob { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RenderJob renderJob = (RenderJob) o; - return name.equals(renderJob.name) && - timeline.equals(renderJob.timeline) && + return timeline.equals(renderJob.timeline) && settings.equals(renderJob.settings); } @Override public int hashCode() { - return Objects.hash(name, timeline, settings); + return Objects.hash(timeline, settings); } @Override public String toString() { return "RenderJob{" + - "name='" + name + '\'' + - ", timeline=" + timeline + + "timeline=" + timeline + ", settings=" + settings + '}'; } + + public static List readQueue(ReplayFile replayFile) throws IOException { + synchronized (replayFile) { + Optional optIn = replayFile.get("renderQueue.json"); + if (!optIn.isPresent()) { + return new ArrayList<>(); + } + try (InputStream in = optIn.get(); + InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + return new GsonBuilder() + .registerTypeAdapter(Timeline.class, new TimelineTypeAdapter()) + .create() + .fromJson(reader, new TypeToken>(){}.getType()); + } + } + } + + public static void writeQueue(ReplayFile replayFile, List renderQueue) throws IOException { + synchronized (replayFile) { + try (OutputStream out = replayFile.write("renderQueue.json"); + OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + new GsonBuilder() + .registerTypeAdapter(Timeline.class, new TimelineTypeAdapter()) + .create() + .toJson(renderQueue, writer); + } + } + } + + private static class TimelineTypeAdapter extends TypeAdapter { + + private final TimelineSerialization serialization; + + public TimelineTypeAdapter(TimelineSerialization serialization) { + this.serialization = serialization; + } + + public TimelineTypeAdapter(PathingRegistry registry) { + this(new TimelineSerialization(registry, null)); + } + + public TimelineTypeAdapter() { + // TODO need to somehow get rid of the reliance on simplepathing + this(new SPTimeline()); + } + + @Override + public void write(JsonWriter out, Timeline value) throws IOException { + out.value(serialization.serialize(Collections.singletonMap("", value))); + } + + @Override + public Timeline read(JsonReader in) throws IOException { + return serialization.deserialize(in.nextString()).get(""); + } + } } diff --git a/src/main/java/com/replaymod/render/utils/Utils.java b/src/main/java/com/replaymod/render/utils/Utils.java index 19f3187c..8dd5e2a1 100644 --- a/src/main/java/com/replaymod/render/utils/Utils.java +++ b/src/main/java/com/replaymod/render/utils/Utils.java @@ -1,24 +1,41 @@ package com.replaymod.render.utils; +import com.replaymod.render.frame.OpenGlFrame; + import java.nio.ByteBuffer; public class Utils { /** * Copies the rgb image (flipped vertically) to the specified position in the target buffer - * @param buffer Source image - * @param bufferWidth Source image width + * @param source Source image * @param xOffset X offset in target image * @param yOffset Y offset in target image * @param to Target image * @param width Target image width */ - public static void openGlBytesToRBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) { - byte[] rowBuf = new byte[bufferWidth * 4]; + public static void openGlBytesToBitmap(OpenGlFrame source, int xOffset, int yOffset, ByteBuffer to, int width) { + openGlBytesToBitmap( + source.getByteBuffer(), source.getSize().getWidth(), source.getBytesPerPixel(), + xOffset, yOffset, to, width); + } + + /** + * Copies the rgb image (flipped vertically) to the specified position in the target buffer + * @param buffer Source image + * @param bufferWidth Source image width + * @param bbp Bytes per pixel + * @param xOffset X offset in target image + * @param yOffset Y offset in target image + * @param to Target image + * @param width Target image width + */ + public static void openGlBytesToBitmap(ByteBuffer buffer, int bufferWidth, int bbp, int xOffset, int yOffset, ByteBuffer to, int width) { + byte[] rowBuf = new byte[bufferWidth * bbp]; // Copy image flipped vertically to target buffer - int rows = buffer.remaining() / 4 / bufferWidth; + int rows = buffer.remaining() / bbp / bufferWidth; for (int i = 0; i < rows; i++) { buffer.get(rowBuf); - to.position(((yOffset + rows - i - 1) * width + xOffset) * 4); + to.position(((yOffset + rows - i - 1) * width + xOffset) * bbp); to.put(rowBuf); } to.rewind(); diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 95c847ed..c5ee6517 100644 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -225,7 +225,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend /** * Whether we're currently reading packets from the login phase. */ - private boolean loginPhase; + private boolean loginPhase = true; /** * Whether we need to restart the current replay. E.g. when jumping backwards in time diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index d20a7f41..6669145a 100644 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -29,6 +29,7 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.embedded.EmbeddedChannel; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.DownloadingTerrainScreen; import net.minecraft.client.network.ClientLoginNetworkHandler; import net.minecraft.util.crash.CrashReport; import net.minecraft.entity.Entity; @@ -671,7 +672,10 @@ public class ReplayHandler { //#endif // Send the packets - replaySender.sendPacketsTill(targetTime); + do { + replaySender.sendPacketsTill(targetTime); + targetTime += 500; + } while (mc.player == null || mc.currentScreen instanceof DownloadingTerrainScreen); replaySender.setAsyncMode(true); replaySender.setReplaySpeed(0); diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java index 6bc20a29..0fd60a70 100644 --- a/src/main/java/com/replaymod/replay/ReplayModReplay.java +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -20,8 +20,6 @@ import com.replaymod.replay.gui.screen.GuiModCompatWarning; import com.replaymod.replay.handler.GuiHandler; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.replay.ReplayFile; -import com.replaymod.replaystudio.replay.ZipReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; import net.minecraft.client.MinecraftClient; import net.minecraft.client.options.KeyBinding; import org.apache.logging.log4j.LogManager; @@ -42,6 +40,7 @@ public class ReplayModReplay implements Module { public static ReplayModReplay instance; private ReplayMod core; + public KeyBindingRegistry.Binding keyPlayPause; private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry(); @@ -116,7 +115,7 @@ public class ReplayModReplay implements Module { } }, true); - registry.registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() { + keyPlayPause = registry.registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() { @Override public void run() { if (replayHandler != null) { @@ -162,28 +161,38 @@ public class ReplayModReplay implements Module { } public void startReplay(File file) throws IOException { - startReplay(new ZipReplayFile(new ReplayStudio(), file)); + startReplay(core.openReplay(file.toPath())); } public void startReplay(ReplayFile replayFile) throws IOException { - startReplay(replayFile, true); + startReplay(replayFile, true, true); } - public void startReplay(ReplayFile replayFile, boolean checkModCompat) throws IOException { + public ReplayHandler startReplay(ReplayFile replayFile, boolean checkModCompat, boolean asyncMode) throws IOException { if (replayHandler != null) { replayHandler.endReplay(); } if (checkModCompat) { ModCompat.ModInfoDifference modDifference = new ModCompat.ModInfoDifference(replayFile.getModInfo()); if (!modDifference.getMissing().isEmpty() || !modDifference.getDiffering().isEmpty()) { - new GuiModCompatWarning(this, replayFile, modDifference).display(); - return; + GuiModCompatWarning screen = new GuiModCompatWarning(modDifference); + screen.loadButton.onClick(() -> { + try { + startReplay(replayFile, false, asyncMode); + } catch (IOException e) { + e.printStackTrace(); + } + }); + screen.display(); + return null; } } - replayHandler = new ReplayHandler(replayFile, true); + replayHandler = new ReplayHandler(replayFile, asyncMode); //#if MC>=11400 KeyBinding.updateKeysByCode(); // see Mixin_ContextualKeyBindings //#endif + + return replayHandler; } public void forcefullyStopReplay() { diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index b9f0f16d..6244acfc 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -1,5 +1,6 @@ package com.replaymod.replay.camera; +import com.replaymod.core.KeyBindingRegistry; import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; import com.replaymod.core.events.SettingsChangedCallback; @@ -13,7 +14,6 @@ import com.replaymod.replaystudio.util.Location; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.client.network.ClientPlayNetworkHandler; -import net.minecraft.client.options.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.decoration.ItemFrameEntity; @@ -405,6 +405,22 @@ public class CameraEntity //$$ } //#endif + //#if MC>=10800 + @Override + public float getSpeed() { + Entity view = getRenderViewEntity(this.client); + if (view != this && view instanceof AbstractClientPlayerEntity) { + return ((AbstractClientPlayerEntity) view).getSpeed(); + } + return 1; + } + //#else + //$$ @Override + //$$ public float getFOVMultiplier() { + //$$ return 1; + //$$ } + //#endif + @Override public boolean isInvisible() { Entity view = getRenderViewEntity(this.client); @@ -585,11 +601,11 @@ public class CameraEntity handleInputEvents(); - Map keyBindings = ReplayMod.instance.getKeyBindingRegistry().getKeyBindings(); - if (keyBindings.get("replaymod.input.rollclockwise").isPressed()) { + Map keyBindings = ReplayMod.instance.getKeyBindingRegistry().getBindings(); + if (keyBindings.get("replaymod.input.rollclockwise").keyBinding.isPressed()) { roll += Utils.isCtrlDown() ? 0.2 : 1; } - if (keyBindings.get("replaymod.input.rollcounterclockwise").isPressed()) { + if (keyBindings.get("replaymod.input.rollcounterclockwise").keyBinding.isPressed()) { roll -= Utils.isCtrlDown() ? 0.2 : 1; } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java index f1c140b4..8954652a 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java @@ -250,4 +250,9 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im markers.add(marker); saveMarkers.accept(markers); } + + @Override + public ReadableDimension getLastSize() { + return super.getLastSize(); + } } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java index 0ef7cec4..7a53b071 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -1,16 +1,19 @@ package com.replaymod.replay.gui.overlay; import com.replaymod.core.ReplayMod; +import com.replaymod.core.events.KeyBindingEventCallback; +import com.replaymod.core.events.KeyEventCallback; import com.replaymod.core.versions.MCVer.Keyboard; import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplaySender; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.AbstractGuiOverlay; import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiSlider; -import de.johni0702.minecraft.gui.element.GuiTexturedButton; import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; import de.johni0702.minecraft.gui.layout.CustomLayout; @@ -22,30 +25,26 @@ import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint; import net.minecraft.client.options.GameOptions; import net.minecraft.client.resource.language.I18n; -//#if MC>=11400 -import com.replaymod.core.events.KeyBindingEventCallback; -import com.replaymod.core.events.KeyEventCallback; -//#else -//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -//$$ import net.minecraftforge.fml.common.gameevent.InputEvent; -//#endif - import static com.replaymod.core.ReplayMod.TEXTURE_SIZE; public class GuiReplayOverlay extends AbstractGuiOverlay { + private final ReplayModReplay mod = ReplayModReplay.instance; + public final GuiPanel topPanel = new GuiPanel(this) .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5)); - public final GuiTexturedButton playPauseButton = new GuiTexturedButton() { + public final GuiButton playPauseButton = new GuiButton() { @Override public GuiElement getTooltip(RenderInfo renderInfo) { GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); if (tooltip != null) { - if (getTextureNormal().getY() == 0) { // Play button - tooltip.setI18nText("replaymod.gui.ingame.menu.unpause"); + String label; + if (getSpriteUV().getY() == 0) { // Play button + label = "replaymod.gui.ingame.menu.unpause"; } else { // Pause button - tooltip.setI18nText("replaymod.gui.ingame.menu.pause"); + label = "replaymod.gui.ingame.menu.pause"; } + tooltip.setText(I18n.translate(label) + " (" + mod.keyPlayPause.getBoundKey() + ")"); } return tooltip; } @@ -61,6 +60,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(5)); private final EventHandler eventHandler = new EventHandler(); + private boolean hidden; public GuiReplayOverlay(final ReplayHandler replayHandler) { timeline = new GuiMarkerTimeline(replayHandler){ @@ -83,7 +83,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { } }); - playPauseButton.setTexturePosH(new ReadablePoint() { + playPauseButton.setSpriteUV(new ReadablePoint() { @Override public int getX() { return 0; @@ -156,8 +156,8 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - // Do not render overlay when user pressed F1 and we are not currently in some popup - if (getMinecraft().options.hudHidden && isAllowUserInput()) { + // Do not render overlay if all hud, or this one specifically, is hidden and we're not in some popup + if ((getMinecraft().options.hudHidden || hidden) && isAllowUserInput()) { // Note that this only applies to when the mouse is visible, otherwise // the draw method isn't called in the first place return; @@ -176,13 +176,8 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { //$$ public // All event handlers need to be public in 1.7.10 //#endif class EventHandler extends EventRegistrations { - //#if MC>=11400 { on(KeyBindingEventCallback.EVENT, this::onKeyBindingEvent); } private void onKeyBindingEvent() { - //#else - //$$ @SubscribeEvent - //$$ public void onKeyBindingEvent(InputEvent.KeyInputEvent event) { - //#endif GameOptions gameSettings = getMinecraft().options; while (gameSettings.keyChat.wasPressed() || gameSettings.keyCommand.wasPressed()) { if (!isMouseVisible()) { @@ -191,20 +186,12 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { } } - //#if MC>=11400 - { on(KeyEventCallback.EVENT, (int key, int scanCode, int action, int modifiers) -> onKeyInput(key, action)); } + { on(KeyEventCallback.EVENT, (int key, int scanCode, int action, int modifiers) -> { onKeyInput(key, action); return false; }); } private void onKeyInput(int key, int action) { - if (action != 0) return; - //#else - //$$ @SubscribeEvent - //$$ public void onKeyInput(InputEvent.KeyInputEvent event) { - //$$ if (!Keyboard.getEventKeyState()) return; - //$$ int key = Keyboard.getEventKey(); - //#endif - GameOptions gameSettings = getMinecraft().options; - // Handle the F1 key binding while the overlay is opened as a gui screen + if (action != KeyEventCallback.ACTION_PRESS) return; + // Allow F1 to be used to hide the replay gui (e.g. for recording with OBS) if (isMouseVisible() && key == Keyboard.KEY_F1) { - gameSettings.hudHidden = !gameSettings.hudHidden; + hidden = !hidden; } } } diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java b/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java index 1ea0fd0d..a91b0acd 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java @@ -1,9 +1,7 @@ package com.replaymod.replay.gui.screen; import com.replaymod.core.utils.ModCompat; -import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.data.ModInfo; -import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.util.I18n; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiPanel; @@ -14,7 +12,6 @@ import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; -import java.io.IOException; import java.util.Map; public class GuiModCompatWarning extends AbstractGuiScreen { @@ -38,7 +35,7 @@ public class GuiModCompatWarning extends AbstractGuiScreen content.getListLayout().setSpacing(8); } - public GuiModCompatWarning(ReplayModReplay mod, ReplayFile replayFile, ModCompat.ModInfoDifference difference) { + public GuiModCompatWarning(ModCompat.ModInfoDifference difference) { VerticalLayout.Data data = new VerticalLayout.Data(0.5); GuiPanel content = this.content.getListPanel(); content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.message1")); @@ -82,13 +79,6 @@ public class GuiModCompatWarning extends AbstractGuiScreen } cancelButton.onClick(() -> getMinecraft().openScreen(null)); - loadButton.onClick(() -> { - try { - mod.startReplay(replayFile, false); - } catch (IOException e) { - e.printStackTrace(); - } - }); } @Override 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 ca5c5bd2..5c227644 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -4,7 +4,14 @@ import com.google.common.base.Supplier; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; +import com.replaymod.render.gui.GuiRenderQueue; +import com.replaymod.render.rendering.VideoRenderer; +import com.replaymod.render.utils.RenderJob; +import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.versions.Image; +import net.minecraft.client.font.TextRenderer; import net.minecraft.util.Formatting; import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; @@ -16,8 +23,6 @@ import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.Setting; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; -import com.replaymod.replaystudio.replay.ZipReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; import de.johni0702.minecraft.gui.container.AbstractGuiContainer; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; @@ -50,11 +55,18 @@ import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static com.replaymod.replay.ReplayModReplay.LOGGER; +import static de.johni0702.minecraft.gui.versions.MCVer.getFontRenderer; //#if MC>=11400 import net.minecraft.text.TranslatableText; @@ -65,39 +77,50 @@ import net.minecraft.text.TranslatableText; public class GuiReplayViewer extends GuiScreen { private final ReplayModReplay mod; - public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(new Runnable() { - @Override - public void run() { - replaySpecificButtons.forEach(b -> b.setEnabled(list.getSelected() != null)); - if (list.getSelected() != null && list.getSelected().incompatible) { - loadButton.setDisabled(); - } - } - }).onSelectionDoubleClicked(() -> { + public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(this::updateButtons).onSelectionDoubleClicked(() -> { if (this.loadButton.isEnabled()) { this.loadButton.onClick(); } }); public final GuiButton loadButton = new GuiButton().onClick(new Runnable() { + private boolean loading = false; + @Override public void run() { - try { - mod.startReplay(list.getSelected().file); - } catch (IOException e) { - e.printStackTrace(); - } finally { - // Disable load button to prevent the player from opening the replay twice at the same time - loadButton.setDisabled(); + // Prevent the player from opening the replay twice at the same time + if (loading) { + return; + } + loading = true; + loadButton.setDisabled(); // visual hint + + List selected = list.getSelected(); + if (selected.size() == 1) { + try { + mod.startReplay(selected.get(0).file); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + Iterator>> replays = selected.stream() + .filter(it -> !it.renderQueue.isEmpty()) + .map(it -> new Pair<>(it.file, it.renderQueue)) + .iterator(); + GuiRenderQueue.processMultipleReplays(GuiReplayViewer.this, mod, replays, () -> { + loading = false; + updateButtons(); + display(); + }); } } - }).setSize(150, 20).setI18nLabel("replaymod.gui.load").setDisabled(); + }).setSize(150, 20); public final GuiButton folderButton = new GuiButton().onClick(new Runnable() { @Override public void run() { try { - File folder = mod.getCore().getReplayFolder(); + File folder = mod.getCore().getReplayFolder().toFile(); MCVer.openFile(folder); } catch (IOException e) { @@ -109,7 +132,7 @@ public class GuiReplayViewer extends GuiScreen { public final GuiButton renameButton = new GuiButton().onClick(new Runnable() { @Override public void run() { - final File file = list.getSelected().file; + final File file = list.getSelected().get(0).file; String name = Utils.fileNameToReplayName(file.getName()); final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(name); final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this, @@ -165,10 +188,9 @@ public class GuiReplayViewer extends GuiScreen { }); } }).setSize(73, 20).setI18nLabel("replaymod.gui.rename").setDisabled(); - public final GuiButton deleteButton = new GuiButton().onClick(new Runnable() { - @Override - public void run() { - String name = list.getSelected().name.getText(); + public final GuiButton deleteButton = new GuiButton().onClick(() -> { + for (GuiReplayEntry entry : list.getSelected()) { + String name = entry.name.getText(); GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this, new GuiLabel().setI18nText("replaymod.gui.viewer.delete.linea").setColor(Colors.BLACK), new GuiLabel().setI18nText("replaymod.gui.viewer.delete.lineb", name + Formatting.RESET).setColor(Colors.BLACK) @@ -178,7 +200,7 @@ public class GuiReplayViewer extends GuiScreen { public void onSuccess(Boolean delete) { if (delete) { try { - FileUtils.forceDelete(list.getSelected().file); + FileUtils.forceDelete(entry.file); } catch (IOException e) { e.printStackTrace(); } @@ -194,12 +216,11 @@ public class GuiReplayViewer extends GuiScreen { } }).setSize(73, 20).setI18nLabel("replaymod.gui.delete").setDisabled(); - public final GuiButton settingsButton = new GuiButton().onClick(new Runnable() { - @Override - public void run() { - new GuiReplaySettings(toMinecraft(), mod.getCore().getSettingsRegistry()).display(); - } - }).setSize(150, 20).setI18nLabel("replaymod.gui.settings"); + public final GuiButton settingsButton = new GuiButton(this) + .setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(20, 0) + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.settings")) + .onClick(() -> new GuiReplaySettings(toMinecraft(), getMod().getCore().getSettingsRegistry()).display()); public final GuiButton cancelButton = new GuiButton().onClick(new Runnable() { @Override @@ -209,11 +230,11 @@ public class GuiReplayViewer extends GuiScreen { }).setSize(73, 20).setI18nLabel("replaymod.gui.cancel"); public final List replaySpecificButtons = new ArrayList<>(); - { replaySpecificButtons.addAll(Arrays.asList(loadButton, renameButton, deleteButton)); } + { replaySpecificButtons.addAll(Arrays.asList(renameButton)); } public final GuiPanel editorButton = new GuiPanel(); public final GuiPanel upperButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)) - .addElements(null, loadButton, settingsButton); + .addElements(null, loadButton); public final GuiPanel lowerButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)) .addElements(null, renameButton, deleteButton, editorButton, cancelButton); public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new VerticalLayout().setSpacing(5)) @@ -223,7 +244,7 @@ public class GuiReplayViewer extends GuiScreen { this.mod = mod; try { - list.setFolder(mod.getCore().getReplayFolder()); + list.setFolder(mod.getCore().getReplayFolder().toFile()); } catch (IOException e) { throw new CrashException(CrashReport.create(e, "Getting replay folder")); } @@ -237,8 +258,40 @@ public class GuiReplayViewer extends GuiScreen { pos(list, 0, 30); size(list, width, y(buttonPanel) - 10 - y(list)); + + pos(settingsButton, width - width(settingsButton) - 5, 5); } }); + + updateButtons(); + } + + public ReplayModReplay getMod() { + return mod; + } + + private void updateButtons() { + List selected = list.getSelected(); + int count = selected.size(); + + replaySpecificButtons.forEach(b -> b.setEnabled(count == 1)); + deleteButton.setEnabled(count > 0); + if (count > 1) { + Set jobs = selected.stream().flatMap(entry -> entry.renderQueue.stream()).collect(Collectors.toSet()); + String[] tooltipLines = jobs.stream().map(RenderJob::getName).toArray(String[]::new); + loadButton.setI18nLabel("replaymod.gui.viewer.bulkrender", jobs.size()); + loadButton.setTooltip(new GuiTooltip().setText(tooltipLines)); + loadButton.setEnabled(!jobs.isEmpty()); + + String[] compatError = VideoRenderer.checkCompat(); + if (compatError != null) { + loadButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError)); + } + } else { + loadButton.setI18nLabel("replaymod.gui.load"); + loadButton.setTooltip(null); + loadButton.setEnabled(count == 1 && !selected.get(0).incompatible); + } } private static final GuiImage DEFAULT_THUMBNAIL = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL); @@ -269,10 +322,10 @@ public class GuiReplayViewer extends GuiScreen { acceptButton.setEnabled(list.getSelected() != null); }).onSelectionDoubleClicked(() -> { close(); - future.set(list.getSelected().file); + future.set(list.getSelected().get(0).file); }); acceptButton.onClick(() -> { - future.set(list.getSelected().file); + future.set(list.getSelected().get(0).file); close(); }); cancelButton.onClick(() -> { @@ -321,6 +374,10 @@ public class GuiReplayViewer extends GuiScreen { public static class GuiReplayList extends AbstractGuiResourceLoadingList implements Typeable { private File folder = null; + // Not actually a child of this element, we just use it for text manipulation + private final GuiTextField filterTextField = new GuiTextField() + .setFocused(true); + public GuiReplayList(GuiContainer container) { super(container); } @@ -332,16 +389,13 @@ public class GuiReplayViewer extends GuiScreen { LOGGER.warn("Failed to list files in {}", folder); return; } + Map lastModified = new HashMap<>(); + Arrays.sort(files, Comparator.comparingLong(f -> lastModified.computeIfAbsent(f, File::lastModified)).reversed()); for (final File file : files) { if (Thread.interrupted()) break; - try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { - // TODO add a getThumbBytes method to ReplayStudio - final Image thumb = Optional.ofNullable(replayFile.get("thumb").orNull()).flatMap(stream -> { + try (ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath())) { + final Image thumb = Optional.ofNullable(replayFile.getThumbBytes().orNull()).flatMap(stream -> { try (InputStream in = stream) { - int i = 7; - while (i > 0) { - i -= in.skip(i); - } return Optional.of(Image.read(in)); } catch (IOException e) { e.printStackTrace(); @@ -349,9 +403,18 @@ public class GuiReplayViewer extends GuiScreen { } }).orElse(null); final ReplayMetaData metaData = replayFile.getMetaData(); + List renderQueue = RenderJob.readQueue(replayFile); if (metaData != null) { - results.consume(() -> new GuiReplayEntry(file, metaData, thumb)); + results.consume(() -> new GuiReplayEntry(file, metaData, thumb, renderQueue) { + @Override + public ReadableDimension calcMinSize() { + if (isFiltered(this)) { + return new Dimension(-4, -4); + } + return super.calcMinSize(); + } + }); } } catch (Exception e) { LOGGER.error("Could not load Replay File {}", file.getName(), e); @@ -364,6 +427,14 @@ public class GuiReplayViewer extends GuiScreen { this.folder = folder; } + private boolean isFiltered(GuiReplayEntry entry) { + String filter = filterTextField.getText().toLowerCase(); + if (filter.isEmpty()) { + return false; + } + return !entry.name.getText().toLowerCase().contains(filter); + } + @Override public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { if (keyCode == Keyboard.KEY_F1) { @@ -372,9 +443,52 @@ public class GuiReplayViewer extends GuiScreen { reg.save(); load(); } + + boolean filterHasPriority = !filterTextField.getText().isEmpty(); + if (filterHasPriority && filterTextField.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) { + scrollY(0); // ensure we scroll to top if most entries are filtered + return true; + } + + if (super.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) { + return true; + } + + if (!filterHasPriority && filterTextField.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) { + scrollY(0); // ensure we scroll to top if most entries are filtered + return true; + } + return false; } + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + super.draw(renderer, size, renderInfo); + + String filter = filterTextField.getText(); + if (!filter.isEmpty()) { + boolean anyMatches = getListPanel().calcMinSize().getHeight() > 0; + + TextRenderer fontRenderer = getFontRenderer(); + int filterTextWidth = fontRenderer.getWidth(filter); + int filterTextHeight = fontRenderer.fontHeight; + renderer.drawRect( + size.getWidth() - 3 - 2 - filterTextWidth - 2, + size.getHeight() - 3 - 2 - filterTextHeight - 2, + 2 + filterTextWidth + 2, + 2 + filterTextHeight + 2, + Colors.WHITE + ); + renderer.drawString( + size.getWidth() - 3 - 2 - filterTextWidth, + size.getHeight() - 3 - 2 - filterTextHeight, + anyMatches ? Colors.BLACK : Colors.DARK_RED, + filter + ); + } + } + @Override protected GuiReplayList getThis() { return this; @@ -404,15 +518,22 @@ public class GuiReplayViewer extends GuiScreen { return new Dimension(dimension.getWidth() + 2, dimension.getHeight() + 2); } }); + public final GuiImage renderQueueIcon = new GuiImage() + .setSize(10, 10) + .setTexture(ReplayMod.TEXTURE, 40, 0, 20, 20); private final long dateMillis; private final boolean incompatible; + private final List renderQueue; - public GuiReplayEntry(File file, ReplayMetaData metaData, Image thumbImage) { + public GuiReplayEntry(File file, ReplayMetaData metaData, Image thumbImage, List renderQueue) { this.file = file; + this.renderQueue = renderQueue; name.setText(Formatting.UNDERLINE + Utils.fileNameToReplayName(file.getName())); - if (StringUtils.isEmpty(metaData.getServerName()) + if (!StringUtils.isEmpty(metaData.getCustomServerName())) { + server.setText(metaData.getCustomServerName()); + } else if (StringUtils.isEmpty(metaData.getServerName()) || !ReplayMod.instance.getSettingsRegistry().get(Setting.SHOW_SERVER_IPS)) { server.setI18nText("replaymod.gui.iphidden").setColor(Colors.DARK_RED); } else { @@ -433,6 +554,12 @@ public class GuiReplayViewer extends GuiScreen { duration.setText(Utils.convertSecondsToShortString(metaData.getDuration() / 1000)); addElements(null, durationPanel); + if (!renderQueue.isEmpty()) { + renderQueueIcon.setTooltip(new GuiTooltip() + .setText(renderQueue.stream().map(RenderJob::getName).toArray(String[]::new))); + addElements(null, renderQueueIcon); + } + setLayout(new CustomLayout() { @Override protected void layout(GuiReplayEntry container, int width, int height) { @@ -442,6 +569,10 @@ public class GuiReplayViewer extends GuiScreen { pos(infoPanel, width(thumbnail) + 5, 0); pos(version, width - width(version), 0); + + if (renderQueueIcon.getContainer() != null) { + pos(renderQueueIcon, width(thumbnail) - width(renderQueueIcon), 0); + } } @Override diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index 29e3c988..88eea893 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -8,11 +8,12 @@ import net.minecraft.client.gui.screen.TitleScreen; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.AbstractButtonWidget; -import net.minecraft.client.resource.language.I18n; //#if MC>=11600 import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; +//#else +//$$ import net.minecraft.client.resource.language.I18n; //#endif //#if FABRIC>=1 @@ -29,6 +30,7 @@ import net.minecraft.client.gui.widget.ButtonWidget; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import static com.replaymod.core.versions.MCVer.*; @@ -152,40 +154,48 @@ public class GuiHandler extends EventRegistrations { } } if (achievements != null && stats != null) { - moveAllButtonsDirectlyBelowUpwards(buttonList, achievements.y, - achievements.x, stats.x + width(stats)); + moveAllButtonsInRect(buttonList, + achievements.x, stats.x + width(stats), + achievements.y, Integer.MAX_VALUE, + -24); } // In 1.13+ Forge, the Options button shares one row with the Open to LAN button //#if MC<11400 //$$ if (openToLan != null) { - //$$ moveAllButtonsDirectlyBelowUpwards(buttonList, openToLan.y, - //$$ openToLan.x, openToLan.x + openToLan.width); + //$$ moveAllButtonsInRect(buttonList, + //$$ openToLan.x, openToLan.x + openToLan.width, + //$$ openToLan.y, Integer.MAX_VALUE, + //$$ -24); //$$ } //#endif } } /** - * Moves all buttons that are within a rectangle below a certain y coordinate upwards by 24 units. + * Moves all buttons that in any way intersect a rectangle by a given amount on the y axis. * @param buttons List of buttons - * @param belowY The Y limit + * @param yStart Top y limit of the rectangle + * @param yEnd Bottom y limit of the rectangle * @param xStart Left x limit of the rectangle * @param xEnd Right x limit of the rectangle + * @param moveBy Signed distance to move the buttons */ - private void moveAllButtonsDirectlyBelowUpwards( + private void moveAllButtonsInRect( //#if MC>=11400 List buttons, //#else //$$ List buttons, //#endif - int belowY, int xStart, - int xEnd + int xEnd, + int yStart, + int yEnd, + int moveBy ) { buttons.stream() - .filter(button -> button.y >= belowY) .filter(button -> button.x <= xEnd && button.x + width(button) >= xStart) - .forEach(button -> button.y -= 24); + .filter(button -> button.y <= yEnd && button.y + height(button) >= yStart) + .forEach(button -> button.y += moveBy); } //#if FABRIC>=1 @@ -222,15 +232,35 @@ public class GuiHandler extends EventRegistrations { //$$ @SubscribeEvent //$$ public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { //$$ Screen guiScreen = getGui(event); + //$$ List buttonList = getButtonList(event); //#endif if (!(guiScreen instanceof TitleScreen)) { return; } + + int x = guiScreen.width / 2 - 100; + // We want to position our button below the realms button + int y = findButton(buttonList, "menu.online", 14) + .map(Optional::of) + // or, if someone removed the realms button, we'll alternatively take the multiplayer one + .orElse(findButton(buttonList, "menu.multiplayer", 2)) + // if we found some button, put our button at its position (we'll move it out of the way shortly) + .map(it -> it.y) + // and if we can't even find that one, then just guess + .orElse(guiScreen.height / 4 + 10 + 4 * 24); + + // Move all buttons above or at our one upwards + moveAllButtonsInRect(buttonList, + x, x + 200, + Integer.MIN_VALUE, y, + -24); + + // Add our button InjectedButton button = new InjectedButton( guiScreen, BUTTON_REPLAY_VIEWER, - guiScreen.width / 2 - 100, - guiScreen.height / 4 + 10 + 4 * 24, + x, + y, 200, 20, "replaymod.gui.replayviewer", diff --git a/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java b/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java index 1a4c2938..12f19084 100644 --- a/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java +++ b/src/main/java/com/replaymod/replay/mixin/Mixin_MoveRealmsButton.java @@ -14,7 +14,7 @@ public abstract class Mixin_MoveRealmsButton { index = 2 ) private int adjustRealmsButton(int height) { - return height - (24 - 10) * 4; + return height - 24 * 4; } } //#endif diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 88380fe9..fd60d3d8 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -3,6 +3,7 @@ package com.replaymod.simplepathing; import com.replaymod.core.KeyBindingRegistry; import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; +import com.replaymod.core.SettingsRegistry; import com.replaymod.core.events.SettingsChangedCallback; import de.johni0702.minecraft.gui.utils.EventRegistrations; import com.replaymod.core.versions.MCVer.Keyboard; @@ -21,6 +22,7 @@ import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.simplepathing.SPTimeline.SPPath; import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.preview.PathPreview; +import net.minecraft.client.gui.screen.Screen; import net.minecraft.util.crash.CrashReport; import net.minecraft.util.crash.CrashException; import org.apache.logging.log4j.LogManager; @@ -39,6 +41,9 @@ public class ReplayModSimplePathing extends EventRegistrations implements Module public static ReplayModSimplePathing instance; private ReplayMod core; + public KeyBindingRegistry.Binding keyPositionKeyframe; + public KeyBindingRegistry.Binding keyTimeKeyframe; + public KeyBindingRegistry.Binding keySyncTime; public static Logger LOGGER = LogManager.getLogger(); @@ -85,11 +90,56 @@ public class ReplayModSimplePathing extends EventRegistrations implements Module core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> { if (guiPathing != null) guiPathing.clearKeyframesButtonPressed(); }, true); - core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { + keySyncTime = core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { if (guiPathing != null) guiPathing.syncTimeButtonPressed(); }, true); - core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> { - if (guiPathing != null) guiPathing.deleteButtonPressed(); + SettingsRegistry settingsRegistry = core.getSettingsRegistry(); + keySyncTime.registerAutoActivationSupport(settingsRegistry.get(Setting.AUTO_SYNC), active -> { + settingsRegistry.set(Setting.AUTO_SYNC, active); + settingsRegistry.save(); + }); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> + guiPathing != null && guiPathing.deleteButtonPressed()); + keyPositionKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positionkeyframe", Keyboard.KEY_I, () -> { + if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, false); + }, true); + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positiononlykeyframe", 0, () -> { + if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, true); + }, true); + keyTimeKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.timekeyframe", Keyboard.KEY_O, () -> { + if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.TIME, false); + }, true); + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.bothkeyframes", 0, () -> { + if (guiPathing != null) { + guiPathing.toggleKeyframe(SPPath.TIME, false); + guiPathing.toggleKeyframe(SPPath.POSITION, false); + } + }, true); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Z, () -> { + if (Screen.hasControlDown() && currentTimeline != null) { + Timeline timeline = currentTimeline.getTimeline(); + if (Screen.hasShiftDown()) { + if (timeline.peekRedoStack() != null) { + timeline.redoLastChange(); + } + } else { + if (timeline.peekUndoStack() != null) { + timeline.undoLastChange(); + } + } + return true; + } + return false; + }); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Y, () -> { + if (Screen.hasControlDown() && currentTimeline != null) { + Timeline timeline = currentTimeline.getTimeline(); + if (timeline.peekRedoStack() != null) { + timeline.redoLastChange(); + } + return true; + } + return false; }); } diff --git a/src/main/java/com/replaymod/simplepathing/Setting.java b/src/main/java/com/replaymod/simplepathing/Setting.java index 79e432b2..575a7386 100644 --- a/src/main/java/com/replaymod/simplepathing/Setting.java +++ b/src/main/java/com/replaymod/simplepathing/Setting.java @@ -7,6 +7,8 @@ import java.util.stream.Collectors; public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting PATH_PREVIEW = make("pathpreview", "pathpreview", true); + public static final Setting AUTO_SYNC = make("autosync", null, true); + public static final Setting TIMELINE_LENGTH = make("timelineLength", null, 30 * 60); public static final SettingsRegistry.MultipleChoiceSettingKeys DEFAULT_INTERPOLATION; static { @@ -27,6 +29,6 @@ public final class Setting extends SettingsRegistry.SettingKeys { } public Setting(String key, String displayString, T defaultValue) { - super("simplepathing", key, "replaymod.gui.settings." + displayString, defaultValue); + super("simplepathing", key, displayString == null ? null : "replaymod.gui.settings." + displayString, defaultValue); } } diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index b152202b..ece93360 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -6,6 +6,7 @@ import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; @@ -21,10 +22,15 @@ import org.apache.commons.lang3.tuple.Pair; import de.johni0702.minecraft.gui.utils.lwjgl.Point; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; +import org.lwjgl.opengl.GL11; import java.util.Comparator; import java.util.Optional; +import static com.replaymod.core.versions.MCVer.BufferBuilder_addPosCol; +import static com.replaymod.core.versions.MCVer.BufferBuilder_beginPosCol; +import static com.replaymod.core.versions.MCVer.Tessellator_getInstance; + public class GuiKeyframeTimeline extends AbstractGuiTimeline implements Draggable { protected static final int KEYFRAME_SIZE = 5; protected static final int KEYFRAME_TEXTURE_X = 74; @@ -101,9 +107,93 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline timeProperty = keyframe.getValue(TimestampProperty.PROPERTY); + if (timeProperty.isPresent()) { v += KEYFRAME_SIZE; renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); + + GuiMarkerTimeline replayTimeline = gui.overlay.timeline; + GuiKeyframeTimeline keyframeTimeline = this; + + ReadableDimension replayTimelineSize = replayTimeline.getLastSize(); + ReadableDimension keyframeTimelineSize = this.getLastSize(); + if (replayTimelineSize == null || keyframeTimelineSize == null) { + return; + } + + // Determine absolute positions for both timelines + Point replayTimelinePos = new Point(0, 0); + Point keyframeTimelinePos = new Point(0, 0); + replayTimeline.getContainer().convertFor(replayTimeline, replayTimelinePos); + keyframeTimeline.getContainer().convertFor(keyframeTimeline, keyframeTimelinePos); + replayTimelinePos.setLocation(-replayTimelinePos.getX(), -replayTimelinePos.getY()); + keyframeTimelinePos.setLocation(-keyframeTimelinePos.getX(), -keyframeTimelinePos.getY()); + + int replayTimelineLeft = replayTimelinePos.getX(); + int replayTimelineRight = replayTimelinePos.getX() + replayTimelineSize.getWidth(); + int replayTimelineTop = replayTimelinePos.getY(); + int replayTimelineBottom = replayTimelinePos.getY() + replayTimelineSize.getHeight(); + int replayTimelineWidth = replayTimelineRight - replayTimelineLeft - BORDER_LEFT - BORDER_RIGHT; + + int keyframeTimelineLeft = keyframeTimelinePos.getX(); + int keyframeTimelineTop = keyframeTimelinePos.getY(); + + float positionXReplayTimeline = BORDER_LEFT + timeProperty.get() / (float) replayTimeline.getLength() * replayTimelineWidth; + float positionXKeyframeTimeline = positonX + KEYFRAME_SIZE / 2f; + + final int color = 0xff0000ff; + BufferBuilder_beginPosCol(GL11.GL_LINE_STRIP); + + // Start just below the top border of the replay timeline + BufferBuilder_addPosCol( + replayTimelineLeft + positionXReplayTimeline, + replayTimelineTop + BORDER_TOP, + 0, + color >> 24 & 0xff, + color >> 16 & 0xff, + color >> 8 & 0xff, + color & 0xff + ); + // Draw vertically over the replay timeline, including its bottom border + BufferBuilder_addPosCol( + replayTimelineLeft + positionXReplayTimeline, + replayTimelineBottom, + 0, + color >> 24 & 0xff, + color >> 16 & 0xff, + color >> 8 & 0xff, + color & 0xff + ); + // Now for the important part: connecting to the keyframe timeline + BufferBuilder_addPosCol( + keyframeTimelineLeft + positionXKeyframeTimeline, + keyframeTimelineTop, + 0, + color >> 24 & 0xff, + color >> 16 & 0xff, + color >> 8 & 0xff, + color & 0xff + ); + // And finally another vertical bit (the timeline is already crammed enough, so only the border) + BufferBuilder_addPosCol( + keyframeTimelineLeft + positionXKeyframeTimeline, + keyframeTimelineTop + BORDER_TOP, + 0, + color >> 24 & 0xff, + color >> 16 & 0xff, + color >> 8 & 0xff, + color & 0xff + ); + + GL11.glEnable(GL11.GL_LINE_SMOOTH); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glPushAttrib(GL11.GL_SCISSOR_BIT); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + GL11.glLineWidth(1); + Tessellator_getInstance().draw(); + GL11.glPopAttrib(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LINE_SMOOTH); } } }); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index a9bf7f44..f805dd58 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -12,6 +12,7 @@ import com.replaymod.pathing.player.RealtimeTimelinePlayer; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.render.gui.GuiRenderQueue; import com.replaymod.render.gui.GuiRenderSettings; import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.camera.CameraEntity; @@ -24,18 +25,17 @@ import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.SPTimeline.SPPath; +import com.replaymod.simplepathing.Setting; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; -import de.johni0702.minecraft.gui.element.AbstractGuiClickable; -import de.johni0702.minecraft.gui.element.AbstractGuiElement; +import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar; import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiTexturedButton; import de.johni0702.minecraft.gui.element.GuiTooltip; -import de.johni0702.minecraft.gui.element.IGuiClickable; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; import de.johni0702.minecraft.gui.layout.CustomLayout; @@ -45,10 +45,10 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; -import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint; +import net.minecraft.client.resource.language.I18n; import net.minecraft.util.crash.CrashReport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -80,7 +80,7 @@ import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER; public class GuiPathing { private static final Logger logger = LogManager.getLogger(); - public final GuiTexturedButton playPauseButton = new GuiTexturedButton() { + public final GuiButton playPauseButton = new GuiButton() { @Override public GuiElement getTooltip(RenderInfo renderInfo) { GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); @@ -97,61 +97,61 @@ public class GuiPathing { } }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip()); - public final GuiTexturedButton renderButton = new GuiTexturedButton().onClick(new Runnable() { + public final GuiButton renderButton = new GuiButton().onClick(new Runnable() { @Override public void run() { - if (!preparePathsForPlayback(false)) return; - - // Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue - SPTimeline spTimeline = mod.getCurrentTimeline(); - Timeline timeline; - try { - TimelineSerialization serialization = new TimelineSerialization(spTimeline, null); - String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline())); - timeline = serialization.deserialize(serialized).get(""); - } catch (Throwable t) { - error(LOGGER, replayHandler.getOverlay(), CrashReport.create(t, "Cloning timeline"), () -> {}); - return; - } - - new GuiRenderSettings(replayHandler, timeline).display(); + Timeline timeline = preparePathsForPlayback(false); + if (timeline == null) return; + GuiScreen screen = GuiRenderSettings.createBaseScreen(); + new GuiRenderQueue(screen, replayHandler, () -> preparePathsForPlayback(false)) { + @Override + protected void close() { + super.close(); + getMinecraft().openScreen(null); + } + }.open(); + screen.display(); } - }).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 0) + }).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 0) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.renderpath")); - public final GuiTexturedButton positionKeyframeButton = new GuiTexturedButton() { + public final GuiButton positionKeyframeButton = new GuiButton() { @Override public GuiElement getTooltip(RenderInfo renderInfo) { GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); if (tooltip != null) { - if (getTextureNormal().getY() == 40) { // Add keyframe - if (getTextureNormal().getX() == 0) { // Position - tooltip.setI18nText("replaymod.gui.ingame.menu.addposkeyframe"); + String label; + if (getSpriteUV().getY() == 40) { // Add keyframe + if (getSpriteUV().getX() == 0) { // Position + label = "replaymod.gui.ingame.menu.addposkeyframe"; } else { // Spectator - tooltip.setI18nText("replaymod.gui.ingame.menu.addspeckeyframe"); + label = "replaymod.gui.ingame.menu.addspeckeyframe"; } } else { // Remove keyframe - if (getTextureNormal().getX() == 0) { // Position - tooltip.setI18nText("replaymod.gui.ingame.menu.removeposkeyframe"); + if (getSpriteUV().getX() == 0) { // Position + label = "replaymod.gui.ingame.menu.removeposkeyframe"; } else { // Spectator - tooltip.setI18nText("replaymod.gui.ingame.menu.removespeckeyframe"); + label = "replaymod.gui.ingame.menu.removespeckeyframe"; } } + tooltip.setText(I18n.translate(label) + " (" + mod.keyPositionKeyframe.getBoundKey() + ")"); } return tooltip; } }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip()); - public final GuiTexturedButton timeKeyframeButton = new GuiTexturedButton() { + public final GuiButton timeKeyframeButton = new GuiButton() { @Override public GuiElement getTooltip(RenderInfo renderInfo) { GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); if (tooltip != null) { - if (getTextureNormal().getY() == 80) { // Add time keyframe - tooltip.setI18nText("replaymod.gui.ingame.menu.addtimekeyframe"); + String label; + if (getSpriteUV().getY() == 80) { // Add time keyframe + label = "replaymod.gui.ingame.menu.addtimekeyframe"; } else { // Remove time keyframe - tooltip.setI18nText("replaymod.gui.ingame.menu.removetimekeyframe"); + label = "replaymod.gui.ingame.menu.removetimekeyframe"; } + tooltip.setText(I18n.translate(label) + " (" + mod.keyTimeKeyframe.getBoundKey() + ")"); } return tooltip; } @@ -161,11 +161,11 @@ public class GuiPathing { @Override public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { if (player.isActive()) { - setCursorPosition((int) player.getTimePassed()); + setCursorPosition((int) player.getTimePassed()).ensureCursorVisibleWithPadding(); } super.draw(renderer, size, renderInfo); } - }.setSize(Integer.MAX_VALUE, 20).setLength(30 * 60 * 1000).setMarkers(); + }.setSize(Integer.MAX_VALUE, 20).setMarkers(); public final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(Integer.MAX_VALUE, 9); {scrollbar.onValueChanged(new Runnable() { @@ -179,20 +179,20 @@ public class GuiPathing { public final GuiTimelineTime timelineTime = new GuiTimelineTime() .setTimeline(timeline); - public final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() { + public final GuiButton zoomInButton = new GuiButton().setSize(9, 9).onClick(new Runnable() { @Override public void run() { zoomTimeline(2d / 3d); } - }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20) + }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 20) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin")); - public final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() { + public final GuiButton zoomOutButton = new GuiButton().setSize(9, 9).onClick(new Runnable() { @Override public void run() { zoomTimeline(3d / 2d); } - }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30) + }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 30) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout")); public final GuiPanel zoomButtonPanel = new GuiPanel() @@ -211,58 +211,17 @@ public class GuiPathing { pos(scrollbar, 0, y(timeline) + height(timeline) + 1); size(scrollbar, x(zoomButtonPanel) - 2, 9); } - }).addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel); + }).addElements(null, timeline, timelineTime, scrollbar, zoomButtonPanel); public final GuiPanel panel = new GuiPanel() .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)) .addElements(new HorizontalLayout.Data(0.5), playPauseButton, renderButton, positionKeyframeButton, timeKeyframeButton, timelinePanel); - /** - * IGuiClickable dummy component that is inserted at a high level. - * During path playback, this catches all click events and forwards them to the - * abort path playback button. - * Dragging does not have to be intercepted as every GUI element should only - * respond to dragging events after it has received and handled a click event. - */ - private final IGuiClickable clickCatcher = new AbstractGuiClickable() { - @Override - public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { - if (player.isActive()) { - // Make sure the mouse is always visible during path playback - // even if the game closes the overlay for some reason (e.g. world change) - replayHandler.getOverlay().setMouseVisible(true); - } - } - - @Override - protected AbstractGuiElement getThis() { - return this; - } - - @Override - protected ReadableDimension calcMinSize() { - return new Dimension(0, 0); - } - - @Override - public boolean mouseClick(ReadablePoint position, int button) { - if (player.isActive()) { - playPauseButton.mouseClick(position, button); - return true; - } - return false; - } - - @Override - public int getLayer() { - return player.isActive() ? 10 : 0; - } - }; - private final ReplayMod core; private final ReplayModSimplePathing mod; private final ReplayHandler replayHandler; + public final GuiReplayOverlay overlay; private final RealtimeTimelinePlayer player; // Whether any error which occured during entity tracker loading has already been shown to the user @@ -275,10 +234,12 @@ public class GuiPathing { this.core = core; this.mod = mod; this.replayHandler = replayHandler; + this.overlay = replayHandler.getOverlay(); this.player = new RealtimeTimelinePlayer(replayHandler); - final GuiReplayOverlay overlay = replayHandler.getOverlay(); - playPauseButton.setTexturePosH(new ReadablePoint() { + timeline.setLength(core.getSettingsRegistry().get(Setting.TIMELINE_LENGTH) * 1000); + + playPauseButton.setSpriteUV(new ReadablePoint() { @Override public int getX() { return 0; @@ -300,14 +261,16 @@ public class GuiPathing { player.getFuture().cancel(false); } else { boolean ignoreTimeKeyframes = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT); - Path timePath = mod.getCurrentTimeline().getTimePath(); - if (!preparePathsForPlayback(ignoreTimeKeyframes)) return; + Timeline timeline = preparePathsForPlayback(ignoreTimeKeyframes); + if (timeline == null) return; + Path timePath = new SPTimeline(timeline).getTimePath(); timePath.setActive(!ignoreTimeKeyframes); + // Start from cursor time unless the control key is pressed (then start from beginning) int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition(); - ListenableFuture future = player.start(mod.getCurrentTimeline().getTimeline(), startTime); + ListenableFuture future = player.start(timeline, startTime); overlay.setCloseable(false); overlay.setMouseVisible(true); core.printInfoToChat("replaymod.chat.pathstarted"); @@ -320,21 +283,19 @@ public class GuiPathing { core.printInfoToChat("replaymod.chat.pathfinished"); } overlay.setCloseable(true); - timePath.setActive(true); } @Override public void onFailure(Throwable t) { t.printStackTrace(); overlay.setCloseable(true); - timePath.setActive(true); } }); } } }); - positionKeyframeButton.setTexturePosH(new ReadablePoint() { + positionKeyframeButton.setSpriteUV(new ReadablePoint() { @Override public int getX() { SPPath keyframePath = mod.getSelectedPath(); @@ -368,11 +329,11 @@ public class GuiPathing { }).onClick(new Runnable() { @Override public void run() { - updateKeyframe(SPPath.POSITION); + toggleKeyframe(SPPath.POSITION, false); } }); - timeKeyframeButton.setTexturePosH(new ReadablePoint() { + timeKeyframeButton.setSpriteUV(new ReadablePoint() { @Override public int getX() { return 0; @@ -395,17 +356,17 @@ public class GuiPathing { }).onClick(new Runnable() { @Override public void run() { - updateKeyframe(SPPath.TIME); + toggleKeyframe(SPPath.TIME, false); } }); - overlay.addElements(null, panel, clickCatcher); + overlay.addElements(null, panel); overlay.setLayout(new CustomLayout(overlay.getLayout()) { @Override protected void layout(GuiReplayOverlay container, int width, int height) { + checkForAutoSync(); pos(panel, 10, y(overlay.topPanel) + height(overlay.topPanel) + 3); size(panel, width - 20, 40); - size(clickCatcher, 0, 0); } }); @@ -459,6 +420,28 @@ public class GuiPathing { }); } + private int prevSpeed = -1; + private int prevTime = -1; + private void checkForAutoSync() { + if (!mod.keySyncTime.isAutoActivating()) { + prevSpeed = -1; + prevTime = -1; + return; + } + + int speed = overlay.speedSlider.getValue(); + if (prevSpeed != speed && prevSpeed != -1) { + syncTimeButtonPressed(); + } + prevSpeed = speed; + + int time = replayHandler.getReplaySender().currentTimeStamp(); + if (prevTime != time && prevTime != -1 && !player.isActive()) { + syncTimeButtonPressed(); + } + prevTime = time; + } + public void syncTimeButtonPressed() { // Current replay time int time = replayHandler.getReplaySender().currentTimeStamp(); @@ -479,16 +462,18 @@ public class GuiPathing { // Cursor time passed int cursorPassed = (int) (timePassed / speed); // Move cursor to new position - timeline.setCursorPosition(keyframeCursor + cursorPassed); + timeline.setCursorPosition(keyframeCursor + cursorPassed).ensureCursorVisibleWithPadding(); // Deselect keyframe to allow the user to add a new one right away mod.setSelected(null, 0); }); } - public void deleteButtonPressed() { + public boolean deleteButtonPressed() { if (mod.getSelectedPath() != null) { - updateKeyframe(mod.getSelectedPath()); + toggleKeyframe(mod.getSelectedPath(), false); + return true; } + return false; } private void startLoadingEntityTracker() { @@ -520,8 +505,26 @@ public class GuiPathing { }).start(); } - private boolean preparePathsForPlayback(boolean ignoreTimeKeyframes) { - SPTimeline timeline = mod.getCurrentTimeline(); + private Timeline preparePathsForPlayback(boolean ignoreTimeKeyframes) { + SPTimeline spTimeline = mod.getCurrentTimeline(); + + if (!validatePathsForPlayback(spTimeline, ignoreTimeKeyframes)) { + return null; + } + + try { + TimelineSerialization serialization = new TimelineSerialization(spTimeline, null); + String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline())); + Timeline timeline = serialization.deserialize(serialized).get(""); + timeline.getPaths().forEach(Path::updateAll); + return timeline; + } catch (Throwable t) { + error(LOGGER, replayHandler.getOverlay(), CrashReport.create(t, "Cloning timeline"), () -> {}); + return null; + } + } + + private boolean validatePathsForPlayback(SPTimeline timeline, boolean ignoreTimeKeyframes) { timeline.getTimeline().getPaths().forEach(Path::updateAll); // Make sure there are at least two position keyframes @@ -603,14 +606,22 @@ public class GuiPathing { /** * Called when either one of the property buttons is pressed. * @param path {@code TIME} for the time property button, {@code POSITION} for the place property button + * @param neverSpectator when true, will insert a position keyframe even when currently spectating an entity */ - private void updateKeyframe(SPPath path) { + public void toggleKeyframe(SPPath path, boolean neverSpectator) { LOGGER.debug("Updating keyframe on path {}" + path); - if (!loadEntityTracker(() -> updateKeyframe(path))) return; + if (!loadEntityTracker(() -> toggleKeyframe(path, neverSpectator))) return; int time = timeline.getCursorPosition(); SPTimeline timeline = mod.getCurrentTimeline(); + if (timeline.getPositionPath().getKeyframes().isEmpty() && + timeline.getTimePath().getKeyframes().isEmpty() && + time > 1000) { + String text = I18n.translate("replaymod.gui.ingame.first_keyframe_not_at_start_warning"); + GuiInfoPopup.open(overlay, text.split("\\\\n")); + } + switch (path) { case TIME: if (mod.getSelectedPath() == path) { @@ -640,7 +651,7 @@ public class GuiPathing { LOGGER.debug("No position keyframe found -> adding new keyframe"); CameraEntity camera = replayHandler.getCameraEntity(); int spectatedId = -1; - if (!replayHandler.isCameraView()) { + if (!replayHandler.isCameraView() && !neverSpectator) { spectatedId = getRenderViewEntity(replayHandler.getOverlay().getMinecraft()).getEntityId(); } timeline.addPositionKeyframe(time, Entity_getX(camera), Entity_getY(camera), Entity_getZ(camera), diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 8936798b..b45b1a1f 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 8936798bea45b8ab9422040420e8ec59aabf055d +Subproject commit b45b1a1f9ae205a79c03726fce885536dd0dabd0 diff --git a/src/main/resources/assets/replaymod/replay_gui.license b/src/main/resources/assets/replaymod/replay_gui.license new file mode 100644 index 00000000..84c37a25 --- /dev/null +++ b/src/main/resources/assets/replaymod/replay_gui.license @@ -0,0 +1,33 @@ +The replay_gui.png file as a whole is governed by the same terms as the entire repository. +Some of its individual icons were originally from other projects and under different licenses. +This file serves to credit the original author(s) and to comply with the terms of the original license (some parts of +which may still apply to the derived replay_gui.png file). + +In particular: + +================================================================================ + +The "Settings" icon (three horizontal sliders) is based on ModMenu's settings icon. +Original: https://github.com/Prospector/ModMenu/blob/74200fb7baa697b4e865b15a707a560757a7ee8f/src/main/resources/assets/modmenu/textures/gui/configure_button.png + +Copyright (c) 2018-2020 Prospector + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 0e47861d..a636691f 100644 Binary files a/src/main/resources/assets/replaymod/replay_gui.png and b/src/main/resources/assets/replaymod/replay_gui.png differ diff --git a/src/main/resources/mixins.core.replaymod.json b/src/main/resources/mixins.core.replaymod.json index 111052a2..c002fc17 100644 --- a/src/main/resources/mixins.core.replaymod.json +++ b/src/main/resources/mixins.core.replaymod.json @@ -12,12 +12,12 @@ "Mixin_RegisterDynamicResourcePacks", //#endif //#if MC>=11400 - "MixinKeyboardListener", "MixinMouse", //#endif //#if MC<10800 //$$ "ResourcePackRepositoryAccessor", //#endif + "MixinKeyboardListener", "MixinMinecraft", "GuiMainMenuAccessor", "GuiScreenAccessor", diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index 09b96cf0..e6c9ee41 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -4,11 +4,13 @@ "mixins": [], "server": [], "client": [ + "AddServerScreenAccessor", "EntityLivingBaseAccessor", "IntegratedServerAccessor", "NetworkManagerAccessor", "SPacketSpawnMobAccessor", "SPacketSpawnPlayerAccessor", + "MixinServerInfo", //#if MC>=11400 "MixinDownloadingPackFinder", "MixinMouseHelper", diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 1ad250dc..9a7ca987 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -18,6 +18,7 @@ "Mixin_ForceChunkLoading", //#endif //#if MC>=11400 + "Mixin_PreserveDepthDuringHandRendering", "MainWindowAccessor", //#endif "WorldRendererAccessor", diff --git a/version.txt b/version.txt index 59aa62c1..437459cd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.4.5 +2.5.0 diff --git a/versions/1.14.4-forge/mapping.txt b/versions/1.14.4-forge/mapping.txt index 1d242ae4..029bf807 100644 --- a/versions/1.14.4-forge/mapping.txt +++ b/versions/1.14.4-forge/mapping.txt @@ -1,3 +1,5 @@ +net.minecraft.potion.EffectInstance net.minecraft.potion.PotionEffect +net.minecraft.client.gui.screen.AddServerScreen net.minecraft.client.gui.GuiScreenAddServer net.minecraft.resources.AbstractResourcePack net.minecraft.client.resources.AbstractResourcePack net.minecraft.resources.FolderPack net.minecraft.client.resources.FolderResourcePack net.minecraft.client.resources.DownloadingPackFinder net.minecraft.client.resources.ResourcePackRepository diff --git a/versions/1.16.3/.gitkeep b/versions/1.16.4/.gitkeep similarity index 100% rename from versions/1.16.3/.gitkeep rename to versions/1.16.4/.gitkeep diff --git a/versions/common.gradle b/versions/common.gradle index 8f1bb427..11bba57d 100644 --- a/versions/common.gradle +++ b/versions/common.gradle @@ -226,24 +226,28 @@ dependencies { 11502: '1.15.2', 11601: '1.16.1', 11603: '1.16.3', + 11604: '1.16.4', ][mcVersion] mappings 'net.fabricmc:yarn:' + [ 11404: '1.14.4+build.16', 11502: '1.15.2+build.14', 11601: '1.16.1+build.17:v2', 11603: '1.16.3+build.1:v2', + 11604: '1.16.4+build.6:v2', ][mcVersion] modCompile 'net.fabricmc:fabric-loader:' + [ 11404: '0.7.8+build.189', 11502: '0.7.8+build.189', 11601: '0.8.8+build.202', 11603: '0.9.1+build.205', + 11604: '0.10.6+build.214', ][mcVersion] def fabricApiVersion = [ 11404: '0.4.3+build.247-1.14', 11502: '0.5.1+build.294-1.15', 11601: '0.14.0+build.371-1.16', 11603: '0.17.1+build.394-1.16', + 11604: '0.25.1+build.416-1.16', ][mcVersion] def fabricApiModules = [ "api-base", @@ -286,7 +290,17 @@ dependencies { shadow 'com.google.api-client:google-api-client-gson:1.20.0', shadeExclusions shadow 'com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions shadow 'com.google.oauth-client:google-oauth-client-jetty:1.20.0' - + + if (mcVersion >= 11400) { // need lwjgl 3 + for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos']) { + shadow('org.lwjgl:lwjgl-tinyexr:3.2.2' + suffix) { + exclude group: 'org.lwjgl', module: 'lwjgl' // comes with MC + } + } + } + + shadow 'com.github.javagl.JglTF:jgltf-model:3af6de4' + if (FABRIC) { shadow 'org.apache.maven:maven-artifact:3.6.1' } @@ -295,7 +309,7 @@ dependencies { shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' - shadow "com.github.ReplayMod:ReplayStudio:b29955e", shadeExclusions + shadow "com.github.ReplayMod:ReplayStudio:830678c", shadeExclusions implementation(jGui){ transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those @@ -381,6 +395,8 @@ task configureRelocation() { } else if (!pkg.startsWith('org/apache/logging')) { pkgs << pkg.substring(0, pkg.indexOf('/', 'org/apache/'.length())) } + } else if (pkg.startsWith('org/lwjgl')) { + return // either bundled with MC or uses natives which we can't relocate } else if (!pkg.startsWith('org/spongepowered')) { pkgs << pkg.substring(0, pkg.indexOf('/', 4)) } diff --git a/versions/mainProject b/versions/mainProject index b8ae5a5b..709c55fe 100644 --- a/versions/mainProject +++ b/versions/mainProject @@ -1 +1 @@ -1.16.3 \ No newline at end of file +1.16.4 \ No newline at end of file