Merge branch '1.10.2' into 1.11

c6dec95 Merge branch '1.9.4' into 1.10.2
4fdfa9e Merge branch '1.8.9' into 1.9.4
44d4a25 Merge branch '1.8' into 1.9.4
01acbc6 Update to 1.8.9
1c60687 Update ReplayStudio and jGui
d9b796d Mixin'd the Shader's mods ShadersRender class to only render hands if the Forge event isn't cancelled
6bbff3b Updated Mixin to 0.6.4-SNAPSHOT and set mixin target level
6f3ac71 Add CustomMainMenu compatibility info to docs
b0a1cdf Search for ffmpeg executable in common locations to simplify setup
d465891 Fixed GuiEditPositionKeyframe's width to fit on all screen sizes Centered all rows of GuiEditPositionKeyframe's VerticalLayout
1b099f9 Add render queue
03534c6 Update docs
4388d69 Only try to create the downloads folder if it does not yet exist
7ad19aa Add mod version and accepted MC version directly to the @Mod annotation
655bea0 Replace @Mod.Instance with direct static references
458dad6 Merge pull request #4 from ReplayMod/1.8-editor
eb5ccb2 Add Replay Editor (for now, trimming only)
6498d62 Fix server game data snapshot being restored in singleplayer (fixes #44)
2c898a8 Prevent rendering without sufficient keyframes (fixes #45)
5d520a5 Fix deserialization of chroma key color (fixes #43)
c681742 Use caching maven proxy for faster and more stable drone builds
fbf165e Added Anti-Aliasing to the Render Settings
This commit is contained in:
johni0702
2017-01-15 00:12:23 +01:00
43 changed files with 1439 additions and 76 deletions

View File

@@ -7,7 +7,10 @@ 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";

View File

@@ -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();
}
}
}

View File

@@ -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 {

View File

@@ -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<Void>() {
@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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<GuiPanel> 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<GuiScreen>() {
@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<GuiPanel> 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<File> 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<Boolean>() {
@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<Boolean>() {
@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<GuiProcessingProgressPopup> {
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;
}
}
}

View File

@@ -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<File> inputReplays = new GuiDropdownMenu<File>(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<Marker> 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<Marker> 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<Marker, String> 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<Marker> 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<Marker> 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<Marker> 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);
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -12,7 +12,10 @@ 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";

View File

@@ -26,7 +26,10 @@ 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";
@@ -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();

View File

@@ -14,7 +14,10 @@ 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";

View File

@@ -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<FMLHandshakeMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FMLHandshakeMessage msg) throws Exception {
if (!(msg instanceof FMLHandshakeMessage.RegistryData)) {
// Pass on everything but RegistryData messages
ctx.fireChannelRead(msg);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,18 +1,27 @@
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";
@@ -21,9 +30,10 @@ public class ReplayModRender {
private ReplayMod core;
private Logger logger;
public static Logger LOGGER;
private Configuration configuration;
private final List<RenderJob> renderQueue = new ArrayList<>();
public ReplayMod getCore() {
return core;
@@ -31,13 +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<RenderJob> getRenderQueue() {
return renderQueue;
}
}

View File

@@ -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<RGBFrame> {
@@ -43,10 +53,11 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
.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<RGBFrame> {
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<File> 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);

View File

@@ -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<GuiRenderQueue> {
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<GuiPanel>() {
@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<RenderJob> 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<Boolean>() {
@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<Boolean>() {
@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<Entry> {
public final GuiLabel label = new GuiLabel(this);
public final RenderJob job;
public Entry(RenderJob job) {
this.job = job;
setLayout(new CustomLayout<Entry>() {
@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;
}
}
}

View File

@@ -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<RenderSettings.AntiAliasing> antiAliasingDropdown = new GuiDropdownMenu<RenderSettings.AntiAliasing>()
.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<RenderSettings>() {
@Override
public RenderSettings createInstance(Type type) {
return getDefaultRenderSettings();
}
}).create().fromJson(json, RenderSettings.class);
RenderSettings settings = new GsonBuilder()
.registerTypeAdapter(RenderSettings.class, (InstanceCreator<RenderSettings>) 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;
}
}

View File

@@ -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<SoundCategory, Float> originalSoundLevels;
private TimelinePlayer timelinePlayer;
private Future<Void> 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);

View File

@@ -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;
}

View File

@@ -28,7 +28,10 @@ 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";

View File

@@ -26,19 +26,22 @@ 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";
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);

View File

@@ -63,7 +63,7 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> 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<T extends GuiEditKeyframe<T>> extends Abst
}
public static class Position extends GuiEditKeyframe<Position> {
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))

View File

@@ -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;
}