Release 2.5.0

This commit is contained in:
Jonas Herzig
2020-11-14 09:57:21 +01:00
103 changed files with 3021 additions and 1102 deletions

View File

@@ -178,7 +178,7 @@ val doRelease by tasks.registering {
defaultTasks("shadowJar")
preprocess {
"1.16.3"(11603, "yarn") {
"1.16.4"(11604, "yarn") {
"1.16.1"(11601, "yarn") {
"1.15.2"(11502, "yarn") {
"1.14.4"(11404, "yarn", file("versions/mapping-fabric-1.15.2-1.14.4.txt")) {

View File

@@ -22,14 +22,17 @@ To render your creations with **Replay Mod** you will need to have FFmpeg instal
### Windows [windows]
Download the latest FFmpeg release from https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.
In your `.minecraft` folder, create a `ffmpeg` folder. Extract the downloaded .zip file into this folder. The FFmpeg executable should end up at `.minecraft\ffmpeg\bin\ffmpeg.exe`.
Extract the downloaded .zip file into your `.minecraft` folder.
Historically the exact location inside the `.minecraft` folder was important, this is no longer the case.
It is sufficient for the `ffmpeg.exe` to be anywhere inside your `.minecraft` folder.
**Notes for alternative launchers**
- Twitch launcher by default installs Minecraft instances in C:\\Users\\*username*\\Twitch\\Minecraft\\Instances\\*instancename*\\
- GD Launcher by default installs Minecraft instances in C:\\Users\\*username*\\AppData\\Roaming\\gdlauncher_next\\instances\\*instancename*\\
- MultiMC by default installs Minecraft instances in C:\\Program Files (x86)\\MultiMC\\instances\\*Instancename*\\.minecraft\\
For these launchers, make sure FFmpeg exists in `instancename\ffmpeg\bin\ffmpeg.exe` (Twitch / GD) or `Instancename\.minecraft\ffmpeg\bin\ffmpeg.exe` (MultiMC)
For these launchers, make sure FFmpeg exists inside the corresponding folder.
### Mac OSX [mac]
On OSX, you can install **FFmpeg** with **[Homebrew](http://brew.sh/)** using `brew install ffmpeg`.
@@ -144,6 +147,8 @@ If two Keyframes are 5 seconds apart, the Camera Path will take 5 seconds to int
You can drag Keyframes on the **Keyframe Timeline** by left-clicking them, holding the mouse button and moving your mouse.
> **Hint:** Made a mistake? You can use `Ctrl+Z` to undo your changes to the keyframe timeline. Changed you mind? Press `Ctrl+Y` (or `Ctrl+Shift+Z`) to redo them.
### Position Keyframes [place]
The basic components of a Camera Path are **Position Keyframes**.
A Position Keyframe stores a **Camera Position** (x, y, z, yaw, pitch, roll).
@@ -354,6 +359,12 @@ There are **7 Encoding Presets** you can choose from:
- **PNG Sequence**
Exports the sequence as individual frames in the **PNG Format**.
**Warning:** This can create a huge amount of files, so make sure to save them in a separate folder.
- **OpenEXR Sequence** (Minecraft 1.14 and above)
Exports the sequence as individual frames in the **OpenEXR Format**.
The images are of perfect quality and may contain
additional image layers beyond the visible one (e.g. [depth](#replaying-render-advanced-depth-map)).
As such, this format is a good choice if you have plenty of disk space to spare and want to perform additional editing
on the video in a third-party video editor.
### Advanced Settings [advanced]
![](img/rendersettings-advanced.jpg)
@@ -380,6 +391,21 @@ Using a **Video Editing Software** like **Adobe After Effects** or **Sony Vegas*
> **Note:** For best results, you should **disable clouds** before rendering, as they are transparent.
#### Depth Map [depth-map]
![](img/depthmapsuzanne.jpg)
"Suzanne" model added into a scene using Blender
This setting causes depth information to be recorded and included in video formats which support it
(currently only [OpenEXR](#replaying-render-settings-quality)).
Using a video editor which supports it (e.g. [Blender](https://www.blender.org/)), you can then for example (among many
other effects) add arbitrary geometry onto your rendered video in such a way that foreground objects in the
video will still occlude background geometry as if it actually was part of the world.
> **Note:** For best results, avoid semi-transparent blocks in front of your geometry, as they cannot be accurately represented in the depth map.
> **Note:** Anti-aliasing is not supported when exporting depth. Instead simply render and edit your video at a higher resolution and scale it down at the very end.
### Command Line Settings [commandline]
![](img/rendersettings-commandline.jpg)
The **Command Line Render Settings**

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

2
jGui

Submodule jGui updated: 6602aac42a...6b16892d64

View File

@@ -29,7 +29,7 @@ val jGuiVersions = listOf(
"1.14.4",
"1.15.2",
"1.16.1",
"1.16.3"
"1.16.4"
)
val replayModVersions = listOf(
"1.7.10",
@@ -46,7 +46,7 @@ val replayModVersions = listOf(
"1.14.4",
"1.15.2",
"1.16.1",
"1.16.3"
"1.16.4"
)
include(":jGui")

View File

@@ -2,7 +2,10 @@ package com.replaymod.core;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.replaymod.core.mixin.KeyBindingAccessor;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
import com.replaymod.core.versions.MCVer;
import net.minecraft.client.options.KeyBinding;
import net.minecraft.util.crash.CrashReport;
@@ -20,24 +23,24 @@ import static com.replaymod.core.ReplayMod.MOD_ID;
//#endif
//#if MC>=11400
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
import com.replaymod.core.events.PreRenderCallback;
import org.lwjgl.glfw.GLFW;
//#else
//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
//$$ import net.minecraftforge.fml.common.gameevent.InputEvent;
//$$ import net.minecraftforge.fml.common.gameevent.TickEvent;
//$$ import org.lwjgl.input.Keyboard;
//$$ import static com.replaymod.core.versions.MCVer.FML_BUS;
//#endif
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class KeyBindingRegistry extends EventRegistrations {
private static final String CATEGORY = "replaymod.title";
@@ -45,23 +48,25 @@ public class KeyBindingRegistry extends EventRegistrations {
static { net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.addCategory(CATEGORY); }
//#endif
private Map<String, KeyBinding> keyBindings = new HashMap<String, KeyBinding>();
private final Map<String, Binding> bindings = new HashMap<>();
private Set<KeyBinding> onlyInReplay = new HashSet<>();
private Multimap<KeyBinding, Runnable> keyBindingHandlers = ArrayListMultimap.create();
private Multimap<KeyBinding, Runnable> repeatedKeyBindingHandlers = ArrayListMultimap.create();
private Multimap<Integer, Runnable> rawHandlers = ArrayListMultimap.create();
private Multimap<Integer, Supplier<Boolean>> rawHandlers = ArrayListMultimap.create();
public void registerKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
keyBindingHandlers.put(registerKeyBinding(name, keyCode, onlyInRepay), whenPressed);
public Binding registerKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
Binding binding = registerKeyBinding(name, keyCode, onlyInRepay);
binding.handlers.add(whenPressed);
return binding;
}
public void registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
repeatedKeyBindingHandlers.put(registerKeyBinding(name, keyCode, onlyInRepay), whenPressed);
public Binding registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
Binding binding = registerKeyBinding(name, keyCode, onlyInRepay);
binding.repeatedHandlers.add(whenPressed);
return binding;
}
private KeyBinding registerKeyBinding(String name, int keyCode, boolean onlyInRepay) {
KeyBinding keyBinding = keyBindings.get(name);
if (keyBinding == null) {
private Binding registerKeyBinding(String name, int keyCode, boolean onlyInRepay) {
Binding binding = bindings.get(name);
if (binding == null) {
//#if FABRIC>=1
if (keyCode == 0) {
keyCode = -1;
@@ -69,27 +74,28 @@ public class KeyBindingRegistry extends EventRegistrations {
Identifier id = new Identifier(MOD_ID, name.substring(LangResourcePack.LEGACY_KEY_PREFIX.length()));
FabricKeyBinding fabricKeyBinding = FabricKeyBinding.Builder.create(id, InputUtil.Type.KEYSYM, keyCode, CATEGORY).build();
net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.register(fabricKeyBinding);
keyBinding = fabricKeyBinding;
KeyBinding keyBinding = fabricKeyBinding;
//#else
//$$ keyBinding = new KeyBinding(name, keyCode, CATEGORY);
//$$ KeyBinding keyBinding = new KeyBinding(name, keyCode, CATEGORY);
//$$ ClientRegistry.registerKeyBinding(keyBinding);
//#endif
keyBindings.put(name, keyBinding);
binding = new Binding(name, keyBinding);
bindings.put(name, binding);
if (onlyInRepay) {
this.onlyInReplay.add(keyBinding);
}
} else if (!onlyInRepay) {
this.onlyInReplay.remove(keyBinding);
this.onlyInReplay.remove(binding.keyBinding);
}
return keyBinding;
return binding;
}
public void registerRaw(int keyCode, Runnable whenPressed) {
public void registerRaw(int keyCode, Supplier<Boolean> whenPressed) {
rawHandlers.put(keyCode, whenPressed);
}
public Map<String, KeyBinding> getKeyBindings() {
return Collections.unmodifiableMap(keyBindings);
public Map<String, Binding> getBindings() {
return Collections.unmodifiableMap(bindings);
}
public Set<KeyBinding> getOnlyInReplay() {
@@ -97,17 +103,9 @@ public class KeyBindingRegistry extends EventRegistrations {
}
//#if MC>=11400
{ on(KeyBindingEventCallback.EVENT, this::handleKeyBindings); }
{ on(KeyEventCallback.EVENT, (keyCode, scanCode, action, modifiers) -> handleRaw(keyCode, action)); }
{ on(PreRenderCallback.EVENT, this::handleRepeatedKeyBindings); }
//#else
//$$ @SubscribeEvent
//$$ public void onKeyInput(InputEvent.KeyInputEvent event) {
//$$ handleKeyBindings();
//$$ handleRaw();
//$$ }
//$$
//$$ @SubscribeEvent
//$$ public void onTick(TickEvent.RenderTickEvent event) {
//$$ if (event.phase != TickEvent.Phase.START) return;
//$$ handleRepeatedKeyBindings();
@@ -115,52 +113,106 @@ public class KeyBindingRegistry extends EventRegistrations {
//#endif
public void handleRepeatedKeyBindings() {
for (Map.Entry<KeyBinding, Collection<Runnable>> entry : repeatedKeyBindingHandlers.asMap().entrySet()) {
if (entry.getKey().isPressed()) {
invokeKeyBindingHandlers(entry.getKey(), entry.getValue());
for (Binding binding : bindings.values()) {
if (binding.keyBinding.isPressed()) {
invokeKeyBindingHandlers(binding, binding.repeatedHandlers);
}
}
}
public void handleKeyBindings() {
for (Map.Entry<KeyBinding, Collection<Runnable>> entry : keyBindingHandlers.asMap().entrySet()) {
while (entry.getKey().wasPressed()) {
invokeKeyBindingHandlers(entry.getKey(), entry.getValue());
{ on(KeyBindingEventCallback.EVENT, this::handleKeyBindings); }
private void handleKeyBindings() {
for (Binding binding : bindings.values()) {
while (binding.keyBinding.wasPressed()) {
invokeKeyBindingHandlers(binding, binding.handlers);
invokeKeyBindingHandlers(binding, binding.repeatedHandlers);
}
}
}
private void invokeKeyBindingHandlers(KeyBinding keyBinding, Collection<Runnable> handlers) {
private void invokeKeyBindingHandlers(Binding binding, Collection<Runnable> handlers) {
for (final Runnable runnable : handlers) {
try {
runnable.run();
} catch (Throwable cause) {
CrashReport crashReport = CrashReport.create(cause, "Handling Key Binding");
CrashReportSection category = crashReport.addElement("Key Binding");
MCVer.addDetail(category, "Key Binding", keyBinding::toString);
MCVer.addDetail(category, "Key Binding", () -> binding.name);
MCVer.addDetail(category, "Handler", runnable::toString);
throw new CrashException(crashReport);
}
}
}
//#if MC>=11400
private void handleRaw(int keyCode, int action) {
if (action != GLFW.GLFW_PRESS) return;
//#else
//$$ private void handleRaw() {
//$$ int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey();
//#endif
for (final Runnable runnable : rawHandlers.get(keyCode)) {
{ on(KeyEventCallback.EVENT, (keyCode, scanCode, action, modifiers) -> handleRaw(keyCode, action)); }
private boolean handleRaw(int keyCode, int action) {
if (action != KeyEventCallback.ACTION_PRESS) return false;
for (final Supplier<Boolean> handler : rawHandlers.get(keyCode)) {
try {
runnable.run();
if (handler.get()) {
return true;
}
} catch (Throwable cause) {
CrashReport crashReport = CrashReport.create(cause, "Handling Raw Key Binding");
CrashReportSection category = crashReport.addElement("Key Binding");
MCVer.addDetail(category, "Key Code", () -> "" + keyCode);
MCVer.addDetail(category, "Handler", runnable::toString);
MCVer.addDetail(category, "Handler", handler::toString);
throw new CrashException(crashReport);
}
}
return false;
}
public class Binding {
public final String name;
public final KeyBinding keyBinding;
private final List<Runnable> handlers = new ArrayList<>();
private final List<Runnable> repeatedHandlers = new ArrayList<>();
private boolean autoActivation;
private Consumer<Boolean> autoActivationUpdate;
public Binding(String name, KeyBinding keyBinding) {
this.name = name;
this.keyBinding = keyBinding;
}
public String getBoundKey() {
return MCVer.getBoundKey(keyBinding);
}
public boolean isBound() {
//#if MC>=11400
return !keyBinding.isUnbound();
//#else
//$$ return keyBinding.getKeyCode() != 0;
//#endif
}
public void trigger() {
KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding;
acc.setPressTime(acc.getPressTime() + 1);
handleKeyBindings();
}
public void registerAutoActivationSupport(boolean active, Consumer<Boolean> update) {
this.autoActivation = active;
this.autoActivationUpdate = update;
}
public boolean supportsAutoActivation() {
return autoActivationUpdate != null;
}
public boolean isAutoActivating() {
return supportsAutoActivation() && autoActivation;
}
public void setAutoActivating(boolean active) {
if (this.autoActivation == active) {
return;
}
this.autoActivation = active;
this.autoActivationUpdate.accept(active);
}
}
}

View File

@@ -1,11 +1,10 @@
package com.replaymod.core;
import com.google.common.io.Files;
import com.google.common.net.PercentEscaper;
import com.replaymod.compat.ReplayModCompat;
import com.replaymod.core.gui.GuiBackgroundProcesses;
import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.core.gui.RestoreReplayGui;
import com.replaymod.core.handler.MainMenuHandler;
import com.replaymod.core.mixin.MinecraftAccessor;
import com.replaymod.core.versions.MCVer;
import com.replaymod.editor.ReplayModEditor;
@@ -13,6 +12,8 @@ import com.replaymod.extras.ReplayModExtras;
import com.replaymod.recording.ReplayModRecording;
import com.replaymod.render.ReplayModRender;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.protocol.ProtocolVersion;
import com.replaymod.replaystudio.util.I18n;
@@ -28,6 +29,7 @@ import net.minecraft.text.LiteralText;
import net.minecraft.text.TranslatableText;
import net.minecraft.util.Formatting;
import net.minecraft.util.crash.CrashException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.FileUtils;
//#if MC>=11400
@@ -89,10 +91,16 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -136,6 +144,7 @@ public class ReplayMod implements
public static final Identifier TEXTURE = new Identifier("replaymod", "replay_gui.png");
public static final int TEXTURE_SIZE = 256;
public static final Identifier LOGO_FAVICON = new Identifier("replaymod", "favicon_logo.png");
private static final MinecraftClient mc = MCVer.getMinecraft();
@@ -216,11 +225,54 @@ public class ReplayMod implements
return settingsRegistry;
}
public File getReplayFolder() throws IOException {
String path = getSettingsRegistry().get(Setting.RECORDING_PATH);
File folder = new File(path.startsWith("./") ? getMinecraft().runDirectory : null, path);
FileUtils.forceMkdir(folder);
return folder;
public Path getReplayFolder() throws IOException {
String str = getSettingsRegistry().get(Setting.RECORDING_PATH);
return Files.createDirectories(getMinecraft().runDirectory.toPath().resolve(str));
}
/**
* Folder into which replay backups are saved before the MarkerProcessor is unleashed.
*/
public Path getRawReplayFolder() throws IOException {
return Files.createDirectories(getReplayFolder().resolve("raw"));
}
/**
* Folder into which replays are recorded.
* Distinct from the main folder, so they cannot be opened while they are still saving.
*/
public Path getRecordingFolder() throws IOException {
return Files.createDirectories(getReplayFolder().resolve("recording"));
}
/**
* Folder in which replay cache files are stored.
* Distinct from the recording folder cause people kept confusing them with recordings.
*/
public Path getCacheFolder() throws IOException {
String str = getSettingsRegistry().get(Setting.CACHE_PATH);
Path path = getMinecraft().runDirectory.toPath().resolve(str);
Files.createDirectories(path);
try {
Files.setAttribute(path, "dos:hidden", true);
} catch (UnsupportedOperationException ignored) {
}
return path;
}
private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false);
public Path getCachePathForReplay(Path replay) throws IOException {
Path replayFolder = getReplayFolder();
Path cacheFolder = getCacheFolder();
Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath());
return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString()));
}
public Path getReplayPathForCache(Path cache) throws IOException {
String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8");
Path replayFolder = getReplayFolder();
return replayFolder.resolve(relative);
}
public static final DirectoryResourcePack jGuiResourcePack;
@@ -323,8 +375,6 @@ public class ReplayMod implements
@Override
public void initClient() {
new MainMenuHandler().register();
//#if MC<=10710
//$$ FML_BUS.register(this); // For runLater(Runnable)
//#endif
@@ -348,31 +398,56 @@ public class ReplayMod implements
runPostStartup(() -> {
final long DAYS = 24 * 60 * 60 * 1000;
// Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging)
// Cleanup any cache folders still remaining in the recording folder (we once used to put them there)
try {
File[] files = new File(getReplayFolder(), "raw").listFiles();
if (files != null) {
for (File file : files) {
if (file.lastModified() + 21 * DAYS < System.currentTimeMillis()) {
FileUtils.forceDelete(file);
Files.walkFileTree(getReplayFolder(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
String name = dir.getFileName().toString();
if (name.endsWith(".mcpr.cache")) {
FileUtils.deleteDirectory(dir.toFile());
return FileVisitResult.SKIP_SUBTREE;
}
return super.preVisitDirectory(dir, attrs);
}
});
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging)
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getRawReplayFolder())) {
for (Path path : paths) {
if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) {
Files.delete(path);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup cache folders 7 days after last modification or when its replay is gone
try {
File[] files = getReplayFolder().listFiles();
if (files != null) {
for (File file : files) {
String name = file.getName();
if (file.isDirectory() && name.endsWith(".mcpr.cache")) {
File replay = new File(getReplayFolder(), name.substring(0, name.length() - ".cache".length()));
if (file.lastModified() + 7 * DAYS < System.currentTimeMillis() || !replay.exists()) {
FileUtils.deleteDirectory(file);
// Move anything which is still in the recording folder into the regular replay folder
// so it can be opened and/or recovered
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getRecordingFolder())) {
for (Path path : paths) {
Path destination = getReplayFolder().resolve(path.getFileName());
if (Files.exists(destination)) {
continue; // better play it save
}
Files.move(path, destination);
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup cache folders 7 days after last modification or when its replay is gone
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getCacheFolder())) {
for (Path path : paths) {
if (Files.isDirectory(path)) {
Path replay = getReplayPathForCache(path);
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
@@ -381,14 +456,13 @@ public class ReplayMod implements
}
// Cleanup deleted corrupted replays
try {
File[] files = getReplayFolder().listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().endsWith(".mcpr.del")) {
if (file.lastModified() + 2 * DAYS < System.currentTimeMillis()) {
FileUtils.deleteDirectory(file);
}
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) {
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 2 * DAYS < System.currentTimeMillis()) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
@@ -397,14 +471,12 @@ public class ReplayMod implements
}
// Restore corrupted replays
try {
File[] files = getReplayFolder().listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().endsWith(".mcpr.tmp")) {
File origFile = new File(file.getParentFile(), Files.getNameWithoutExtension(file.getName()));
new RestoreReplayGui(GuiScreen.wrap(mc.currentScreen), origFile).display();
}
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) {
Path original = path.resolveSibling(FilenameUtils.getBaseName(name));
new RestoreReplayGui(this, GuiScreen.wrap(mc.currentScreen), original.toFile()).display();
}
}
} catch (IOException e) {
@@ -700,4 +772,17 @@ public class ReplayMod implements
return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion, MCVer.getProtocolVersion());
}
}
public ReplayFile openReplay(Path path) throws IOException {
return openReplay(path, path);
}
public ReplayFile openReplay(Path input, Path output) throws IOException {
return new ZipReplayFile(
new ReplayStudio(),
input != null ? input.toFile() : null,
output.toFile(),
getCachePathForReplay(output).toFile()
);
}
}

View File

@@ -3,6 +3,7 @@ package com.replaymod.core;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> NOTIFICATIONS = make("notifications", "notifications", true);
public static final Setting<String> RECORDING_PATH = advanced("recordingPath", null, "./replay_recordings/");
public static final Setting<String> CACHE_PATH = advanced("cachePath", null, "./.replay_cache/");
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>("core", key, displayName, defaultValue);

View File

@@ -72,7 +72,7 @@ public class SettingsRegistry {
JsonElement category = root.get(key.getCategory());
if (category != null && category.isJsonObject()) {
JsonElement valueElem = category.getAsJsonObject().get(key.getKey());
if (!valueElem.isJsonPrimitive()) continue;
if (valueElem == null || !valueElem.isJsonPrimitive()) continue;
JsonPrimitive value = valueElem.getAsJsonPrimitive();
if (key.getDefault() instanceof Boolean && value.isBoolean()) {
entry.setValue(value.getAsBoolean());

View File

@@ -1,4 +1,3 @@
//#if MC>=11400
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
@@ -14,4 +13,3 @@ public interface KeyBindingEventCallback {
void onKeybindingEvent();
}
//#endif

View File

@@ -1,4 +1,3 @@
//#if MC>=11400
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
@@ -7,11 +6,21 @@ public interface KeyEventCallback {
Event<KeyEventCallback> EVENT = Event.create((listeners) ->
(key, scanCode, action, modifiers) -> {
for (KeyEventCallback listener : listeners) {
listener.onKeyEvent(key, scanCode, action, modifiers);
if (listener.onKeyEvent(key, scanCode, action, modifiers)) {
return true;
}
}
return false;
}
);
void onKeyEvent(int key, int scanCode, int action, int modifiers);
//#if MC>=11400
int ACTION_RELEASE = org.lwjgl.glfw.GLFW.GLFW_RELEASE;
int ACTION_PRESS = org.lwjgl.glfw.GLFW.GLFW_PRESS;
//#else
//$$ int ACTION_RELEASE = 0;
//$$ int ACTION_PRESS = 1;
//#endif
boolean onKeyEvent(int key, int scanCode, int action, int modifiers);
}
//#endif

View File

@@ -38,7 +38,8 @@ public class GuiBackgroundProcesses extends EventRegistrations {
//#endif
if (guiScreen != getMinecraft().currentScreen) return; // people tend to construct GuiScreens without opening them
VanillaGuiScreen.setup(guiScreen).setLayout(new CustomLayout<GuiScreen>() {
VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(guiScreen);
vanillaGui.setLayout(new CustomLayout<GuiScreen>(vanillaGui.getLayout()) {
@Override
protected void layout(GuiScreen container, int width, int height) {
pos(panel, width - 5 - width(panel), 5);
@@ -76,6 +77,11 @@ public class GuiBackgroundProcesses extends EventRegistrations {
});
}
@Override
public int getLayer() {
return 1;
}
@Override
public ReadableDimension getMinSize() {
ReadableDimension minSize = inner.getMinSize();

View File

@@ -0,0 +1,17 @@
package com.replaymod.core.gui;
import com.replaymod.core.ReplayMod;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
public class GuiReplayButton extends GuiButton {
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
super.draw(renderer, size, renderInfo);
renderer.bindTexture(ReplayMod.LOGO_FAVICON);
renderer.drawTexturedRect(3, 3, 0, 0, size.getWidth() - 6, size.getHeight() - 6, 1, 1, 1, 1);
}
}

View File

@@ -2,32 +2,40 @@ package com.replaymod.core.gui;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.util.crash.CrashReport;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.function.Consumer;
import static com.replaymod.replaystudio.util.Utils.readInt;
import static com.replaymod.replaystudio.util.Utils.writeInt;
public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
private static Logger LOGGER = LogManager.getLogger();
public final GuiScreen parent;
public final File file;
@@ -37,8 +45,10 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
textPanel, buttonPanel).setLayout(new VerticalLayout().setSpacing(20));
public final GuiButton yesButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.yes");
public final GuiButton noButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.no");
private final ReplayMod core;
public RestoreReplayGui(GuiScreen parent, File file) {
public RestoreReplayGui(ReplayMod core, GuiScreen parent, File file) {
this.core = core;
this.parent = parent;
this.file = file;
@@ -47,12 +57,62 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())),
new GuiLabel().setI18nText("replaymod.gui.restorereplay3"));
yesButton.onClick(() -> {
recoverInBackground();
parent.display();
});
noButton.onClick(() -> {
try {
ReplayStudio studio = new ReplayStudio();
ReplayFile replayFile = new ZipReplayFile(studio, null, file);
File tmp = new File(file.getParentFile(), file.getName() + ".tmp");
File deleted = new File(file.getParentFile(), file.getName() + ".del");
if (deleted.exists()) {
FileUtils.deleteDirectory(deleted);
}
Files.move(tmp, deleted);
} catch (IOException e) {
e.printStackTrace();
}
parent.display();
});
setLayout(new CustomLayout<RestoreReplayGui>() {
@Override
protected void layout(RestoreReplayGui container, int width, int height) {
pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2);
}
});
}
@Override
protected RestoreReplayGui getThis() {
return this;
}
private void recoverInBackground() {
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK);
GuiProgressBar progressBar = new GuiProgressBar().setHeight(14);
GuiPanel savingProcess = new GuiPanel()
.setLayout(new VerticalLayout())
.addElements(new VerticalLayout.Data(0.5), label, progressBar);
new Thread(() -> {
core.runLater(() -> core.getBackgroundProcesses().addProcess(savingProcess));
try {
tryRecover(progressBar::setProgress);
} catch (IOException e) {
LOGGER.error("Recovering replay file:", e);
CrashReport crashReport = CrashReport.create(e, "Recovering replay file");
core.runLater(() -> Utils.error(LOGGER, VanillaGuiScreen.wrap(getMinecraft().currentScreen), crashReport, () -> {}));
} finally {
core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingProcess));
}
}).start();
}
private void tryRecover(Consumer<Float> progress) throws IOException {
ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath());
// Commit all not-yet-committed files into the main zip file.
// If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data!
replayFile.save();
progress.accept(0.4f);
ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration
@@ -88,37 +148,10 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
out.write(json.getBytes());
}
}
progress.accept(0.6f);
replayFile.save();
progress.accept(0.9f);
replayFile.close();
} catch (IOException e) {
e.printStackTrace();
}
parent.display();
});
noButton.onClick(() -> {
try {
File tmp = new File(file.getParentFile(), file.getName() + ".tmp");
File deleted = new File(file.getParentFile(), file.getName() + ".del");
if (deleted.exists()) {
FileUtils.deleteDirectory(deleted);
}
Files.move(tmp, deleted);
} catch (IOException e) {
e.printStackTrace();
}
parent.display();
});
setLayout(new CustomLayout<RestoreReplayGui>() {
@Override
protected void layout(RestoreReplayGui container, int width, int height) {
pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2);
}
});
}
@Override
protected RestoreReplayGui getThis() {
return this;
progress.accept(1f);
}
}

View File

@@ -1,124 +0,0 @@
package com.replaymod.core.handler;
import com.replaymod.core.mixin.GuiMainMenuAccessor;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.TitleScreen;
import net.minecraft.client.gui.screen.Screen;
import org.lwjgl.opengl.GL11;
import java.util.List;
import static com.replaymod.core.versions.MCVer.*;
//#if MC>=11400 && MC<11600
//$$ import net.minecraft.realms.RealmsScreenProxy;
//#endif
//#if FABRIC>=1
import de.johni0702.minecraft.gui.versions.callbacks.InitScreenCallback;
//#else
//$$ import net.minecraftforge.client.event.GuiScreenEvent;
//$$ import net.minecraftforge.common.MinecraftForge;
//$$ import net.minecraftforge.eventbus.api.SubscribeEvent;
//#endif
//#if MC>=11400
import net.minecraft.client.gui.widget.AbstractButtonWidget;
import net.minecraft.client.resource.language.I18n;
//#else
//$$ import net.minecraft.client.gui.GuiButton;
//#endif
/**
* Moves certain buttons on the main menu upwards so we can inject our own.
*/
public class MainMenuHandler extends EventRegistrations {
//#if FABRIC>=1
{ on(InitScreenCallback.EVENT, this::onInit); }
public void onInit(Screen guiScreen, List<AbstractButtonWidget> buttonList) {
//#else
//$$ @SubscribeEvent
//$$ public void onInit(GuiScreenEvent.InitGuiEvent.Post event) {
//$$ Screen guiScreen = getGui(event);
//$$ List<Widget> buttonList = getButtonList(event);
//#endif
if (guiScreen instanceof TitleScreen) {
TitleScreen gui = (TitleScreen) guiScreen;
int realmsOffset = 0;
//#if MC>=11400
final String BUTTON_REALMS = I18n.translate("menu.online");
for (AbstractButtonWidget button : buttonList) {
//#else
//$$ for (GuiButton button : buttonList) {
//#endif
// Buttons that aren't in a rectangle directly above our space don't need moving
if (button.x + width(button) < gui.width / 2 - 100
|| button.x > gui.width / 2 + 100
|| button.y > gui.height / 4 + 10 + 4 * 24 + 20) continue;
// Move button up to make space for one rows of buttons
// and then move back down by 10 to compensate for the space to the exit button that was already there
int offset = -1 * 24 + 10;
button.y += offset;
//#if MC>=11400
//#if MC>=11400
if (BUTTON_REALMS.equals(button.getMessage())) {
//#else
//$$ if (button.id == 14) {
//#endif
realmsOffset = offset;
}
//#endif
}
//#if MC>=11400 && MC<11600
//$$ GuiMainMenuAccessor guiA = (GuiMainMenuAccessor) gui;
//$$ if (realmsOffset != 0 && guiA.getRealmsNotification() instanceof RealmsScreenProxy) {
//$$ guiA.setRealmsNotification(new RealmsNotificationProxy((RealmsScreenProxy) guiA.getRealmsNotification(), realmsOffset));
//$$ }
//#endif
}
}
//#if MC>=11400 && MC<11600
//$$ private static class RealmsNotificationProxy extends Screen {
//$$ private final RealmsScreenProxy proxy;
//$$ private final int offset;
//$$
//$$ private RealmsNotificationProxy(RealmsScreenProxy proxy, int offset) {
//#if MC>=11400
//$$ super(null);
//#endif
//$$ this.proxy = proxy;
//$$ this.offset = offset;
//$$ }
//$$
//$$ @Override
//$$ public void init(MinecraftClient mc, int width, int height) {
//$$ proxy.init(mc, width, height);
//$$ }
//$$
//$$ @Override
//$$ public void tick() {
//$$ proxy.tick();
//$$ }
//$$
//$$ @Override
//$$ public void render(int mouseX, int mouseY, float partialTicks) {
//$$ GL11.glTranslated(0, offset, 0);
//$$ proxy.render(mouseX, mouseY - offset, partialTicks);
//$$ GL11.glTranslated(0, -offset, 0);
//$$ }
//$$
//$$ @Override
//$$ public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) {
//$$ return proxy.mouseClicked(mouseX, mouseY - offset, mouseButton);
//$$ }
//$$
//$$ @Override
//$$ public void removed() {
//$$ proxy.removed();
//$$ }
//$$ }
//#endif
}

View File

@@ -1,20 +1,51 @@
//#if MC>=11400
package com.replaymod.core.mixin;
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
import net.minecraft.client.Keyboard;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
//#if MC>=11400
import net.minecraft.client.Keyboard;
//#else
//$$ import net.minecraft.client.settings.KeyBinding;
//$$ import org.lwjgl.input.Keyboard;
//#endif
//#if MC>=11400
@Mixin(Keyboard.class)
public class MixinKeyboardListener {
@Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/options/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V", shift = At.Shift.AFTER))
private static final String ON_KEY_PRESSED = "Lnet/minecraft/client/options/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V";
@Inject(method = "onKey", at = @At(value = "INVOKE", target = ON_KEY_PRESSED), cancellable = true)
private void beforeKeyBindingTick(long windowPointer, int key, int scanCode, int action, int modifiers, CallbackInfo ci) {
if (KeyEventCallback.EVENT.invoker().onKeyEvent(key, scanCode, action, modifiers)) {
ci.cancel();
}
}
@Inject(method = "onKey", at = @At(value = "INVOKE", target = ON_KEY_PRESSED, shift = At.Shift.AFTER))
private void afterKeyBindingTick(long windowPointer, int key, int scanCode, int action, int modifiers, CallbackInfo ci) {
KeyBindingEventCallback.EVENT.invoker().onKeybindingEvent();
KeyEventCallback.EVENT.invoker().onKeyEvent(key, scanCode, action, modifiers);
}
}
//#else
//$$ @Mixin(KeyBinding.class)
//$$ public class MixinKeyboardListener {
//$$ @Inject(method = "onTick", at = @At("HEAD"), cancellable = true)
//$$ private static void beforeKeyBindingTick(CallbackInfo ci) {
//$$ int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey();
//$$ int action = Keyboard.getEventKeyState() ? KeyEventCallback.ACTION_PRESS : KeyEventCallback.ACTION_RELEASE;
//$$ if (KeyEventCallback.EVENT.invoker().onKeyEvent(keyCode, 0, action, 0)) {
//$$ ci.cancel();
//$$ }
//$$ }
//$$
//$$ @Inject(method = "onTick", at = @At("RETURN"))
//$$ private static void afterKeyBindingTick(CallbackInfo ci) {
//$$ KeyBindingEventCallback.EVENT.invoker().onKeybindingEvent();
//$$ }
//$$ }
//#endif

View File

@@ -367,4 +367,9 @@ public class Utils {
open();
}
}
public static <T> T configure(T instance, Consumer<T> configure) {
configure.accept(instance);
return instance;
}
}

View File

@@ -7,6 +7,7 @@ import com.replaymod.replaystudio.us.myles.ViaVersion.api.protocol.ProtocolVersi
import com.replaymod.replaystudio.us.myles.ViaVersion.packets.State;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.options.KeyBinding;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.client.render.Tessellator;
import net.minecraft.client.render.entity.EntityRenderDispatcher;
@@ -37,7 +38,6 @@ import java.util.ArrayList;
//#if MC>=11400
import com.replaymod.core.mixin.AbstractButtonWidgetAccessor;
import net.minecraft.SharedConstants;
import net.minecraft.client.gui.screen.TitleScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.AbstractButtonWidget;
@@ -597,13 +597,6 @@ public class MCVer {
//#endif
) {
GuiScreenAccessor acc = (GuiScreenAccessor) screen;
//#if MC>=11400
if (screen instanceof TitleScreen && isModLoaded("modmenu")) {
// Since we bypass the usual addButton, we need to manually move our buttons down
// https://github.com/Prospector/ModMenu/blob/eea70ec37581a7229142cf21df795fdacc6a7b4c/src/main/java/io/github/prospector/modmenu/mixin/TitleScreenMixin.java#L42
button.y += 12;
}
//#endif
acc.getButtons().add(button);
//#if MC>=11400
acc.getChildren().add(button);
@@ -748,6 +741,23 @@ public class MCVer {
//#endif
}
public static String getBoundKey(KeyBinding keyBinding) {
try {
//#if MC>=11600
return keyBinding.getBoundKeyLocalizedText().getString();
//#else
//#if MC>=11400
//$$ return keyBinding.getLocalizedName();
//#else
//$$ return Keyboard.getKeyName(keyBinding.getKeyCode());
//#endif
//#endif
} catch (ArrayIndexOutOfBoundsException e) {
// Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55
return "Unknown";
}
}
//#if MC>=11400
private static Boolean hasOptifine;
public static boolean hasOptifine() {
@@ -869,6 +879,10 @@ public class MCVer {
//$$ public static final int KEY_Z = org.lwjgl.input.Keyboard.KEY_Z;
//#endif
public static boolean hasControlDown() {
return Screen.hasControlDown();
}
public static boolean isKeyDown(int keyCode) {
//#if MC>=11500
return InputUtil.isKeyPressed(getMinecraft().getWindow().getHandle(), keyCode);
@@ -893,6 +907,10 @@ public class MCVer {
//$$ public static boolean getEventKeyState() {
//$$ return org.lwjgl.input.Keyboard.getEventKeyState();
//$$ }
//$$
//$$ public static String getKeyName(int code) {
//$$ return org.lwjgl.input.Keyboard.getKeyName(code);
//$$ }
//#endif
}
}

View File

@@ -5,14 +5,12 @@ import com.replaymod.core.utils.Utils;
import com.replaymod.editor.ReplayModEditor;
import com.replaymod.replay.gui.overlay.GuiMarkerTimeline;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime;
@@ -42,14 +40,14 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
private final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(300, 9);
private final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9)
private final GuiButton zoomInButton = new GuiButton().setSize(9, 9)
.onClick(() -> zoomTimeline(2d / 3d))
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 20)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin"));
private final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9)
private final GuiButton zoomOutButton = new GuiButton().setSize(9, 9)
.onClick(() -> zoomTimeline(3d / 2d))
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 30)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout"));
private final GuiPanel zoomButtonPanel = new GuiPanel()
@@ -62,7 +60,7 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
super(container);
this.inputPath = inputPath;
try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) {
try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) {
markers = replayFile.getMarkers().or(HashSet::new);
timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers);
timeline.setSize(300, 20)
@@ -149,7 +147,7 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
ProgressPopup progressPopup = new ProgressPopup(this);
new Thread(() -> {
try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) {
try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) {
replayFile.writeMarkers(markers);
replayFile.save();
} catch (IOException e) {

View File

@@ -1,8 +1,8 @@
package com.replaymod.editor.gui;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.Studio;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.filter.SquashFilter;
import com.replaymod.replaystudio.filter.StreamFilter;
@@ -11,13 +11,11 @@ import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.protocol.PacketTypeRegistry;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.stream.IteratorStream;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair;
import com.replaymod.replaystudio.util.Utils;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -50,7 +48,7 @@ public class MarkerProcessor {
public static final String MARKER_NAME_SPLIT = "_RM_SPLIT";
private static boolean hasWork(Path path) throws IOException {
try (ZipReplayFile inputReplayFile = new ZipReplayFile(new ReplayStudio(), path.toFile())) {
try (ReplayFile inputReplayFile = ReplayMod.instance.openReplay(path)) {
return inputReplayFile.getMarkers().or(HashSet::new).stream().anyMatch(m -> m.getName() != null && m.getName().startsWith("_RM_"));
}
}
@@ -106,27 +104,33 @@ public class MarkerProcessor {
return suffixes;
}
public static void apply(Path path, Consumer<Float> progress) throws IOException {
public static List<Pair<Path, ReplayMetaData>> apply(Path path, Consumer<Float> progress) throws IOException {
ReplayMod mod = ReplayMod.instance;
if (!hasWork(path)) {
return;
ReplayMetaData metaData;
try (ReplayFile inputReplayFile = mod.openReplay(path)) {
metaData = inputReplayFile.getMetaData();
}
return Collections.singletonList(new Pair<>(path, metaData));
}
String replayName = FilenameUtils.getBaseName(path.getFileName().toString());
int splitCounter = 0;
PacketTypeRegistry registry = MCVer.getPacketTypeRegistry(true);
Studio studio = new ReplayStudio();
SquashFilter squashFilter = new SquashFilter();
squashFilter.init(studio, null);
Path inputPath = path.resolveSibling("raw").resolve(path.getFileName());
List<Pair<Path, ReplayMetaData>> outputPaths = new ArrayList<>();
Path rawFolder = ReplayMod.instance.getRawReplayFolder();
Path inputPath = rawFolder.resolve(path.getFileName());
for (int i = 1; Files.exists(inputPath); i++) {
inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr");
}
Files.createDirectories(inputPath.getParent());
Files.move(path, inputPath);
try (ZipReplayFile inputReplayFile = new ZipReplayFile(studio, inputPath.toFile())) {
try (ReplayFile inputReplayFile = mod.openReplay(inputPath)) {
List<Marker> markers = inputReplayFile.getMarkers().or(HashSet::new)
.stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList());
Iterator<Marker> markerIterator = markers.iterator();
@@ -142,8 +146,8 @@ public class MarkerProcessor {
Marker nextMarker = markerIterator.next();
while (nextPacket != null && outputFileSuffixes.hasNext()) {
File outputFile = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr").toFile();
try (ZipReplayFile outputReplayFile = new ZipReplayFile(studio, null, outputFile)) {
Path outputPath = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr");
try (ReplayFile outputReplayFile = mod.openReplay(null, outputPath)) {
long duration = 0;
Set<Marker> outputMarkers = new HashSet<>();
ReplayMetaData metaData = inputReplayFile.getMetaData();
@@ -240,6 +244,8 @@ public class MarkerProcessor {
}
outputReplayFile.save();
outputPaths.add(new Pair(outputPath, metaData));
}
}
@@ -248,5 +254,7 @@ public class MarkerProcessor {
cutFilter.release();
}
}
return outputPaths;
}
}

View File

@@ -43,7 +43,7 @@ public class GuiHandler extends EventRegistrations {
replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> {
if (Utils.ifMinimalModeDoPopup(replayViewer, () -> {})) return;
try {
new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) {
new GuiEditReplay(replayViewer, replayViewer.list.getSelected().get(0).file.toPath()) {
@Override
protected void close() {
super.close();

View File

@@ -11,6 +11,8 @@ import de.johni0702.minecraft.gui.element.IGuiImage;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.effect.StatusEffects;
//#if FABRIC>=1
import com.replaymod.core.events.PreRenderCallback;
@@ -21,6 +23,7 @@ import com.replaymod.core.events.PostRenderCallback;
//#endif
public class FullBrightness extends EventRegistrations implements Extra {
private ReplayMod core;
private ReplayModReplay module;
private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 16).setSize(19, 16);
@@ -35,6 +38,7 @@ public class FullBrightness extends EventRegistrations implements Extra {
@Override
public void register(final ReplayMod mod) throws Exception {
this.core = mod;
this.module = ReplayModReplay.instance;
this.mc = mod.getMinecraft();
@@ -58,6 +62,16 @@ public class FullBrightness extends EventRegistrations implements Extra {
register();
}
public Type getType() {
String str = core.getSettingsRegistry().get(Setting.FULL_BRIGHTNESS);
for (Type type : Type.values()) {
if (type.toString().equals(str)) {
return type;
}
}
return Type.Gamma;
}
//#if FABRIC>=1
{ on(PreRenderCallback.EVENT, this::preRender); }
private void preRender() {
@@ -67,9 +81,21 @@ public class FullBrightness extends EventRegistrations implements Extra {
//$$ if (event.phase != TickEvent.Phase.START) return;
//#endif
if (active && module.getReplayHandler() != null) {
Type type = getType();
if (type == Type.Gamma || type == Type.Both) {
originalGamma = mc.options.gamma;
mc.options.gamma = 1000;
}
if (type == Type.NightVision || type == Type.Both) {
if (mc.player != null) {
mc.player.addStatusEffect(new StatusEffectInstance(StatusEffects.NIGHT_VISION
//#if MC<=10809
//$$ .id
//#endif
, Integer.MAX_VALUE));
}
}
}
}
//#if FABRIC>=1
@@ -81,8 +107,20 @@ public class FullBrightness extends EventRegistrations implements Extra {
//$$ if (event.phase != TickEvent.Phase.END) return;
//#endif
if (active && module.getReplayHandler() != null) {
Type type = getType();
if (type == Type.Gamma || type == Type.Both) {
mc.options.gamma = originalGamma;
}
if (type == Type.NightVision || type == Type.Both) {
if (mc.player != null) {
mc.player.removeStatusEffect(StatusEffects.NIGHT_VISION
//#if MC<=10809
//$$ .id
//#endif
);
}
}
}
}
{ on(ReplayOpenedCallback.EVENT, replayHandler -> updateIndicator(replayHandler.getOverlay())); }
@@ -93,4 +131,16 @@ public class FullBrightness extends EventRegistrations implements Extra {
overlay.statusIndicatorPanel.removeElement(indicator);
}
}
enum Type {
Gamma,
NightVision,
Both,
;
@Override
public String toString() {
return "replaymod.gui.settings.fullbrightness." + name().toLowerCase();
}
}
}

View File

@@ -2,31 +2,23 @@ package com.replaymod.extras;
import com.replaymod.core.KeyBindingRegistry;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.mixin.KeyBindingAccessor;
import com.replaymod.replay.events.ReplayOpenedCallback;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.LayoutData;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.resource.language.I18n;
//#if MC>=11400
//#else
//$$ import org.lwjgl.input.Keyboard;
//#endif
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -44,15 +36,15 @@ public class HotkeyButtons extends EventRegistrations implements Extra {
{ on(ReplayOpenedCallback.EVENT, replayHandler -> new Gui(mod, replayHandler.getOverlay())); }
public static final class Gui {
private final GuiTexturedButton toggleButton;
private final GuiButton toggleButton;
private final GridLayout panelLayout;
private final GuiPanel panel;
private boolean open;
public Gui(ReplayMod mod, GuiReplayOverlay overlay) {
toggleButton = new GuiTexturedButton(overlay).setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(0, 120)
toggleButton = new GuiButton(overlay).setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(0, 120)
.onClick(new Runnable() {
@Override
public void run() {
@@ -73,50 +65,51 @@ public class HotkeyButtons extends EventRegistrations implements Extra {
}.setLayout(panelLayout = new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(1));
final KeyBindingRegistry keyBindingRegistry = mod.getKeyBindingRegistry();
keyBindingRegistry.getKeyBindings().values().stream()
.sorted(Comparator.comparing(it -> I18n.translate(it.getTranslationKey())))
keyBindingRegistry.getBindings().values().stream()
.sorted(Comparator.comparing(it -> I18n.translate(it.name)))
.forEachOrdered(keyBinding -> {
GuiButton button = new GuiButton(){
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
// There doesn't seem to be an KeyBindingUpdate event, so we'll just update it every time
String keyName = "???";
try {
//#if MC>=11600
keyName = keyBinding.getBoundKeyLocalizedText().asString();
//#else
//#if MC>=11400
//$$ keyName = keyBinding.getLocalizedName();
//#else
//$$ keyName = Keyboard.getKeyName(keyBinding.getKeyCode());
//#endif
//#endif
} catch (ArrayIndexOutOfBoundsException e) {
// Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55
setLabel(keyBinding.isBound() ? keyBinding.getBoundKey() : "");
if (keyBinding.supportsAutoActivation()) {
setTooltip(new GuiTooltip().setText(new String[]{
I18n.translate("replaymod.gui.ingame.autoactivating"),
I18n.translate("replaymod.gui.ingame.autoactivating."
+ (keyBinding.isAutoActivating() ? "disable" : "enable")),
}));
setLabelColor(keyBinding.isAutoActivating() ? 0x00ff00 : 0xe0e0e0);
}
setLabel(keyName);
super.draw(renderer, size, renderInfo);
}
}.onClick(() -> {
KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding;
acc.setPressTime(acc.getPressTime() + 1);
keyBindingRegistry.handleKeyBindings();
if (keyBinding.supportsAutoActivation() && Screen.hasControlDown()) {
keyBinding.setAutoActivating(!keyBinding.isAutoActivating());
} else {
keyBinding.trigger();
}
});
panel.addElements(null, new GuiPanel().setSize(150, 20).setLayout(new HorizontalLayout().setSpacing(2))
.addElements(new HorizontalLayout.Data(0.5),
new GuiPanel().setLayout(new CustomLayout<GuiPanel>() {
GuiLabel label = new GuiLabel().setI18nText(keyBinding.name);
panel.addElements(null, new GuiPanel().setLayout(new CustomLayout<GuiPanel>() {
@Override
protected void layout(GuiPanel container, int width, int height) {
size(button, width, height);
}
width(button, Math.max(10 /* consistent min width */, width(button)) + 10 /* padding */);
height(button, 20);
@Override
public ReadableDimension calcMinSize(GuiContainer<?> container) {
return new Dimension(Math.max(10, button.getMinSize().getWidth()) + 10, 20);
int textWidth = width(label);
x(label, width(button) + 4);
width(label, width - x(label));
if (textWidth > width - x(label)) {
height(label, height(label) * 2); // split over two lines
}
}).addElements(null, button),
new GuiLabel().setI18nText(keyBinding.getTranslationKey())
));
y(label, (height - height(label)) / 2);
}
}).addElements(null, button, label).setSize(150, 20));
});
overlay.setLayout(new CustomLayout<GuiReplayOverlay>(overlay.getLayout()) {

View File

@@ -2,9 +2,18 @@ package com.replaymod.extras;
import com.replaymod.core.SettingsRegistry;
import java.util.Arrays;
import java.util.stream.Collectors;
public final class Setting<T> {
public static final SettingsRegistry.SettingKey<Boolean> ASK_FOR_OPEN_EYE =
new SettingsRegistry.SettingKeys<>("advanced", "askForOpenEye", null, true);
public static final SettingsRegistry.SettingKey<Boolean> SKIP_POST_SCREENSHOT_GUI =
new SettingsRegistry.SettingKeys<>("advanced", "skipPostScreenshotGui", null, false);
public static final SettingsRegistry.MultipleChoiceSettingKeys<String> FULL_BRIGHTNESS = new SettingsRegistry.MultipleChoiceSettingKeys<>(
"advanced", "fullBrightness", "replaymod.gui.settings.fullbrightness",
FullBrightness.Type.Gamma.toString());
static {
FULL_BRIGHTNESS.setChoices(Arrays.stream(FullBrightness.Type.values()).map(Object::toString).collect(Collectors.toList()));
}
}

View File

@@ -20,6 +20,6 @@ public class AdvancedScreenshots implements Extra {
}
private void takeScreenshot() {
ReplayMod.instance.runLater(() -> new GuiCreateScreenshot(mod).display());
ReplayMod.instance.runLater(() -> new GuiCreateScreenshot(mod).open());
}
}

View File

@@ -27,7 +27,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable {
private final ReplayMod mod;
public GuiCreateScreenshot(ReplayMod mod) {
super(null, null);
super(GuiRenderSettings.createBaseScreen(), null, null);
this.mod = mod;
@@ -64,7 +64,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable {
} catch (Throwable t) {
error(LOGGER, GuiCreateScreenshot.this, CrashReport.create(t, "Rendering video"), () -> {});
display(); // Re-show the render settings gui and the new error popup
getScreen().display(); // Re-show the render settings gui and the new error popup
}
});
});
@@ -75,6 +75,18 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable {
return container;
}
@Override
public void open() {
super.open();
getScreen().display();
}
@Override
public void close() {
super.close();
getMinecraft().openScreen(null);
}
@Override
public void load() {
// pause replay when opening this gui

View File

@@ -4,7 +4,8 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.versions.MCVer;
import com.replaymod.extras.ReplayModExtras;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.rendering.Channel;
import com.replaymod.render.rendering.FrameConsumer;
import com.replaymod.replay.ReplayModReplay;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
@@ -13,8 +14,9 @@ import net.minecraft.util.crash.CrashReport;
import java.io.File;
import java.io.IOException;
import java.util.Map;
public class ScreenshotWriter implements FrameConsumer<RGBFrame> {
public class ScreenshotWriter implements FrameConsumer<BitmapFrame> {
private final File outputFile;
@@ -23,7 +25,9 @@ public class ScreenshotWriter implements FrameConsumer<RGBFrame> {
}
@Override
public void consume(RGBFrame frame) {
public void consume(Map<Channel, BitmapFrame> channels) {
BitmapFrame frame = channels.get(Channel.BRGA);
// skip the first frame, in which not all chunks are properly loaded
if (frame.getFrameId() == 0) return;

View File

@@ -122,6 +122,11 @@ public class ModCoreInstaller {
public static int initialize(File gameDir, String minecraftVersion) {
if (isInitalized()) return -1;
JsonHolder jsonHolder = fetchJSON(VERSION_URL);
if (!jsonHolder.has(minecraftVersion)) {
System.out.println("No ModCore target for " + minecraftVersion + ". This in fine, unless you're specifically looking for ModCore.");
return -2;
}
dataDir = new File(gameDir, "modcore");
if (!dataDir.exists()) {
if (!dataDir.mkdirs()) {
@@ -129,11 +134,6 @@ public class ModCoreInstaller {
return 1;
}
}
JsonHolder jsonHolder = fetchJSON(VERSION_URL);
if (!jsonHolder.has(minecraftVersion)) {
System.out.println("No ModCore target for " + minecraftVersion + ". This in fine, unless you're specifically looking for ModCore.");
return -2;
}
String latestRemote = jsonHolder.optString(minecraftVersion);
boolean failed = jsonHolder.getKeys().size() == 0 || (jsonHolder.has("success") && !jsonHolder.optBoolean("success"));

View File

@@ -5,10 +5,15 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.gui.GuiRenderQueue;
import com.replaymod.render.gui.GuiRenderSettings;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization;
import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
@@ -18,32 +23,46 @@ import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
import de.johni0702.minecraft.gui.element.IGuiButton;
import de.johni0702.minecraft.gui.function.Closeable;
import de.johni0702.minecraft.gui.function.Typeable;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.Consumer;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import net.minecraft.util.crash.CrashReport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static de.johni0702.minecraft.gui.versions.MCVer.getClipboardString;
import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString;
/**
* Gui for loading and saving {@link Timeline Timelines}.
*/
public class GuiKeyframeRepository extends GuiScreen implements Closeable {
public class GuiKeyframeRepository extends GuiScreen implements Closeable, Typeable {
private static final Logger LOGGER = LogManager.getLogger();
public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT);
public final GuiLabel title = new GuiLabel(contentPanel).setI18nText("replaymod.gui.keyframerepository.title");
public final GuiVerticalList list = new GuiVerticalList(contentPanel).setDrawShadow(true).setDrawSlider(true);
public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(5));
public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() {
public final GuiButton overwriteButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
@@ -51,14 +70,16 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
Utils.addCallback(popup.getFuture(), doIt -> {
if (doIt) {
timelines.put(selectedEntry.name, currentTimeline);
for (Entry entry : selectedEntries) {
timelines.put(entry.name, currentTimeline);
}
overwriteButton.setDisabled();
save();
}
}, Throwable::printStackTrace);
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled();
public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() {
public final GuiButton saveAsButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true);
@@ -100,12 +121,12 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
});
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.saveas");
public final GuiButton loadButton = new GuiButton(buttonPanel).onClick(new Runnable() {
public final GuiButton loadButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
getMinecraft().openScreen(null);
try {
Timeline timeline = timelines.get(selectedEntry.name);
Timeline timeline = timelines.get(selectedEntries.iterator().next().name);
for (Path path : timeline.getPaths()) {
path.updateAll();
}
@@ -115,9 +136,10 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
}
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.load").setDisabled();
public final GuiButton renameButton = new GuiButton(buttonPanel).onClick(new Runnable() {
public final GuiButton renameButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
Entry selectedEntry = selectedEntries.iterator().next();
final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(selectedEntry.name);
final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
new GuiLabel().setI18nText("replaymod.gui.rename").setColor(Colors.BLACK),
@@ -158,7 +180,7 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
});
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.rename").setDisabled();
public final GuiButton removeButton = new GuiButton(buttonPanel).onClick(new Runnable() {
public final GuiButton removeButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
@@ -168,14 +190,13 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
@Override
public void onSuccess(Boolean delete) {
if (delete) {
timelines.remove(selectedEntry.name);
list.getListPanel().removeElement(selectedEntry);
for (Entry entry : selectedEntries) {
timelines.remove(entry.name);
list.getListPanel().removeElement(entry);
}
selectedEntry = null;
overwriteButton.setDisabled();
loadButton.setDisabled();
renameButton.setDisabled();
removeButton.setDisabled();
selectedEntries.clear();
updateButtons();
save();
}
}
@@ -188,13 +209,102 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.remove").setDisabled();
public final GuiButton copyButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
Map<String, Timeline> toBeSerialized = new HashMap<>();
for (Entry entry : selectedEntries) {
toBeSerialized.put(entry.name, timelines.get(entry.name));
}
try {
TimelineSerialization serialization = new TimelineSerialization(registry, null);
setClipboardString(serialization.serialize(toBeSerialized));
} catch (Throwable t) {
t.printStackTrace();
CrashReport report = CrashReport.create(t, "Copying timeline(s)");
Utils.error(LOGGER, GuiKeyframeRepository.this, report, () -> {});
}
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.copy").setDisabled();
public final GuiButton pasteButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
try {
TimelineSerialization serialization = new TimelineSerialization(registry, null);
for (Map.Entry<String, Timeline> entry : serialization.deserialize(getClipboardString()).entrySet()) {
String name = entry.getKey();
while (timelines.containsKey(name)) {
name += " (Copy)";
}
timelines.put(name, entry.getValue());
list.getListPanel().addElements(null, new Entry(name));
}
save();
} catch (Throwable t) {
// Intentionally not making a fuzz about it cause this will likely happen when they have anything
// else in their clipboard.
// If it's actually not working, they'll go complain and we'll just check the log.
t.printStackTrace();
}
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.paste");
public final GuiButton addToQueueButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler();
GuiRenderQueue queue = new GuiRenderQueue(GuiKeyframeRepository.this, replayHandler, null);
queue.open();
Set<String> selected = selectedEntries.stream().map(e -> e.name).collect(Collectors.toSet());
ArrayDeque<Map.Entry<String, Timeline>> toBeAdded = new ArrayDeque<>();
// Iterating over timelines to get consistent ordering (cause selectedEntries is an unordered set)
// and we need to get the Timelines anyway.
for (Map.Entry<String, Timeline> entry : timelines.entrySet()) {
if (selected.contains(entry.getKey())) {
toBeAdded.offerLast(entry);
}
}
new Runnable() {
@Override
public void run() {
Map.Entry<String, Timeline> entry = toBeAdded.pollFirst();
if (entry == null) {
return;
}
String name = entry.getKey();
Timeline timeline = entry.getValue();
GuiRenderSettings settingsGui = queue.addJob(timeline);
settingsGui.buttonPanel.removeElement(settingsGui.renderButton);
settingsGui.setOutputFileBaseName(name);
Runnable orgOnClick = settingsGui.queueButton.getOnClick();
settingsGui.queueButton.onClick(() -> {
orgOnClick.run();
this.run();
});
settingsGui.open();
}
}.run();
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.rendersettings.addtoqueue");
public final GuiPanel buttonPanel = new GuiPanel(contentPanel)
.setLayout(new GridLayout()
.setColumns(4)
.setSpacingX(5)
.setSpacingY(5))
.addElements(null,
overwriteButton, saveAsButton, renameButton, removeButton,
loadButton, addToQueueButton, copyButton, pasteButton);
private final Map<String, Timeline> timelines = new LinkedHashMap<>();
private final Timeline currentTimeline;
private final SettableFuture<Timeline> future = SettableFuture.create();
private final PathingRegistry registry;
private final ReplayFile replayFile;
private Entry selectedEntry;
private final Set<Entry> selectedEntries = new HashSet<>();
{
setBackground(Background.NONE);
@@ -232,6 +342,19 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
if (entry.getKey().isEmpty()) continue; // don't show auto-save slot
list.getListPanel().addElements(null, new Entry(entry.getKey()));
}
updateButtons();
}
private void updateButtons() {
int selected = selectedEntries.size();
overwriteButton.setEnabled(selected >= 1);
loadButton.setEnabled(selected == 1);
renameButton.setEnabled(selected == 1);
removeButton.setEnabled(selected >= 1);
copyButton.setEnabled(selected >= 1);
addToQueueButton.setEnabled(selected >= 1);
}
@Override
@@ -258,6 +381,33 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
}
}
@Override
public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) {
if (MCVer.Keyboard.hasControlDown()) {
switch (keyCode) {
case MCVer.Keyboard.KEY_A:
if (selectedEntries.size() < timelines.size()) {
for (GuiElement<?> child : list.getListPanel().getChildren()) {
if (child instanceof Entry) {
selectedEntries.add((Entry) child);
}
}
} else {
selectedEntries.clear();
}
updateButtons();
return true;
case MCVer.Keyboard.KEY_C:
copyButton.onClick();
return true;
case MCVer.Keyboard.KEY_V:
pasteButton.onClick();
return true;
}
}
return false;
}
public class Entry extends AbstractGuiClickableContainer<Entry> {
public final GuiLabel label = new GuiLabel(this);
private String name;
@@ -281,13 +431,20 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
@Override
protected void onClick() {
selectedEntry = this;
buttonPanel.forEach(IGuiButton.class).setEnabled();
if (!MCVer.Keyboard.hasControlDown()) {
selectedEntries.clear();
}
if (selectedEntries.contains(this)) {
selectedEntries.remove(this);
} else {
selectedEntries.add(this);
}
updateButtons();
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
if (selectedEntry == this) {
if (selectedEntries.contains(this)) {
renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK);
renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE);
}

View File

@@ -30,6 +30,7 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations {
private final ReplayHandler replayHandler;
private Timeline timeline;
protected long startOffset;
private boolean wasAsyncMode;
private long lastTime;
private long lastTimestamp;
private ListenableFuture<Void> future;
@@ -69,6 +70,7 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations {
}.max(iter).getTime();
}
wasAsyncMode = replayHandler.getReplaySender().isAsyncMode();
replayHandler.getReplaySender().setSyncModeAndWait();
register();
lastTime = 0;
@@ -103,7 +105,9 @@ public abstract class AbstractTimelinePlayer extends EventRegistrations {
MinecraftAccessor mcA = (MinecraftAccessor) mc;
mcA.setTimer(((ReplayTimer) mcA.getTimer()).getWrapped());
replayHandler.getReplaySender().setReplaySpeed(0);
if (wasAsyncMode) {
replayHandler.getReplaySender().setAsyncMode(true);
}
unregister();
return;
}

View File

@@ -0,0 +1,17 @@
package com.replaymod.recording;
import net.minecraft.client.network.ServerInfo;
/** Extension interface for {@link net.minecraft.client.network.ServerInfo}. */
public interface ServerInfoExt {
static ServerInfoExt from(ServerInfo base) {
return (ServerInfoExt) base;
}
/** Per-server optional overwrite for {@link Setting#AUTO_START_RECORDING}. */
Boolean getAutoRecording();
/** Per-server optional overwrite for {@link Setting#AUTO_START_RECORDING}. */
void setAutoRecording(Boolean autoRecording);
}

View File

@@ -69,11 +69,11 @@ public class GuiRecordingControls extends EventRegistrations {
updateState();
}).setSize(98, 20);
public GuiRecordingControls(ReplayMod core, PacketListener packetListener) {
public GuiRecordingControls(ReplayMod core, PacketListener packetListener, boolean autoStart) {
this.core = core;
this.packetListener = packetListener;
paused = stopped = !core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING);
paused = stopped = !autoStart;
updateState();
}
@@ -107,7 +107,8 @@ public class GuiRecordingControls extends EventRegistrations {
.orElse(MCVer.findButton(buttonList, "menu.disconnect", 1))
.<Function<Integer, Integer>>map(it -> (height) -> it.y)
.orElse((height) -> height / 4 + 120 - 16);
VanillaGuiScreen.setup(guiScreen).setLayout(new CustomLayout<de.johni0702.minecraft.gui.container.GuiScreen>() {
VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(guiScreen);
vanillaGui.setLayout(new CustomLayout<de.johni0702.minecraft.gui.container.GuiScreen>(vanillaGui.getLayout()) {
@Override
protected void layout(de.johni0702.minecraft.gui.container.GuiScreen container, int width, int height) {
pos(panel, width / 2 - 100, yPos.apply(height) + 16 + 8);

View File

@@ -0,0 +1,161 @@
package com.replaymod.recording.gui;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.function.Focusable;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.crash.CrashReport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static com.replaymod.core.versions.MCVer.getMinecraft;
import static de.johni0702.minecraft.gui.utils.Utils.link;
public class GuiSavingReplay {
private static final MinecraftClient mc = getMinecraft();
private static final Logger logger = LogManager.getLogger();
private final GuiLabel label = new GuiLabel()
.setI18nText("replaymod.gui.replaysaving.title")
.setColor(Colors.BLACK);
private final GuiProgressBar progressBar = new GuiProgressBar()
.setHeight(14);
private final GuiPanel panel = new GuiPanel()
.setLayout(new VerticalLayout().setSpacing(2))
.addElements(new VerticalLayout.Data(0.5), label, progressBar);
private final ReplayMod core;
private final List<Runnable> apply = new ArrayList<>();
public GuiSavingReplay(ReplayMod core) {
this.core = core;
}
public void open() {
core.getBackgroundProcesses().addProcess(panel);
}
public void close() {
core.getBackgroundProcesses().removeProcess(panel);
AbstractGuiScreen<?> currentScreen = GuiScreen.from(mc.currentScreen);
if (currentScreen instanceof GuiReplayViewer) {
((GuiReplayViewer) currentScreen).list.load();
}
}
public GuiProgressBar getProgressBar() {
return progressBar;
}
public void presentRenameDialog(List<Pair<Path, ReplayMetaData>> outputPaths) {
panel.removeElement(progressBar);
link(outputPaths.stream().map(it -> addOutput(it.getKey(), it.getValue())).toArray(Focusable[]::new));
GuiButton applyButton = new GuiButton()
.setSize(150, 20)
.setI18nLabel("replaymod.gui.done")
.onClick(() -> {
apply.forEach(Runnable::run);
close();
});
panel.addElements(new VerticalLayout.Data(0.5), applyButton);
}
private GuiTextField addOutput(Path path, ReplayMetaData metaData) {
String originalName = Utils.fileNameToReplayName(path.getFileName().toString());
GuiTextField textField = new GuiTextField()
.setSize(130, 20)
.setText(originalName)
.setI18nHint("replaymod.gui.delete")
.setTextColorDisabled(Colors.RED)
.setTooltip(createTooltip(path, metaData));
GuiButton clearButton = new GuiButton()
.setSize(20, 20)
.setLabel("X")
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.delete"))
.onClick(() -> textField.setText(""));
GuiPanel row = new GuiPanel()
.setLayout(new HorizontalLayout())
.addElements(null, textField, clearButton);
panel.addElements(new VerticalLayout.Data(0.5), row);
apply.add(() -> applyOutput(path, textField.getText()));
return textField;
}
private GuiPanel createTooltip(Path path, ReplayMetaData metaData) {
GuiTooltip tooltip = new GuiTooltip();
GuiReplayViewer.GuiReplayEntry entry = new GuiReplayViewer.GuiReplayEntry(path.toFile(), metaData, null, new ArrayList<>());
return new GuiPanel().setLayout(new CustomLayout<GuiPanel>() {
@Override
protected void layout(GuiPanel container, int width, int height) {
pos(entry, 4, 4);
size(entry, width - 8, height - 8);
size(tooltip, width, height);
}
@Override
public ReadableDimension calcMinSize(GuiContainer<?> container) {
ReadableDimension size = entry.calcMinSize();
return new Dimension(size.getWidth() + 8, size.getHeight() + 8);
}
}).addElements(null, tooltip, entry);
}
private void applyOutput(Path path, String newName) {
if (newName.isEmpty()) {
try {
Files.delete(path);
} catch (IOException e) {
logger.error("Deleting replay file:", e);
CrashReport crashReport = CrashReport.create(e, "Deleting replay file");
core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, () -> {}));
}
return;
}
try {
Path replaysFolder = core.getReplayFolder();
Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName));
for (int i = 1; Files.exists(newPath); i++) {
newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")"));
}
Files.move(path, newPath);
} catch (IOException e) {
logger.error("Renaming replay file:", e);
CrashReport crashReport = CrashReport.create(e, "Renaming replay file");
core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, () -> {}));
}
}
}

View File

@@ -4,6 +4,7 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.ModCompat;
import com.replaymod.core.utils.Utils;
import com.replaymod.editor.gui.MarkerProcessor;
import com.replaymod.recording.ServerInfoExt;
import com.replaymod.recording.Setting;
import com.replaymod.recording.gui.GuiRecordingControls;
import com.replaymod.recording.gui.GuiRecordingOverlay;
@@ -11,10 +12,10 @@ import com.replaymod.recording.mixin.NetworkManagerAccessor;
import com.replaymod.recording.packet.PacketListener;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import io.netty.channel.Channel;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ServerInfo;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.network.ClientConnection;
import org.apache.logging.log4j.Logger;
@@ -30,7 +31,7 @@ import net.minecraft.world.World;
//#endif
//#endif
import java.io.File;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@@ -89,14 +90,26 @@ public class ConnectionEventHandler {
}
String worldName;
String serverName = null;
boolean autoStart = core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING);
if (local) {
//#if MC>=11600
worldName = mc.getServer().getSaveProperties().getLevelName();
//#else
//$$ worldName = mc.getServer().getLevelName();
//#endif
serverName = worldName;
} else if (mc.getCurrentServerEntry() != null) {
worldName = mc.getCurrentServerEntry().address;
ServerInfo serverInfo = mc.getCurrentServerEntry();
worldName = serverInfo.address;
if (!I18n.translate("selectServer.defaultName").equals(serverInfo.name)) {
serverName = serverInfo.name;
}
Boolean autoStartServer = ServerInfoExt.from(serverInfo).getAutoRecording();
if (autoStartServer != null) {
autoStart = autoStartServer;
}
//#if MC>=11100
} else if (mc.isConnectedToRealms()) {
// we can't access the server name without tapping too deep in the Realms Library
@@ -107,34 +120,38 @@ public class ConnectionEventHandler {
return;
}
File folder = core.getReplayFolder();
if (ReplayMod.isMinimalMode()) {
// Recording controls are not supported in minimal mode, so always auto-start
autoStart = true;
}
String name = sdf.format(Calendar.getInstance().getTime());
File currentFile = new File(folder, Utils.replayNameToFileName(name));
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile);
Path outputPath = core.getRecordingFolder().resolve(Utils.replayNameToFileName(name));
ReplayFile replayFile = core.openReplay(outputPath);
replayFile.writeModInfo(ModCompat.getInstalledNetworkMods());
ReplayMetaData metaData = new ReplayMetaData();
metaData.setSingleplayer(local);
metaData.setServerName(worldName);
metaData.setCustomServerName(serverName);
metaData.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion());
metaData.setDate(System.currentTimeMillis());
metaData.setMcVersion(ReplayMod.getMinecraftVersion());
packetListener = new PacketListener(core, currentFile.toPath(), replayFile, metaData);
packetListener = new PacketListener(core, outputPath, replayFile, metaData);
Channel channel = ((NetworkManagerAccessor) networkManager).getChannel();
channel.pipeline().addBefore(packetHandlerKey, "replay_recorder", packetListener);
recordingEventHandler = new RecordingEventHandler(packetListener);
recordingEventHandler.register();
guiControls = new GuiRecordingControls(core, packetListener);
guiControls = new GuiRecordingControls(core, packetListener, autoStart);
guiControls.register();
guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls);
guiOverlay.register();
if (core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING) || ReplayMod.isMinimalMode()) {
if (autoStart) {
core.printInfoToChat("replaymod.chat.recordingstarted");
} else {
packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0);

View File

@@ -2,14 +2,23 @@ package com.replaymod.recording.handler;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.SettingsRegistry;
import com.replaymod.core.gui.GuiReplayButton;
import com.replaymod.recording.ServerInfoExt;
import com.replaymod.recording.Setting;
import com.replaymod.recording.mixin.AddServerScreenAccessor;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiToggleButton;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.client.gui.screen.AddServerScreen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
import net.minecraft.client.network.ServerInfo;
import net.minecraft.client.resource.language.I18n;
//#if FABRIC>=1
import de.johni0702.minecraft.gui.versions.callbacks.InitScreenCallback;
@@ -51,7 +60,8 @@ public class GuiHandler extends EventRegistrations {
settingsRegistry.save();
});
VanillaGuiScreen.setup(gui).setLayout(new CustomLayout<GuiScreen>() {
VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(gui);
vanillaGui.setLayout(new CustomLayout<GuiScreen>(vanillaGui.getLayout()) {
@Override
protected void layout(GuiScreen container, int width, int height) {
//size(recordingCheckbox, 200, 20);
@@ -59,5 +69,34 @@ public class GuiHandler extends EventRegistrations {
}
}).addElements(null, recordingCheckbox);
}
if (gui instanceof AddServerScreen) {
VanillaGuiScreen vanillaGui = VanillaGuiScreen.wrap(gui);
GuiButton replayButton = new GuiReplayButton().onClick(() -> {
ServerInfo serverInfo = ((AddServerScreenAccessor) gui).getServer();
ServerInfoExt serverInfoExt = ServerInfoExt.from(serverInfo);
Boolean state = serverInfoExt.getAutoRecording();
GuiToggleButton<String> autoRecording = new GuiToggleButton<String>()
.setI18nLabel("replaymod.gui.settings.autostartrecording")
.setValues(
I18n.translate("replaymod.gui.settings.default"),
I18n.translate("options.off"),
I18n.translate("options.on")
)
.setSelected(state == null ? 0 : state ? 2 : 1);
autoRecording.onClick(() -> {
int selected = autoRecording.getSelected();
serverInfoExt.setAutoRecording(selected == 0 ? null : selected == 2);
});
GuiInfoPopup.open(vanillaGui, autoRecording);
});
vanillaGui.setLayout(new CustomLayout<GuiScreen>(vanillaGui.getLayout()) {
@Override
protected void layout(GuiScreen container, int width, int height) {
size(replayButton, 20, 20);
pos(replayButton, width - width(replayButton) - 5, 5);
}
}).addElements(null, replayButton);
}
}
}

View File

@@ -0,0 +1,12 @@
package com.replaymod.recording.mixin;
import net.minecraft.client.gui.screen.AddServerScreen;
import net.minecraft.client.network.ServerInfo;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(AddServerScreen.class)
public interface AddServerScreenAccessor {
@Accessor
ServerInfo getServer();
}

View File

@@ -0,0 +1,47 @@
package com.replaymod.recording.mixin;
import com.replaymod.recording.ServerInfoExt;
import net.minecraft.client.network.ServerInfo;
import net.minecraft.nbt.CompoundTag;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerInfo.class)
public abstract class MixinServerInfo implements ServerInfoExt {
private Boolean autoRecording;
@Override
public Boolean getAutoRecording() {
return autoRecording;
}
@Override
public void setAutoRecording(Boolean autoRecording) {
this.autoRecording = autoRecording;
}
@Inject(method = "serialize", at = @At("RETURN"))
private void serialize(CallbackInfoReturnable<CompoundTag> ci) {
CompoundTag tag = ci.getReturnValue();
if (autoRecording != null) {
tag.putBoolean("autoRecording", autoRecording);
}
}
@Inject(method = "deserialize", at = @At("RETURN"))
private static void deserialize(CompoundTag tag, CallbackInfoReturnable<ServerInfo> ci) {
ServerInfoExt serverInfo = ServerInfoExt.from(ci.getReturnValue());
if (tag.contains("autoRecording")) {
serverInfo.setAutoRecording(tag.getBoolean("autoRecording"));
}
}
@Inject(method = "copyFrom", at = @At("RETURN"))
public void copyFrom(ServerInfo serverInfo, CallbackInfo ci) {
ServerInfoExt from = ServerInfoExt.from(serverInfo);
this.autoRecording = from.getAutoRecording();
}
}

View File

@@ -11,6 +11,7 @@ import com.replaymod.core.versions.MCVer;
import com.replaymod.editor.gui.MarkerProcessor;
import com.replaymod.recording.ReplayModRecording;
import com.replaymod.recording.Setting;
import com.replaymod.recording.gui.GuiSavingReplay;
import com.replaymod.recording.handler.ConnectionEventHandler;
import com.replaymod.recording.mixin.SPacketSpawnMobAccessor;
import com.replaymod.recording.mixin.SPacketSpawnPlayerAccessor;
@@ -19,9 +20,8 @@ import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.utils.Colors;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -67,7 +67,9 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@@ -245,9 +247,9 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
}
});
GuiLabel savingLabel = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK);
GuiSavingReplay guiSavingReplay = new GuiSavingReplay(core);
new Thread(() -> {
core.runLater(() -> core.getBackgroundProcesses().addProcess(savingLabel));
core.runLater(guiSavingReplay::open);
saveService.shutdown();
try {
@@ -261,22 +263,26 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
logger.error("Failed to close packet output stream:", e);
}
List<Pair<Path, ReplayMetaData>> outputPaths;
synchronized (replayFile) {
try {
replayFile.save();
replayFile.close();
if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS) && !ReplayMod.isMinimalMode()) {
MarkerProcessor.apply(outputPath, progress -> {});
outputPaths = MarkerProcessor.apply(outputPath, guiSavingReplay.getProgressBar()::setProgress);
} else {
outputPaths = Collections.singletonList(new Pair<>(outputPath, metaData));
}
} catch (Exception e) {
logger.error("Saving replay file:", e);
CrashReport crashReport = CrashReport.create(e, "Saving replay file");
core.runLater(() -> Utils.error(logger, VanillaGuiScreen.setup(mc.currentScreen), crashReport, () -> {}));
core.runLater(() -> Utils.error(logger, VanillaGuiScreen.wrap(mc.currentScreen), crashReport, guiSavingReplay::close));
return;
}
}
core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingLabel));
core.runLater(() -> guiSavingReplay.presentRenameDialog(outputPaths));
}).start();
}

View File

@@ -0,0 +1,227 @@
package com.replaymod.render;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replay.camera.CameraEntity;
import de.javagl.jgltf.impl.v2.Accessor;
import de.javagl.jgltf.impl.v2.Animation;
import de.javagl.jgltf.impl.v2.AnimationChannel;
import de.javagl.jgltf.impl.v2.AnimationChannelTarget;
import de.javagl.jgltf.impl.v2.AnimationSampler;
import de.javagl.jgltf.impl.v2.Asset;
import de.javagl.jgltf.impl.v2.Buffer;
import de.javagl.jgltf.impl.v2.BufferView;
import de.javagl.jgltf.impl.v2.Camera;
import de.javagl.jgltf.impl.v2.CameraPerspective;
import de.javagl.jgltf.impl.v2.GlTF;
import de.javagl.jgltf.impl.v2.Node;
import de.javagl.jgltf.model.io.v2.GltfAssetV2;
import de.javagl.jgltf.model.io.v2.GltfAssetWriterV2;
import de.johni0702.minecraft.gui.utils.lwjgl.vector.Quaternion;
import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector4f;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import org.apache.commons.io.FilenameUtils;
import org.lwjgl.opengl.GL11;
//#if MC>=11400
import net.minecraft.util.math.Vec3d;
//#else
//#endif
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import static com.replaymod.core.utils.Utils.configure;
public class CameraPathExporter {
private final MinecraftClient mc = MCVer.getMinecraft();
private final RenderSettings settings;
private int framesDone;
private ByteBuffer timeBuffer;
private ByteBuffer cameraTranslationBuffer;
private ByteBuffer cameraRotationBuffer;
public CameraPathExporter(RenderSettings settings) {
this.settings = settings;
}
public void setup(int totalFrames) {
timeBuffer = ByteBuffer.allocate(4 * totalFrames).order(ByteOrder.LITTLE_ENDIAN);
cameraTranslationBuffer = ByteBuffer.allocate(4 * totalFrames * 3).order(ByteOrder.LITTLE_ENDIAN);
cameraRotationBuffer = ByteBuffer.allocate(4 * totalFrames * 4).order(ByteOrder.LITTLE_ENDIAN);
}
public void recordFrame(float tickDelta) {
//#if MC>=10800
Entity entity = mc.getCameraEntity() == null ? mc.player : mc.getCameraEntity();
//#else
//$$ Entity entity = mc.renderViewEntity == null ? mc.thePlayer : mc.renderViewEntity;
//#endif
//#if MC>=11400
net.minecraft.client.render.Camera camera = mc.gameRenderer.getCamera();
Vec3d vec = camera.getPos();
float x = (float) vec.getX();
float y = (float) vec.getY();
float z = (float) vec.getZ();
float yaw = camera.getYaw() + 180;
float pitch = camera.getPitch();
//#else
//#if MC>=10800
//$$ float eyeHeight = entity.getEyeHeight();
//#else
//$$ float eyeHeight = 1.62f - entity.yOffset;
//#endif
//$$ float x = (float) (entity.prevPosX + (entity.posX - entity.prevPosX) * tickDelta);
//$$ float y = (float) (entity.prevPosY + (entity.posY - entity.prevPosY) * tickDelta + eyeHeight);
//$$ float z = (float) (entity.prevPosZ + (entity.posZ - entity.prevPosZ) * tickDelta);
//$$ float yaw = entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * tickDelta + 180;
//$$ float pitch = entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * tickDelta;
//#endif
float roll = entity instanceof CameraEntity ? ((CameraEntity) entity).roll : 0;
Quaternion quatYaw = new Quaternion();
Quaternion quatPitch = new Quaternion();
Quaternion quatRoll = new Quaternion();
quatYaw.setFromAxisAngle(new Vector4f(0, -1, 0, (float) Math.toRadians(yaw)));
quatPitch.setFromAxisAngle(new Vector4f(-1, 0, 0, (float) Math.toRadians(pitch)));
quatRoll.setFromAxisAngle(new Vector4f(0, 0, 1, (float) Math.toRadians(roll)));
Quaternion quaternion = new Quaternion(0, 0, 0, 1);
Quaternion.mul(quaternion, quatYaw, quaternion);
Quaternion.mul(quaternion, quatPitch, quaternion);
Quaternion.mul(quaternion, quatRoll, quaternion);
quaternion.normalise(quaternion);
float[] translation = new float[] { x, y, z };
float[] rotation = new float[] { quaternion.getX(), quaternion.getY(), quaternion.getZ(), quaternion.getW() };
timeBuffer.putFloat(framesDone / (float) settings.getFramesPerSecond());
for (float f : translation) {
cameraTranslationBuffer.putFloat(f);
}
for (float f : rotation) {
cameraRotationBuffer.putFloat(f);
}
framesDone++;
}
public void finish() throws IOException {
int timeBufferSize = timeBuffer.rewind().remaining();
int cameraTranslationBufferSize = cameraTranslationBuffer.rewind().remaining();
int cameraRotationBufferSize = cameraRotationBuffer.rewind().remaining();
int binaryDataSize = 0;
binaryDataSize += timeBufferSize;
binaryDataSize += cameraTranslationBufferSize;
binaryDataSize += cameraRotationBufferSize;
ByteBuffer binaryData = ByteBuffer.allocate(binaryDataSize);
int timeBufferOffset = binaryData.position();
binaryData.put(timeBuffer);
int cameraTranslationBufferOffset = binaryData.position();
binaryData.put(cameraTranslationBuffer);
int cameraRotationBufferOffset = binaryData.position();
binaryData.put(cameraRotationBuffer);
binaryData.rewind();
GlTF glTF = new GlTF();
glTF.setAsset(configure(new Asset(), asset -> {
asset.setVersion("2.0");
asset.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion());
}));
glTF.addAnimations(configure(new Animation(), animation -> {
animation.addChannels(configure(new AnimationChannel(), channel -> {
channel.setTarget(configure(new AnimationChannelTarget(), target -> {
target.setNode(0);
target.setPath("translation");
}));
channel.setSampler(0);
}));
animation.addChannels(configure(new AnimationChannel(), channel -> {
channel.setTarget(configure(new AnimationChannelTarget(), target -> {
target.setNode(0);
target.setPath("rotation");
}));
channel.setSampler(1);
}));
animation.addSamplers(configure(new AnimationSampler(), sampler -> {
sampler.setInput(0);
sampler.setOutput(1);
}));
animation.addSamplers(configure(new AnimationSampler(), sampler -> {
sampler.setInput(0);
sampler.setOutput(2);
}));
}));
glTF.addCameras(configure(new Camera(), camera -> {
camera.setType("perspective");
camera.setPerspective(configure(new CameraPerspective(), perspective -> {
float aspectRatio = (float) settings.getVideoWidth() / (float) settings.getVideoHeight();
perspective.setAspectRatio(aspectRatio);
perspective.setYfov((float) Math.toRadians(mc.options.fov));
perspective.setZnear(0.05f);
perspective.setZfar((float) mc.options.viewDistance * 16 * 4);
}));
}));
glTF.addNodes(configure(new Node(), node -> node.setCamera(0)));
glTF.addBuffers(configure(new Buffer(), buffer -> buffer.setByteLength(binaryData.limit())));
// Time
glTF.addBufferViews(configure(new BufferView(), bufferView -> {
bufferView.setBuffer(0);
bufferView.setByteOffset(timeBufferOffset);
bufferView.setByteLength(timeBufferSize);
}));
glTF.addAccessors(configure(new Accessor(), accessor -> {
accessor.setBufferView(0);
accessor.setType("SCALAR");
accessor.setComponentType(GL11.GL_FLOAT);
accessor.setCount(framesDone);
}));
// Camera translation
glTF.addBufferViews(configure(new BufferView(), bufferView -> {
bufferView.setBuffer(0);
bufferView.setByteOffset(cameraTranslationBufferOffset);
bufferView.setByteLength(cameraTranslationBufferSize);
}));
glTF.addAccessors(configure(new Accessor(), accessor -> {
accessor.setBufferView(1);
accessor.setType("VEC3");
accessor.setComponentType(GL11.GL_FLOAT);
accessor.setCount(framesDone);
}));
// Camera rotation
glTF.addBufferViews(configure(new BufferView(), bufferView -> {
bufferView.setBuffer(0);
bufferView.setByteOffset(cameraRotationBufferOffset);
bufferView.setByteLength(cameraRotationBufferSize);
}));
glTF.addAccessors(configure(new Accessor(), accessor -> {
accessor.setBufferView(2);
accessor.setType("VEC4");
accessor.setComponentType(GL11.GL_FLOAT);
accessor.setCount(framesDone);
}));
java.nio.file.Path videoPath = settings.getOutputFile().toPath();
java.nio.file.Path glbBasePath = Files.isDirectory(videoPath)
? videoPath.resolve("camera.glb")
: videoPath.resolveSibling(videoPath.getFileName() + ".glb");
java.nio.file.Path glbPath = glbBasePath;
for (int i = 0; Files.exists(glbPath); i++) {
String baseName = FilenameUtils.getBaseName(glbBasePath.getFileName().toString());
glbPath = glbBasePath.resolveSibling(baseName + "." + i + ".glb");
}
try (OutputStream out = Files.newOutputStream(glbPath)) {
GltfAssetV2 asset = new GltfAssetV2(glTF, binaryData);
new GltfAssetWriterV2().writeBinary(asset, out);
}
}
}

View File

@@ -0,0 +1,125 @@
//#if MC>=11400
package com.replaymod.render;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.rendering.Channel;
import com.replaymod.render.rendering.FrameConsumer;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.util.crash.CrashReport;
import org.lwjgl.PointerBuffer;
import org.lwjgl.util.tinyexr.EXRChannelInfo;
import org.lwjgl.util.tinyexr.EXRHeader;
import org.lwjgl.util.tinyexr.EXRImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*;
import static org.lwjgl.util.tinyexr.TinyEXR.*;
public class EXRWriter implements FrameConsumer<BitmapFrame> {
private final Path outputFolder;
public EXRWriter(Path outputFolder) throws IOException {
this.outputFolder = outputFolder;
Files.createDirectories(outputFolder);
}
@Override
public void consume(Map<Channel, BitmapFrame> channels) {
BitmapFrame bgraFrame = channels.get(Channel.BRGA);
BitmapFrame depthFrame = channels.get(Channel.DEPTH);
Path path = outputFolder.resolve(bgraFrame.getFrameId() + ".exr");
ReadableDimension size = bgraFrame.getSize();
ByteBuffer bgra = bgraFrame.getByteBuffer();
int width = size.getWidth();
int height = size.getHeight();
int numChannels = 4 + (depthFrame != null ? 1 : 0);
stackPush();
EXRHeader header = EXRHeader.mallocStack(); InitEXRHeader(header);
EXRChannelInfo.Buffer channelInfos = EXRChannelInfo.mallocStack(numChannels);
IntBuffer pixelTypes = stackMallocInt(numChannels);
IntBuffer requestedPixelTypes = stackMallocInt(numChannels);
EXRImage image = EXRImage.mallocStack(); InitEXRImage(image);
PointerBuffer imagePointers = stackMallocPointer(numChannels);
FloatBuffer images = memAllocFloat(width * height * numChannels);
PointerBuffer err = stackMallocPointer(1);
try {
header.num_channels(numChannels);
header.channels(channelInfos);
header.pixel_types(pixelTypes);
header.requested_pixel_types(requestedPixelTypes);
// Some readers ignore this, so we use the most expected order
memASCII("A", true, channelInfos.get(0).name());
memASCII("B", true, channelInfos.get(1).name());
memASCII("G", true, channelInfos.get(2).name());
memASCII("R", true, channelInfos.get(3).name());
for (int i = 0; i < numChannels; i++) {
pixelTypes.put(i, TINYEXR_PIXELTYPE_FLOAT);
requestedPixelTypes.put(i, TINYEXR_PIXELTYPE_HALF);
}
if (depthFrame != null) {
memASCII("Z", true, channelInfos.get(4).name());
requestedPixelTypes.put(4, TINYEXR_PIXELTYPE_FLOAT);
}
image.num_channels(numChannels);
image.width(width);
image.height(height);
image.images(imagePointers);
FloatBuffer[] bgrChannels = new FloatBuffer[4];
FloatBuffer depthChannel = null;
for (int i = 0; i < numChannels; i++) {
FloatBuffer channel = images.slice();
channel.position(width * height * i);
imagePointers.put(i, channel.slice());
if (i == 4) {
depthChannel = channel;
} else {
bgrChannels[(i + 3) % 4] = channel;
}
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
for (FloatBuffer channel : bgrChannels) {
channel.put(((int) bgra.get() & 0xff) / 255f);
}
}
}
if (depthFrame != null && depthChannel != null) {
depthChannel.put(depthFrame.getByteBuffer().asFloatBuffer());
}
int ret = SaveEXRImageToFile(image, header, path.toString(), err);
if (ret != TINYEXR_SUCCESS) {
String message = err.getStringASCII(0);
FreeEXRErrorMessage(err.getByteBuffer(0));
throw new IOException(message);
}
} catch (Throwable t) {
MCVer.getMinecraft().setCrashReport(CrashReport.create(t, "Exporting EXR frame"));
} finally {
memFree(images);
stackPop();
channels.values().forEach(it -> ByteBufferPool.release(it.getByteBuffer()));
}
}
@Override
public void close() {
}
}
//#endif

View File

@@ -1,7 +1,8 @@
package com.replaymod.render;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.rendering.Channel;
import com.replaymod.render.rendering.FrameConsumer;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.ByteBufferPool;
@@ -21,12 +22,13 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.replaymod.render.ReplayModRender.LOGGER;
import static org.apache.commons.lang3.Validate.isTrue;
public class VideoWriter implements FrameConsumer<RGBFrame> {
public class FFmpegWriter implements FrameConsumer<BitmapFrame> {
private final VideoRenderer renderer;
private final RenderSettings settings;
@@ -38,7 +40,7 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
private ByteArrayOutputStream ffmpegLog = new ByteArrayOutputStream(4096);
public VideoWriter(final VideoRenderer renderer) throws IOException {
public FFmpegWriter(final VideoRenderer renderer) throws IOException {
this.renderer = renderer;
this.settings = renderer.getRenderSettings();
@@ -103,11 +105,11 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
}
@Override
public void consume(RGBFrame frame) {
public void consume(Map<Channel, BitmapFrame> channels) {
BitmapFrame frame = channels.get(Channel.BRGA);
try {
checkSize(frame.getSize());
channel.write(frame.getByteBuffer());
ByteBufferPool.release(frame.getByteBuffer());
} catch (Throwable t) {
if (aborted) {
return;
@@ -127,6 +129,8 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
MCVer.addDetail(exportDetails, "Export command", settings::getExportCommand);
MCVer.addDetail(exportDetails, "Export args", commandArgs::toString);
MCVer.getMinecraft().setCrashReport(report);
} finally {
channels.values().forEach(it -> ByteBufferPool.release(it.getByteBuffer()));
}
}

View File

@@ -7,6 +7,12 @@ import net.minecraft.client.resource.language.I18n;
import net.minecraft.util.Util;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
@@ -73,6 +79,8 @@ public class RenderSettings {
BLEND(null, "blend"),
EXR(null, "exr"),
PNG("\"%FILENAME%-%06d.png\"", "png");
private final String preset;
@@ -105,6 +113,13 @@ public class RenderSettings {
public boolean isSupported() {
if (this == BLEND) {
return RenderMethod.BLEND.isSupported();
} else if (this == EXR) {
// Need LJWGL 3
//#if MC>=11400
return true;
//#else
//$$ return false;
//#endif
} else {
return true;
}
@@ -150,6 +165,8 @@ public class RenderSettings {
private final int sphericalFovX;
private final int sphericalFovY;
private final boolean injectSphericalMetadata;
private final boolean depthMap;
private final boolean cameraPathExport;
private final AntiAliasing antiAliasing;
private final String exportCommand;
@@ -180,6 +197,8 @@ public class RenderSettings {
int sphericalFovX,
int sphericalFovY,
boolean injectSphericalMetadata,
boolean depthMap,
boolean cameraPathExport,
AntiAliasing antiAliasing,
String exportCommand,
String exportArguments,
@@ -200,6 +219,8 @@ public class RenderSettings {
this.sphericalFovX = sphericalFovX;
this.sphericalFovY = sphericalFovY;
this.injectSphericalMetadata = injectSphericalMetadata;
this.depthMap = depthMap;
this.cameraPathExport = cameraPathExport;
this.antiAliasing = antiAliasing;
this.exportCommand = exportCommand;
this.exportArguments = exportArguments;
@@ -223,6 +244,8 @@ public class RenderSettings {
sphericalFovX,
sphericalFovY,
injectSphericalMetadata,
depthMap,
cameraPathExport,
antiAliasing,
exportCommand,
exportArguments,
@@ -277,11 +300,35 @@ public class RenderSettings {
switch (Util.getOperatingSystem()) {
case WINDOWS:
// Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder
File inDotMinecraft = new File(MCVer.getMinecraft().runDirectory, "ffmpeg/bin/ffmpeg.exe");
File dotMinecraft = MCVer.getMinecraft().runDirectory;
File inDotMinecraft = new File(dotMinecraft, "ffmpeg/bin/ffmpeg.exe");
if (inDotMinecraft.exists()) {
LOGGER.debug("FFmpeg found in .minecraft/ffmpeg");
return inDotMinecraft.getAbsolutePath();
}
// But a significant amount of people are not even able to follow instruction to do that.
// Instead they'll regularly put it at `.minecraft/ffmpeg-version/bin/ffmpeg.exe`
// or `.minecraft/ffmpeg/ffmpeg-version/bin/ffmpeg.exe`
// So, for support's sake, let's just search the entire .minecraft folder for an `ffmpeg.exe` file.
// We retain above check to have a fast path for any old installations.
try {
Path[] result = new Path[1];
Files.walkFileTree(dotMinecraft.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if ("ffmpeg.exe".equals(file.getFileName().toString())) {
result[0] = file;
return FileVisitResult.TERMINATE;
}
return super.visitFile(file, attrs);
}
});
if (result[0] != null) {
return result[0].toAbsolutePath().toString();
}
} catch (IOException e) {
LOGGER.debug("Error searching .minecraft for ffmpeg.exe:", e);
}
break;
case OSX:
// The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves
@@ -373,6 +420,14 @@ public class RenderSettings {
return injectSphericalMetadata;
}
public boolean isDepthMap() {
return depthMap;
}
public boolean isCameraPathExport() {
return cameraPathExport;
}
public AntiAliasing getAntiAliasing() {
return antiAliasing;
}

View File

@@ -2,8 +2,13 @@ package com.replaymod.render;
import com.replaymod.core.Module;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.render.utils.RenderJob;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.events.ReplayClosedCallback;
import com.replaymod.replay.events.ReplayOpenedCallback;
import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashException;
@@ -17,8 +22,6 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static com.replaymod.core.versions.MCVer.*;
public class ReplayModRender extends EventRegistrations implements Module {
{ instance = this; }
public static ReplayModRender instance;
@@ -27,6 +30,7 @@ public class ReplayModRender extends EventRegistrations implements Module {
public static Logger LOGGER = LogManager.getLogger();
private ReplayFile replayFile;
private final List<RenderJob> renderQueue = new ArrayList<>();
public ReplayModRender(ReplayMod core) {
@@ -41,8 +45,6 @@ public class ReplayModRender extends EventRegistrations implements Module {
@Override
public void initClient() {
on(ReplayClosedCallback.EVENT, replayHandler -> renderQueue.clear());
register();
}
@@ -64,4 +66,31 @@ public class ReplayModRender extends EventRegistrations implements Module {
public List<RenderJob> getRenderQueue() {
return renderQueue;
}
{ on(ReplayOpenedCallback.EVENT, this::onReplayOpened); }
private void onReplayOpened(ReplayHandler replayHandler) {
replayFile = replayHandler.getReplayFile();
try {
renderQueue.addAll(RenderJob.readQueue(replayFile));
} catch (IOException e) {
throw new CrashException(CrashReport.create(e, "Reading timeline"));
}
}
{ on(ReplayClosedCallback.EVENT, replayHandler -> onReplayClosed()); }
private void onReplayClosed() {
renderQueue.clear();
replayFile = null;
}
public void saveRenderQueue() {
try {
RenderJob.writeQueue(replayFile, renderQueue);
} catch (IOException e) {
e.printStackTrace();
VanillaGuiScreen screen = VanillaGuiScreen.wrap(getCore().getMinecraft().currentScreen);
CrashReport report = CrashReport.create(e, "Reading timeline");
Utils.error(LOGGER, screen, report, () -> {});
}
}
}

View File

@@ -3,14 +3,17 @@ package com.replaymod.render.blend;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.capturer.RenderInfo;
import com.replaymod.render.capturer.WorldRenderer;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.rendering.Channel;
import com.replaymod.render.rendering.FrameCapturer;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
public class BlendFrameCapturer implements FrameCapturer<RGBFrame> {
public class BlendFrameCapturer implements FrameCapturer<BitmapFrame> {
protected final WorldRenderer worldRenderer;
protected final RenderInfo renderInfo;
protected int framesDone;
@@ -26,7 +29,7 @@ public class BlendFrameCapturer implements FrameCapturer<RGBFrame> {
}
@Override
public RGBFrame process() {
public Map<Channel, BitmapFrame> process() {
if (framesDone == 0) {
BlendState.getState().setup();
}
@@ -37,7 +40,8 @@ public class BlendFrameCapturer implements FrameCapturer<RGBFrame> {
worldRenderer.renderWorld(MCVer.getRenderPartialTicks(), null);
BlendState.getState().postFrame(framesDone);
return new RGBFrame(framesDone++, new Dimension(0, 0), ByteBufferPool.allocate(0));
BitmapFrame frame = new BitmapFrame(framesDone++, new Dimension(0, 0), 0, ByteBufferPool.allocate(0));
return Collections.singletonMap(Channel.BRGA, frame);
}
@Override

View File

@@ -1,6 +1,10 @@
package com.replaymod.render.capturer;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.rendering.Channel;
import java.util.Collections;
import java.util.Map;
public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
public enum Data implements CaptureData {
@@ -25,14 +29,15 @@ public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer<CubicOpenGlFra
}
@Override
public CubicOpenGlFrame process() {
public Map<Channel, CubicOpenGlFrame> process() {
float partialTicks = renderInfo.updateForNextFrame();
int frameId = framesDone++;
return new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT),
CubicOpenGlFrame frame = new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT),
renderFrame(frameId, partialTicks, Data.RIGHT),
renderFrame(frameId, partialTicks, Data.FRONT),
renderFrame(frameId, partialTicks, Data.BACK),
renderFrame(frameId, partialTicks, Data.TOP),
renderFrame(frameId, partialTicks, Data.BOTTOM));
return Collections.singletonMap(Channel.BRGA, frame);
}
}

View File

@@ -4,7 +4,7 @@ import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.OpenGlFrame;
public class CubicPboOpenGlFrameCapturer extends
MultiFramePboOpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
PboOpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
private final int frameSize;
public CubicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, int frameSize) {

View File

@@ -1,5 +1,6 @@
package com.replaymod.render.capturer;
import com.replaymod.render.rendering.Channel;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.frame.CubicOpenGlFrame;
@@ -17,6 +18,8 @@ import net.minecraft.util.Identifier;
import static com.mojang.blaze3d.platform.GlStateManager.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
@@ -125,16 +128,22 @@ public class ODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
}
@Override
public ODSOpenGlFrame process() {
public Map<Channel, ODSOpenGlFrame> process() {
bindProgram();
leftEyeVariable.set(true);
CubicOpenGlFrame leftFrame = left.process();
Map<Channel, CubicOpenGlFrame> leftChannels = left.process();
leftEyeVariable.set(false);
CubicOpenGlFrame rightFrame = right.process();
Map<Channel, CubicOpenGlFrame> rightChannels = right.process();
unbindProgram();
if (leftFrame != null && rightFrame != null) {
return new ODSOpenGlFrame(leftFrame, rightFrame);
if (leftChannels != null && rightChannels != null) {
Map<Channel, ODSOpenGlFrame> result = new HashMap<>();
for (Channel channel : Channel.values()) {
CubicOpenGlFrame leftFrame = leftChannels.get(channel);
CubicOpenGlFrame rightFrame = rightChannels.get(channel);
result.put(channel, new ODSOpenGlFrame(leftFrame, rightFrame));
}
return result;
}
return null;
}

View File

@@ -112,7 +112,7 @@ public abstract class OpenGlFrameCapturer<F extends Frame, D extends CaptureData
frameBuffer().endWrite();
buffer.rewind();
return new OpenGlFrame(frameId, new Dimension(getFrameWidth(), getFrameHeight()), buffer);
return new OpenGlFrame(frameId, new Dimension(getFrameWidth(), getFrameHeight()), 4, buffer);
}
protected void resize(int width, int height) {

View File

@@ -1,6 +1,8 @@
package com.replaymod.render.capturer;
import com.mojang.blaze3d.platform.GlStateManager;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.rendering.Channel;
import com.replaymod.render.rendering.Frame;
import com.replaymod.render.utils.ByteBufferPool;
import com.replaymod.render.utils.PixelBufferObject;
@@ -9,17 +11,21 @@ import org.lwjgl.opengl.GL12;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
public abstract class MultiFramePboOpenGlFrameCapturer<F extends Frame, D extends Enum<D> & CaptureData>
public abstract class PboOpenGlFrameCapturer<F extends Frame, D extends Enum<D> & CaptureData>
extends OpenGlFrameCapturer<F, D> {
private final boolean withDepth;
private final D[] data;
private PixelBufferObject pbo, otherPBO;
public MultiFramePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, Class<D> type, int framePixels) {
public PboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, Class<D> type, int framePixels) {
super(worldRenderer, renderInfo);
withDepth = renderInfo.getRenderSettings().isDepthMap();
data = type.getEnumConstants();
int bufferSize = framePixels * 4 * data.length;
int bufferSize = framePixels * (4 /* bgra */ + (withDepth ? 4 /* float */ : 0)) * data.length;
pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
}
@@ -37,29 +43,36 @@ public abstract class MultiFramePboOpenGlFrameCapturer<F extends Frame, D extend
return framesDone >= renderInfo.getTotalFrames() + 2;
}
private F readFromPbo(ByteBuffer pboBuffer, int bytesPerPixel) {
OpenGlFrame[] frames = new OpenGlFrame[data.length];
int frameBufferSize = getFrameWidth() * getFrameHeight() * bytesPerPixel;
for (int i = 0; i < frames.length; i++) {
ByteBuffer frameBuffer = ByteBufferPool.allocate(frameBufferSize);
pboBuffer.limit(pboBuffer.position() + frameBufferSize);
frameBuffer.put(pboBuffer);
frameBuffer.rewind();
frames[i] = new OpenGlFrame(framesDone - 2, frameSize, bytesPerPixel, frameBuffer);
}
return create(frames);
}
@Override
public F process() {
F frame = null;
public Map<Channel, F> process() {
Map<Channel, F> channels = null;
if (framesDone > 1) {
// Read pbo to memory
pbo.bind();
ByteBuffer pboBuffer = pbo.mapReadOnly();
OpenGlFrame[] frames = new OpenGlFrame[data.length];
int frameBufferSize = getFrameWidth() * getFrameHeight() * 4;
for (int i = 0; i < frames.length; i++) {
ByteBuffer frameBuffer = ByteBufferPool.allocate(frameBufferSize);
pboBuffer.limit(pboBuffer.position() + frameBufferSize);
frameBuffer.put(pboBuffer);
frameBuffer.rewind();
frames[i] = new OpenGlFrame(framesDone - 2, frameSize, frameBuffer);
channels = new HashMap<>();
channels.put(Channel.BRGA, readFromPbo(pboBuffer, 4));
if (withDepth) {
channels.put(Channel.DEPTH, readFromPbo(pboBuffer, 4));
}
pbo.unmap();
pbo.unbind();
frame = create(frames);
}
if (framesDone < renderInfo.getTotalFrames()) {
@@ -72,7 +85,7 @@ public abstract class MultiFramePboOpenGlFrameCapturer<F extends Frame, D extend
framesDone++;
swapPBOs();
return frame;
return channels;
}
@Override
@@ -82,6 +95,10 @@ public abstract class MultiFramePboOpenGlFrameCapturer<F extends Frame, D extend
int offset = captureData.ordinal() * getFrameWidth() * getFrameHeight() * 4;
frameBuffer().beginWrite(true);
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE, offset);
if (withDepth) {
offset += data.length * getFrameWidth() * getFrameHeight() * 4;
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL11.GL_DEPTH_COMPONENT, GL11.GL_FLOAT, offset);
}
frameBuffer().endWrite();
pbo.unbind();

View File

@@ -1,6 +1,10 @@
package com.replaymod.render.capturer;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.rendering.Channel;
import java.util.Collections;
import java.util.Map;
public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
@@ -9,8 +13,9 @@ public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame,
}
@Override
public OpenGlFrame process() {
public Map<Channel, OpenGlFrame> process() {
float partialTicks = renderInfo.updateForNextFrame();
return renderFrame(framesDone++, partialTicks);
OpenGlFrame frame = renderFrame(framesDone++, partialTicks);
return Collections.singletonMap(Channel.BRGA, frame);
}
}

View File

@@ -1,81 +1,20 @@
package com.replaymod.render.capturer;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.utils.ByteBufferPool;
import com.replaymod.render.utils.PixelBufferObject;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SimplePboOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
private final int bufferSize;
private PixelBufferObject pbo, otherPBO;
public class SimplePboOpenGlFrameCapturer extends PboOpenGlFrameCapturer<OpenGlFrame, SimplePboOpenGlFrameCapturer.SinglePass> {
public SimplePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
super(worldRenderer, renderInfo);
ReadableDimension size = renderInfo.getFrameSize();
bufferSize = size.getHeight() * size.getWidth() * 4;
pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
}
private void swapPBOs() {
PixelBufferObject old = pbo;
pbo = otherPBO;
otherPBO = old;
super(worldRenderer, renderInfo, SinglePass.class,
renderInfo.getFrameSize().getWidth() * renderInfo.getFrameSize().getHeight());
}
@Override
public boolean isDone() {
return framesDone >= renderInfo.getTotalFrames() + 2;
protected OpenGlFrame create(OpenGlFrame[] from) {
return from[0];
}
@Override
public OpenGlFrame process() {
OpenGlFrame frame = null;
if (framesDone > 1) {
// Read pbo to memory
pbo.bind();
ByteBuffer pboBuffer = pbo.mapReadOnly();
ByteBuffer buffer = ByteBufferPool.allocate(bufferSize);
buffer.put(pboBuffer);
buffer.rewind();
pbo.unmap();
pbo.unbind();
frame = new OpenGlFrame(framesDone - 2, frameSize, buffer);
}
if (framesDone < renderInfo.getTotalFrames()) {
// Then fill it again
renderFrame(framesDone, renderInfo.updateForNextFrame());
}
framesDone++;
swapPBOs();
return frame;
}
@Override
protected OpenGlFrame captureFrame(int frameId, CaptureData data) {
pbo.bind();
frameBuffer().beginWrite(true);
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE, 0);
frameBuffer().endWrite();
pbo.unbind();
return null;
}
@Override
public void close() throws IOException {
super.close();
pbo.delete();
otherPBO.delete();
public enum SinglePass implements CaptureData {
SINGLE_PASS
}
}

View File

@@ -2,6 +2,10 @@ package com.replaymod.render.capturer;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
import com.replaymod.render.rendering.Channel;
import java.util.Collections;
import java.util.Map;
public class StereoscopicOpenGlFrameCapturer
extends OpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
@@ -19,11 +23,12 @@ public class StereoscopicOpenGlFrameCapturer
}
@Override
public StereoscopicOpenGlFrame process() {
public Map<Channel, StereoscopicOpenGlFrame> process() {
float partialTicks = renderInfo.updateForNextFrame();
int frameId = framesDone++;
OpenGlFrame left = renderFrame(frameId, partialTicks, Data.LEFT_EYE);
OpenGlFrame right = renderFrame(frameId, partialTicks, Data.RIGHT_EYE);
return new StereoscopicOpenGlFrame(left, right);
StereoscopicOpenGlFrame frame = new StereoscopicOpenGlFrame(left, right);
return Collections.singletonMap(Channel.BRGA, frame);
}
}

View File

@@ -4,7 +4,7 @@ import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
public class StereoscopicPboOpenGlFrameCapturer
extends MultiFramePboOpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
extends PboOpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
public StereoscopicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
super(worldRenderer, renderInfo, StereoscopicOpenGlFrameCapturer.Data.class,

View File

@@ -6,19 +6,19 @@ import org.apache.commons.lang3.Validate;
import java.nio.ByteBuffer;
public class RGBFrame implements Frame {
public class BitmapFrame implements Frame {
private final int frameId;
private final ReadableDimension size;
private final int bytesPerPixel;
private final ByteBuffer byteBuffer;
public RGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) {
Validate.isTrue(size.getWidth() * size.getHeight() * 4 == byteBuffer.remaining(),
public BitmapFrame(int frameId, ReadableDimension size, int bytesPerPixel, ByteBuffer byteBuffer) {
Validate.isTrue(size.getWidth() * size.getHeight() * bytesPerPixel == byteBuffer.remaining(),
"Buffer size is %d (cap: %d) but should be %d",
byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 4);
byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * bytesPerPixel);
this.frameId = frameId;
this.size = size;
this.bytesPerPixel = bytesPerPixel;
this.byteBuffer = byteBuffer;
}
@@ -30,6 +30,10 @@ public class RGBFrame implements Frame {
return this.size;
}
public int getBytesPerPixel() {
return bytesPerPixel;
}
public ByteBuffer getByteBuffer() {
return this.byteBuffer;
}

View File

@@ -7,14 +7,14 @@ import java.nio.ByteBuffer;
public class OpenGlFrame implements Frame {
private final int frameId;
private final ReadableDimension size;
private final int bytesPerPixel;
private final ByteBuffer byteBuffer;
public OpenGlFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) {
public OpenGlFrame(int frameId, ReadableDimension size, int bytesPerPixel, ByteBuffer byteBuffer) {
this.frameId = frameId;
this.size = size;
this.bytesPerPixel = bytesPerPixel;
this.byteBuffer = byteBuffer;
}
@@ -26,6 +26,10 @@ public class OpenGlFrame implements Frame {
return this.size;
}
public int getBytesPerPixel() {
return bytesPerPixel;
}
public ByteBuffer getByteBuffer() {
return this.byteBuffer;
}

View File

@@ -2,7 +2,7 @@ package com.replaymod.render.gui;
import com.replaymod.core.ReplayMod;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.FFmpegWriter;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
@@ -23,7 +23,7 @@ import static com.replaymod.core.versions.MCVer.addDetail;
import static com.replaymod.render.ReplayModRender.LOGGER;
public class GuiExportFailed extends GuiScreen {
public static GuiExportFailed tryToRecover(VideoWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
public static GuiExportFailed tryToRecover(FFmpegWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
// Always log the error first
LOGGER.error("Rendering video:", e);
@@ -74,7 +74,7 @@ public class GuiExportFailed extends GuiScreen {
setBackground(Background.DIRT);
}
public GuiExportFailed(VideoWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
public GuiExportFailed(FFmpegWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
logList.getListPanel().addElements(null,
Arrays.stream(e.getLog().replace("\t", " ").split("\n"))
.map(l -> new GuiLabel().setText(l))
@@ -98,6 +98,8 @@ public class GuiExportFailed extends GuiScreen {
oldSettings.getSphericalFovX(),
oldSettings.getSphericalFovY(),
oldSettings.isInjectSphericalMetadata(),
oldSettings.isDepthMap(),
oldSettings.isCameraPathExport(),
oldSettings.getAntiAliasing(),
oldSettings.getExportCommand(),
oldSettings.getEncodingPreset().getValue(),

View File

@@ -1,17 +1,20 @@
package com.replaymod.render.gui;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.ReplayModRender;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.FFmpegWriter;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.RenderJob;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.ReplaySender;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.util.I18n;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
@@ -20,35 +23,47 @@ import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.function.Typeable;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.NoticeScreen;
import net.minecraft.util.crash.CrashReport;
import org.apache.commons.io.IOUtils;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import static com.replaymod.render.ReplayModRender.LOGGER;
//#if MC>=11400
import net.minecraft.text.TranslatableText;
//#else
//$$ import com.replaymod.replaystudio.util.I18n;
//#endif
public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> implements Typeable {
private final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.renderqueue.title").setColor(Colors.BLACK);
private final GuiVerticalList list = new GuiVerticalList().setDrawShadow(true).setDrawSlider(true);
private final GuiButton addButton = new GuiButton().setI18nLabel("replaymod.gui.renderqueue.add").setSize(150, 20);
private final GuiButton renameButton = new GuiButton().setI18nLabel("replaymod.gui.rename").setSize(73, 20);
private final GuiButton editButton = new GuiButton().setI18nLabel("replaymod.gui.edit").setSize(73, 20);
private final GuiButton removeButton = new GuiButton().setI18nLabel("replaymod.gui.remove").setSize(73, 20);
private final GuiButton renderButton = new GuiButton().setI18nLabel("replaymod.gui.render").setSize(150, 20);
private final GuiButton renderButton = new GuiButton().setSize(150, 20);
private final GuiButton closeButton = new GuiButton().setI18nLabel("replaymod.gui.close").setSize(150, 20).onClick(this::close);
/*
@@ -56,7 +71,7 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
|---------------------------------|
| Add | Render |
|---------------------------------|
| Rename | Remove | Close |
| Edit | Remove | Close |
|---------------------------------|
*/
@@ -66,12 +81,14 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
addButton,
renderButton,
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)).addElements(null,
renameButton, removeButton),
editButton, removeButton),
closeButton);
private final AbstractGuiScreen container;
private final AbstractGuiScreen<?> container;
private final ReplayHandler replayHandler;
private Entry selectedEntry;
private final Set<Entry> selectedEntries = new HashSet<>();
private final Supplier<Timeline> timelineSupplier;
private boolean opened;
{
popup.setLayout(new CustomLayout<GuiPanel>() {
@@ -92,120 +109,59 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
}).addElements(null, title, list, buttonPanel);
}
public GuiRenderQueue(AbstractGuiScreen container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) {
private final ReplayModRender mod = ReplayModRender.instance;
private final List<RenderJob> jobs = mod.getRenderQueue();
public GuiRenderQueue(AbstractGuiScreen<?> container, ReplayHandler replayHandler, Supplier<Timeline> timelineSupplier) {
super(container);
this.container = container;
this.replayHandler = replayHandler;
this.timelineSupplier = timelineSupplier;
LOGGER.trace("Opening render queue popup");
setBackgroundColor(Colors.DARK_TRANSPARENT);
List<RenderJob> queue = ReplayModRender.instance.getRenderQueue();
for (RenderJob renderJob : queue) {
for (RenderJob renderJob : jobs) {
LOGGER.trace("Adding {} to job queue list", renderJob);
list.getListPanel().addElements(null, new Entry(renderJob));
}
addButton.onClick(() -> {
LOGGER.trace("Add button clicked");
// Open popup
GuiYesNoPopup popup = GuiYesNoPopup.open(container)
.setYesI18nLabel("replaymod.gui.add").setNoI18nLabel("replaymod.gui.cancel");
popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5));
// Add content
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK);
GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true);
popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField);
// Disable "Yes" button while name is empty
nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> {
if (popup.getYesButton().isEnabled()) {
popup.getYesButton().onClick();
}
});
popup.getYesButton().setDisabled();
// Register callback
Futures.addCallback(popup.getFuture(), new FutureCallback<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");
}
}
addButton.onClick(this::addButtonClicked);
@Override
public void onFailure(Throwable t) {
LOGGER.error("Add Job popup:", t);
}
});
});
if (guiRenderSettings != null) {
addButton.setEnabled(guiRenderSettings.renderButton.isEnabled());
}
renameButton.onClick(() -> {
LOGGER.trace("Rename button clicked for {}", selectedEntry.job);
// Open popup
GuiYesNoPopup popup = GuiYesNoPopup.open(container)
.setYesI18nLabel("replaymod.gui.rename").setNoI18nLabel("replaymod.gui.cancel");
popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5));
// Add content
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK);
GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true)
.setText(selectedEntry.job.getName());
popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField);
// Disable "Yes" button while name is empty
nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> {
if (popup.getYesButton().isEnabled()) {
popup.getYesButton().onClick();
}
});
// Register callback
Futures.addCallback(popup.getFuture(), new FutureCallback<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);
}
});
editButton.onClick(() -> {
Entry job = selectedEntries.iterator().next();
GuiRenderSettings gui = job.edit();
gui.open();
});
removeButton.onClick(() -> {
LOGGER.trace("Remove button clicked for {}", selectedEntry.job);
list.getListPanel().removeElement(selectedEntry);
queue.remove(selectedEntry.job);
selectedEntry = null;
for (Entry entry : selectedEntries) {
LOGGER.trace("Remove button clicked for {}", entry.job);
list.getListPanel().removeElement(entry);
jobs.remove(entry.job);
}
selectedEntries.clear();
updateButtons();
mod.saveRenderQueue();
});
renderButton.onClick(() -> {
LOGGER.trace("Render button clicked");
ReplayMod.instance.runLaterWithoutLock(() -> processQueue(queue));
List<RenderJob> renderQueue = new ArrayList<>();
for (Entry entry : selectedEntries) {
renderQueue.add(entry.job);
}
ReplayMod.instance.runLaterWithoutLock(() -> processQueue(container, replayHandler, renderQueue, () -> {}));
});
updateButtons();
}
private void processQueue(Iterable<RenderJob> queue) {
private static void processQueue(AbstractGuiScreen<?> container, ReplayHandler replayHandler, Iterable<RenderJob> queue, Runnable done) {
MinecraftClient mc = MCVer.getMinecraft();
// Close all GUIs (so settings in GuiRenderSettings are saved)
getMinecraft().openScreen(null);
mc.openScreen(null);
// Start rendering
int jobsDone = 0;
for (RenderJob renderJob : queue) {
@@ -213,7 +169,7 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
try {
VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline());
videoRenderer.renderVideo();
} catch (VideoWriter.NoFFmpegException e) {
} catch (FFmpegWriter.NoFFmpegException e) {
LOGGER.error("Rendering video:", e);
NoticeScreen errorScreen = new NoticeScreen(
//#if MC>=11400
@@ -225,29 +181,150 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
//$$ I18n.format("replaymod.gui.rendering.error.message")
//#endif
);
getMinecraft().openScreen(errorScreen);
mc.openScreen(errorScreen);
return;
} catch (VideoWriter.FFmpegStartupException e) {
} catch (FFmpegWriter.FFmpegStartupException e) {
int jobsToSkip = jobsDone;
GuiExportFailed.tryToRecover(e, newSettings -> {
// Update current job with fixed ffmpeg arguments
renderJob.setSettings(newSettings);
// Restart queue, skipping the already completed jobs
ReplayMod.instance.runLaterWithoutLock(() -> processQueue(Iterables.skip(queue, jobsToSkip)));
ReplayMod.instance.runLaterWithoutLock(() -> processQueue(container, replayHandler, Iterables.skip(queue, jobsToSkip), done));
});
return;
} catch (Throwable t) {
Utils.error(LOGGER, this, CrashReport.create(t, "Rendering video"), () -> {});
Utils.error(LOGGER, container, CrashReport.create(t, "Rendering video"), () -> {});
container.display(); // Re-show the queue popup and the new error popup
return;
}
jobsDone++;
}
done.run();
}
public static void processMultipleReplays(
AbstractGuiScreen<?> container,
ReplayModReplay mod,
Iterator<Pair<File, List<RenderJob>>> queue,
Runnable done
) {
if (!queue.hasNext()) {
done.run();
return;
}
Pair<File, List<RenderJob>> next = queue.next();
LOGGER.info("Opening replay {} for {} render jobs", next.getKey(), next.getValue().size());
ReplayHandler replayHandler;
ReplayFile replayFile = null;
try {
replayFile = mod.getCore().openReplay(next.getKey().toPath());
replayHandler = mod.startReplay(replayFile, true, false);
} catch (IOException e) {
Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {});
container.display(); // Re-show the queue popup and the new error popup
IOUtils.closeQuietly(replayFile);
return;
}
if (replayHandler == null) {
LOGGER.warn("Replay failed to open (missing mods?), skipping..");
IOUtils.closeQuietly(replayFile);
processMultipleReplays(container, mod, queue, done);
return;
}
ReplaySender replaySender = replayHandler.getReplaySender();
MinecraftClient mc = mod.getCore().getMinecraft();
int jumpTo = 1000;
while (mc.world == null && jumpTo < replayHandler.getReplayDuration()) {
replaySender.sendPacketsTill(jumpTo);
jumpTo += 1000;
}
if (mc.world == null) {
LOGGER.warn("Replay failed to load world (corrupted?), skipping..");
IOUtils.closeQuietly(replayFile);
processMultipleReplays(container, mod, queue, done);
return;
}
processQueue(container, replayHandler, next.getValue(), () -> {
try {
replayHandler.endReplay();
} catch (IOException e) {
Utils.error(LOGGER, container, CrashReport.create(e, "Closing replay"), () -> {});
container.display(); // Re-show the queue popup and the new error popup
return;
}
processMultipleReplays(container, mod, queue, done);
});
}
private GuiRenderSettings addButtonClicked() {
Timeline timeline = timelineSupplier.get();
if (timeline != null) {
GuiRenderSettings popup = addJob(timeline);
popup.open();
return popup;
} else {
return null;
}
}
public GuiRenderSettings addJob(Timeline timeline) {
return new GuiRenderSettings(container, replayHandler, timeline) {
{
if (!jobs.isEmpty()) {
buttonPanel.removeElement(renderButton);
}
queueButton.onClick(() -> {
RenderSettings settings = save(false);
RenderJob newJob = new RenderJob();
newJob.setSettings(settings);
newJob.setTimeline(timeline);
LOGGER.trace("Adding new job: {}", newJob);
jobs.add(newJob);
list.getListPanel().addElements(null, new Entry(newJob));
updateButtons();
mod.saveRenderQueue();
// Need to close the inner popup before we can open the outer one
close();
if (!opened) {
GuiRenderQueue.this.open();
}
});
}
@Override
public void close() {
super.close();
if (!opened && jobs.isEmpty()) {
GuiRenderQueue.this.close();
}
}
};
}
@Override
public void open() {
if (jobs.isEmpty() && timelineSupplier != null) {
if (addButtonClicked() == null) {
close();
}
return;
}
super.open();
opened = true;
}
@Override
protected void close() {
if (opened) {
super.close();
}
opened = false;
}
@Override
@@ -256,9 +333,35 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
}
public void updateButtons() {
renameButton.setEnabled(selectedEntry != null);
removeButton.setEnabled(selectedEntry != null);
renderButton.setEnabled(!list.getListPanel().getChildren().isEmpty());
int selected = selectedEntries.size();
addButton.setEnabled(timelineSupplier != null);
editButton.setEnabled(selected == 1);
removeButton.setEnabled(selected >= 1);
renderButton.setEnabled(selected > 0);
renderButton.setI18nLabel("replaymod.gui.renderqueue.render" + (selected > 0 ? "selected" : "all"));
String[] compatError = VideoRenderer.checkCompat();
if (compatError != null) {
renderButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError));
}
}
@Override
public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) {
if (MCVer.Keyboard.hasControlDown() && keyCode == MCVer.Keyboard.KEY_A) {
if (selectedEntries.size() < list.getListPanel().getChildren().size()) {
for (GuiElement<?> child : list.getListPanel().getChildren()) {
if (child instanceof Entry) {
selectedEntries.add((Entry) child);
}
}
} else {
selectedEntries.clear();
}
updateButtons();
return true;
}
return false;
}
public class Entry extends AbstractGuiClickableContainer<Entry> {
@@ -284,27 +387,42 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
@Override
protected void onClick() {
selectedEntry = this;
if (!MCVer.Keyboard.hasControlDown()) {
selectedEntries.clear();
}
if (selectedEntries.contains(this)) {
selectedEntries.remove(this);
} else {
selectedEntries.add(this);
}
updateButtons();
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
if (selectedEntry == this) {
if (selectedEntries.contains(this)) {
renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK);
renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE);
}
super.draw(renderer, size, renderInfo);
}
public void setName(String name) {
job.setName(name);
label.setText(name);
}
@Override
protected Entry getThis() {
return this;
}
public GuiRenderSettings edit() {
GuiRenderSettings gui = new GuiRenderSettings(container, replayHandler, job.getTimeline());
gui.buttonPanel.removeElement(gui.renderButton);
gui.queueButton.setI18nLabel("replaymod.gui.done").onClick(() -> {
job.setSettings(gui.save(false));
label.setText(job.getName());
mod.saveRenderQueue();
gui.close();
});
gui.load(job.getSettings());
return gui;
}
}
}

View File

@@ -10,10 +10,11 @@ import com.google.gson.JsonSyntaxException;
import com.replaymod.core.ReplayMod;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.ReplayModRender;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.FFmpegWriter;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replaystudio.pathing.path.Timeline;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
@@ -21,11 +22,11 @@ import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.*;
import de.johni0702.minecraft.gui.element.advanced.GuiColorPicker;
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
import de.johni0702.minecraft.gui.function.Closeable;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.popup.GuiFileChooserPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.Consumer;
@@ -44,6 +45,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
@@ -57,8 +59,9 @@ import static com.replaymod.render.ReplayModRender.LOGGER;
import net.minecraft.text.TranslatableText;
//#endif
public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT);
public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
{ disablePopupBackground(); }
public final GuiPanel contentPanel = new GuiPanel(popup).setBackgroundColor(new Color(0, 0, 0, 230));
public final GuiVerticalList settingsList = new GuiVerticalList(contentPanel).setDrawSlider(true);
public final GuiDropdownMenu<RenderSettings.RenderMethod> renderMethodDropdown =
@@ -193,6 +196,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiCheckbox injectSphericalMetadata = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.sphericalmetadata");
public final GuiCheckbox depthMap = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.depthmap");
public final GuiCheckbox cameraPathExport = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.camerapath");
public final GuiDropdownMenu<RenderSettings.AntiAliasing> antiAliasingDropdown = new GuiDropdownMenu<RenderSettings.AntiAliasing>()
.setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE);
@@ -203,16 +212,25 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
chromaKeyingCheckbox, chromaKeyingColor,
injectSphericalMetadata, sphericalFovSlider,
depthMap, new GuiLabel(),
cameraPathExport, new GuiLabel(),
new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown));
public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")
.setSize(55, 20).setMaxLength(100);
.setSize(55, 20).setMaxLength(100).onTextChanged((old) -> updateInputs());
public final GuiTextField exportArguments = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.arguments")
.setMinSize(new Dimension(245, 20)).setMaxLength(500);
.setMinSize(new Dimension(245, 20)).setMaxLength(500).onTextChanged((old) -> updateInputs());
public final GuiButton exportReset = new GuiButton().setLabel("X")
.setSize(20, 20)
.onClick(() -> {
exportCommand.setText("");
exportArguments.setText(encodingPresetDropdown.getSelectedValue().getValue());
updateInputs();
});
public final GuiPanel commandlinePanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(10))
.addElements(null,
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).addElements(null, exportCommand, exportArguments),
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).addElements(null, exportCommand, exportArguments, exportReset),
new GuiLabel(new GuiPanel().setLayout(new CustomLayout<GuiPanel>() {
@Override
protected void layout(GuiPanel container, int width, int height) {
@@ -227,12 +245,9 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(4));
public final GuiButton queueButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override
public void run() {
new GuiRenderQueue(GuiRenderSettings.this, GuiRenderSettings.this, replayHandler, timeline).open();
}
}).setSize(100, 20).setI18nLabel("replaymod.gui.renderqueue.open");
public final GuiButton queueButton = new GuiButton(buttonPanel)
.setSize(100, 20)
.setI18nLabel("replaymod.gui.rendersettings.addtoqueue");
public final GuiButton renderButton = new GuiButton(buttonPanel).onClick(() -> ReplayMod.instance.runLaterWithoutLock(new Runnable() {
@Override
public void run() {
@@ -241,11 +256,11 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
try {
VideoRenderer videoRenderer = new VideoRenderer(save(false), replayHandler, timeline);
videoRenderer.renderVideo();
} catch (VideoWriter.NoFFmpegException e) {
} catch (FFmpegWriter.NoFFmpegException e) {
LOGGER.error("Rendering video:", e);
NoticeScreen errorScreen = new NoticeScreen(
//#if MC>=11400
GuiRenderSettings.this::display,
getScreen()::display,
new TranslatableText("replaymod.gui.rendering.error.title"),
new TranslatableText("replaymod.gui.rendering.error.message")
//#else
@@ -254,7 +269,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
//#endif
);
getMinecraft().openScreen(errorScreen);
} catch (VideoWriter.FFmpegStartupException e) {
} catch (FFmpegWriter.FFmpegStartupException e) {
GuiExportFailed.tryToRecover(e, newSettings -> {
// Update settings with fixed ffmpeg arguments
exportArguments.setText(newSettings.getExportArguments());
@@ -263,26 +278,17 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
});
} catch (Throwable t) {
error(LOGGER, GuiRenderSettings.this, CrashReport.create(t, "Rendering video"), () -> {});
display(); // Re-show the render settings gui and the new error popup
getScreen().display(); // Re-show the render settings gui and the new error popup
}
}
})).setSize(100, 20).setI18nLabel("replaymod.gui.render");
public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override
public void run() {
getMinecraft().openScreen(null);
}
}).setSize(100, 20).setI18nLabel("replaymod.gui.cancel");
public final GuiButton cancelButton = new GuiButton(buttonPanel)
.onClick(this::close)
.setSize(100, 20)
.setI18nLabel("replaymod.gui.cancel");
{
setBackground(Background.NONE);
Utils.link(videoWidth, videoHeight, bitRateField);
setLayout(new CustomLayout<GuiScreen>() {
@Override
protected void layout(GuiScreen container, int width, int height) {
pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2);
}
});
contentPanel.setLayout(new CustomLayout<GuiPanel>() {
@Override
@@ -294,7 +300,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
@Override
public ReadableDimension calcMinSize(GuiContainer<?> container) {
ReadableDimension screenSize = getMinSize();
ReadableDimension screenSize = getContainer().getMinSize();
return new Dimension(screenSize.getWidth() - 40, screenSize.getHeight() - 40);
}
});
@@ -319,12 +325,15 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
});
}
private final AbstractGuiScreen<?> screen;
private final ReplayHandler replayHandler;
private final Timeline timeline;
private File outputFile;
private boolean userDefinedOutputFileName;
public GuiRenderSettings(ReplayHandler replayHandler, Timeline timeline) {
public GuiRenderSettings(AbstractGuiScreen<?> container, ReplayHandler replayHandler, Timeline timeline) {
super(container);
this.screen = container;
this.replayHandler = replayHandler;
this.timeline = timeline;
@@ -358,17 +367,26 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
videoWidth.setEnabled(!renderMethod.hasFixedAspectRatio());
// Validate video width and height
String error = updateResolution();
if (error == null) {
renderButton.setEnabled().setTooltip(null);
String resolutionError = updateResolution();
if (resolutionError == null) {
queueButton.setEnabled().setTooltip(null);
videoWidth.setTextColor(Colors.WHITE);
videoHeight.setTextColor(Colors.WHITE);
} else {
renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(error));
queueButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(resolutionError));
videoWidth.setTextColor(Colors.RED);
videoHeight.setTextColor(Colors.RED);
}
String[] compatError = VideoRenderer.checkCompat();
if (resolutionError != null) {
renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(resolutionError));
} else if (compatError != null) {
renderButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError));
} else {
renderButton.setEnabled().setTooltip(null);
}
// Enable/Disable bitrate input field and dropdown
if (encodingPresetDropdown.getSelectedValue().hasBitrateSetting()) {
bitRateField.setEnabled();
@@ -402,15 +420,29 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
}
// Enable/Disable various options for blend export
boolean isEXR = encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.EXR;
boolean isBlend = renderMethod == RenderSettings.RenderMethod.BLEND;
boolean isFFmpeg = !isBlend && !isEXR;
if (isBlend) {
videoWidth.setDisabled();
videoHeight.setDisabled();
}
encodingPresetDropdown.setEnabled(!isBlend);
exportCommand.setEnabled(!isBlend);
exportArguments.setEnabled(!isBlend);
antiAliasingDropdown.setEnabled(!isBlend);
exportCommand.setEnabled(isFFmpeg);
exportArguments.setEnabled(isFFmpeg);
antiAliasingDropdown.setEnabled(isFFmpeg);
if (isEXR) {
depthMap.setEnabled().setTooltip(null);
} else {
depthMap.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
.setI18nText("replaymod.gui.rendersettings.depthmap.onlyexr"));
}
// Enable/Disable export args reset button
boolean commandChanged = !exportCommand.getText().isEmpty();
boolean argsChanged = !encodingPresetDropdown.getSelectedValue().getValue().equals(exportArguments.getText());
exportReset.setEnabled(commandChanged || argsChanged);
}
protected String updateResolution() {
@@ -511,7 +543,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
userDefinedOutputFileName = false;
} else if (savedOutputFile.exists()) {
String name = generateOutputFile(encodingPreset).getName();
this.outputFile = new File(savedOutputFile.isDirectory() ? savedOutputFile : savedOutputFile.getParentFile(), name);
boolean isFolder = savedOutputFile.isDirectory() && !savedOutputFile.getName().endsWith(".exr");
this.outputFile = new File(isFolder ? savedOutputFile : savedOutputFile.getParentFile(), name);
userDefinedOutputFileName = false;
} else {
this.outputFile = conformExtension(savedOutputFile, encodingPreset);
@@ -531,6 +564,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
}
sphericalFovSlider.setValue((settings.getSphericalFovX() - MIN_SPHERICAL_FOV) / SPHERICAL_FOV_STEP_SIZE);
injectSphericalMetadata.setChecked(settings.isInjectSphericalMetadata());
depthMap.setChecked(settings.isDepthMap());
cameraPathExport.setChecked(settings.isCameraPathExport());
antiAliasingDropdown.setSelected(settings.getAntiAliasing());
exportCommand.setText(settings.getExportCommand());
String exportArguments = settings.getExportArguments();
@@ -561,7 +596,9 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null,
sphericalFov, Math.min(180, sphericalFov),
injectSphericalMetadata.isChecked() && (serialize || injectSphericalMetadata.isEnabled()),
antiAliasingDropdown.getSelectedValue(),
depthMap.isChecked() && (serialize || depthMap.isEnabled()),
cameraPathExport.isChecked(),
serialize || antiAliasingDropdown.isEnabled() ? antiAliasingDropdown.getSelectedValue() : RenderSettings.AntiAliasing.NONE,
exportCommand.getText(),
exportArguments.getText(),
net.minecraft.client.gui.screen.Screen.hasControlDown()
@@ -574,6 +611,24 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
return new File(folder, fileName + "." + encodingPreset.getFileExtension());
}
public AbstractGuiScreen<?> getScreen() {
return screen;
}
public void setOutputFileBaseName(String base) {
RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue();
File file = new File(outputFile.getParentFile(), base + "." + preset.getFileExtension());
// Ensure the file name is valid
try {
//noinspection ResultOfMethodCallIgnored
file.toPath();
outputFile = file;
outputFileButton.setLabel(file.getName());
} catch (InvalidPathException ignored) {
setOutputFileBaseName("filename_invalid_" + base.hashCode());
}
}
protected File conformExtension(File file, RenderSettings.EncodingPreset preset) {
String name = file.getName();
if (name.contains(".")) {
@@ -588,7 +643,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
private RenderSettings getDefaultRenderSettings() {
return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null,
true, false, false, false, null, 360, 180, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
true, false, false, false, null, 360, 180, false, false, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
}
@Override
public void open() {
super.open();
}
@Override
@@ -600,9 +660,21 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
} catch (IOException e) {
LOGGER.error("Saving render settings:", e);
}
super.close();
}
public ReplayHandler getReplayHandler() {
return replayHandler;
}
@Override
protected GuiRenderSettings getThis() {
return this;
}
public static GuiScreen createBaseScreen() {
GuiScreen screen = new GuiScreen();
screen.setBackground(AbstractGuiScreen.Background.NONE);
return screen;
}
}

View File

@@ -1,7 +1,7 @@
package com.replaymod.render.gui;
import com.replaymod.core.utils.Utils;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.rendering.VideoRenderer;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
@@ -265,9 +265,8 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable {
guiRenderer.drawTexturedRect(x, y, 0, 0, width, height, videoWidth, videoHeight, videoWidth, videoHeight);
}
public void updatePreview(RGBFrame frame) {
public void updatePreview(ByteBuffer buffer, ReadableDimension size) {
if (previewCheckbox.isChecked() && previewTexture != null) {
ByteBuffer buffer = frame.getByteBuffer();
buffer.mark();
synchronized (this) {
//#if MC>=11400
@@ -279,7 +278,6 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable {
// Note: Optifine changes the texture data array to be three times as long (for use by shaders),
// we only want to initialize the first third and since we use our frame size, not the array size,
// we're good to go.
ReadableDimension size = frame.getSize();
int width = size.getWidth();
for (int y = 0; y < size.getHeight(); y++) {
for (int x = 0; x < width; x++) {

View File

@@ -63,15 +63,6 @@ public class ChunkLoadingRenderGlobal {
}
private void install() {
//#if FABRIC>=1
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sodium")) {
throw new UnsupportedOperationException(
"Rendering is not currently supported while Sodium is installed.\n" +
"See https://github.com/ReplayMod/ReplayMod/issues/150\n" +
"For now, you need to uninstall Sodium before rendering!"
);
}
//#endif
try {
//#if MC>=11400
Field hookField = WorldRenderer.class.getField("replayModRender_hook");

View File

@@ -0,0 +1,24 @@
package com.replaymod.render.mixin;
import com.replaymod.render.hooks.EntityRendererHandler;
import net.minecraft.client.render.GameRenderer;
import org.lwjgl.opengl.GL11;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(GameRenderer.class)
public abstract class Mixin_PreserveDepthDuringHandRendering {
@ModifyArg(
method = "renderWorld",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"),
index = 0
)
private int replayModRender_skipClearWhenRecordingDepth(int mask) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this).replayModRender_getHandler();
if (handler != null && handler.getSettings().isDepthMap()) {
mask = mask & ~GL11.GL_DEPTH_BUFFER_BIT;
}
return mask;
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import java.nio.ByteBuffer;
import static com.replaymod.render.utils.Utils.openGlBytesToBitmap;
public class CubicToBitmapProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, BitmapFrame> {
@Override
public BitmapFrame process(CubicOpenGlFrame rawFrame) {
int size = rawFrame.getLeft().getSize().getWidth();
int bpp = rawFrame.getLeft().getBytesPerPixel();
int width = size * 4;
int height = size * 3;
ByteBuffer result = ByteBufferPool.allocate(width * height * bpp);
openGlBytesToBitmap(rawFrame.getLeft(), 0, size, result, width);
openGlBytesToBitmap(rawFrame.getFront(), size, size, result, width);
openGlBytesToBitmap(rawFrame.getRight(), size * 2, size, result, width);
openGlBytesToBitmap(rawFrame.getBack(), size * 3, size, result, width);
openGlBytesToBitmap(rawFrame.getTop(), size, 0, result, width);
openGlBytesToBitmap(rawFrame.getBottom(), size, size * 2, result, width);
ByteBufferPool.release(rawFrame.getLeft().getByteBuffer());
ByteBufferPool.release(rawFrame.getRight().getByteBuffer());
ByteBufferPool.release(rawFrame.getFront().getByteBuffer());
ByteBufferPool.release(rawFrame.getBack().getByteBuffer());
ByteBufferPool.release(rawFrame.getTop().getByteBuffer());
ByteBufferPool.release(rawFrame.getBottom().getByteBuffer());
return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width, height), bpp, result);
}
}

View File

@@ -1,33 +0,0 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import java.nio.ByteBuffer;
import static com.replaymod.render.utils.Utils.openGlBytesToRBG;
public class CubicToRGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, RGBFrame> {
@Override
public RGBFrame process(CubicOpenGlFrame rawFrame) {
int size = rawFrame.getLeft().getSize().getWidth();
int width = size * 4;
int height = size * 3;
ByteBuffer result = ByteBufferPool.allocate(width * height * 4);
openGlBytesToRBG(rawFrame.getLeft().getByteBuffer(), size, 0, size, result, width);
openGlBytesToRBG(rawFrame.getFront().getByteBuffer(), size, size, size, result, width);
openGlBytesToRBG(rawFrame.getRight().getByteBuffer(), size, size * 2, size, result, width);
openGlBytesToRBG(rawFrame.getBack().getByteBuffer(), size, size * 3, size, result, width);
openGlBytesToRBG(rawFrame.getTop().getByteBuffer(), size, size, 0, result, width);
openGlBytesToRBG(rawFrame.getBottom().getByteBuffer(), size, size, size * 2, result, width);
ByteBufferPool.release(rawFrame.getLeft().getByteBuffer());
ByteBufferPool.release(rawFrame.getRight().getByteBuffer());
ByteBufferPool.release(rawFrame.getFront().getByteBuffer());
ByteBufferPool.release(rawFrame.getBack().getByteBuffer());
ByteBufferPool.release(rawFrame.getTop().getByteBuffer());
ByteBufferPool.release(rawFrame.getBottom().getByteBuffer());
return new RGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
}
}

View File

@@ -1,7 +1,7 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import org.apache.commons.lang3.Validate;
@@ -10,7 +10,7 @@ import java.nio.ByteBuffer;
import static java.lang.Math.PI;
public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, RGBFrame> {
public class EquirectangularToBitmapProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, BitmapFrame> {
private static final byte IMAGE_BACK = 0;
private static final byte IMAGE_FRONT = 1;
private static final byte IMAGE_LEFT = 2;
@@ -26,7 +26,7 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
private final int[][] imageX;
private final int[][] imageY;
public EquirectangularToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
public EquirectangularToBitmapProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
// calculate the dimensions of the original equirectangular projection
// (before cropping according to FOV)
width = outputWidth;
@@ -103,16 +103,17 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
}
@Override
public RGBFrame process(CubicOpenGlFrame rawFrame) {
public BitmapFrame process(CubicOpenGlFrame rawFrame) {
Validate.isTrue(rawFrame.getLeft().getSize().getWidth() == frameSize, "Frame size must be %d but was %d",
frameSize, rawFrame.getLeft().getSize().getWidth());
ByteBuffer result = ByteBufferPool.allocate(width * height * 4);
int bpp = rawFrame.getLeft().getBytesPerPixel();
ByteBuffer result = ByteBufferPool.allocate(width * height * bpp);
ByteBuffer[] images = {
rawFrame.getBack().getByteBuffer(), rawFrame.getFront().getByteBuffer(),
rawFrame.getLeft().getByteBuffer(), rawFrame.getRight().getByteBuffer(),
rawFrame.getTop().getByteBuffer(), rawFrame.getBottom().getByteBuffer()
};
byte[] pixel = new byte[4];
byte[] pixel = new byte[bpp];
byte[] image;
int[] imageX, imageY;
for (int y = 0; y < height; y++) {
@@ -121,7 +122,7 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
imageY = this.imageY[y];
for (int x = 0; x < width; x++) {
ByteBuffer source = images[image[x]];
source.position((imageX[x] + imageY[x] * frameSize) * 4);
source.position((imageX[x] + imageY[x] * frameSize) * bpp);
source.get(pixel);
result.put(pixel);
}
@@ -134,7 +135,7 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
ByteBufferPool.release(rawFrame.getBack().getByteBuffer());
ByteBufferPool.release(rawFrame.getTop().getByteBuffer());
ByteBufferPool.release(rawFrame.getBottom().getByteBuffer());
return new RGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width, height), bpp, result);
}
public int getFrameSize() {

View File

@@ -0,0 +1,33 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.BitmapFrame;
import java.nio.FloatBuffer;
public class GlToAbsoluteDepthProcessor extends AbstractFrameProcessor<BitmapFrame, BitmapFrame> {
// absolute depth is 2 * near * far / (far + near - (far - near) * (2 * z - 1))
// precomputed: [ a ] [ b ] [ c ]
private final float a;
private final float b;
private final float c;
public GlToAbsoluteDepthProcessor(float near, float far) {
a = 2 * near * far;
b = far + near;
c = far - near;
}
@Override
public BitmapFrame process(BitmapFrame frame) {
FloatBuffer buffer = frame.getByteBuffer().asFloatBuffer();
int len = buffer.remaining();
for (int i = 0; i < len; i++) {
float z = buffer.get(i);
z = a / (b - c * (2 * z - 1));
buffer.put(i, z);
}
return frame;
}
}

View File

@@ -1,7 +1,7 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.ODSOpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
@@ -9,25 +9,26 @@ import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import java.io.IOException;
import java.nio.ByteBuffer;
public class ODSToRGBProcessor extends AbstractFrameProcessor<ODSOpenGlFrame, RGBFrame> {
private final EquirectangularToRGBProcessor processor;
public class ODSToBitmapProcessor extends AbstractFrameProcessor<ODSOpenGlFrame, BitmapFrame> {
private final EquirectangularToBitmapProcessor processor;
public ODSToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
processor = new EquirectangularToRGBProcessor(outputWidth, outputHeight / 2, sphericalFovX);
public ODSToBitmapProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
processor = new EquirectangularToBitmapProcessor(outputWidth, outputHeight / 2, sphericalFovX);
}
@Override
public RGBFrame process(ODSOpenGlFrame rawFrame) {
RGBFrame leftFrame = processor.process(rawFrame.getLeft());
RGBFrame rightFrame = processor.process(rawFrame.getRight());
public BitmapFrame process(ODSOpenGlFrame rawFrame) {
BitmapFrame leftFrame = processor.process(rawFrame.getLeft());
BitmapFrame rightFrame = processor.process(rawFrame.getRight());
ReadableDimension size = new Dimension(leftFrame.getSize().getWidth(), leftFrame.getSize().getHeight() * 2);
ByteBuffer result = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * 4);
int bpp = rawFrame.getLeft().getLeft().getBytesPerPixel();
ByteBuffer result = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * bpp);
result.put(leftFrame.getByteBuffer());
result.put(rightFrame.getByteBuffer());
result.rewind();
ByteBufferPool.release(leftFrame.getByteBuffer());
ByteBufferPool.release(rightFrame.getByteBuffer());
return new RGBFrame(rawFrame.getFrameId(), size, result);
return new BitmapFrame(rawFrame.getFrameId(), size, bpp, result);
}
@Override

View File

@@ -1,21 +1,22 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import java.nio.ByteBuffer;
public class OpenGlToRGBProcessor extends AbstractFrameProcessor<OpenGlFrame, RGBFrame> {
public class OpenGlToBitmapProcessor extends AbstractFrameProcessor<OpenGlFrame, BitmapFrame> {
private byte[] row, rowSwap;
@Override
public RGBFrame process(OpenGlFrame rawFrame) {
public BitmapFrame process(OpenGlFrame rawFrame) {
// Flip whole image in place
ReadableDimension size = rawFrame.getSize();
int rowSize = size.getWidth() * 4;
int bpp = rawFrame.getBytesPerPixel();
int rowSize = size.getWidth() * bpp;
if (row == null || row.length < rowSize) {
row = new byte[rowSize];
rowSwap = new byte[rowSize];
@@ -37,6 +38,6 @@ public class OpenGlToRGBProcessor extends AbstractFrameProcessor<OpenGlFrame, RG
buffer.put(rowSwap);
}
buffer.rewind();
return new RGBFrame(rawFrame.getFrameId(), size, buffer);
return new BitmapFrame(rawFrame.getFrameId(), size, bpp, buffer);
}
}

View File

@@ -0,0 +1,26 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import java.nio.ByteBuffer;
import static com.replaymod.render.utils.Utils.openGlBytesToBitmap;
public class StereoscopicToBitmapProcessor extends AbstractFrameProcessor<StereoscopicOpenGlFrame, BitmapFrame> {
@Override
public BitmapFrame process(StereoscopicOpenGlFrame rawFrame) {
ReadableDimension size = rawFrame.getLeft().getSize();
int width = size.getWidth();
int bpp = rawFrame.getLeft().getBytesPerPixel();
ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * bpp);
openGlBytesToBitmap(rawFrame.getLeft(), 0, 0, result, width * 2);
openGlBytesToBitmap(rawFrame.getRight(), size.getWidth(), 0, result, width * 2);
ByteBufferPool.release(rawFrame.getLeft().getByteBuffer());
ByteBufferPool.release(rawFrame.getRight().getByteBuffer());
return new BitmapFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), bpp, result);
}
}

View File

@@ -1,27 +0,0 @@
package com.replaymod.render.processor;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
import com.replaymod.render.utils.ByteBufferPool;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import java.nio.ByteBuffer;
import static com.replaymod.render.utils.Utils.openGlBytesToRBG;
public class StereoscopicToRGBProcessor extends AbstractFrameProcessor<StereoscopicOpenGlFrame, RGBFrame> {
@Override
public RGBFrame process(StereoscopicOpenGlFrame rawFrame) {
ReadableDimension size = rawFrame.getLeft().getSize();
int width = size.getWidth();
ByteBuffer leftBuffer = rawFrame.getLeft().getByteBuffer();
ByteBuffer rightBuffer = rawFrame.getRight().getByteBuffer();
ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * 4);
openGlBytesToRBG(leftBuffer, width, 0, 0, result, width * 2);
openGlBytesToRBG(rightBuffer, width, size.getWidth(), 0, result, width * 2);
ByteBufferPool.release(leftBuffer);
ByteBufferPool.release(rightBuffer);
return new RGBFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), result);
}
}

View File

@@ -0,0 +1,6 @@
package com.replaymod.render.rendering;
public enum Channel {
BRGA,
DEPTH,
}

View File

@@ -1,11 +1,12 @@
package com.replaymod.render.rendering;
import java.io.Closeable;
import java.util.Map;
public interface FrameCapturer<R extends Frame> extends Closeable {
boolean isDone();
R process();
Map<Channel, R> process();
}

View File

@@ -1,9 +1,10 @@
package com.replaymod.render.rendering;
import java.io.Closeable;
import java.util.Map;
public interface FrameConsumer<P extends Frame> extends Closeable {
void consume(P frame);
void consume(Map<Channel, P> channels);
}

View File

@@ -3,6 +3,8 @@ package com.replaymod.render.rendering;
import com.replaymod.core.mixin.MinecraftAccessor;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.capturer.WorldRenderer;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.processor.GlToAbsoluteDepthProcessor;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashException;
@@ -13,11 +15,14 @@ import org.lwjgl.glfw.GLFW;
//$$ import org.lwjgl.opengl.Display;
//#endif
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.replaymod.core.versions.MCVer.getMinecraft;
//#if MC>=11400
import static com.replaymod.core.versions.MCVer.getWindow;
//#endif
@@ -27,6 +32,7 @@ public class Pipeline<R extends Frame, P extends Frame> implements Runnable {
private final WorldRenderer worldRenderer;
private final FrameCapturer<R> capturer;
private final FrameProcessor<R, P> processor;
private final GlToAbsoluteDepthProcessor depthProcessor;
private int consumerNextFrame;
private final Object consumerLock = new Object();
private final FrameConsumer<P> consumer;
@@ -38,6 +44,10 @@ public class Pipeline<R extends Frame, P extends Frame> implements Runnable {
this.capturer = capturer;
this.processor = processor;
this.consumer = consumer;
float near = 0.05f;
float far = getMinecraft().options.viewDistance * 16 * 4;
this.depthProcessor = new GlToAbsoluteDepthProcessor(near, far);
}
@Override
@@ -70,7 +80,7 @@ public class Pipeline<R extends Frame, P extends Frame> implements Runnable {
processService.shutdown();
return;
}
R rawFrame = capturer.process();
Map<Channel, R> rawFrame = capturer.process();
if (rawFrame != null) {
processService.submit(new ProcessTask(rawFrame));
}
@@ -99,25 +109,37 @@ public class Pipeline<R extends Frame, P extends Frame> implements Runnable {
}
private class ProcessTask implements Runnable {
private final R rawFrame;
private final Map<Channel, R> rawChannels;
public ProcessTask(R rawFrame) {
this.rawFrame = rawFrame;
public ProcessTask(Map<Channel, R> rawChannels) {
this.rawChannels = rawChannels;
}
@Override
public void run() {
try {
P processedFrame = processor.process(rawFrame);
Integer frameId = null;
Map<Channel, P> processedChannels = new HashMap<>();
for (Map.Entry<Channel, R> entry : rawChannels.entrySet()) {
P processedFrame = processor.process(entry.getValue());
if (entry.getKey() == Channel.DEPTH && processedFrame instanceof BitmapFrame) {
depthProcessor.process((BitmapFrame) processedFrame);
}
processedChannels.put(entry.getKey(), processedFrame);
frameId = processedFrame.getFrameId();
}
if (frameId == null) {
return;
}
synchronized (consumerLock) {
while (consumerNextFrame != processedFrame.getFrameId()) {
while (consumerNextFrame != frameId) {
try {
consumerLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
consumer.consume(processedFrame);
consumer.consume(processedChannels);
consumerNextFrame++;
consumerLock.notifyAll();
}

View File

@@ -14,19 +14,21 @@ import com.replaymod.render.capturer.WorldRenderer;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.ODSOpenGlFrame;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
import com.replaymod.render.hooks.EntityRendererHandler;
import com.replaymod.render.processor.CubicToRGBProcessor;
import com.replaymod.render.processor.CubicToBitmapProcessor;
import com.replaymod.render.processor.DummyProcessor;
import com.replaymod.render.processor.EquirectangularToRGBProcessor;
import com.replaymod.render.processor.ODSToRGBProcessor;
import com.replaymod.render.processor.OpenGlToRGBProcessor;
import com.replaymod.render.processor.StereoscopicToRGBProcessor;
import com.replaymod.render.processor.EquirectangularToBitmapProcessor;
import com.replaymod.render.processor.ODSToBitmapProcessor;
import com.replaymod.render.processor.OpenGlToBitmapProcessor;
import com.replaymod.render.processor.StereoscopicToBitmapProcessor;
import com.replaymod.render.utils.PixelBufferObject;
import java.util.Map;
public class Pipelines {
public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
switch (method) {
case DEFAULT:
return newDefaultPipeline(renderInfo, consumer);
@@ -44,51 +46,51 @@ public class Pipelines {
throw new UnsupportedOperationException("Unknown method: " + method);
}
public static Pipeline<OpenGlFrame, RGBFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline<OpenGlFrame, BitmapFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
FrameCapturer<OpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) {
capturer = new SimplePboOpenGlFrameCapturer(worldRenderer, renderInfo);
} else {
capturer = new SimpleOpenGlFrameCapturer(worldRenderer, renderInfo);
}
return new Pipeline<>(worldRenderer, capturer, new OpenGlToRGBProcessor(), consumer);
return new Pipeline<>(worldRenderer, capturer, new OpenGlToBitmapProcessor(), consumer);
}
public static Pipeline<StereoscopicOpenGlFrame, RGBFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline<StereoscopicOpenGlFrame, BitmapFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
FrameCapturer<StereoscopicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) {
capturer = new StereoscopicPboOpenGlFrameCapturer(worldRenderer, renderInfo);
} else {
capturer = new StereoscopicOpenGlFrameCapturer(worldRenderer, renderInfo);
}
return new Pipeline<>(worldRenderer, capturer, new StereoscopicToRGBProcessor(), consumer);
return new Pipeline<>(worldRenderer, capturer, new StereoscopicToBitmapProcessor(), consumer);
}
public static Pipeline<CubicOpenGlFrame, RGBFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline<CubicOpenGlFrame, BitmapFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
FrameCapturer<CubicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) {
capturer = new CubicPboOpenGlFrameCapturer(worldRenderer, renderInfo, settings.getVideoWidth() / 4);
} else {
capturer = new CubicOpenGlFrameCapturer(worldRenderer, renderInfo, settings.getVideoWidth() / 4);
}
return new Pipeline<>(worldRenderer, capturer, new CubicToRGBProcessor(), consumer);
return new Pipeline<>(worldRenderer, capturer, new CubicToBitmapProcessor(), consumer);
}
public static Pipeline<CubicOpenGlFrame, RGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline<CubicOpenGlFrame, BitmapFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
EquirectangularToRGBProcessor processor = new EquirectangularToRGBProcessor(settings.getVideoWidth(),
EquirectangularToBitmapProcessor processor = new EquirectangularToBitmapProcessor(settings.getVideoWidth(),
settings.getVideoHeight(), settings.getSphericalFovX());
FrameCapturer<CubicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
if (PixelBufferObject.SUPPORTED || settings.isDepthMap()) {
capturer = new CubicPboOpenGlFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize());
} else {
capturer = new CubicOpenGlFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize());
@@ -96,11 +98,11 @@ public class Pipelines {
return new Pipeline<>(worldRenderer, capturer, processor, consumer);
}
public static Pipeline<ODSOpenGlFrame, RGBFrame> newODSPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
public static Pipeline<ODSOpenGlFrame, BitmapFrame> newODSPipeline(RenderInfo renderInfo, FrameConsumer<BitmapFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
ODSToRGBProcessor processor = new ODSToRGBProcessor(settings.getVideoWidth(),
ODSToBitmapProcessor processor = new ODSToBitmapProcessor(settings.getVideoWidth(),
settings.getVideoHeight(), settings.getSphericalFovX());
FrameCapturer<ODSOpenGlFrame> capturer =
@@ -108,13 +110,13 @@ public class Pipelines {
return new Pipeline<>(worldRenderer, capturer, processor, consumer);
}
public static Pipeline<RGBFrame, RGBFrame> newBlendPipeline(RenderInfo renderInfo) {
public static Pipeline<BitmapFrame, BitmapFrame> newBlendPipeline(RenderInfo renderInfo) {
RenderSettings settings = renderInfo.getRenderSettings();
WorldRenderer worldRenderer = new EntityRendererHandler(settings, renderInfo);
FrameCapturer<RGBFrame> capturer = new BlendFrameCapturer(worldRenderer, renderInfo);
FrameConsumer<RGBFrame> consumer = new FrameConsumer<RGBFrame>() {
FrameCapturer<BitmapFrame> capturer = new BlendFrameCapturer(worldRenderer, renderInfo);
FrameConsumer<BitmapFrame> consumer = new FrameConsumer<BitmapFrame>() {
@Override
public void consume(RGBFrame frame) {
public void consume(Map<Channel, BitmapFrame> channels) {
}
@Override

View File

@@ -7,13 +7,14 @@ import com.replaymod.core.versions.MCVer;
import com.replaymod.pathing.player.AbstractTimelinePlayer;
import com.replaymod.pathing.player.ReplayTimer;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.render.CameraPathExporter;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.ReplayModRender;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.FFmpegWriter;
import com.replaymod.render.blend.BlendState;
import com.replaymod.render.capturer.RenderInfo;
import com.replaymod.render.events.ReplayRenderCallback;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.BitmapFrame;
import com.replaymod.render.gui.GuiRenderingDone;
import com.replaymod.render.gui.GuiVideoRenderer;
import com.replaymod.render.metadata.MetadataInjector;
@@ -43,6 +44,7 @@ import org.lwjgl.opengl.GL11;
//#endif
//#if MC>=11400
import com.replaymod.render.EXRWriter;
import com.replaymod.render.mixin.MainWindowAccessor;
import net.minecraft.client.gui.screen.Screen;
import org.lwjgl.glfw.GLFW;
@@ -83,7 +85,8 @@ public class VideoRenderer implements RenderInfo {
private final ReplayHandler replayHandler;
private final Timeline timeline;
private final Pipeline renderingPipeline;
private final VideoWriter videoWriter;
private final FFmpegWriter ffmpegWriter;
private final CameraPathExporter cameraPathExporter;
private int fps;
private boolean mouseWasGrabbed;
@@ -119,16 +122,41 @@ public class VideoRenderer implements RenderInfo {
BlendState.setState(new BlendState(settings.getOutputFile()));
this.renderingPipeline = Pipelines.newBlendPipeline(this);
this.videoWriter = null;
this.ffmpegWriter = null;
} else {
this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this,
videoWriter = new VideoWriter(this) {
@Override
public void consume(RGBFrame frame) {
gui.updatePreview(frame);
super.consume(frame);
FrameConsumer<BitmapFrame> frameConsumer;
if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) {
ffmpegWriter = null;
//#if MC>=11400
frameConsumer = new EXRWriter(settings.getOutputFile().toPath());
//#else
//$$ throw new UnsupportedOperationException("EXR requires LWJGL3");
//#endif
} else {
frameConsumer = ffmpegWriter = new FFmpegWriter(this);
}
});
FrameConsumer<BitmapFrame> previewingFrameConsumer = new FrameConsumer<BitmapFrame>() {
@Override
public void consume(Map<Channel, BitmapFrame> channels) {
BitmapFrame bgra = channels.get(Channel.BRGA);
if (bgra != null) {
gui.updatePreview(bgra.getByteBuffer(), bgra.getSize());
}
frameConsumer.consume(channels);
}
@Override
public void close() throws IOException {
frameConsumer.close();
}
};
this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, previewingFrameConsumer);
}
if (settings.isCameraPathExport()) {
this.cameraPathExporter = new CameraPathExporter(settings);
} else {
this.cameraPathExporter = null;
}
}
@@ -267,6 +295,10 @@ public class VideoRenderer implements RenderInfo {
//$$ mc.displayHeight = displayHeightBefore;
//#endif
if (cameraPathExporter != null) {
cameraPathExporter.recordFrame(timer.tickDelta);
}
framesDone++;
return timer.tickDelta;
}
@@ -328,6 +360,10 @@ public class VideoRenderer implements RenderInfo {
totalFrames = (int) (duration*fps/1000);
if (cameraPathExporter != null) {
cameraPathExporter.setup(totalFrames);
}
updateDisplaySize();
//#if MC<=10809
@@ -378,13 +414,21 @@ public class VideoRenderer implements RenderInfo {
}
//#endif
if (!hasFailed() && cameraPathExporter != null) {
try {
cameraPathExporter.finish();
} catch (IOException e) {
setFailure(e);
}
}
new SoundHandler().playRenderSuccessSound();
try {
if (!hasFailed() && videoWriter != null) {
new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display();
if (!hasFailed() && ffmpegWriter != null) {
new GuiRenderingDone(ReplayModRender.instance, ffmpegWriter.getVideoFile(), totalFrames, settings).display();
}
} catch (VideoWriter.FFmpegStartupException e) {
} catch (FFmpegWriter.FFmpegStartupException e) {
setFailure(e);
}
@@ -652,8 +696,8 @@ public class VideoRenderer implements RenderInfo {
}
public void cancel() {
if (videoWriter != null) {
videoWriter.abort();
if (ffmpegWriter != null) {
ffmpegWriter.abort();
}
this.cancelled = true;
renderingPipeline.cancel();
@@ -683,4 +727,17 @@ public class VideoRenderer implements RenderInfo {
return getVideoTime();
}
}
public static String[] checkCompat() {
//#if FABRIC>=1
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sodium")) {
return new String[] {
"Rendering is not currently supported while Sodium is installed.",
"See https://github.com/ReplayMod/ReplayMod/issues/150",
"For now, you need to uninstall Sodium before rendering!"
};
}
//#endif
return null;
}
}

View File

@@ -1,12 +1,30 @@
package com.replaymod.render.utils;
import com.google.common.base.Optional;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.replaymod.render.RenderSettings;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.simplepathing.SPTimeline;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class RenderJob {
private String name;
private Timeline timeline;
private RenderSettings settings;
@@ -14,7 +32,7 @@ public class RenderJob {
}
public String getName() {
return this.name;
return settings.getOutputFile().getName();
}
public Timeline getTimeline() {
@@ -25,10 +43,6 @@ public class RenderJob {
return this.settings;
}
public void setName(String name) {
this.name = name;
}
public void setTimeline(Timeline timeline) {
this.timeline = timeline;
}
@@ -42,22 +56,76 @@ public class RenderJob {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RenderJob renderJob = (RenderJob) o;
return name.equals(renderJob.name) &&
timeline.equals(renderJob.timeline) &&
return timeline.equals(renderJob.timeline) &&
settings.equals(renderJob.settings);
}
@Override
public int hashCode() {
return Objects.hash(name, timeline, settings);
return Objects.hash(timeline, settings);
}
@Override
public String toString() {
return "RenderJob{" +
"name='" + name + '\'' +
", timeline=" + timeline +
"timeline=" + timeline +
", settings=" + settings +
'}';
}
public static List<RenderJob> readQueue(ReplayFile replayFile) throws IOException {
synchronized (replayFile) {
Optional<InputStream> optIn = replayFile.get("renderQueue.json");
if (!optIn.isPresent()) {
return new ArrayList<>();
}
try (InputStream in = optIn.get();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return new GsonBuilder()
.registerTypeAdapter(Timeline.class, new TimelineTypeAdapter())
.create()
.fromJson(reader, new TypeToken<List<RenderJob>>(){}.getType());
}
}
}
public static void writeQueue(ReplayFile replayFile, List<RenderJob> renderQueue) throws IOException {
synchronized (replayFile) {
try (OutputStream out = replayFile.write("renderQueue.json");
OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
new GsonBuilder()
.registerTypeAdapter(Timeline.class, new TimelineTypeAdapter())
.create()
.toJson(renderQueue, writer);
}
}
}
private static class TimelineTypeAdapter extends TypeAdapter<Timeline> {
private final TimelineSerialization serialization;
public TimelineTypeAdapter(TimelineSerialization serialization) {
this.serialization = serialization;
}
public TimelineTypeAdapter(PathingRegistry registry) {
this(new TimelineSerialization(registry, null));
}
public TimelineTypeAdapter() {
// TODO need to somehow get rid of the reliance on simplepathing
this(new SPTimeline());
}
@Override
public void write(JsonWriter out, Timeline value) throws IOException {
out.value(serialization.serialize(Collections.singletonMap("", value)));
}
@Override
public Timeline read(JsonReader in) throws IOException {
return serialization.deserialize(in.nextString()).get("");
}
}
}

View File

@@ -1,24 +1,41 @@
package com.replaymod.render.utils;
import com.replaymod.render.frame.OpenGlFrame;
import java.nio.ByteBuffer;
public class Utils {
/**
* Copies the rgb image (flipped vertically) to the specified position in the target buffer
* @param buffer Source image
* @param bufferWidth Source image width
* @param source Source image
* @param xOffset X offset in target image
* @param yOffset Y offset in target image
* @param to Target image
* @param width Target image width
*/
public static void openGlBytesToRBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) {
byte[] rowBuf = new byte[bufferWidth * 4];
public static void openGlBytesToBitmap(OpenGlFrame source, int xOffset, int yOffset, ByteBuffer to, int width) {
openGlBytesToBitmap(
source.getByteBuffer(), source.getSize().getWidth(), source.getBytesPerPixel(),
xOffset, yOffset, to, width);
}
/**
* Copies the rgb image (flipped vertically) to the specified position in the target buffer
* @param buffer Source image
* @param bufferWidth Source image width
* @param bbp Bytes per pixel
* @param xOffset X offset in target image
* @param yOffset Y offset in target image
* @param to Target image
* @param width Target image width
*/
public static void openGlBytesToBitmap(ByteBuffer buffer, int bufferWidth, int bbp, int xOffset, int yOffset, ByteBuffer to, int width) {
byte[] rowBuf = new byte[bufferWidth * bbp];
// Copy image flipped vertically to target buffer
int rows = buffer.remaining() / 4 / bufferWidth;
int rows = buffer.remaining() / bbp / bufferWidth;
for (int i = 0; i < rows; i++) {
buffer.get(rowBuf);
to.position(((yOffset + rows - i - 1) * width + xOffset) * 4);
to.position(((yOffset + rows - i - 1) * width + xOffset) * bbp);
to.put(rowBuf);
}
to.rewind();

View File

@@ -225,7 +225,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
/**
* Whether we're currently reading packets from the login phase.
*/
private boolean loginPhase;
private boolean loginPhase = true;
/**
* Whether we need to restart the current replay. E.g. when jumping backwards in time

View File

@@ -29,6 +29,7 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.DownloadingTerrainScreen;
import net.minecraft.client.network.ClientLoginNetworkHandler;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.entity.Entity;
@@ -671,7 +672,10 @@ public class ReplayHandler {
//#endif
// Send the packets
do {
replaySender.sendPacketsTill(targetTime);
targetTime += 500;
} while (mc.player == null || mc.currentScreen instanceof DownloadingTerrainScreen);
replaySender.setAsyncMode(true);
replaySender.setReplaySpeed(0);

View File

@@ -20,8 +20,6 @@ import com.replaymod.replay.gui.screen.GuiModCompatWarning;
import com.replaymod.replay.handler.GuiHandler;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.options.KeyBinding;
import org.apache.logging.log4j.LogManager;
@@ -42,6 +40,7 @@ public class ReplayModReplay implements Module {
public static ReplayModReplay instance;
private ReplayMod core;
public KeyBindingRegistry.Binding keyPlayPause;
private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry();
@@ -116,7 +115,7 @@ public class ReplayModReplay implements Module {
}
}, true);
registry.registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() {
keyPlayPause = registry.registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() {
@Override
public void run() {
if (replayHandler != null) {
@@ -162,28 +161,38 @@ public class ReplayModReplay implements Module {
}
public void startReplay(File file) throws IOException {
startReplay(new ZipReplayFile(new ReplayStudio(), file));
startReplay(core.openReplay(file.toPath()));
}
public void startReplay(ReplayFile replayFile) throws IOException {
startReplay(replayFile, true);
startReplay(replayFile, true, true);
}
public void startReplay(ReplayFile replayFile, boolean checkModCompat) throws IOException {
public ReplayHandler startReplay(ReplayFile replayFile, boolean checkModCompat, boolean asyncMode) throws IOException {
if (replayHandler != null) {
replayHandler.endReplay();
}
if (checkModCompat) {
ModCompat.ModInfoDifference modDifference = new ModCompat.ModInfoDifference(replayFile.getModInfo());
if (!modDifference.getMissing().isEmpty() || !modDifference.getDiffering().isEmpty()) {
new GuiModCompatWarning(this, replayFile, modDifference).display();
return;
GuiModCompatWarning screen = new GuiModCompatWarning(modDifference);
screen.loadButton.onClick(() -> {
try {
startReplay(replayFile, false, asyncMode);
} catch (IOException e) {
e.printStackTrace();
}
});
screen.display();
return null;
}
}
replayHandler = new ReplayHandler(replayFile, true);
replayHandler = new ReplayHandler(replayFile, asyncMode);
//#if MC>=11400
KeyBinding.updateKeysByCode(); // see Mixin_ContextualKeyBindings
//#endif
return replayHandler;
}
public void forcefullyStopReplay() {

View File

@@ -1,5 +1,6 @@
package com.replaymod.replay.camera;
import com.replaymod.core.KeyBindingRegistry;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.SettingsRegistry;
import com.replaymod.core.events.SettingsChangedCallback;
@@ -13,7 +14,6 @@ import com.replaymod.replaystudio.util.Location;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.options.KeyBinding;
import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.decoration.ItemFrameEntity;
@@ -405,6 +405,22 @@ public class CameraEntity
//$$ }
//#endif
//#if MC>=10800
@Override
public float getSpeed() {
Entity view = getRenderViewEntity(this.client);
if (view != this && view instanceof AbstractClientPlayerEntity) {
return ((AbstractClientPlayerEntity) view).getSpeed();
}
return 1;
}
//#else
//$$ @Override
//$$ public float getFOVMultiplier() {
//$$ return 1;
//$$ }
//#endif
@Override
public boolean isInvisible() {
Entity view = getRenderViewEntity(this.client);
@@ -585,11 +601,11 @@ public class CameraEntity
handleInputEvents();
Map<String, KeyBinding> keyBindings = ReplayMod.instance.getKeyBindingRegistry().getKeyBindings();
if (keyBindings.get("replaymod.input.rollclockwise").isPressed()) {
Map<String, KeyBindingRegistry.Binding> keyBindings = ReplayMod.instance.getKeyBindingRegistry().getBindings();
if (keyBindings.get("replaymod.input.rollclockwise").keyBinding.isPressed()) {
roll += Utils.isCtrlDown() ? 0.2 : 1;
}
if (keyBindings.get("replaymod.input.rollcounterclockwise").isPressed()) {
if (keyBindings.get("replaymod.input.rollcounterclockwise").keyBinding.isPressed()) {
roll -= Utils.isCtrlDown() ? 0.2 : 1;
}

View File

@@ -250,4 +250,9 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline<GuiMarkerTimeline> im
markers.add(marker);
saveMarkers.accept(markers);
}
@Override
public ReadableDimension getLastSize() {
return super.getLastSize();
}
}

View File

@@ -1,16 +1,19 @@
package com.replaymod.replay.gui.overlay;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
import com.replaymod.core.versions.MCVer.Keyboard;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.ReplaySender;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiSlider;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline;
import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -22,30 +25,26 @@ import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint;
import net.minecraft.client.options.GameOptions;
import net.minecraft.client.resource.language.I18n;
//#if MC>=11400
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
//#else
//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
//$$ import net.minecraftforge.fml.common.gameevent.InputEvent;
//#endif
import static com.replaymod.core.ReplayMod.TEXTURE_SIZE;
public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
private final ReplayModReplay mod = ReplayModReplay.instance;
public final GuiPanel topPanel = new GuiPanel(this)
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5));
public final GuiTexturedButton playPauseButton = new GuiTexturedButton() {
public final GuiButton playPauseButton = new GuiButton() {
@Override
public GuiElement getTooltip(RenderInfo renderInfo) {
GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo);
if (tooltip != null) {
if (getTextureNormal().getY() == 0) { // Play button
tooltip.setI18nText("replaymod.gui.ingame.menu.unpause");
String label;
if (getSpriteUV().getY() == 0) { // Play button
label = "replaymod.gui.ingame.menu.unpause";
} else { // Pause button
tooltip.setI18nText("replaymod.gui.ingame.menu.pause");
label = "replaymod.gui.ingame.menu.pause";
}
tooltip.setText(I18n.translate(label) + " (" + mod.keyPlayPause.getBoundKey() + ")");
}
return tooltip;
}
@@ -61,6 +60,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(5));
private final EventHandler eventHandler = new EventHandler();
private boolean hidden;
public GuiReplayOverlay(final ReplayHandler replayHandler) {
timeline = new GuiMarkerTimeline(replayHandler){
@@ -83,7 +83,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
}
});
playPauseButton.setTexturePosH(new ReadablePoint() {
playPauseButton.setSpriteUV(new ReadablePoint() {
@Override
public int getX() {
return 0;
@@ -156,8 +156,8 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
// Do not render overlay when user pressed F1 and we are not currently in some popup
if (getMinecraft().options.hudHidden && isAllowUserInput()) {
// Do not render overlay if all hud, or this one specifically, is hidden and we're not in some popup
if ((getMinecraft().options.hudHidden || hidden) && isAllowUserInput()) {
// Note that this only applies to when the mouse is visible, otherwise
// the draw method isn't called in the first place
return;
@@ -176,13 +176,8 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
//$$ public // All event handlers need to be public in 1.7.10
//#endif
class EventHandler extends EventRegistrations {
//#if MC>=11400
{ on(KeyBindingEventCallback.EVENT, this::onKeyBindingEvent); }
private void onKeyBindingEvent() {
//#else
//$$ @SubscribeEvent
//$$ public void onKeyBindingEvent(InputEvent.KeyInputEvent event) {
//#endif
GameOptions gameSettings = getMinecraft().options;
while (gameSettings.keyChat.wasPressed() || gameSettings.keyCommand.wasPressed()) {
if (!isMouseVisible()) {
@@ -191,20 +186,12 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
}
}
//#if MC>=11400
{ on(KeyEventCallback.EVENT, (int key, int scanCode, int action, int modifiers) -> onKeyInput(key, action)); }
{ on(KeyEventCallback.EVENT, (int key, int scanCode, int action, int modifiers) -> { onKeyInput(key, action); return false; }); }
private void onKeyInput(int key, int action) {
if (action != 0) return;
//#else
//$$ @SubscribeEvent
//$$ public void onKeyInput(InputEvent.KeyInputEvent event) {
//$$ if (!Keyboard.getEventKeyState()) return;
//$$ int key = Keyboard.getEventKey();
//#endif
GameOptions gameSettings = getMinecraft().options;
// Handle the F1 key binding while the overlay is opened as a gui screen
if (action != KeyEventCallback.ACTION_PRESS) return;
// Allow F1 to be used to hide the replay gui (e.g. for recording with OBS)
if (isMouseVisible() && key == Keyboard.KEY_F1) {
gameSettings.hudHidden = !gameSettings.hudHidden;
hidden = !hidden;
}
}
}

View File

@@ -1,9 +1,7 @@
package com.replaymod.replay.gui.screen;
import com.replaymod.core.utils.ModCompat;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.data.ModInfo;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.util.I18n;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiPanel;
@@ -14,7 +12,6 @@ import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import java.io.IOException;
import java.util.Map;
public class GuiModCompatWarning extends AbstractGuiScreen<GuiModCompatWarning> {
@@ -38,7 +35,7 @@ public class GuiModCompatWarning extends AbstractGuiScreen<GuiModCompatWarning>
content.getListLayout().setSpacing(8);
}
public GuiModCompatWarning(ReplayModReplay mod, ReplayFile replayFile, ModCompat.ModInfoDifference difference) {
public GuiModCompatWarning(ModCompat.ModInfoDifference difference) {
VerticalLayout.Data data = new VerticalLayout.Data(0.5);
GuiPanel content = this.content.getListPanel();
content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.message1"));
@@ -82,13 +79,6 @@ public class GuiModCompatWarning extends AbstractGuiScreen<GuiModCompatWarning>
}
cancelButton.onClick(() -> getMinecraft().openScreen(null));
loadButton.onClick(() -> {
try {
mod.startReplay(replayFile, false);
} catch (IOException e) {
e.printStackTrace();
}
});
}
@Override

View File

@@ -4,7 +4,14 @@ import com.google.common.base.Supplier;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.render.gui.GuiRenderQueue;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.RenderJob;
import com.replaymod.replaystudio.us.myles.ViaVersion.api.Pair;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.versions.Image;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.util.Formatting;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.SettingsRegistry;
@@ -16,8 +23,6 @@ import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.Setting;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.AbstractGuiContainer;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
@@ -50,11 +55,18 @@ import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.replaymod.replay.ReplayModReplay.LOGGER;
import static de.johni0702.minecraft.gui.versions.MCVer.getFontRenderer;
//#if MC>=11400
import net.minecraft.text.TranslatableText;
@@ -65,39 +77,50 @@ import net.minecraft.text.TranslatableText;
public class GuiReplayViewer extends GuiScreen {
private final ReplayModReplay mod;
public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(new Runnable() {
@Override
public void run() {
replaySpecificButtons.forEach(b -> b.setEnabled(list.getSelected() != null));
if (list.getSelected() != null && list.getSelected().incompatible) {
loadButton.setDisabled();
}
}
}).onSelectionDoubleClicked(() -> {
public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(this::updateButtons).onSelectionDoubleClicked(() -> {
if (this.loadButton.isEnabled()) {
this.loadButton.onClick();
}
});
public final GuiButton loadButton = new GuiButton().onClick(new Runnable() {
private boolean loading = false;
@Override
public void run() {
// Prevent the player from opening the replay twice at the same time
if (loading) {
return;
}
loading = true;
loadButton.setDisabled(); // visual hint
List<GuiReplayEntry> selected = list.getSelected();
if (selected.size() == 1) {
try {
mod.startReplay(list.getSelected().file);
mod.startReplay(selected.get(0).file);
} catch (IOException e) {
e.printStackTrace();
} finally {
// Disable load button to prevent the player from opening the replay twice at the same time
loadButton.setDisabled();
}
} else {
Iterator<Pair<File, List<RenderJob>>> replays = selected.stream()
.filter(it -> !it.renderQueue.isEmpty())
.map(it -> new Pair<>(it.file, it.renderQueue))
.iterator();
GuiRenderQueue.processMultipleReplays(GuiReplayViewer.this, mod, replays, () -> {
loading = false;
updateButtons();
display();
});
}
}
}).setSize(150, 20).setI18nLabel("replaymod.gui.load").setDisabled();
}).setSize(150, 20);
public final GuiButton folderButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
try {
File folder = mod.getCore().getReplayFolder();
File folder = mod.getCore().getReplayFolder().toFile();
MCVer.openFile(folder);
} catch (IOException e) {
@@ -109,7 +132,7 @@ public class GuiReplayViewer extends GuiScreen {
public final GuiButton renameButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
final File file = list.getSelected().file;
final File file = list.getSelected().get(0).file;
String name = Utils.fileNameToReplayName(file.getName());
final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(name);
final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this,
@@ -165,10 +188,9 @@ public class GuiReplayViewer extends GuiScreen {
});
}
}).setSize(73, 20).setI18nLabel("replaymod.gui.rename").setDisabled();
public final GuiButton deleteButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
String name = list.getSelected().name.getText();
public final GuiButton deleteButton = new GuiButton().onClick(() -> {
for (GuiReplayEntry entry : list.getSelected()) {
String name = entry.name.getText();
GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this,
new GuiLabel().setI18nText("replaymod.gui.viewer.delete.linea").setColor(Colors.BLACK),
new GuiLabel().setI18nText("replaymod.gui.viewer.delete.lineb", name + Formatting.RESET).setColor(Colors.BLACK)
@@ -178,7 +200,7 @@ public class GuiReplayViewer extends GuiScreen {
public void onSuccess(Boolean delete) {
if (delete) {
try {
FileUtils.forceDelete(list.getSelected().file);
FileUtils.forceDelete(entry.file);
} catch (IOException e) {
e.printStackTrace();
}
@@ -194,12 +216,11 @@ public class GuiReplayViewer extends GuiScreen {
}
}).setSize(73, 20).setI18nLabel("replaymod.gui.delete").setDisabled();
public final GuiButton settingsButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
new GuiReplaySettings(toMinecraft(), mod.getCore().getSettingsRegistry()).display();
}
}).setSize(150, 20).setI18nLabel("replaymod.gui.settings");
public final GuiButton settingsButton = new GuiButton(this)
.setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(20, 0)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.settings"))
.onClick(() -> new GuiReplaySettings(toMinecraft(), getMod().getCore().getSettingsRegistry()).display());
public final GuiButton cancelButton = new GuiButton().onClick(new Runnable() {
@Override
@@ -209,11 +230,11 @@ public class GuiReplayViewer extends GuiScreen {
}).setSize(73, 20).setI18nLabel("replaymod.gui.cancel");
public final List<GuiButton> replaySpecificButtons = new ArrayList<>();
{ replaySpecificButtons.addAll(Arrays.asList(loadButton, renameButton, deleteButton)); }
{ replaySpecificButtons.addAll(Arrays.asList(renameButton)); }
public final GuiPanel editorButton = new GuiPanel();
public final GuiPanel upperButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5))
.addElements(null, loadButton, settingsButton);
.addElements(null, loadButton);
public final GuiPanel lowerButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5))
.addElements(null, renameButton, deleteButton, editorButton, cancelButton);
public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new VerticalLayout().setSpacing(5))
@@ -223,7 +244,7 @@ public class GuiReplayViewer extends GuiScreen {
this.mod = mod;
try {
list.setFolder(mod.getCore().getReplayFolder());
list.setFolder(mod.getCore().getReplayFolder().toFile());
} catch (IOException e) {
throw new CrashException(CrashReport.create(e, "Getting replay folder"));
}
@@ -237,8 +258,40 @@ public class GuiReplayViewer extends GuiScreen {
pos(list, 0, 30);
size(list, width, y(buttonPanel) - 10 - y(list));
pos(settingsButton, width - width(settingsButton) - 5, 5);
}
});
updateButtons();
}
public ReplayModReplay getMod() {
return mod;
}
private void updateButtons() {
List<GuiReplayEntry> selected = list.getSelected();
int count = selected.size();
replaySpecificButtons.forEach(b -> b.setEnabled(count == 1));
deleteButton.setEnabled(count > 0);
if (count > 1) {
Set<RenderJob> jobs = selected.stream().flatMap(entry -> entry.renderQueue.stream()).collect(Collectors.toSet());
String[] tooltipLines = jobs.stream().map(RenderJob::getName).toArray(String[]::new);
loadButton.setI18nLabel("replaymod.gui.viewer.bulkrender", jobs.size());
loadButton.setTooltip(new GuiTooltip().setText(tooltipLines));
loadButton.setEnabled(!jobs.isEmpty());
String[] compatError = VideoRenderer.checkCompat();
if (compatError != null) {
loadButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError));
}
} else {
loadButton.setI18nLabel("replaymod.gui.load");
loadButton.setTooltip(null);
loadButton.setEnabled(count == 1 && !selected.get(0).incompatible);
}
}
private static final GuiImage DEFAULT_THUMBNAIL = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL);
@@ -269,10 +322,10 @@ public class GuiReplayViewer extends GuiScreen {
acceptButton.setEnabled(list.getSelected() != null);
}).onSelectionDoubleClicked(() -> {
close();
future.set(list.getSelected().file);
future.set(list.getSelected().get(0).file);
});
acceptButton.onClick(() -> {
future.set(list.getSelected().file);
future.set(list.getSelected().get(0).file);
close();
});
cancelButton.onClick(() -> {
@@ -321,6 +374,10 @@ public class GuiReplayViewer extends GuiScreen {
public static class GuiReplayList extends AbstractGuiResourceLoadingList<GuiReplayList, GuiReplayEntry> implements Typeable {
private File folder = null;
// Not actually a child of this element, we just use it for text manipulation
private final GuiTextField filterTextField = new GuiTextField()
.setFocused(true);
public GuiReplayList(GuiContainer container) {
super(container);
}
@@ -332,16 +389,13 @@ public class GuiReplayViewer extends GuiScreen {
LOGGER.warn("Failed to list files in {}", folder);
return;
}
Map<File, Long> lastModified = new HashMap<>();
Arrays.sort(files, Comparator.<File>comparingLong(f -> lastModified.computeIfAbsent(f, File::lastModified)).reversed());
for (final File file : files) {
if (Thread.interrupted()) break;
try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) {
// TODO add a getThumbBytes method to ReplayStudio
final Image thumb = Optional.ofNullable(replayFile.get("thumb").orNull()).flatMap(stream -> {
try (ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath())) {
final Image thumb = Optional.ofNullable(replayFile.getThumbBytes().orNull()).flatMap(stream -> {
try (InputStream in = stream) {
int i = 7;
while (i > 0) {
i -= in.skip(i);
}
return Optional.of(Image.read(in));
} catch (IOException e) {
e.printStackTrace();
@@ -349,9 +403,18 @@ public class GuiReplayViewer extends GuiScreen {
}
}).orElse(null);
final ReplayMetaData metaData = replayFile.getMetaData();
List<RenderJob> renderQueue = RenderJob.readQueue(replayFile);
if (metaData != null) {
results.consume(() -> new GuiReplayEntry(file, metaData, thumb));
results.consume(() -> new GuiReplayEntry(file, metaData, thumb, renderQueue) {
@Override
public ReadableDimension calcMinSize() {
if (isFiltered(this)) {
return new Dimension(-4, -4);
}
return super.calcMinSize();
}
});
}
} catch (Exception e) {
LOGGER.error("Could not load Replay File {}", file.getName(), e);
@@ -364,6 +427,14 @@ public class GuiReplayViewer extends GuiScreen {
this.folder = folder;
}
private boolean isFiltered(GuiReplayEntry entry) {
String filter = filterTextField.getText().toLowerCase();
if (filter.isEmpty()) {
return false;
}
return !entry.name.getText().toLowerCase().contains(filter);
}
@Override
public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) {
if (keyCode == Keyboard.KEY_F1) {
@@ -372,9 +443,52 @@ public class GuiReplayViewer extends GuiScreen {
reg.save();
load();
}
boolean filterHasPriority = !filterTextField.getText().isEmpty();
if (filterHasPriority && filterTextField.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) {
scrollY(0); // ensure we scroll to top if most entries are filtered
return true;
}
if (super.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) {
return true;
}
if (!filterHasPriority && filterTextField.typeKey(mousePosition, keyCode, keyChar, ctrlDown, shiftDown)) {
scrollY(0); // ensure we scroll to top if most entries are filtered
return true;
}
return false;
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
super.draw(renderer, size, renderInfo);
String filter = filterTextField.getText();
if (!filter.isEmpty()) {
boolean anyMatches = getListPanel().calcMinSize().getHeight() > 0;
TextRenderer fontRenderer = getFontRenderer();
int filterTextWidth = fontRenderer.getWidth(filter);
int filterTextHeight = fontRenderer.fontHeight;
renderer.drawRect(
size.getWidth() - 3 - 2 - filterTextWidth - 2,
size.getHeight() - 3 - 2 - filterTextHeight - 2,
2 + filterTextWidth + 2,
2 + filterTextHeight + 2,
Colors.WHITE
);
renderer.drawString(
size.getWidth() - 3 - 2 - filterTextWidth,
size.getHeight() - 3 - 2 - filterTextHeight,
anyMatches ? Colors.BLACK : Colors.DARK_RED,
filter
);
}
}
@Override
protected GuiReplayList getThis() {
return this;
@@ -404,15 +518,22 @@ public class GuiReplayViewer extends GuiScreen {
return new Dimension(dimension.getWidth() + 2, dimension.getHeight() + 2);
}
});
public final GuiImage renderQueueIcon = new GuiImage()
.setSize(10, 10)
.setTexture(ReplayMod.TEXTURE, 40, 0, 20, 20);
private final long dateMillis;
private final boolean incompatible;
private final List<RenderJob> renderQueue;
public GuiReplayEntry(File file, ReplayMetaData metaData, Image thumbImage) {
public GuiReplayEntry(File file, ReplayMetaData metaData, Image thumbImage, List<RenderJob> renderQueue) {
this.file = file;
this.renderQueue = renderQueue;
name.setText(Formatting.UNDERLINE + Utils.fileNameToReplayName(file.getName()));
if (StringUtils.isEmpty(metaData.getServerName())
if (!StringUtils.isEmpty(metaData.getCustomServerName())) {
server.setText(metaData.getCustomServerName());
} else if (StringUtils.isEmpty(metaData.getServerName())
|| !ReplayMod.instance.getSettingsRegistry().get(Setting.SHOW_SERVER_IPS)) {
server.setI18nText("replaymod.gui.iphidden").setColor(Colors.DARK_RED);
} else {
@@ -433,6 +554,12 @@ public class GuiReplayViewer extends GuiScreen {
duration.setText(Utils.convertSecondsToShortString(metaData.getDuration() / 1000));
addElements(null, durationPanel);
if (!renderQueue.isEmpty()) {
renderQueueIcon.setTooltip(new GuiTooltip()
.setText(renderQueue.stream().map(RenderJob::getName).toArray(String[]::new)));
addElements(null, renderQueueIcon);
}
setLayout(new CustomLayout<GuiReplayEntry>() {
@Override
protected void layout(GuiReplayEntry container, int width, int height) {
@@ -442,6 +569,10 @@ public class GuiReplayViewer extends GuiScreen {
pos(infoPanel, width(thumbnail) + 5, 0);
pos(version, width - width(version), 0);
if (renderQueueIcon.getContainer() != null) {
pos(renderQueueIcon, width(thumbnail) - width(renderQueueIcon), 0);
}
}
@Override

View File

@@ -8,11 +8,12 @@ import net.minecraft.client.gui.screen.TitleScreen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.AbstractButtonWidget;
import net.minecraft.client.resource.language.I18n;
//#if MC>=11600
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableText;
//#else
//$$ import net.minecraft.client.resource.language.I18n;
//#endif
//#if FABRIC>=1
@@ -29,6 +30,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static com.replaymod.core.versions.MCVer.*;
@@ -152,40 +154,48 @@ public class GuiHandler extends EventRegistrations {
}
}
if (achievements != null && stats != null) {
moveAllButtonsDirectlyBelowUpwards(buttonList, achievements.y,
achievements.x, stats.x + width(stats));
moveAllButtonsInRect(buttonList,
achievements.x, stats.x + width(stats),
achievements.y, Integer.MAX_VALUE,
-24);
}
// In 1.13+ Forge, the Options button shares one row with the Open to LAN button
//#if MC<11400
//$$ if (openToLan != null) {
//$$ moveAllButtonsDirectlyBelowUpwards(buttonList, openToLan.y,
//$$ openToLan.x, openToLan.x + openToLan.width);
//$$ moveAllButtonsInRect(buttonList,
//$$ openToLan.x, openToLan.x + openToLan.width,
//$$ openToLan.y, Integer.MAX_VALUE,
//$$ -24);
//$$ }
//#endif
}
}
/**
* Moves all buttons that are within a rectangle below a certain y coordinate upwards by 24 units.
* Moves all buttons that in any way intersect a rectangle by a given amount on the y axis.
* @param buttons List of buttons
* @param belowY The Y limit
* @param yStart Top y limit of the rectangle
* @param yEnd Bottom y limit of the rectangle
* @param xStart Left x limit of the rectangle
* @param xEnd Right x limit of the rectangle
* @param moveBy Signed distance to move the buttons
*/
private void moveAllButtonsDirectlyBelowUpwards(
private void moveAllButtonsInRect(
//#if MC>=11400
List<AbstractButtonWidget> buttons,
//#else
//$$ List<GuiButton> buttons,
//#endif
int belowY,
int xStart,
int xEnd
int xEnd,
int yStart,
int yEnd,
int moveBy
) {
buttons.stream()
.filter(button -> button.y >= belowY)
.filter(button -> button.x <= xEnd && button.x + width(button) >= xStart)
.forEach(button -> button.y -= 24);
.filter(button -> button.y <= yEnd && button.y + height(button) >= yStart)
.forEach(button -> button.y += moveBy);
}
//#if FABRIC>=1
@@ -222,15 +232,35 @@ public class GuiHandler extends EventRegistrations {
//$$ @SubscribeEvent
//$$ public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
//$$ Screen guiScreen = getGui(event);
//$$ List<Widget> buttonList = getButtonList(event);
//#endif
if (!(guiScreen instanceof TitleScreen)) {
return;
}
int x = guiScreen.width / 2 - 100;
// We want to position our button below the realms button
int y = findButton(buttonList, "menu.online", 14)
.map(Optional::of)
// or, if someone removed the realms button, we'll alternatively take the multiplayer one
.orElse(findButton(buttonList, "menu.multiplayer", 2))
// if we found some button, put our button at its position (we'll move it out of the way shortly)
.map(it -> it.y)
// and if we can't even find that one, then just guess
.orElse(guiScreen.height / 4 + 10 + 4 * 24);
// Move all buttons above or at our one upwards
moveAllButtonsInRect(buttonList,
x, x + 200,
Integer.MIN_VALUE, y,
-24);
// Add our button
InjectedButton button = new InjectedButton(
guiScreen,
BUTTON_REPLAY_VIEWER,
guiScreen.width / 2 - 100,
guiScreen.height / 4 + 10 + 4 * 24,
x,
y,
200,
20,
"replaymod.gui.replayviewer",

View File

@@ -14,7 +14,7 @@ public abstract class Mixin_MoveRealmsButton {
index = 2
)
private int adjustRealmsButton(int height) {
return height - (24 - 10) * 4;
return height - 24 * 4;
}
}
//#endif

View File

@@ -3,6 +3,7 @@ package com.replaymod.simplepathing;
import com.replaymod.core.KeyBindingRegistry;
import com.replaymod.core.Module;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.SettingsRegistry;
import com.replaymod.core.events.SettingsChangedCallback;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.core.versions.MCVer.Keyboard;
@@ -21,6 +22,7 @@ import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import com.replaymod.simplepathing.gui.GuiPathing;
import com.replaymod.simplepathing.preview.PathPreview;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashException;
import org.apache.logging.log4j.LogManager;
@@ -39,6 +41,9 @@ public class ReplayModSimplePathing extends EventRegistrations implements Module
public static ReplayModSimplePathing instance;
private ReplayMod core;
public KeyBindingRegistry.Binding keyPositionKeyframe;
public KeyBindingRegistry.Binding keyTimeKeyframe;
public KeyBindingRegistry.Binding keySyncTime;
public static Logger LOGGER = LogManager.getLogger();
@@ -85,11 +90,56 @@ public class ReplayModSimplePathing extends EventRegistrations implements Module
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> {
if (guiPathing != null) guiPathing.clearKeyframesButtonPressed();
}, true);
core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> {
keySyncTime = core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> {
if (guiPathing != null) guiPathing.syncTimeButtonPressed();
}, true);
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> {
if (guiPathing != null) guiPathing.deleteButtonPressed();
SettingsRegistry settingsRegistry = core.getSettingsRegistry();
keySyncTime.registerAutoActivationSupport(settingsRegistry.get(Setting.AUTO_SYNC), active -> {
settingsRegistry.set(Setting.AUTO_SYNC, active);
settingsRegistry.save();
});
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () ->
guiPathing != null && guiPathing.deleteButtonPressed());
keyPositionKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positionkeyframe", Keyboard.KEY_I, () -> {
if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, false);
}, true);
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.positiononlykeyframe", 0, () -> {
if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.POSITION, true);
}, true);
keyTimeKeyframe = core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.timekeyframe", Keyboard.KEY_O, () -> {
if (guiPathing != null) guiPathing.toggleKeyframe(SPPath.TIME, false);
}, true);
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.bothkeyframes", 0, () -> {
if (guiPathing != null) {
guiPathing.toggleKeyframe(SPPath.TIME, false);
guiPathing.toggleKeyframe(SPPath.POSITION, false);
}
}, true);
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Z, () -> {
if (Screen.hasControlDown() && currentTimeline != null) {
Timeline timeline = currentTimeline.getTimeline();
if (Screen.hasShiftDown()) {
if (timeline.peekRedoStack() != null) {
timeline.redoLastChange();
}
} else {
if (timeline.peekUndoStack() != null) {
timeline.undoLastChange();
}
}
return true;
}
return false;
});
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_Y, () -> {
if (Screen.hasControlDown() && currentTimeline != null) {
Timeline timeline = currentTimeline.getTimeline();
if (timeline.peekRedoStack() != null) {
timeline.redoLastChange();
}
return true;
}
return false;
});
}

View File

@@ -7,6 +7,8 @@ import java.util.stream.Collectors;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> PATH_PREVIEW = make("pathpreview", "pathpreview", true);
public static final Setting<Boolean> AUTO_SYNC = make("autosync", null, true);
public static final Setting<Integer> TIMELINE_LENGTH = make("timelineLength", null, 30 * 60);
public static final SettingsRegistry.MultipleChoiceSettingKeys<String> DEFAULT_INTERPOLATION;
static {
@@ -27,6 +29,6 @@ public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
}
public Setting(String key, String displayString, T defaultValue) {
super("simplepathing", key, "replaymod.gui.settings." + displayString, defaultValue);
super("simplepathing", key, displayString == null ? null : "replaymod.gui.settings." + displayString, defaultValue);
}
}

View File

@@ -6,6 +6,7 @@ import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.gui.overlay.GuiMarkerTimeline;
import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path;
@@ -21,10 +22,15 @@ import org.apache.commons.lang3.tuple.Pair;
import de.johni0702.minecraft.gui.utils.lwjgl.Point;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import org.lwjgl.opengl.GL11;
import java.util.Comparator;
import java.util.Optional;
import static com.replaymod.core.versions.MCVer.BufferBuilder_addPosCol;
import static com.replaymod.core.versions.MCVer.BufferBuilder_beginPosCol;
import static com.replaymod.core.versions.MCVer.Tessellator_getInstance;
public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline> implements Draggable {
protected static final int KEYFRAME_SIZE = 5;
protected static final int KEYFRAME_TEXTURE_X = 74;
@@ -101,9 +107,93 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
}
renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
}
if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) {
Optional<Integer> timeProperty = keyframe.getValue(TimestampProperty.PROPERTY);
if (timeProperty.isPresent()) {
v += KEYFRAME_SIZE;
renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
GuiMarkerTimeline replayTimeline = gui.overlay.timeline;
GuiKeyframeTimeline keyframeTimeline = this;
ReadableDimension replayTimelineSize = replayTimeline.getLastSize();
ReadableDimension keyframeTimelineSize = this.getLastSize();
if (replayTimelineSize == null || keyframeTimelineSize == null) {
return;
}
// Determine absolute positions for both timelines
Point replayTimelinePos = new Point(0, 0);
Point keyframeTimelinePos = new Point(0, 0);
replayTimeline.getContainer().convertFor(replayTimeline, replayTimelinePos);
keyframeTimeline.getContainer().convertFor(keyframeTimeline, keyframeTimelinePos);
replayTimelinePos.setLocation(-replayTimelinePos.getX(), -replayTimelinePos.getY());
keyframeTimelinePos.setLocation(-keyframeTimelinePos.getX(), -keyframeTimelinePos.getY());
int replayTimelineLeft = replayTimelinePos.getX();
int replayTimelineRight = replayTimelinePos.getX() + replayTimelineSize.getWidth();
int replayTimelineTop = replayTimelinePos.getY();
int replayTimelineBottom = replayTimelinePos.getY() + replayTimelineSize.getHeight();
int replayTimelineWidth = replayTimelineRight - replayTimelineLeft - BORDER_LEFT - BORDER_RIGHT;
int keyframeTimelineLeft = keyframeTimelinePos.getX();
int keyframeTimelineTop = keyframeTimelinePos.getY();
float positionXReplayTimeline = BORDER_LEFT + timeProperty.get() / (float) replayTimeline.getLength() * replayTimelineWidth;
float positionXKeyframeTimeline = positonX + KEYFRAME_SIZE / 2f;
final int color = 0xff0000ff;
BufferBuilder_beginPosCol(GL11.GL_LINE_STRIP);
// Start just below the top border of the replay timeline
BufferBuilder_addPosCol(
replayTimelineLeft + positionXReplayTimeline,
replayTimelineTop + BORDER_TOP,
0,
color >> 24 & 0xff,
color >> 16 & 0xff,
color >> 8 & 0xff,
color & 0xff
);
// Draw vertically over the replay timeline, including its bottom border
BufferBuilder_addPosCol(
replayTimelineLeft + positionXReplayTimeline,
replayTimelineBottom,
0,
color >> 24 & 0xff,
color >> 16 & 0xff,
color >> 8 & 0xff,
color & 0xff
);
// Now for the important part: connecting to the keyframe timeline
BufferBuilder_addPosCol(
keyframeTimelineLeft + positionXKeyframeTimeline,
keyframeTimelineTop,
0,
color >> 24 & 0xff,
color >> 16 & 0xff,
color >> 8 & 0xff,
color & 0xff
);
// And finally another vertical bit (the timeline is already crammed enough, so only the border)
BufferBuilder_addPosCol(
keyframeTimelineLeft + positionXKeyframeTimeline,
keyframeTimelineTop + BORDER_TOP,
0,
color >> 24 & 0xff,
color >> 16 & 0xff,
color >> 8 & 0xff,
color & 0xff
);
GL11.glEnable(GL11.GL_LINE_SMOOTH);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glPushAttrib(GL11.GL_SCISSOR_BIT);
GL11.glDisable(GL11.GL_SCISSOR_TEST);
GL11.glLineWidth(1);
Tessellator_getInstance().draw();
GL11.glPopAttrib();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_LINE_SMOOTH);
}
}
});

View File

@@ -12,6 +12,7 @@ import com.replaymod.pathing.player.RealtimeTimelinePlayer;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.render.gui.GuiRenderQueue;
import com.replaymod.render.gui.GuiRenderSettings;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.camera.CameraEntity;
@@ -24,18 +25,17 @@ import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.simplepathing.ReplayModSimplePathing;
import com.replaymod.simplepathing.SPTimeline;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import com.replaymod.simplepathing.Setting;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.AbstractGuiClickable;
import de.johni0702.minecraft.gui.element.AbstractGuiElement;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiClickable;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime;
import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -45,10 +45,10 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.util.crash.CrashReport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -80,7 +80,7 @@ import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER;
public class GuiPathing {
private static final Logger logger = LogManager.getLogger();
public final GuiTexturedButton playPauseButton = new GuiTexturedButton() {
public final GuiButton playPauseButton = new GuiButton() {
@Override
public GuiElement getTooltip(RenderInfo renderInfo) {
GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo);
@@ -97,61 +97,61 @@ public class GuiPathing {
}
}.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip());
public final GuiTexturedButton renderButton = new GuiTexturedButton().onClick(new Runnable() {
public final GuiButton renderButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
if (!preparePathsForPlayback(false)) return;
// Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue
SPTimeline spTimeline = mod.getCurrentTimeline();
Timeline timeline;
try {
TimelineSerialization serialization = new TimelineSerialization(spTimeline, null);
String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline()));
timeline = serialization.deserialize(serialized).get("");
} catch (Throwable t) {
error(LOGGER, replayHandler.getOverlay(), CrashReport.create(t, "Cloning timeline"), () -> {});
return;
Timeline timeline = preparePathsForPlayback(false);
if (timeline == null) return;
GuiScreen screen = GuiRenderSettings.createBaseScreen();
new GuiRenderQueue(screen, replayHandler, () -> preparePathsForPlayback(false)) {
@Override
protected void close() {
super.close();
getMinecraft().openScreen(null);
}
new GuiRenderSettings(replayHandler, timeline).display();
}.open();
screen.display();
}
}).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 0)
}).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 0)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.renderpath"));
public final GuiTexturedButton positionKeyframeButton = new GuiTexturedButton() {
public final GuiButton positionKeyframeButton = new GuiButton() {
@Override
public GuiElement getTooltip(RenderInfo renderInfo) {
GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo);
if (tooltip != null) {
if (getTextureNormal().getY() == 40) { // Add keyframe
if (getTextureNormal().getX() == 0) { // Position
tooltip.setI18nText("replaymod.gui.ingame.menu.addposkeyframe");
String label;
if (getSpriteUV().getY() == 40) { // Add keyframe
if (getSpriteUV().getX() == 0) { // Position
label = "replaymod.gui.ingame.menu.addposkeyframe";
} else { // Spectator
tooltip.setI18nText("replaymod.gui.ingame.menu.addspeckeyframe");
label = "replaymod.gui.ingame.menu.addspeckeyframe";
}
} else { // Remove keyframe
if (getTextureNormal().getX() == 0) { // Position
tooltip.setI18nText("replaymod.gui.ingame.menu.removeposkeyframe");
if (getSpriteUV().getX() == 0) { // Position
label = "replaymod.gui.ingame.menu.removeposkeyframe";
} else { // Spectator
tooltip.setI18nText("replaymod.gui.ingame.menu.removespeckeyframe");
label = "replaymod.gui.ingame.menu.removespeckeyframe";
}
}
tooltip.setText(I18n.translate(label) + " (" + mod.keyPositionKeyframe.getBoundKey() + ")");
}
return tooltip;
}
}.setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTooltip(new GuiTooltip());
public final GuiTexturedButton timeKeyframeButton = new GuiTexturedButton() {
public final GuiButton timeKeyframeButton = new GuiButton() {
@Override
public GuiElement getTooltip(RenderInfo renderInfo) {
GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo);
if (tooltip != null) {
if (getTextureNormal().getY() == 80) { // Add time keyframe
tooltip.setI18nText("replaymod.gui.ingame.menu.addtimekeyframe");
String label;
if (getSpriteUV().getY() == 80) { // Add time keyframe
label = "replaymod.gui.ingame.menu.addtimekeyframe";
} else { // Remove time keyframe
tooltip.setI18nText("replaymod.gui.ingame.menu.removetimekeyframe");
label = "replaymod.gui.ingame.menu.removetimekeyframe";
}
tooltip.setText(I18n.translate(label) + " (" + mod.keyTimeKeyframe.getBoundKey() + ")");
}
return tooltip;
}
@@ -161,11 +161,11 @@ public class GuiPathing {
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
if (player.isActive()) {
setCursorPosition((int) player.getTimePassed());
setCursorPosition((int) player.getTimePassed()).ensureCursorVisibleWithPadding();
}
super.draw(renderer, size, renderInfo);
}
}.setSize(Integer.MAX_VALUE, 20).setLength(30 * 60 * 1000).setMarkers();
}.setSize(Integer.MAX_VALUE, 20).setMarkers();
public final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(Integer.MAX_VALUE, 9);
{scrollbar.onValueChanged(new Runnable() {
@@ -179,20 +179,20 @@ public class GuiPathing {
public final GuiTimelineTime<GuiKeyframeTimeline> timelineTime = new GuiTimelineTime<GuiKeyframeTimeline>()
.setTimeline(timeline);
public final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() {
public final GuiButton zoomInButton = new GuiButton().setSize(9, 9).onClick(new Runnable() {
@Override
public void run() {
zoomTimeline(2d / 3d);
}
}).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20)
}).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 20)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin"));
public final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() {
public final GuiButton zoomOutButton = new GuiButton().setSize(9, 9).onClick(new Runnable() {
@Override
public void run() {
zoomTimeline(3d / 2d);
}
}).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30)
}).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setSpriteUV(40, 30)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout"));
public final GuiPanel zoomButtonPanel = new GuiPanel()
@@ -211,58 +211,17 @@ public class GuiPathing {
pos(scrollbar, 0, y(timeline) + height(timeline) + 1);
size(scrollbar, x(zoomButtonPanel) - 2, 9);
}
}).addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel);
}).addElements(null, timeline, timelineTime, scrollbar, zoomButtonPanel);
public final GuiPanel panel = new GuiPanel()
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5))
.addElements(new HorizontalLayout.Data(0.5),
playPauseButton, renderButton, positionKeyframeButton, timeKeyframeButton, timelinePanel);
/**
* IGuiClickable dummy component that is inserted at a high level.
* During path playback, this catches all click events and forwards them to the
* abort path playback button.
* Dragging does not have to be intercepted as every GUI element should only
* respond to dragging events after it has received and handled a click event.
*/
private final IGuiClickable clickCatcher = new AbstractGuiClickable() {
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
if (player.isActive()) {
// Make sure the mouse is always visible during path playback
// even if the game closes the overlay for some reason (e.g. world change)
replayHandler.getOverlay().setMouseVisible(true);
}
}
@Override
protected AbstractGuiElement getThis() {
return this;
}
@Override
protected ReadableDimension calcMinSize() {
return new Dimension(0, 0);
}
@Override
public boolean mouseClick(ReadablePoint position, int button) {
if (player.isActive()) {
playPauseButton.mouseClick(position, button);
return true;
}
return false;
}
@Override
public int getLayer() {
return player.isActive() ? 10 : 0;
}
};
private final ReplayMod core;
private final ReplayModSimplePathing mod;
private final ReplayHandler replayHandler;
public final GuiReplayOverlay overlay;
private final RealtimeTimelinePlayer player;
// Whether any error which occured during entity tracker loading has already been shown to the user
@@ -275,10 +234,12 @@ public class GuiPathing {
this.core = core;
this.mod = mod;
this.replayHandler = replayHandler;
this.overlay = replayHandler.getOverlay();
this.player = new RealtimeTimelinePlayer(replayHandler);
final GuiReplayOverlay overlay = replayHandler.getOverlay();
playPauseButton.setTexturePosH(new ReadablePoint() {
timeline.setLength(core.getSettingsRegistry().get(Setting.TIMELINE_LENGTH) * 1000);
playPauseButton.setSpriteUV(new ReadablePoint() {
@Override
public int getX() {
return 0;
@@ -300,14 +261,16 @@ public class GuiPathing {
player.getFuture().cancel(false);
} else {
boolean ignoreTimeKeyframes = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT);
Path timePath = mod.getCurrentTimeline().getTimePath();
if (!preparePathsForPlayback(ignoreTimeKeyframes)) return;
Timeline timeline = preparePathsForPlayback(ignoreTimeKeyframes);
if (timeline == null) return;
Path timePath = new SPTimeline(timeline).getTimePath();
timePath.setActive(!ignoreTimeKeyframes);
// Start from cursor time unless the control key is pressed (then start from beginning)
int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition();
ListenableFuture<Void> future = player.start(mod.getCurrentTimeline().getTimeline(), startTime);
ListenableFuture<Void> future = player.start(timeline, startTime);
overlay.setCloseable(false);
overlay.setMouseVisible(true);
core.printInfoToChat("replaymod.chat.pathstarted");
@@ -320,21 +283,19 @@ public class GuiPathing {
core.printInfoToChat("replaymod.chat.pathfinished");
}
overlay.setCloseable(true);
timePath.setActive(true);
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
overlay.setCloseable(true);
timePath.setActive(true);
}
});
}
}
});
positionKeyframeButton.setTexturePosH(new ReadablePoint() {
positionKeyframeButton.setSpriteUV(new ReadablePoint() {
@Override
public int getX() {
SPPath keyframePath = mod.getSelectedPath();
@@ -368,11 +329,11 @@ public class GuiPathing {
}).onClick(new Runnable() {
@Override
public void run() {
updateKeyframe(SPPath.POSITION);
toggleKeyframe(SPPath.POSITION, false);
}
});
timeKeyframeButton.setTexturePosH(new ReadablePoint() {
timeKeyframeButton.setSpriteUV(new ReadablePoint() {
@Override
public int getX() {
return 0;
@@ -395,17 +356,17 @@ public class GuiPathing {
}).onClick(new Runnable() {
@Override
public void run() {
updateKeyframe(SPPath.TIME);
toggleKeyframe(SPPath.TIME, false);
}
});
overlay.addElements(null, panel, clickCatcher);
overlay.addElements(null, panel);
overlay.setLayout(new CustomLayout<GuiReplayOverlay>(overlay.getLayout()) {
@Override
protected void layout(GuiReplayOverlay container, int width, int height) {
checkForAutoSync();
pos(panel, 10, y(overlay.topPanel) + height(overlay.topPanel) + 3);
size(panel, width - 20, 40);
size(clickCatcher, 0, 0);
}
});
@@ -459,6 +420,28 @@ public class GuiPathing {
});
}
private int prevSpeed = -1;
private int prevTime = -1;
private void checkForAutoSync() {
if (!mod.keySyncTime.isAutoActivating()) {
prevSpeed = -1;
prevTime = -1;
return;
}
int speed = overlay.speedSlider.getValue();
if (prevSpeed != speed && prevSpeed != -1) {
syncTimeButtonPressed();
}
prevSpeed = speed;
int time = replayHandler.getReplaySender().currentTimeStamp();
if (prevTime != time && prevTime != -1 && !player.isActive()) {
syncTimeButtonPressed();
}
prevTime = time;
}
public void syncTimeButtonPressed() {
// Current replay time
int time = replayHandler.getReplaySender().currentTimeStamp();
@@ -479,16 +462,18 @@ public class GuiPathing {
// Cursor time passed
int cursorPassed = (int) (timePassed / speed);
// Move cursor to new position
timeline.setCursorPosition(keyframeCursor + cursorPassed);
timeline.setCursorPosition(keyframeCursor + cursorPassed).ensureCursorVisibleWithPadding();
// Deselect keyframe to allow the user to add a new one right away
mod.setSelected(null, 0);
});
}
public void deleteButtonPressed() {
public boolean deleteButtonPressed() {
if (mod.getSelectedPath() != null) {
updateKeyframe(mod.getSelectedPath());
toggleKeyframe(mod.getSelectedPath(), false);
return true;
}
return false;
}
private void startLoadingEntityTracker() {
@@ -520,8 +505,26 @@ public class GuiPathing {
}).start();
}
private boolean preparePathsForPlayback(boolean ignoreTimeKeyframes) {
SPTimeline timeline = mod.getCurrentTimeline();
private Timeline preparePathsForPlayback(boolean ignoreTimeKeyframes) {
SPTimeline spTimeline = mod.getCurrentTimeline();
if (!validatePathsForPlayback(spTimeline, ignoreTimeKeyframes)) {
return null;
}
try {
TimelineSerialization serialization = new TimelineSerialization(spTimeline, null);
String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline()));
Timeline timeline = serialization.deserialize(serialized).get("");
timeline.getPaths().forEach(Path::updateAll);
return timeline;
} catch (Throwable t) {
error(LOGGER, replayHandler.getOverlay(), CrashReport.create(t, "Cloning timeline"), () -> {});
return null;
}
}
private boolean validatePathsForPlayback(SPTimeline timeline, boolean ignoreTimeKeyframes) {
timeline.getTimeline().getPaths().forEach(Path::updateAll);
// Make sure there are at least two position keyframes
@@ -603,14 +606,22 @@ public class GuiPathing {
/**
* Called when either one of the property buttons is pressed.
* @param path {@code TIME} for the time property button, {@code POSITION} for the place property button
* @param neverSpectator when true, will insert a position keyframe even when currently spectating an entity
*/
private void updateKeyframe(SPPath path) {
public void toggleKeyframe(SPPath path, boolean neverSpectator) {
LOGGER.debug("Updating keyframe on path {}" + path);
if (!loadEntityTracker(() -> updateKeyframe(path))) return;
if (!loadEntityTracker(() -> toggleKeyframe(path, neverSpectator))) return;
int time = timeline.getCursorPosition();
SPTimeline timeline = mod.getCurrentTimeline();
if (timeline.getPositionPath().getKeyframes().isEmpty() &&
timeline.getTimePath().getKeyframes().isEmpty() &&
time > 1000) {
String text = I18n.translate("replaymod.gui.ingame.first_keyframe_not_at_start_warning");
GuiInfoPopup.open(overlay, text.split("\\\\n"));
}
switch (path) {
case TIME:
if (mod.getSelectedPath() == path) {
@@ -640,7 +651,7 @@ public class GuiPathing {
LOGGER.debug("No position keyframe found -> adding new keyframe");
CameraEntity camera = replayHandler.getCameraEntity();
int spectatedId = -1;
if (!replayHandler.isCameraView()) {
if (!replayHandler.isCameraView() && !neverSpectator) {
spectatedId = getRenderViewEntity(replayHandler.getOverlay().getMinecraft()).getEntityId();
}
timeline.addPositionKeyframe(time, Entity_getX(camera), Entity_getY(camera), Entity_getZ(camera),

View File

@@ -0,0 +1,33 @@
The replay_gui.png file as a whole is governed by the same terms as the entire repository.
Some of its individual icons were originally from other projects and under different licenses.
This file serves to credit the original author(s) and to comply with the terms of the original license (some parts of
which may still apply to the derived replay_gui.png file).
In particular:
================================================================================
The "Settings" icon (three horizontal sliders) is based on ModMenu's settings icon.
Original: https://github.com/Prospector/ModMenu/blob/74200fb7baa697b4e865b15a707a560757a7ee8f/src/main/resources/assets/modmenu/textures/gui/configure_button.png
Copyright (c) 2018-2020 Prospector
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -12,12 +12,12 @@
"Mixin_RegisterDynamicResourcePacks",
//#endif
//#if MC>=11400
"MixinKeyboardListener",
"MixinMouse",
//#endif
//#if MC<10800
//$$ "ResourcePackRepositoryAccessor",
//#endif
"MixinKeyboardListener",
"MixinMinecraft",
"GuiMainMenuAccessor",
"GuiScreenAccessor",

View File

@@ -4,11 +4,13 @@
"mixins": [],
"server": [],
"client": [
"AddServerScreenAccessor",
"EntityLivingBaseAccessor",
"IntegratedServerAccessor",
"NetworkManagerAccessor",
"SPacketSpawnMobAccessor",
"SPacketSpawnPlayerAccessor",
"MixinServerInfo",
//#if MC>=11400
"MixinDownloadingPackFinder",
"MixinMouseHelper",

View File

@@ -18,6 +18,7 @@
"Mixin_ForceChunkLoading",
//#endif
//#if MC>=11400
"Mixin_PreserveDepthDuringHandRendering",
"MainWindowAccessor",
//#endif
"WorldRendererAccessor",

View File

@@ -1 +1 @@
2.4.5
2.5.0

View File

@@ -1,3 +1,5 @@
net.minecraft.potion.EffectInstance net.minecraft.potion.PotionEffect
net.minecraft.client.gui.screen.AddServerScreen net.minecraft.client.gui.GuiScreenAddServer
net.minecraft.resources.AbstractResourcePack net.minecraft.client.resources.AbstractResourcePack
net.minecraft.resources.FolderPack net.minecraft.client.resources.FolderResourcePack
net.minecraft.client.resources.DownloadingPackFinder net.minecraft.client.resources.ResourcePackRepository

Some files were not shown because too many files have changed in this diff Show More