Release 2.5.0
This commit is contained in:
@@ -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")) {
|
||||
|
||||
@@ -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]
|
||||

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

|
||||
"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]
|
||||

|
||||
The **Command Line Render Settings**
|
||||
|
||||
BIN
docs/img/depthmapsuzanne.jpg
Normal file
BIN
docs/img/depthmapsuzanne.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 573 KiB |
BIN
docs/img/depthmapsuzanne.png
Normal file
BIN
docs/img/depthmapsuzanne.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
2
jGui
2
jGui
Submodule jGui updated: 6602aac42a...6b16892d64
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
17
src/main/java/com/replaymod/core/gui/GuiReplayButton.java
Normal file
17
src/main/java/com/replaymod/core/gui/GuiReplayButton.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -367,4 +367,9 @@ public class Utils {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T configure(T instance, Consumer<T> configure) {
|
||||
configure.accept(instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
17
src/main/java/com/replaymod/recording/ServerInfoExt.java
Normal file
17
src/main/java/com/replaymod/recording/ServerInfoExt.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
161
src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java
Normal file
161
src/main/java/com/replaymod/recording/gui/GuiSavingReplay.java
Normal 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, () -> {}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
227
src/main/java/com/replaymod/render/CameraPathExporter.java
Normal file
227
src/main/java/com/replaymod/render/CameraPathExporter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/main/java/com/replaymod/render/EXRWriter.java
Normal file
125
src/main/java/com/replaymod/render/EXRWriter.java
Normal 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
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, () -> {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
public enum Channel {
|
||||
BRGA,
|
||||
DEPTH,
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -250,4 +250,9 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline<GuiMarkerTimeline> im
|
||||
markers.add(marker);
|
||||
saveMarkers.accept(markers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableDimension getLastSize() {
|
||||
return super.getLastSize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
Submodule src/main/resources/assets/replaymod/lang updated: 8936798bea...b45b1a1f9a
33
src/main/resources/assets/replaymod/replay_gui.license
Normal file
33
src/main/resources/assets/replaymod/replay_gui.license
Normal 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 |
@@ -12,12 +12,12 @@
|
||||
"Mixin_RegisterDynamicResourcePacks",
|
||||
//#endif
|
||||
//#if MC>=11400
|
||||
"MixinKeyboardListener",
|
||||
"MixinMouse",
|
||||
//#endif
|
||||
//#if MC<10800
|
||||
//$$ "ResourcePackRepositoryAccessor",
|
||||
//#endif
|
||||
"MixinKeyboardListener",
|
||||
"MixinMinecraft",
|
||||
"GuiMainMenuAccessor",
|
||||
"GuiScreenAccessor",
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
"mixins": [],
|
||||
"server": [],
|
||||
"client": [
|
||||
"AddServerScreenAccessor",
|
||||
"EntityLivingBaseAccessor",
|
||||
"IntegratedServerAccessor",
|
||||
"NetworkManagerAccessor",
|
||||
"SPacketSpawnMobAccessor",
|
||||
"SPacketSpawnPlayerAccessor",
|
||||
"MixinServerInfo",
|
||||
//#if MC>=11400
|
||||
"MixinDownloadingPackFinder",
|
||||
"MixinMouseHelper",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"Mixin_ForceChunkLoading",
|
||||
//#endif
|
||||
//#if MC>=11400
|
||||
"Mixin_PreserveDepthDuringHandRendering",
|
||||
"MainWindowAccessor",
|
||||
//#endif
|
||||
"WorldRendererAccessor",
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.4.5
|
||||
2.5.0
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user