diff --git a/.drone.yml b/.drone.yml index 95a6e8ec..3ea70281 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,6 +5,8 @@ pipeline: build: image: maven:3.3.9-jdk-8 commands: + - curl --fail --create-dirs -o $HOME/.gradle/init.d/mirror.gradle https://maven.johni0702.de/config/gradle + - curl --fail --create-dirs -o $HOME/.m2/settings.xml https://maven.johni0702.de/config/maven - JAVA_OPTS=-Xmx1516M ./gradlew setupDecompWorkspace - ./gradlew :build archive: diff --git a/.drone.yml.sig b/.drone.yml.sig index 9ba8d384..95a141a6 100644 --- a/.drone.yml.sig +++ b/.drone.yml.sig @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gSkFWQV9PUFRTPS1YbXgxNTE2TSAuL2dyYWRsZXcgc2V0dXBEZWNvbXBXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgOmJ1aWxkCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.qVj9Hk63K6c5ZdLL9epHfoDp33mwYrHONzbZ7RFYtxQ \ No newline at end of file +eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gSkFWQV9PUFRTPS1YbXgxNTE2TSAuL2dyYWRsZXcgc2V0dXBEZWNvbXBXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgOmJ1aWxkCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.nzPW0_TpMIjnFZZ5Ci439QDKlaDo1a3DCH-VCYUgpx8 \ No newline at end of file diff --git a/ReplayStudio b/ReplayStudio index d73f5623..9f5f52cd 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit d73f56233b628cd6f074ab3d4aee7ea47c0f0e1d +Subproject commit 9f5f52cd93428fcac81ed2b9e16398335987053a diff --git a/build.gradle b/build.gradle index 010ac6d4..c2d6f9e3 100755 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,8 @@ minecraft { version = '1.10.2-12.18.2.2099' runDir = "eclipse" mappings = "snapshot_20160518" + replace '@MOD_VERSION@', project.version + replace '@MC_VERSION@', project.minecraft.version } repositories { @@ -59,7 +61,7 @@ configurations { dependencies { compile 'org.projectlombok:lombok:1.16.4' - compile 'org.spongepowered:mixin:0.5.9-SNAPSHOT' + compile 'org.spongepowered:mixin:0.6.4-SNAPSHOT' shade 'com.googlecode.mp4parser:isoparser:1.1.7' shade 'org.apache.commons:commons-exec:1.3' shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0' diff --git a/docs/content.md b/docs/content.md index 4d53d664..0b3f97fe 100644 --- a/docs/content.md +++ b/docs/content.md @@ -2,7 +2,14 @@ ## Installing the Replay Mod [replaymod] The **ReplayMod** requires **Minecraft Forge** to be installed. -For the use with the **Replay Mod**, we recommend using Forge Version `1.8-11.14.3.1450`, which can be downloaded [here](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html) +Depending on your **Replay Mod** version we recommend the following **Forge** versions: + +- Replay Mod 2.0.0 or later for Minecraft 1.11: Forge [1.11-13.19.1.2189](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.11.html) +- Replay Mod 2.0.0 or later for Minecraft 1.10.2: Forge [1.10.2-12.18.2.2099](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.10.2.html) +- Replay Mod 2.0.0 or later for Minecraft 1.9.4: Forge [1.9.4-12.17.0.1976](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.9.4.html) +- Replay Mod 2.0.0 or later for Minecraft 1.8: Forge [1.8-11.14.4.1563](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html) +- Replay Mod 1.0.8 or older for Minecraft 1.8: Forge [1.8-11.14.3.1450](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html) + Other Forge Versions might, but don't necessarily work. If you don't know how to install Forge, follow [this tutorial](https://www.youtube.com/watch?v=4i7-RystzC4). @@ -25,6 +32,10 @@ Next, download this **Batch Script** to quickly install FFmpeg: . -Other Optifine versions for Minecraft 1.8.0 might, but don't have to be compatible as well. - -> **Warning:** If you have Optifine installed, you **can not render videos** from within the Replay Mod. -This might change in future Replay Mod versions. ### Shaders Mod [shaders] -You can use the Replay Mod together with [karyonix' Shaders Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/1286604-shaders-mod-updated-by-karyonix), -but rendering videos results in **possible crashes** and **malformed videos**. -Therefore, you have to **uninstall the Shaders Mod before rendering**. +[Karyonix' Shaders Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/1286604-shaders-mod-updated-by-karyonix) +is no longer compatible with Minecraft Forge starting with 1.9.4. As such it is not compatible with the Replay Mod either. +Please use [Optifine](https://optifine.net/) instead. + +### Custom Main Menu [custom-main-menu] +The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar. +If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890235** (text: `replaymod.gui.replayviewer`) and **17890236** (text: `replaymod.gui.replaycenter`) + +Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default. +Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons. +To do so, you need to modify the Custom Main Menu configuration file. +Usually, you can find it at `.minecraft/config/CustomMainMenu/mainmenu.json` (if you're using a mod pack launcher, it normally has its own .minecraft folder). +For an explanation of this config file, have a look at Custom Main Menu's page. + +You can find a list of already modified config files [here](https://gist.github.com/Johni0702/3f3fab81dbf7ada83d045d9fe8f345aa). ## Troubleshooting [troubleshooting] If you need help installing the **Replay Mod** or **FFmpeg**, please read [this forum thread](https://www.replaymod.com/forum/thread/220) by [bela333](https://www.replaymod.com/user/bela333) - it covers most of the problems that users encountered so far. +## Settings [settings] +To access the **Replay Mod Settings** from the Main Menu click the **"Mods"** button, select the **Replay Mod** from the list and +the click the **"Config"** button. + +When in a Replay, you can either bind a hotkey to the **Replay Mod Settings** in Minecraft's Control settings +or use the hotkey GUI by clicking on the arrow button in the lower left corner. + # Recording [recording] ![](img/recording-indicator.jpg) The **Recording Indicator** which is displayed while recording @@ -88,6 +108,8 @@ You can move through block similar to Spectator Mode. Using the **Mouse Wheel**, you can modify your flying speed. Scrolling **up** causes you to fly faster, while scrolling **down** slows you down. +If you prefer the camera to be controlled similar to vanilla Creative Mode, you can change that in the **Replay Settings**. + ### Camera Roll [roll] ![](img/camera-tilted.jpg) A Screenshot taken with a **tilted camera** @@ -251,13 +273,15 @@ The **Keyframe Repository** with two **Path Presets** Using the `X` key, you can open the **Keyframe Repository**. Every Replay File has a separate Keyframe Repository which can contain several **Keyframe Presets**. -To save the Keyframes currently on your Timeline, click the **"Save current Path"** button. -You can change the Preset's name using the Text Input Field in the upper right corner. -To load a Keyframe Preset, select a Preset from the list on the left side and click the **"Load"** button. +To save the Keyframes currently on your Timeline, click the **"Save as..."** button. +You can change a Preset's name, select it from the list and click the **"Rename"** button. +To load a Keyframe Preset, select a Preset from the list and click the **"Load"** button. Using the **"Remove"** button, you can permanently delete a Keyframe Preset from the **Keyframe Repository**. ## Custom Objects [objects] +> Custom Objects are not available in the 2.0.0 versions. They are expected to return eventually. + [YouTube](_Xdpg828fbE) ### Introduction [introduction] @@ -416,12 +440,10 @@ There are **7 Encoding Presets** you can choose from: ### Advanced Settings [advanced] ![](img/rendersettings-advanced.jpg) -The **Advanced Render Settings** Screen +The **Advanced Render Settings** -By clicking the **"Advanced Settings" Button** in the lower right corner, you can access more settings. - -#### Hide Nametags [nametags] -If you don't want to see Entity's Nametags in the rendered video, simply tick the checkbox. Hidden Nametags also include Nametags of **Armor Stands** and **Mobs**, **not only Players**! +#### Render Nametags [nametags] +If you don't want to see Entity's Nametags in the rendered video, simply un-tick the checkbox. Hidden Nametags also include Nametags of **Armor Stands** and **Mobs**, **not only Players**! #### Stabilize Camera [stabilize] This setting is only applicable for **Cubic Rendering** and **Equirectangular Rendering**. @@ -443,14 +465,14 @@ Using a **Video Editing Software** like **Adobe After Effects** or **Sony Vegas* ### Command Line Settings [commandline] ![](img/rendersettings-commandline.jpg) -The **Command Line Render Settings** Screen +The **Command Line Render Settings** > **Note**: This is for advanced users only. If you do not know what your're doing, leave these settings as they are. The **Replay Mod** runs [FFmpeg](http://ffmpeg.org/) via the **Command Line** to encode videos. -You can customize both the **executed Command** and the **Command Line Arguments** in the **Command Line Settings** Tag of the **Render Settings Screen**. +You can customize both the **executed Command** and the **Command Line Arguments** in the **Command Line Settings** part of the **Render Settings Screen**. #### Custom Command [command] If you leave the left input field blank, `ffmpeg` will be used as **command**. If you haven't set your **PATH variable** to link to your FFmpeg distribution, simply enter the full path to your FFmpeg executable (e.g. `C:/ffmpeg/ffmpeg.exe` or `/usr/local/bin/ffmpeg`). @@ -532,7 +554,7 @@ By clicking on a player's **Name or Head**, you can **spectate** this player. Next to each player, there's a **checkbox** using which you can toggle the player's **visibility**. This way, you can hide certain players from the Replay. -Above the individual checkboxes, there are two checkbox buttons - one to **hide all players** and one to **show all players**. +Below the individual checkboxes, there are two checkbox buttons - one to **hide all players** and one to **show all players**. If you want to **save the player visibility settings**, check **"Remember Hidden Players" checkbox**. If checked, invisible players will stay invisible after closing the Replay (this can of course be reverted), diff --git a/docs/img/keyframe-repository.jpg b/docs/img/keyframe-repository.jpg index 1ee28ac6..fbb934a6 100644 Binary files a/docs/img/keyframe-repository.jpg and b/docs/img/keyframe-repository.jpg differ diff --git a/docs/img/keyframe-repository.png b/docs/img/keyframe-repository.png index c6621936..d5fb5025 100644 Binary files a/docs/img/keyframe-repository.png and b/docs/img/keyframe-repository.png differ diff --git a/docs/img/player-overview.jpg b/docs/img/player-overview.jpg index a04b396b..2724dc11 100644 Binary files a/docs/img/player-overview.jpg and b/docs/img/player-overview.jpg differ diff --git a/docs/img/player-overview.png b/docs/img/player-overview.png new file mode 100644 index 00000000..088e79fc Binary files /dev/null and b/docs/img/player-overview.png differ diff --git a/docs/img/rendersettings-advanced.jpg b/docs/img/rendersettings-advanced.jpg index 7e4adf8a..456720ad 100644 Binary files a/docs/img/rendersettings-advanced.jpg and b/docs/img/rendersettings-advanced.jpg differ diff --git a/docs/img/rendersettings-advanced.png b/docs/img/rendersettings-advanced.png new file mode 100644 index 00000000..dbe1c4c6 Binary files /dev/null and b/docs/img/rendersettings-advanced.png differ diff --git a/docs/img/rendersettings-commandline.jpg b/docs/img/rendersettings-commandline.jpg index b1769772..3916b68a 100644 Binary files a/docs/img/rendersettings-commandline.jpg and b/docs/img/rendersettings-commandline.jpg differ diff --git a/docs/img/rendersettings-commandline.png b/docs/img/rendersettings-commandline.png new file mode 100644 index 00000000..8fa9a126 Binary files /dev/null and b/docs/img/rendersettings-commandline.png differ diff --git a/docs/img/rendersettings-video.jpg b/docs/img/rendersettings-video.jpg index 0a800543..c002accf 100644 Binary files a/docs/img/rendersettings-video.jpg and b/docs/img/rendersettings-video.jpg differ diff --git a/docs/img/rendersettings-video.png b/docs/img/rendersettings-video.png new file mode 100644 index 00000000..a23ea936 Binary files /dev/null and b/docs/img/rendersettings-video.png differ diff --git a/jGui b/jGui index 5c0ca7d7..33c65436 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 5c0ca7d7d800c52898eccb6b854c7dec17a34338 +Subproject commit 33c65436f094417e9a5174db2ad13bbfa271be14 diff --git a/src/main/java/com/replaymod/compat/ReplayModCompat.java b/src/main/java/com/replaymod/compat/ReplayModCompat.java index 9b681d50..f8f7f831 100644 --- a/src/main/java/com/replaymod/compat/ReplayModCompat.java +++ b/src/main/java/com/replaymod/compat/ReplayModCompat.java @@ -2,19 +2,18 @@ package com.replaymod.compat; import com.replaymod.compat.optifine.DisableFastRender; import com.replaymod.compat.shaders.ShaderBeginRender; -import com.replaymod.core.ReplayMod; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.eventhandler.EventBus; -@Mod(modid = ReplayModCompat.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModCompat.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModCompat { public static final String MOD_ID = "replaymod-compat"; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; - @Mod.EventHandler public void init(FMLInitializationEvent event) { EventBus bus = MinecraftForge.EVENT_BUS; diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java new file mode 100644 index 00000000..b81d176f --- /dev/null +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java @@ -0,0 +1,22 @@ +package com.replaymod.compat.shaders.mixin; + +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraftforge.client.ForgeHooksClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "shadersmod/client/ShadersRender", remap = false) +public abstract class MixinShadersRender { + + @Inject(method = "renderHand0", at = @At("HEAD"), cancellable = true) + private static void replayModCompat_disableRenderHand0(EntityRenderer er, float partialTicks, int renderPass, CallbackInfo ci) { + if (ForgeHooksClient.renderFirstPersonHand(er.mc.renderGlobal, partialTicks, renderPass)) { + ci.cancel(); + } + } + +} diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index d028b85b..2329ceaa 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -34,6 +34,8 @@ import java.util.List; @Mod(modid = ReplayMod.MOD_ID, useMetadata = true, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", guiFactory = "com.replaymod.core.gui.GuiFactory") public class ReplayMod { diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index e9a618ea..9a02c2dd 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -1,17 +1,41 @@ package com.replaymod.core.utils; -import net.minecraft.client.Minecraft; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.AbstractGuiScrollable; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiScrollable; +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.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.popup.GuiInfoPopup; +import de.johni0702.minecraft.gui.utils.Colors; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.network.NetworkPlayerInfo; import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.crash.CrashReport; import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; import org.lwjgl.util.Dimension; import org.lwjgl.util.ReadableDimension; +import javax.annotation.Nullable; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; import java.util.UUID; +import static net.minecraft.client.Minecraft.getMinecraft; + public class Utils { public static final BufferedImage DEFAULT_THUMBNAIL; @@ -63,7 +87,7 @@ public class Utils { } public static ResourceLocation getResourceLocationForPlayerUUID(UUID uuid) { - NetworkPlayerInfo info = Minecraft.getMinecraft().getConnection().getPlayerInfo(uuid); + NetworkPlayerInfo info = getMinecraft().getConnection().getPlayerInfo(uuid); ResourceLocation skinLocation; if (info != null && info.hasLocationSkin()) { skinLocation = info.getLocationSkin(); @@ -76,4 +100,80 @@ public class Utils { public static boolean isCtrlDown() { return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); } + + public static GuiInfoPopup error(Logger logger, GuiContainer container, CrashReport crashReport, Runnable onClose) { + // Convert crash report to string + String crashReportStr = crashReport.getCompleteReport(); + + // Log via logger + logger.error(crashReportStr); + + // Try to save the crash report + if (crashReport.getFile() == null) { + try { + File folder = new File(getMinecraft().mcDataDir, "crash-reports"); + File file = new File(folder, "crash-" + (new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss")).format(new Date()) + "-client.txt"); + logger.debug("Saving crash report to file: {}", file); + crashReport.saveToFile(file); + } catch (Throwable t) { + logger.error("Saving crash report file:", t); + } + } else { + logger.debug("Not saving crash report as file already exists: {}", crashReport.getFile()); + } + + logger.trace("Opening crash report popup GUI"); + GuiCrashReportPopup popup = new GuiCrashReportPopup(container, crashReportStr); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + logger.trace("Crash report popup closed"); + if (onClose != null) { + onClose.run(); + } + } + + @Override + public void onFailure(Throwable t) { + logger.error("During error popup:", t); + } + }); + return popup; + } + + private static class GuiCrashReportPopup extends GuiInfoPopup { + private final GuiScrollable scrollable; + + public GuiCrashReportPopup(GuiContainer container, String crashReport) { + super(container); + setBackgroundColor(Colors.DARK_TRANSPARENT); + + // Add crash report to scrollable info + getInfo().addElements(new VerticalLayout.Data(0.5), + new GuiLabel().setColor(Colors.BLACK).setI18nText("replaymod.gui.unknownerror"), + scrollable = new GuiScrollable().setScrollDirection(AbstractGuiScrollable.Direction.VERTICAL) + .setLayout(new VerticalLayout().setSpacing(2)) + .addElements(null,Arrays.stream(crashReport.replace("\t", " ").split("\n")).map( + l -> new GuiLabel().setText(l).setColor(Colors.BLACK)).toArray(GuiElement[]::new))); + + // Replace close button with panel containing close and copy buttons + GuiButton copyToClipboardButton = new GuiButton().setI18nLabel("chat.copy").onClick(() -> + GuiScreen.setClipboardString(crashReport)).setSize(150, 20); + GuiButton closeButton = getCloseButton(); + popup.removeElement(closeButton); + popup.addElements(new VerticalLayout.Data(1), + new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).setSize(305, 20) + .addElements(null, copyToClipboardButton, closeButton)); + + // And open it + open(); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + // Re-size the scrollable containing the crash report to 3/4 of the screen + scrollable.setSize(size.getWidth() * 3 / 4, size.getHeight() * 3 / 4); + super.draw(renderer, size, renderInfo); + } + } } diff --git a/src/main/java/com/replaymod/editor/ReplayModEditor.java b/src/main/java/com/replaymod/editor/ReplayModEditor.java new file mode 100644 index 00000000..66a8aa93 --- /dev/null +++ b/src/main/java/com/replaymod/editor/ReplayModEditor.java @@ -0,0 +1,41 @@ +package com.replaymod.editor; + +import com.replaymod.core.ReplayMod; +import com.replaymod.editor.handler.GuiHandler; +import com.replaymod.online.Setting; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.logging.log4j.Logger; + +@Mod(modid = ReplayModEditor.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) +public class ReplayModEditor { + public static final String MOD_ID = "replaymod-editor"; + + @Mod.Instance(MOD_ID) + public static ReplayModEditor instance; + + private ReplayMod core; + + public static Logger LOGGER; + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + ReplayModEditor.LOGGER = event.getModLog(); + core = ReplayMod.instance; + + core.getSettingsRegistry().register(Setting.class); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + new GuiHandler(this).register(); + } + + public ReplayMod getCore() { + return core; + } +} diff --git a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java new file mode 100644 index 00000000..98ef0760 --- /dev/null +++ b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java @@ -0,0 +1,360 @@ +package com.replaymod.editor.gui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.gson.JsonObject; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; +import com.replaymod.replaystudio.PacketData; +import com.replaymod.replaystudio.Studio; +import com.replaymod.replaystudio.filter.StreamFilter; +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.stream.PacketStream; +import com.replaymod.replaystudio.studio.ReplayStudio; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.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.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.GuiInfoPopup; +import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; +import de.johni0702.minecraft.gui.utils.Colors; +import net.minecraft.crash.CrashReport; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.util.ReadableDimension; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.replaymod.editor.ReplayModEditor.LOGGER; + +public class GuiReplayEditor extends GuiScreen { + private final ReplayMod mod; + + private volatile ProcessingStage processingStage; + private volatile double progress; + private GuiProcessingProgressPopup popup; + + public GuiButton currentTabButton; + public GuiPanel currentTabPanel; + + public final GuiPanel tabButtons = new GuiPanel(this).setLayout(new GridLayout().setSpacingX(5)); + public final List tabPanels = new ArrayList<>(); + + public final GuiButton saveButton = new GuiButton().setI18nLabel("replaymod.gui.save").setSize(100, 20); + public final GuiButton backButton = new GuiButton().setI18nLabel("replaymod.gui.back").setSize(100, 20); + + public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(5)) + .addElements(null, saveButton, backButton); + + public GuiReplayEditor(GuiScreen parent, ReplayMod mod) { + this.mod = mod; + backButton.onClick(parent::display); + + setTitle(new GuiLabel().setI18nText("replaymod.gui.replayeditor")); + + makeTab("trim", () -> new GuiTrimPanel(this)); + makeTab("connect", GuiPanel::new).setDisabled(); // Not yet implemented + makeTab("modify", GuiPanel::new).setDisabled(); // Not yet implemented + + setLayout(new CustomLayout() { + @Override + protected void layout(GuiScreen container, int width, int height) { + // Move all inactive panels aside + tabPanels.forEach(e -> pos(e, Integer.MIN_VALUE, Integer.MIN_VALUE)); + + pos(buttonPanel, width - 10 - width(buttonPanel), height - 10 - height(buttonPanel)); + + if (currentTabPanel != null) { + pos(currentTabPanel, 10, 50); + size(currentTabPanel, width - 20, y(buttonPanel) - 10 - y(currentTabPanel)); + } + + pos(tabButtons, 10, 20); + size(tabButtons, width - 20, 20); + } + }); + } + + public GuiButton makeTab(String name, Supplier panel) { + GuiButton button = new GuiButton().setI18nLabel("replaymod.gui.editor." + name + ".title") + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.editor." + name + ".description")) + .setSize(Integer.MAX_VALUE, Integer.MAX_VALUE); + button.onClick(() -> { + // Remove previous tab + if (currentTabButton != null) { + currentTabButton.setEnabled(); + removeElement(currentTabPanel); + } + + // Activate new tab + currentTabButton = button.setDisabled(); + addElements(null, currentTabPanel = panel.get()); + }); + // Add new tab button to tabs panel + tabButtons.addElements(null, button); + ((GridLayout) tabButtons.getLayout()).setColumns(tabButtons.getChildren().size()); + return button; + } + + public void save(File inputFile, PacketStream.FilterInfo...filters) { + save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> { + Studio studio = new ReplayStudio(); + try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); + ReplayOutputStream out = outputReplay.writePacketData()) { + // The input replay file MUST be closed before saving the output file + try (ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); + ReplayInputStream in = inputReplay.getPacketData()) { + ReplayMetaData metaData = inputReplay.getMetaData(); + PacketStream stream = studio.createReplayStream(in, true); + + stream.addFilter(new ProgressFilter(metaData.getDuration())); + for (PacketStream.FilterInfo info : filters) { + stream.addFilter(info.getFilter(), info.getFrom(), info.getTo()); + LOGGER.debug("Added filter {}", info); + } + + LOGGER.info("Built pipeline: {}", stream); + stream.start(); + + long lastTimestamp = 0; + + PacketData data; + while ((data = stream.next()) != null) { + out.write(data); + lastTimestamp = data.getTime(); + } + + for (PacketData d : stream.end()) { + out.write(d); + lastTimestamp = d.getTime(); + } + + updateProgress(ProcessingStage.FINALIZING, 0); + + // Update duration of new replay + metaData.setDuration((int) lastTimestamp); + outputReplay.writeMetaData(metaData); + } + out.close(); + outputReplay.save(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public void save(String sourceName, Consumer runnable) { + GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(sourceName); + GuiYesNoPopup popup = GuiYesNoPopup.open(this, + nameField + ).setYesI18nLabel("replaymod.gui.editor.savemode.override").setNoI18nLabel("replaymod.gui.cancel"); + nameField.onEnter(() -> { + if (popup.getYesButton().isEnabled()) { + popup.getYesButton().onClick(); + } + }).onTextChanged(obj -> { + popup.getYesButton().setEnabled(!nameField.getText().isEmpty()); + popup.setYesI18nLabel("replaymod.gui.editor.savemode." + + (nameField.getText().equals(sourceName) ? "override" : "newfile")); + }); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Boolean result) { + if (result == Boolean.TRUE) { + // Sanitize their input + String name = nameField.getText().trim().replace("[^a-zA-Z0-9\\.\\- ]", "_"); + // This file is what they want + File replayFolder; + try { + replayFolder = mod.getReplayFolder(); + } catch (IOException e) { + Utils.error(LOGGER, GuiReplayEditor.this, CrashReport.makeCrashReport(e, "Getting replay folder"), null); + return; + } + File targetFile = new File(replayFolder, name + ".mcpr"); + if (targetFile.exists()) { + LOGGER.trace("Selected file already exists, asking for confirmation"); + Futures.addCallback(GuiYesNoPopup.open(GuiReplayEditor.this, + new GuiLabel().setColor(Colors.BLACK) + .setI18nText("replaymod.gui.replaymodified.warning1", name), + new GuiLabel().setColor(Colors.BLACK) + .setI18nText("replaymod.gui.replaymodified.warning2")) + .setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no") + .getFuture(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Boolean result) { + if (result == Boolean.TRUE) { + LOGGER.trace("Saving output in {}", targetFile); + runEdit(() -> runnable.accept(targetFile)); + } else { + LOGGER.trace("Not overwriting file {}, reopening naming dialog", targetFile); + save(sourceName, runnable); + } + } + + @Override + public void onFailure(Throwable t) { + LOGGER.error("During save file confirmation dialog:", t); + } + }); + } else { + LOGGER.trace("Saving output in {}", targetFile); + runEdit(() -> runnable.accept(targetFile)); + } + } else { + LOGGER.trace("Saving cancelled"); + } + } + + @Override + public void onFailure(Throwable t) { + LOGGER.error("During save file dialog:", t); + } + }); + } + + public void runEdit(Runnable runnable) { + popup = new GuiProcessingProgressPopup(this); + processingStage = ProcessingStage.INITIALIZING; + progress = 0; + new Thread(() -> { + LOGGER.info("Starting editing of replay"); + try { + runnable.run(); + } catch (Throwable t) { + CrashReport crashReport = CrashReport.makeCrashReport(t, "Editing replay file"); + mod.runLater(() -> Utils.error(LOGGER, this, crashReport, popup::close)); + return; + } + LOGGER.info("Successfully finished editing"); + mod.runLater(() -> { + popup.close(); + GuiInfoPopup.open(this, "replaymod.gui.editor.progress.status.finished"); + }); + }).start(); + } + + public void updateProgress(ProcessingStage stage, double progress) { + if (this.processingStage != stage) { + LOGGER.trace("Changing state from {} to {}", this.processingStage, stage); + } + this.processingStage = stage; + this.progress = progress; + } + + public ReplayMod getMod() { + return mod; + } + + public enum ProcessingStage { + INITIALIZING("initializing", 0.01), + WORKING("writing.raw", 0.89), + FINALIZING("writing.final", 0.1), + DONE("finished", 0); + + private final String i18nKey; + private final double progressFraction; + + ProcessingStage(String i18nKey, double progressFraction) { + this.i18nKey = "replaymod.gui.editor.progress.status." + i18nKey; + this.progressFraction = progressFraction; + } + + public double getOverallProgress(double progress) { + progress *= progressFraction; + for (ProcessingStage processingStage : values()) { + if (processingStage == this) break; + progress += processingStage.progressFraction; + } + return progress; + } + } + + private class ProgressFilter implements StreamFilter { + private final long total; + + public ProgressFilter(long total) { + this.total = total; + } + + @Override + public String getName() { + return "progress"; + } + + @Override + public void init(Studio studio, JsonObject config) { + + } + + @Override + public void onStart(PacketStream stream) { + updateProgress(ProcessingStage.WORKING, 0); + } + + @Override + public boolean onPacket(PacketStream stream, PacketData data) { + updateProgress(ProcessingStage.WORKING, (double) data.getTime() / total); + return true; + } + + @Override + public void onEnd(PacketStream stream, long timestamp) {} + } + + private class GuiProcessingProgressPopup extends AbstractGuiPopup { + private final GuiProgressBar progressBar = new GuiProgressBar().setSize(300, 20); + + { + popup.setLayout(new VerticalLayout().setSpacing(10)); + popup.addElements(new VerticalLayout.Data(0.5), + new GuiLabel().setI18nText("replaymod.gui.editor.progress.title").setColor(Colors.BLACK), + new GuiLabel().setI18nText("replaymod.gui.editor.progress.pleasewait").setColor(Colors.BLACK), + progressBar); + } + + public GuiProcessingProgressPopup(GuiContainer container) { + super(container); + open(); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + // Update progress bar + progressBar.setI18nLabel(processingStage.i18nKey); + progressBar.setProgress((float) processingStage.getOverallProgress(progress)); + + super.draw(renderer, size, renderInfo); + } + + @Override + public void close() { + super.close(); + } + + @Override + protected GuiProcessingProgressPopup getThis() { + return this; + } + } +} diff --git a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java new file mode 100644 index 00000000..ae5fd79e --- /dev/null +++ b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java @@ -0,0 +1,198 @@ +package com.replaymod.editor.gui; + +import com.google.gson.JsonObject; +import com.replaymod.core.utils.Utils; +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.filter.ChangeTimestampFilter; +import com.replaymod.replaystudio.filter.RemoveFilter; +import com.replaymod.replaystudio.filter.SquashFilter; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; +import com.replaymod.replaystudio.replay.ZipReplayFile; +import com.replaymod.replaystudio.stream.PacketStream; +import com.replaymod.replaystudio.studio.ReplayStudio; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiNumberField; +import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; +import de.johni0702.minecraft.gui.layout.GridLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import net.minecraft.client.resources.I18n; +import net.minecraft.crash.CrashReport; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOCase; +import org.apache.commons.io.filefilter.SuffixFileFilter; +import org.lwjgl.util.Dimension; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import static com.replaymod.editor.ReplayModEditor.LOGGER; +import static java.util.Optional.ofNullable; + +public class GuiTrimPanel extends GuiPanel { + // Special value indicating no replay files were found + private static final File NO_REPLAY = new File("."); + // Special value for the initial "Select Marker Keyframe" entry + private static final Marker NO_MARKER = new Marker(); static {NO_MARKER.setTime(-1);} + + private final GuiReplayEditor gui; + + public final GuiDropdownMenu inputReplays = new GuiDropdownMenu(this) + .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) + .setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName())); + + public final GuiNumberField startHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField startMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField startSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField startMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiDropdownMenu startMarker = new GuiDropdownMenu<>(); + + public final GuiNumberField endHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField endMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField endSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); + public final GuiNumberField endMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiDropdownMenu endMarker = new GuiDropdownMenu<>(); + + public final GuiPanel timePanel = new GuiPanel(this) + .setLayout(new GridLayout().setCellsEqualSize(false).setSpacingX(20).setSpacingY(5).setColumns(3)) + .addElements(new GridLayout.Data(0, 0.5), + new GuiLabel().setI18nText("replaymod.gui.start"), + new GuiPanel().setLayout(new HorizontalLayout().setSpacing(2)).addElements( + new HorizontalLayout.Data(0.5), + startHour, new GuiLabel().setI18nText("replaymod.gui.hours"), + startMin, new GuiLabel().setI18nText("replaymod.gui.minutes"), + startSec, new GuiLabel().setI18nText("replaymod.gui.seconds"), + startMilli, new GuiLabel().setI18nText("replaymod.gui.milliseconds") + ), startMarker, + new GuiLabel().setI18nText("replaymod.gui.end"), + new GuiPanel().setLayout(new HorizontalLayout().setSpacing(2)).addElements( + new HorizontalLayout.Data(0.5), + endHour, new GuiLabel().setI18nText("replaymod.gui.hours"), + endMin, new GuiLabel().setI18nText("replaymod.gui.minutes"), + endSec, new GuiLabel().setI18nText("replaymod.gui.seconds"), + endMilli, new GuiLabel().setI18nText("replaymod.gui.milliseconds") + ), endMarker); + + public GuiTrimPanel(GuiReplayEditor gui) { + this.gui = gui; + + setLayout(new VerticalLayout().setSpacing(10)); + + Function toString = m -> { + if (m == NO_MARKER) { + return I18n.format("replaymod.gui.editor.trim.marker"); + } else { + return ofNullable(m.getName()).orElse(I18n.format("replaymod.gui.ingame.unnamedmarker")) + + " (" + Utils.convertSecondsToShortString(m.getTime() / 1000) + ")"; + } + }; + startMarker.setToString(toString).setMinSize(new Dimension(100, 20)).onSelection(i -> + onSelectedMarkerChanged(startMarker, startHour, startMin, startSec, startMilli)); + endMarker.setToString(toString).setMinSize(new Dimension(100, 20)).onSelection(i -> + onSelectedMarkerChanged(endMarker, endHour, endMin, endSec, endMilli)); + + File[] files = null; + try { + File folder = gui.getMod().getReplayFolder(); + files = folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE)); + } catch (IOException e) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Listing available replays"), gui.backButton::onClick); + } + if (files == null) { + LOGGER.warn("Replay file list is null, has the replay folder been deleted?"); + files = new File[0]; + } else { + LOGGER.debug("Found {} replays in the replay folder", files.length); + } + if (files.length == 0) { + inputReplays.setValues(NO_REPLAY); + } else { + inputReplays.setValues(files); + } + + updateSelectedReplay(); + updateReadyState(); + + gui.saveButton.onClick(() -> { + // Read start and end times + int start = ((startHour.getInteger() * 60 + startMin.getInteger()) * 60 + startSec.getInteger()) * 1000 + startMilli.getInteger(); + int end = ((endHour.getInteger() * 60 + endMin.getInteger()) * 60 + endSec.getInteger()) * 1000 + endMilli.getInteger(); + // Configure offset of change timestamp filter + ChangeTimestampFilter ctf = new ChangeTimestampFilter(); + JsonObject config = new JsonObject(); + config.addProperty("offset", -start); + ctf.init(null, config); + // Pass filters to save dialog + gui.save(inputReplays.getSelectedValue(), + new PacketStream.FilterInfo(new SquashFilter(), -1, start), + new PacketStream.FilterInfo(ctf, start, end), + new PacketStream.FilterInfo(new RemoveFilter(), end, -1) + ); + }); + } + + private void onSelectedMarkerChanged(GuiDropdownMenu dropdown, GuiNumberField hour, GuiNumberField min, + GuiNumberField sec, GuiNumberField milli) { + Marker marker = dropdown.getSelectedValue(); + if (marker != NO_MARKER) { + int time = marker.getTime(); + setTime(time, hour, min, sec, milli); + } + } + + private void setTime(int time, GuiNumberField hour, GuiNumberField min, + GuiNumberField sec, GuiNumberField milli) { + milli.setValue(time % 1000); time /= 1000; + sec.setValue(time % 60); time /= 60; + min.setValue(time % 60); time /= 60; + hour.setValue(time); + } + + private void updateSelectedReplay() { + File file = inputReplays.getSelectedValue(); + // Load markers and meta data from replay file + ReplayMetaData metaData; + Set markers; + if (file == NO_REPLAY) { + metaData = new ReplayMetaData(); + markers = Collections.emptySet(); + } else { + try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { + metaData = replayFile.getMetaData(); + markers = replayFile.getMarkers().or(Collections.emptySet()); + LOGGER.debug("Loaded {} markers from replay {}", markers.size(), file); + } catch (Exception e) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Reading markers from replay"), null); + metaData = new ReplayMetaData(); + markers = Collections.emptySet(); + } + } + + // Sort and add initial "Select Event Marker" element + List markerList = new ArrayList<>(markers); + markerList.sort(Comparator.comparing(Marker::getTime)); + markerList.add(0, NO_MARKER); + + // Set marker dropdown values + Marker[] markerArray = markerList.stream().toArray(Marker[]::new); + startMarker.setValues(markerArray).setSelected(0); + endMarker.setValues(markerArray).setSelected(0); + + // Set start and end time values + setTime(0, startHour, startMin, startSec, startMilli); + setTime(metaData.getDuration(), endHour, endMin, endSec, endMilli); + } + + private void updateReadyState() { + gui.saveButton.setEnabled(inputReplays.getSelectedValue() != NO_REPLAY); + } +} diff --git a/src/main/java/com/replaymod/editor/handler/GuiHandler.java b/src/main/java/com/replaymod/editor/handler/GuiHandler.java new file mode 100644 index 00000000..adae091a --- /dev/null +++ b/src/main/java/com/replaymod/editor/handler/GuiHandler.java @@ -0,0 +1,48 @@ +package com.replaymod.editor.handler; + +import com.replaymod.editor.ReplayModEditor; +import com.replaymod.editor.gui.GuiReplayEditor; +import de.johni0702.minecraft.gui.container.GuiScreen; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.resources.I18n; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +public class GuiHandler { + private static final int BUTTON_REPLAY_EDITOR = 17890237; + + private final ReplayModEditor mod; + + public GuiHandler(ReplayModEditor mod) { + this.mod = mod; + } + + public void register() { + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { + if (!(event.getGui() instanceof GuiMainMenu)) { + return; + } + + GuiButton button = new GuiButton(BUTTON_REPLAY_EDITOR, event.getGui().width / 2 + 2, + event.getGui().height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayeditor")); + button.width = button.width / 2 - 2; + event.getButtonList().add(button); + } + + @SubscribeEvent + public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) { + if(!event.getButton().enabled) return; + + if (event.getGui() instanceof GuiMainMenu) { + if (event.getButton().id == BUTTON_REPLAY_EDITOR) { + new GuiReplayEditor(GuiScreen.wrap(event.getGui()), mod.getCore()).display(); + } + } + } +} diff --git a/src/main/java/com/replaymod/extras/FullBrightness.java b/src/main/java/com/replaymod/extras/FullBrightness.java index 5ee2d52f..9564df54 100644 --- a/src/main/java/com/replaymod/extras/FullBrightness.java +++ b/src/main/java/com/replaymod/extras/FullBrightness.java @@ -10,14 +10,12 @@ import de.johni0702.minecraft.gui.element.IGuiImage; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import net.minecraft.client.settings.GameSettings; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import org.lwjgl.input.Keyboard; public class FullBrightness implements Extra { - @Mod.Instance(ReplayModReplay.MOD_ID) - private static ReplayModReplay module; + private ReplayModReplay module; private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 13).setSize(19, 13); @@ -27,6 +25,7 @@ public class FullBrightness implements Extra { @Override public void register(final ReplayMod mod) throws Exception { + this.module = ReplayModReplay.instance; this.gameSettings = mod.getMinecraft().gameSettings; mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.lighting", Keyboard.KEY_Z, new Runnable() { diff --git a/src/main/java/com/replaymod/extras/LocalizationExtra.java b/src/main/java/com/replaymod/extras/LocalizationExtra.java index 7ddb6dff..7b7001c5 100644 --- a/src/main/java/com/replaymod/extras/LocalizationExtra.java +++ b/src/main/java/com/replaymod/extras/LocalizationExtra.java @@ -12,7 +12,6 @@ import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.data.IMetadataSection; import net.minecraft.client.resources.data.MetadataSerializer; import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.Mod; import org.apache.commons.lang3.StringEscapeUtils; import java.awt.image.BufferedImage; @@ -26,11 +25,12 @@ import java.util.Map; import java.util.Set; public class LocalizationExtra implements Extra { - @Mod.Instance(ReplayModOnline.MOD_ID) - private static ReplayModOnline module; + private ReplayModOnline module; @Override public void register(ReplayMod mod) throws Exception { + this.module = ReplayModOnline.instance; + final Minecraft mc = mod.getMinecraft(); Thread localizedResourcePackLoader = new Thread(new Runnable() { @Override diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java index 750111a6..e82eff09 100644 --- a/src/main/java/com/replaymod/extras/ReplayModExtras.java +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -12,16 +12,16 @@ import org.apache.logging.log4j.Logger; import java.util.Arrays; import java.util.List; -@Mod(modid = ReplayModExtras.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModExtras.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModExtras { public static final String MOD_ID = "replaymod-extras"; @Mod.Instance(MOD_ID) public static ReplayModExtras instance; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; - private static final List> builtin = Arrays.asList( PlayerOverview.class, UriSchemeExtra.class, @@ -44,7 +44,7 @@ public class ReplayModExtras { for (Class cls : builtin) { try { Extra extra = cls.newInstance(); - extra.register(core); + extra.register(ReplayMod.instance); } catch (Throwable t) { logger.warn("Failed to load extra " + cls.getName() + ": ", t); } diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java index 9bf4f3ee..ff32dd91 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java @@ -13,7 +13,6 @@ import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.entity.player.EntityPlayer; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.lwjgl.input.Keyboard; @@ -21,14 +20,15 @@ import java.io.IOException; import java.util.*; public class PlayerOverview implements Extra { - @Mod.Instance(ReplayModReplay.MOD_ID) - private static ReplayModReplay module; + private ReplayModReplay module; private final Set hiddenPlayers = new HashSet<>(); private boolean savingEnabled; @Override public void register(final ReplayMod mod) throws Exception { + this.module = ReplayModReplay.instance; + mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.playeroverview", Keyboard.KEY_B, new Runnable() { @Override public void run() { diff --git a/src/main/java/com/replaymod/extras/urischeme/UriSchemeExtra.java b/src/main/java/com/replaymod/extras/urischeme/UriSchemeExtra.java index e0ba8cef..07526bbb 100644 --- a/src/main/java/com/replaymod/extras/urischeme/UriSchemeExtra.java +++ b/src/main/java/com/replaymod/extras/urischeme/UriSchemeExtra.java @@ -4,7 +4,6 @@ import com.replaymod.core.ReplayMod; import com.replaymod.extras.Extra; import com.replaymod.online.ReplayModOnline; import de.johni0702.minecraft.gui.container.GuiScreen; -import net.minecraftforge.fml.common.Mod; import org.apache.commons.io.IOUtils; import java.io.IOException; @@ -13,14 +12,14 @@ import java.net.ServerSocket; import java.net.Socket; public class UriSchemeExtra implements Extra { - @Mod.Instance(ReplayModOnline.MOD_ID) - private static ReplayModOnline module; + private ReplayModOnline module; private ReplayMod mod; @Override public void register(final ReplayMod mod) throws Exception { this.mod = mod; + this.module = ReplayModOnline.instance; UriScheme uriScheme = UriScheme.create(); if (uriScheme == null) { diff --git a/src/main/java/com/replaymod/online/ReplayModOnline.java b/src/main/java/com/replaymod/online/ReplayModOnline.java index 70071ac7..6e915b9c 100644 --- a/src/main/java/com/replaymod/online/ReplayModOnline.java +++ b/src/main/java/com/replaymod/online/ReplayModOnline.java @@ -26,18 +26,19 @@ import java.io.IOException; import static net.minecraft.client.Minecraft.getMinecraft; -@Mod(modid = ReplayModOnline.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModOnline.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModOnline { public static final String MOD_ID = "replaymod-online"; @Mod.Instance(MOD_ID) public static ReplayModOnline instance; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; + private ReplayMod core; - @Mod.Instance(ReplayModReplay.MOD_ID) - private static ReplayModReplay replayModule; + private ReplayModReplay replayModule; private Logger logger; @@ -53,6 +54,8 @@ public class ReplayModOnline { @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { logger = event.getModLog(); + core = ReplayMod.instance; + replayModule = ReplayModReplay.instance; core.getSettingsRegistry().register(Setting.class); @@ -64,8 +67,10 @@ public class ReplayModOnline { @Mod.EventHandler public void init(FMLInitializationEvent event) { - if (!getDownloadsFolder().mkdirs()) { - logger.warn("Failed to create downloads folder: " + getDownloadsFolder()); + if (!getDownloadsFolder().exists()){ + if (!getDownloadsFolder().mkdirs()) { + logger.warn("Failed to create downloads folder: " + getDownloadsFolder()); + } } new GuiHandler(this).register(); diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java index f35123b1..70775777 100644 --- a/src/main/java/com/replaymod/recording/ReplayModRecording.java +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -14,15 +14,17 @@ import net.minecraftforge.fml.common.network.NetworkRegistry; import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; -@Mod(modid = ReplayModRecording.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModRecording.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModRecording { public static final String MOD_ID = "replaymod-recording"; @Mod.Instance(MOD_ID) public static ReplayModRecording instance; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; + private ReplayMod core; private Logger logger; @@ -31,6 +33,7 @@ public class ReplayModRecording { @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { logger = event.getModLog(); + core = ReplayMod.instance; core.getSettingsRegistry().register(Setting.class); diff --git a/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java b/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java new file mode 100644 index 00000000..79553b3b --- /dev/null +++ b/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java @@ -0,0 +1,25 @@ +package com.replaymod.recording.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import net.minecraftforge.fml.common.network.handshake.FMLHandshakeMessage; + +/** + * Filters out all handshake packets that were sent for recording but must + * not actually be handled. + * This handler is only present when connected to the integrated server as + * otherwise all packets must be handled. + * + * When in single player, the game state packets must never be handled + * otherwise wired bugs related to semi-singletons can occur. + * See https://bugs.replaymod.com/show_bug.cgi?id=44 + */ +public class FMLHandshakeFilter extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, FMLHandshakeMessage msg) throws Exception { + if (!(msg instanceof FMLHandshakeMessage.RegistryData)) { + // Pass on everything but RegistryData messages + ctx.fireChannelRead(msg); + } + } +} diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java b/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java index 9ef0b6da..c6e2e4d5 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java @@ -1,7 +1,11 @@ package com.replaymod.recording.mixin; +import com.replaymod.recording.handler.FMLHandshakeFilter; +import io.netty.channel.ChannelPipeline; import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraftforge.fml.common.network.handshake.FMLHandshakeCodec; import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; +import net.minecraftforge.fml.relauncher.Side; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -11,17 +15,32 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(value = NetworkDispatcher.class, remap = false) public abstract class MixinNetworkDispatcher { + @Shadow + private Side side; + @Shadow private EmbeddedChannel handshakeChannel; /** - * Always sets fml:isLocal to false. + * Always sets fml:isLocal to false on the server side. * This effectively removes the difference in the FML handshake between SP and MP * and forces the block/item ids, etc. to always be send. - * This might have undesired side effects but at least it works at all. + * Injects a {@link FMLHandshakeFilter} on the client side to filter out + * those extra, unexpected packets. */ @Inject(method = "insertIntoChannel", at=@At("HEAD")) - public void replayModRecording_forceIsLocalToFalse(CallbackInfo cb) { - handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).set(false); + public void replayModRecording_setupForLocalRecording(CallbackInfo cb) { + // If we're in multiplayer, everything is fine as is + if (!handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).get()) return; + + if (side == Side.SERVER) { + // On the server side, force all packets to be sent + handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).set(false); + } else { + // On the client side, discard additional packets + ChannelPipeline pipeline = handshakeChannel.pipeline(); + pipeline.addAfter(pipeline.context(FMLHandshakeCodec.class).name(), + "replaymod_filter", new FMLHandshakeFilter()); + } } } diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index 6ff08de6..37977dda 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -1,6 +1,8 @@ package com.replaymod.render; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import net.minecraft.client.resources.I18n; import org.lwjgl.util.ReadableColor; @@ -45,7 +47,7 @@ public class RenderSettings { } public String getValue() { - return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - " + preset; + return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - %FILTERS%" + preset; } public String getFileExtension() { @@ -64,6 +66,19 @@ public class RenderSettings { } } + @AllArgsConstructor + public enum AntiAliasing { + NONE(1), X2(2), X4(4), X8(8); + + @Getter + private final int factor; + + @Override + public String toString() { + return I18n.format("replaymod.gui.rendersettings.antialiasing." + name().toLowerCase()); + } + } + private final RenderMethod renderMethod; private final EncodingPreset encodingPreset; private final int videoWidth; @@ -78,9 +93,48 @@ public class RenderSettings { private final boolean stabilizeRoll; private final ReadableColor chromaKeyingColor; private final boolean inject360Metadata; + private final AntiAliasing antiAliasing; private final String exportCommand; private final String exportArguments; private final boolean highPerformance; + + /** + * @return the width of the output video during rendering, including the upscale for Anti-Aliasing. + */ + public int getVideoWidth() { + return videoWidth * antiAliasing.getFactor(); + } + + /** + * @return the height of the output video during rendering, including the upscale for Anti-Aliasing. + */ + public int getVideoHeight() { + return videoHeight * antiAliasing.getFactor(); + } + + /** + * @return the actual width of the output video. + */ + public int getTargetVideoWidth() { + return videoWidth; + } + + /** + * @return the actual height of the output video. + */ + public int getTargetVideoHeight() { + return videoHeight; + } + + public String getVideoFilters() { + if (antiAliasing == AntiAliasing.NONE) { + return ""; + } else { + double factor = 1.0 / antiAliasing.getFactor(); + return String.format("-vf scale=iw*%1$s:ih*%1$s ", factor); + } + } + } diff --git a/src/main/java/com/replaymod/render/ReplayModRender.java b/src/main/java/com/replaymod/render/ReplayModRender.java index 6ddb36ae..a1915bc1 100644 --- a/src/main/java/com/replaymod/render/ReplayModRender.java +++ b/src/main/java/com/replaymod/render/ReplayModRender.java @@ -1,30 +1,39 @@ package com.replaymod.render; import com.replaymod.core.ReplayMod; +import com.replaymod.render.utils.RenderJob; +import com.replaymod.replay.events.ReplayCloseEvent; import net.minecraft.crash.CrashReport; import net.minecraft.util.ReportedException; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; -@Mod(modid = ReplayModRender.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModRender.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModRender { public static final String MOD_ID = "replaymod-render"; @Mod.Instance(MOD_ID) public static ReplayModRender instance; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; + private ReplayMod core; - private Logger logger; + public static Logger LOGGER; private Configuration configuration; + private final List renderQueue = new ArrayList<>(); public ReplayMod getCore() { return core; @@ -32,12 +41,20 @@ public class ReplayModRender { @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { - logger = event.getModLog(); + LOGGER = event.getModLog(); + core = ReplayMod.instance; configuration = new Configuration(event.getSuggestedConfigurationFile()); + MinecraftForge.EVENT_BUS.register(this); + core.getSettingsRegistry().register(Setting.class); } + @SubscribeEvent + public void onReplayClose(ReplayCloseEvent.Post event) { + renderQueue.clear(); + } + public File getVideoFolder() { String path = core.getSettingsRegistry().get(Setting.RENDER_PATH); File folder = new File(path.startsWith("./") ? core.getMinecraft().mcDataDir : null, path); @@ -52,4 +69,8 @@ public class ReplayModRender { public Configuration getConfiguration() { return configuration; } + + public List getRenderQueue() { + return renderQueue; + } } diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/VideoWriter.java index 1e98ab37..c68dbda6 100755 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/VideoWriter.java @@ -7,17 +7,27 @@ import com.replaymod.render.utils.StreamPipe; import net.minecraft.client.Minecraft; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.Util; +import net.minecraftforge.fml.common.versioning.ComparableVersion; import org.apache.commons.exec.CommandLine; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; import org.lwjgl.util.ReadableDimension; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; 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 { @@ -43,10 +53,11 @@ public class VideoWriter implements FrameConsumer { .replace("%HEIGHT%", String.valueOf(settings.getVideoHeight())) .replace("%FPS%", String.valueOf(settings.getFramesPerSecond())) .replace("%FILENAME%", fileName) - .replace("%BITRATE%", String.valueOf(settings.getBitRate())); + .replace("%BITRATE%", String.valueOf(settings.getBitRate())) + .replace("%FILTERS%", settings.getVideoFilters()); - String executable = settings.getExportCommand().isEmpty() ? "ffmpeg" : settings.getExportCommand(); - System.out.println("Starting " + settings.getExportCommand() + " with args: " + commandArgs); + String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand(); + System.out.println("Starting " + executable + " with args: " + commandArgs); String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings(); process = new ProcessBuilder(cmdline).directory(outputFolder).start(); File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log"); @@ -57,6 +68,54 @@ public class VideoWriter implements FrameConsumer { channel = Channels.newChannel(outputStream); } + private String findFFmpeg() { + switch (Util.getOSType()) { + case WINDOWS: + // Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder + File inDotMinecraft = new File(Minecraft.getMinecraft().mcDataDir, "ffmpeg/bin/ffmpeg.exe"); + if (inDotMinecraft.exists()) { + LOGGER.debug("FFmpeg found in .minecraft/ffmpeg"); + return inDotMinecraft.getAbsolutePath(); + } + break; + case OSX: + // The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves + for (String path : new String[]{"/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"}) { + File file = new File(path); + if (file.exists()) { + LOGGER.debug("Found FFmpeg at {}", path); + return path; + } else { + LOGGER.debug("FFmpeg not located at {}", path); + } + } + // Homebrew doesn't seem to reliably symlink its installed binaries either + File homebrewFolder = new File("/usr/local/Cellar/ffmpeg"); + String[] homebrewVersions = homebrewFolder.list(); + if (homebrewVersions != null) { + Optional latestOpt = Arrays.stream(homebrewVersions) + .map(ComparableVersion::new) // Convert file name to comparable version + .sorted(Comparator.reverseOrder()) // Sort for latest version + .map(ComparableVersion::toString) // Convert back to file name + .map(v -> new File(new File(homebrewFolder, v), "bin/ffmpeg")) // Convert to binary files + .filter(File::exists) // Filter invalid installations (missing executable) + .findFirst(); // Take first one + if (latestOpt.isPresent()) { + File latest = latestOpt.get(); + LOGGER.debug("Found {} versions of FFmpeg installed with homebrew, chose {}", + homebrewVersions.length, latest); + return latest.getAbsolutePath(); + } + } + break; + case LINUX: // Linux users are entrusted to have their PATH configured correctly (most package manager do this) + case SOLARIS: // Never heard of anyone running this mod on Solaris having any problems + case UNKNOWN: // Unknown OS, just try to use "ffmpeg" + } + LOGGER.debug("Using default FFmpeg executable"); + return "ffmpeg"; + } + @Override public void close() throws IOException { IOUtils.closeQuietly(outputStream); diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java new file mode 100644 index 00000000..9a96835b --- /dev/null +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -0,0 +1,277 @@ +package com.replaymod.render.gui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.replaymod.render.ReplayModRender; +import com.replaymod.render.rendering.VideoRenderer; +import com.replaymod.render.utils.RenderJob; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.util.I18n; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer; +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.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiTextField; +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 net.minecraft.client.gui.GuiErrorScreen; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; + +import static com.replaymod.render.ReplayModRender.LOGGER; + +public class GuiRenderQueue extends AbstractGuiPopup { + 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 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 closeButton = new GuiButton().setI18nLabel("replaymod.gui.close").setSize(150, 20).onClick(this::close); + + /* + + |---------------------------------| + | Add | Render | + |---------------------------------| + | Rename | Remove | Close | + |---------------------------------| + + */ + private final GuiPanel buttonPanel = new GuiPanel() + .setLayout(new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(2)) + .addElements(null, + addButton, + renderButton, + new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)).addElements(null, + renameButton, removeButton), + closeButton); + + private final GuiContainer container; + private Entry selectedEntry; + + { + popup.setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(title, width / 2 - width(title) / 2, 0); + pos(list, 0, y(title) + height(title) + 5); + pos(buttonPanel, width / 2 - width(buttonPanel) / 2, height - height(buttonPanel)); + size(list, width, y(buttonPanel) - y(list) - 10); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + ReadableDimension screenSize = GuiRenderQueue.this.container.getMinSize(); + return new Dimension(screenSize.getWidth() - 40, + screenSize.getHeight() - 20 - buttonPanel.getMinSize().getHeight() - title.getMinSize().getHeight()); + } + }).addElements(null, title, list, buttonPanel); + } + + public GuiRenderQueue(GuiContainer container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) { + super(container); + this.container = container; + LOGGER.trace("Opening render queue popup"); + + setBackgroundColor(Colors.DARK_TRANSPARENT); + + List queue = ReplayModRender.instance.getRenderQueue(); + + for (RenderJob renderJob : queue) { + 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"); + } + } + + @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); + } + }); + }); + + removeButton.onClick(() -> { + LOGGER.trace("Remove button clicked for {}", selectedEntry.job); + list.getListPanel().removeElement(selectedEntry); + queue.remove(selectedEntry.job); + selectedEntry = null; + updateButtons(); + }); + + renderButton.onClick(() -> { + LOGGER.trace("Render button clicked"); + // Close all GUIs (so settings in GuiRenderSettings are saved) + getMinecraft().displayGuiScreen(null); + // Start rendering + for (RenderJob renderJob : queue) { + LOGGER.info("Starting render job {}", renderJob); + try { + VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline()); + videoRenderer.renderVideo(); + } catch (IOException e) { + LOGGER.error("Rendering video:", e); + GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"), + I18n.format("replaymod.gui.rendering.error.message")); + getMinecraft().displayGuiScreen(errorScreen); + return; + } catch (Throwable t) { + CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video"); + throw new ReportedException(crashReport); + } + } + }); + + updateButtons(); + } + + @Override + public void open() { + super.open(); + } + + @Override + protected GuiRenderQueue getThis() { + return this; + } + + public void updateButtons() { + renameButton.setEnabled(selectedEntry != null); + removeButton.setEnabled(selectedEntry != null); + renderButton.setEnabled(!list.getListPanel().getChildren().isEmpty()); + } + + public class Entry extends AbstractGuiClickableContainer { + public final GuiLabel label = new GuiLabel(this); + public final RenderJob job; + + public Entry(RenderJob job) { + this.job = job; + + setLayout(new CustomLayout() { + @Override + protected void layout(Entry container, int width, int height) { + pos(label, 5, height / 2 - height(label) / 2); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + return new Dimension(buttonPanel.calcMinSize().getWidth(), 16); + } + }); + label.setText(job.getName()); + } + + @Override + protected void onClick() { + selectedEntry = this; + updateButtons(); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + if (selectedEntry == 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; + } + } +} diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 247db259..14cd84fc 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -32,13 +32,14 @@ import net.minecraft.crash.CrashReport; import net.minecraft.util.ReportedException; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; +import org.lwjgl.util.Color; import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; -import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; @@ -149,13 +150,18 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { public final GuiCheckbox inject360Metadata = new GuiCheckbox() .setI18nLabel("replaymod.gui.rendersettings.360metadata"); + public final GuiDropdownMenu antiAliasingDropdown = new GuiDropdownMenu() + .setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE); + public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15)) .addElements(null, nametagCheckbox, new GuiPanel().setLayout( new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15)) .addElements(new GridLayout.Data(0, 0.5), new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, chromaKeyingCheckbox, chromaKeyingColor, - inject360Metadata)); + inject360Metadata, + new GuiLabel(), // to show the anti-aliasing options in a new line + new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown)); public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command") .setSize(55, 20).setMaxLength(100); @@ -179,6 +185,12 @@ 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 renderButton = new GuiButton(buttonPanel).onClick(new Runnable() { @Override public void run() { @@ -198,13 +210,13 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { throw new ReportedException(crashReport); } } - }).setSize(148, 20).setI18nLabel("replaymod.gui.render"); + }).setSize(100, 20).setI18nLabel("replaymod.gui.render"); public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() { @Override public void run() { getMinecraft().displayGuiScreen(null); } - }).setSize(148, 20).setI18nLabel("replaymod.gui.cancel"); + }).setSize(100, 20).setI18nLabel("replaymod.gui.cancel"); { setBackground(Background.NONE); @@ -261,12 +273,10 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { this.timeline = timeline; String json = getConfigProperty(ReplayModRender.instance.getConfiguration()).getString(); - RenderSettings settings = new GsonBuilder().registerTypeAdapter(RenderSettings.class, new InstanceCreator() { - @Override - public RenderSettings createInstance(Type type) { - return getDefaultRenderSettings(); - } - }).create().fromJson(json, RenderSettings.class); + RenderSettings settings = new GsonBuilder() + .registerTypeAdapter(RenderSettings.class, (InstanceCreator) type -> getDefaultRenderSettings()) + .registerTypeAdapter(ReadableColor.class, new Gson().getAdapter(Color.class)) + .create().fromJson(json, RenderSettings.class); load(settings); } @@ -347,8 +357,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { public void load(RenderSettings settings) { renderMethodDropdown.setSelected(settings.getRenderMethod()); encodingPresetDropdown.setSelected(settings.getEncodingPreset()); - videoWidth.setValue(settings.getVideoWidth()); - videoHeight.setValue(settings.getVideoHeight()); + videoWidth.setValue(settings.getTargetVideoWidth()); + videoHeight.setValue(settings.getTargetVideoHeight()); frameRateSlider.setValue(settings.getFramesPerSecond() - 10); if (settings.getBitRate() % (1 << 20) == 0) { bitRateField.setValue(settings.getBitRate() >> 20); @@ -380,6 +390,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { chromaKeyingColor.setColor(settings.getChromaKeyingColor()); } inject360Metadata.setChecked(settings.isInject360Metadata()); + antiAliasingDropdown.setSelected(settings.getAntiAliasing()); exportCommand.setText(settings.getExportCommand()); exportArguments.setText(settings.getExportArguments()); @@ -401,6 +412,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()), chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null, inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()), + antiAliasingDropdown.getSelectedValue(), exportCommand.getText(), exportArguments.getText(), net.minecraft.client.gui.GuiScreen.isCtrlKeyDown() @@ -415,7 +427,7 @@ 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, false, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); + true, false, false, false, null, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); } @Override @@ -431,4 +443,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { return configuration.get("rendersettings", "settings", "{}", "Last state of the render settings GUI. Internal use only."); } + + public ReplayHandler getReplayHandler() { + return replayHandler; + } } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 204f032b..2fea303a 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -1,6 +1,7 @@ package com.replaymod.render.rendering; import com.replaymod.pathing.player.AbstractTimelinePlayer; +import com.replaymod.pathing.player.ReplayTimer; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; @@ -55,6 +56,7 @@ public class VideoRenderer implements RenderInfo { private boolean debugInfoWasShown; private Map originalSoundLevels; + private TimelinePlayer timelinePlayer; private Future timelinePlayerFuture; private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal; @@ -174,7 +176,8 @@ public class VideoRenderer implements RenderInfo { } private void setup() { - timelinePlayerFuture = new TimelinePlayer(replayHandler).start(timeline); + timelinePlayer = new TimelinePlayer(replayHandler); + timelinePlayerFuture = timelinePlayer.start(timeline); if (!OpenGlHelper.isFramebufferEnabled()) { Display.setResizable(false); @@ -226,6 +229,8 @@ public class VideoRenderer implements RenderInfo { if (!timelinePlayerFuture.isDone()) { timelinePlayerFuture.cancel(false); } + // Tear down of the timeline player might only happen the next tick after it was cancelled + timelinePlayer.onTick(new ReplayTimer.UpdatedEvent()); if (!OpenGlHelper.isFramebufferEnabled()) { Display.setResizable(true); diff --git a/src/main/java/com/replaymod/render/utils/RenderJob.java b/src/main/java/com/replaymod/render/utils/RenderJob.java new file mode 100644 index 00000000..bae927d0 --- /dev/null +++ b/src/main/java/com/replaymod/render/utils/RenderJob.java @@ -0,0 +1,12 @@ +package com.replaymod.render.utils; + +import com.replaymod.render.RenderSettings; +import com.replaymod.replaystudio.pathing.path.Timeline; +import lombok.Data; + +@Data +public class RenderJob { + private String name; + private Timeline timeline; + private RenderSettings settings; +} diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java index 44718b54..24465548 100644 --- a/src/main/java/com/replaymod/replay/ReplayModReplay.java +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -28,15 +28,17 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Optional; -@Mod(modid = ReplayModReplay.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModReplay.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModReplay { public static final String MOD_ID = "replaymod-replay"; @Mod.Instance(MOD_ID) public static ReplayModReplay instance; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; + private ReplayMod core; private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry(); @@ -51,6 +53,7 @@ public class ReplayModReplay { @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { logger = event.getModLog(); + core = ReplayMod.instance; core.getSettingsRegistry().register(Setting.class); diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index e130b408..c1d11e22 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -26,20 +26,23 @@ import org.apache.logging.log4j.Logger; import java.io.IOException; -@Mod(modid = ReplayModSimplePathing.MOD_ID, useMetadata = true) +@Mod(modid = ReplayModSimplePathing.MOD_ID, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + useMetadata = true) public class ReplayModSimplePathing implements PathingRegistry { public static final String MOD_ID = "replaymod-simplepathing"; - @Mod.Instance(ReplayMod.MOD_ID) - private static ReplayMod core; + private ReplayMod core; - private Logger logger; + public static Logger LOGGER; private GuiPathing guiPathing; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { - logger = event.getModLog(); + LOGGER = event.getModLog(); + core = ReplayMod.instance; core.getSettingsRegistry().register(Setting.class); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index edb160a5..bc61f618 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -63,7 +63,7 @@ public abstract class GuiEditKeyframe> extends Abst setBackgroundColor(Colors.DARK_TRANSPARENT); popup.setLayout(new VerticalLayout().setSpacing(10)) - .addElements(new VerticalLayout.Data(0.5), title, inputs, timePanel, buttons); + .addElements(new VerticalLayout.Data(0.5, false), title, inputs, timePanel, buttons); } public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) { @@ -179,13 +179,13 @@ public abstract class GuiEditKeyframe> extends Abst } public static class Position extends GuiEditKeyframe { - public final GuiNumberField xField = newGuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField yField = newGuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField zField = newGuiNumberField().setSize(150, 20).setPrecision(10); + public final GuiNumberField xField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final GuiNumberField yField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final GuiNumberField zField = newGuiNumberField().setSize(60, 20).setPrecision(5); - public final GuiNumberField yawField = newGuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField pitchField = newGuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField rollField = newGuiNumberField().setSize(150, 20).setPrecision(5); + public final GuiNumberField yawField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5); { inputs.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 91815070..8eb27501 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -23,6 +23,7 @@ import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.property.Property; +import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; import com.replaymod.simplepathing.ReplayModSimplePathing; @@ -41,6 +42,7 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; +import net.minecraft.crash.CrashReport; import net.minecraft.entity.Entity; import net.minecraftforge.fml.common.Loader; import org.apache.commons.lang3.tuple.Triple; @@ -56,10 +58,14 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import static com.replaymod.core.utils.Utils.error; +import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER; + /** * Gui plug-in to the GuiReplayOverlay for simple pathing. @@ -91,7 +97,19 @@ public class GuiPathing { @Override public void run() { if (!preparePathsForPlayback()) return; - new GuiRenderSettings(replayHandler, mod.getCurrentTimeline()).display(); + + // Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue + Timeline timeline = mod.getCurrentTimeline(); + try { + TimelineSerialization serialization = new TimelineSerialization(mod, null); + String serialized = serialization.serialize(Collections.singletonMap("", timeline)); + timeline = serialization.deserialize(serialized).get(""); + } catch (Throwable t) { + error(LOGGER, replayHandler.getOverlay(), CrashReport.makeCrashReport(t, "Cloning timeline"), () -> {}); + return; + } + + new GuiRenderSettings(replayHandler, timeline).display(); } }).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 0) .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.renderpath")); @@ -512,6 +530,13 @@ public class GuiPathing { lastTime = time; } + // Make sure there are at least two position- and two time-keyframes + if (timeline.getPaths().get(POSITION_PATH).getSegments().isEmpty() + || timeline.getPaths().get(TIME_PATH).getSegments().isEmpty()) { + GuiInfoPopup.open(replayHandler.getOverlay(), "replaymod.chat.morekeyframes"); + return false; + } + return true; } diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index ecdf54ef..3c85d8d8 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -255,6 +255,8 @@ replaymod.gui.editor.trim.description=Removes the beginning and end of a Replay replaymod.gui.editor.connect.description=Connects multiple Replays in the specified order replaymod.gui.editor.modify.description=Provides several filters to modify Replay Files +replaymod.gui.editor.trim.marker=Select Event Marker + replaymod.gui.editor.progress.title=Editing Replay File... replaymod.gui.editor.progress.pleasewait=Please wait while the Replay is being edited. @@ -424,6 +426,18 @@ replaymod.gui.rendersettings.presets.webm.custom=WEBM - Custom Bitrate replaymod.gui.rendersettings.presets.mkv.lossless=MKV - Lossless replaymod.gui.rendersettings.presets.png=PNG Sequence +replaymod.gui.rendersettings.antialiasing=Anti-Aliasing +replaymod.gui.rendersettings.antialiasing.none=None +replaymod.gui.rendersettings.antialiasing.x2=2x +replaymod.gui.rendersettings.antialiasing.x4=4x +replaymod.gui.rendersettings.antialiasing.x8=8x + +#Render Queue GUI +replaymod.gui.renderqueue.title=Render Queue +replaymod.gui.renderqueue.open=Open Queue +replaymod.gui.renderqueue.jobname=Job Name: +replaymod.gui.renderqueue.add=Add current configuration + #Rendering GUI replaymod.gui.rendering.title=Rendering Video replaymod.gui.rendering.pause=Pause Rendering diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index f6ac199a..065f181d 100755 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -100,6 +100,23 @@ "screenshots": [], "dependencies": ["required-after:replaymod-replay"] }, + { + "modid": "replaymod-editor", + "name": "Replay Mod - Replay Editor", + "description": "Replay Editor Module of the ReplayMod", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": [] + }, { "modid": "replaymod-extras", "name": "Replay Mod - Extras", diff --git a/src/main/resources/mixins.compat.shaders.replaymod.json b/src/main/resources/mixins.compat.shaders.replaymod.json index eea17922..792af576 100644 --- a/src/main/resources/mixins.compat.shaders.replaymod.json +++ b/src/main/resources/mixins.compat.shaders.replaymod.json @@ -6,7 +6,9 @@ "client": [ "MixinShaderEntityRenderer", "MixinShaderRenderChunk", - "MixinShaderRenderGlobal" + "MixinShaderRenderGlobal", + "MixinShadersRender" ], + "compatibilityLevel": "JAVA_8", "refmap": "mixins.replaymod.refmap.json" } \ No newline at end of file