Compare commits

...

47 Commits

Author SHA1 Message Date
Jonas Herzig
ed566732df Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-05-31 09:36:46 +02:00
Jonas Herzig
91573d9487 Update jGui, ReplayStudio and Translations
jGui:
9e84e72 Fix inversion of empty selection

ReplayStudio:
ff12b1d Remove invalid filter from services files
c6a9325 Proper value wrapping in CatmullRomSplineInterpolator (fixes #8 GH)
2017-05-31 09:16:19 +02:00
Jonas Herzig
d71358bcba Add confirmation dialog before overwriting path presets (fixes #65) 2017-05-21 19:34:46 +02:00
Jonas Herzig
1a55983986 Move addCallback method from integration test Utils to main Utils 2017-05-21 19:34:46 +02:00
Jonas Herzig
18c5bcd469 Be more cooperative with other mods on the ingame menu (fixes #42) 2017-05-21 18:59:29 +02:00
Jonas Herzig
c054fe83f6 Register keys for simplepathing only once (fixes #63) 2017-05-21 17:11:15 +02:00
Jonas Herzig
f13297c5a5 Fix default interpolator handling in keyframe gui and after loading (fixes #64) 2017-05-21 16:29:59 +02:00
Jonas Herzig
391f304c5f Show error popup if entity tracker fails to load 2017-05-21 12:42:13 +02:00
Jonas Herzig
0e0eaaa9a7 Fix entity tracker being set even if it failed loading (fixes #50) 2017-05-21 12:27:20 +02:00
Jonas Herzig
a34bbbcd68 Fix NPE when spectating a player without a camera entity 2017-05-21 11:57:30 +02:00
Jonas Herzig
60879fbbed Change local class to be an inner class because Srg2Source breaks with it 2017-04-21 17:24:43 +02:00
Jonas Herzig
dfafbecf35 Load translations from github repo, only reload language not all resource packs 2017-04-21 16:36:47 +02:00
Jonas Herzig
8a6ec5191b Add missing tooltips to player overview and various missing chat messages 2017-04-21 16:36:47 +02:00
Jonas Herzig
5677fc54cb Re-add warning to replay editor 2017-04-21 16:36:47 +02:00
Jonas Herzig
748a91e7b2 Fix missing tooltips on fav/like/dislike buttons in replay center 2017-04-21 16:36:47 +02:00
Jonas Herzig
ee24866f29 Fix upload replay button not being updated when name/tags change 2017-04-21 16:36:47 +02:00
Jonas Herzig
dba085c700 Move translations into separate repo 2017-04-21 16:36:47 +02:00
Jonas Herzig
3f0e3e725e Fix NPE when receiving Replay|Restrict messages (fixes #16 GH) 2017-04-21 13:38:08 +02:00
johni0702
40e1d850da Fix crash when saving the last keyframe in the edit gui (fixes #61) 2017-04-14 17:31:16 +02:00
johni0702
03aada1f5f Update Mixin to 0.6.8 (fixes #9 GH) 2017-04-14 17:31:16 +02:00
johni0702
0c1dc658ad Fix interpolator being lost when moving keyframe to the end (fixes #62) 2017-04-14 17:31:16 +02:00
johni0702
fcbbbc9ea1 Add integration test. Run with ./gradlew runIntegrationTest 2017-04-14 17:31:15 +02:00
johni0702
fe6ded0143 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
Selecting a keyframe, double clicking it to open the gui, changing the
timestamp, saving and then clicking the Add/Remove Keyframe button used to
crash the client. This is fixed by updating the selection whenever the time is
changed.
2017-04-13 18:18:17 +02:00
johni0702
f58fa8f9c2 Fix Login GUI not respecting other GUIs opened on startup
Previously the Login GUI would simply replace the currently active GUI.
Now the currently active GUI is stored and then re-opened after login.
2017-04-13 18:12:07 +02:00
johni0702
4fc3a3166c Fix OpenEye being installed into working dir instead of mcDataDir 2017-04-13 18:06:21 +02:00
johni0702
24c58cbbe7 Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
cd16211 Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default Modified Keyframe GUI to allow for custom Path interpolator
2017-01-29 11:25:30 +01:00
johni0702
1ffeb010d5 Update jGui 2017-01-29 11:14:54 +01:00
johni0702
c122d30065 Merge branch '1.8-path-segments' into 1.8 2017-01-29 11:12:06 +01:00
johni0702
27fd25ae84 Fix crash when server responds with garbage to language query (fixes #51) 2017-01-28 13:18:36 +01:00
johni0702
db6b0ff5a6 Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60) 2017-01-28 13:12:28 +01:00
johni0702
d61d0ac670 Fix hand of invisible spectated player being visible (fixes #59) 2017-01-28 12:54:42 +01:00
johni0702
f7460f9bf5 Re-enable time path after path playback (fixes #55) 2017-01-28 11:46:11 +01:00
johni0702
bdbffd5c33 Add Catmull-Rom spline interpolator to simplepathing 2017-01-28 11:03:59 +01:00
johni0702
5cbfd2884e Generate interpolator settings from InterpolatorType enum 2017-01-28 10:51:24 +01:00
johni0702
6095aef3d1 Fix loading and storing of default interpolator with settings 2017-01-28 10:33:45 +01:00
johni0702
3f52336852 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
This reverts commit 70e3e54038.

As of c6293bc, this branch uses FG2.2 which fully supports lambdas.
2017-01-24 13:29:09 +01:00
johni0702
c6293bcbb0 Update to FG2.2 2017-01-24 13:27:57 +01:00
johni0702
c8126edbb2 Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
2017-01-24 13:25:53 +01:00
johni0702
64898ce288 Fix NPE when saving recorded resource pack 2017-01-24 13:08:55 +01:00
johni0702
70e3e54038 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
Lambdas are not properly re-obfuscated prior to FG2.2 causing said errors
when used. This is the only instance of such a lambda.
FG2.2 only supports MC 1.8.8 and up as such the lambda was replaced with
an anonymous inner class to solve the problem.
2017-01-24 13:07:50 +01:00
johni0702
3f2b3d62f7 Fix replacing source file in Replay Editor (fixes #57) 2017-01-24 09:43:14 +01:00
johni0702
d2def94fc1 Fix duration of replays corrupted because of JVM/OS crash
A JVM or OS crash can lead to incomplete packets at the end of a recording.
The duration of a corrupted replay file is not restored properly if that file
contains an incomplete packet. Furthermore, the entity tracker does not load
properly with such an incomplete packet. Both bugs are fixed with this commit.

To fix these bugs for replays that have already been recovered, simply open
them in the Replay Viewer, modify them somehow (save a new path) and force
the game to quit forcefully. The replays will be recovered the next time the
game starts and both bugs will be fixed.
2017-01-23 18:00:38 +01:00
johni0702
2c64030459 Refactor simple pathing timeline logic into dedicated class with unit tests
Fix logic for user-set interpolators
2017-01-21 13:30:47 +01:00
johni0702
4deb374a7f Enable validation on focus change for all number input fields 2017-01-20 16:16:01 +01:00
johni0702
5302bcaed4 Fix id and text of Replay Editor button in docs 2017-01-20 16:02:42 +01:00
johni0702
057edccf26 Manually inject root cert for cross-signed LetsEncrypt cert in requests
JREs prior to 8u101 do not ship with the root certificate that cross-signed
the LetsEncrypt X3 CA used to sign server certificates.
As the standard native Minecraft launcher for Windows and OSX ship with an
older Java version, this commit imports the required root cert and uses it
wherever communication with the replaymod.com server is established.

Effectively reverts 682fb4b
2017-01-19 17:07:14 +01:00
CrushedPixel
cd16211309 Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default
Modified Keyframe GUI to allow for custom Path interpolator
2017-01-13 12:05:07 +01:00
64 changed files with 3316 additions and 1127 deletions

4
.gitmodules vendored
View File

@@ -5,3 +5,7 @@
[submodule "ReplayStudio"] [submodule "ReplayStudio"]
path = ReplayStudio path = ReplayStudio
url = https://github.com/ReplayMod/ReplayStudio url = https://github.com/ReplayMod/ReplayStudio
[submodule "src/main/resources/assets/replaymod/lang"]
path = src/main/resources/assets/replaymod/lang
url = https://github.com/ReplayMod/Translations

View File

@@ -1,7 +1,5 @@
import groovy.json.JsonOutput import groovy.json.JsonOutput
import java.util.zip.ZipInputStream
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
@@ -19,7 +17,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:2.1-SNAPSHOT' classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT'
classpath 'org.spongepowered:mixingradle:0.3-SNAPSHOT' classpath 'org.spongepowered:mixingradle:0.3-SNAPSHOT'
} }
} }
@@ -61,7 +59,7 @@ configurations {
dependencies { dependencies {
compile 'org.projectlombok:lombok:1.16.4' compile 'org.projectlombok:lombok:1.16.4'
compile 'org.spongepowered:mixin:0.6.4-SNAPSHOT' compile 'org.spongepowered:mixin:0.6.8-SNAPSHOT'
shade 'com.googlecode.mp4parser:isoparser:1.1.7' shade 'com.googlecode.mp4parser:isoparser:1.1.7'
shade 'org.apache.commons:commons-exec:1.3' shade 'org.apache.commons:commons-exec:1.3'
shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0' shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0'
@@ -103,27 +101,6 @@ jar {
} }
from noticeDir from noticeDir
def langDir = file("$buildDir/languages")
doFirst {
try {
langDir.deleteDir()
langDir.mkdirs()
def dir = new File(langDir, 'assets/replaymod/lang/')
dir.mkdirs()
def zip = new ZipInputStream(new URL('http://replaymod.com/api/grab_languages').openStream())
def e;
while ((e = zip.nextEntry) != null) {
new File(dir, e.getName()) << zip
zip.closeEntry()
}
} catch(Exception e) {
e.printStackTrace();
}
}
from (langDir) {
exclude '**/en_US.lang'
}
from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) { from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) {
exclude '**/NOTICE*' exclude '**/NOTICE*'
// exclude everything taken in from jGui for running the mod in a dev environment // exclude everything taken in from jGui for running the mod in a dev environment
@@ -188,6 +165,13 @@ sourceSets {
} }
refMap = "mixins.replaymod.refmap.json" refMap = "mixins.replaymod.refmap.json"
} }
integrationTest {
compileClasspath += main.runtimeClasspath + main.output
java {
srcDir file('src/integration-test/java')
}
resources.srcDir file('src/integration-test/resources')
}
} }
task copySrg(type: Copy, dependsOn: 'genSrgs') { task copySrg(type: Copy, dependsOn: 'genSrgs') {
@@ -199,6 +183,30 @@ setupDecompWorkspace.dependsOn copySrg
setupDevWorkspace.dependsOn copySrg setupDevWorkspace.dependsOn copySrg
project.tasks.idea.dependsOn copySrg project.tasks.idea.dependsOn copySrg
task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) {
main = 'GradleStart'
standardOutput = System.out
errorOutput = System.err
workingDir file(minecraft.runDir)
def testDir = new File(minecraft.runDir, "integration-test")
doFirst {
testDir.deleteDir()
testDir.mkdirs()
}
doLast {
testDir.deleteDir()
}
afterEvaluate {
def runClient = tasks.getByName("runClient")
runIntegrationTest.jvmArgs = runClient.jvmArgs + "-Dfml.noGrab=true"
runIntegrationTest.args = runClient.args + "--gameDir" + testDir.canonicalPath
runIntegrationTest.classpath runClient.classpath + sourceSets.integrationTest.output
}
}
def generateVersionsJson() { def generateVersionsJson() {
// List all tags // List all tags
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()

View File

@@ -56,7 +56,7 @@ Please use [Optifine](https://optifine.net/) instead.
### Custom Main Menu [custom-main-menu] ### Custom Main Menu [custom-main-menu]
The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar. The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar.
If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890235** (text: `replaymod.gui.replayviewer`) and **17890236** (text: `replaymod.gui.replaycenter`) If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890237** (text: `replaymod.gui.replayeditor`) and **17890236** (text: `replaymod.gui.replaycenter`)
Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default. Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default.
Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons. Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons.

2
jGui

Submodule jGui updated: 2ed5da06b7...1e92ad85df

View File

@@ -0,0 +1,30 @@
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Entry point for running integration tests from within the IDE.
* This is <b>not</b> called when running integration tests directly from gradle.
*/
public class IntegrationTest {
public static void main(String[] args) throws Throwable {
// Prevent MC from grabbing our mouse during the test (it's not needed)
System.setProperty("fml.noGrab", "true");
// Make sure the test folder exists and is fresh
File gameDir = new File(System.getProperty("user.dir"), "integration-test");
if (gameDir.exists()) {
FileUtils.forceDelete(gameDir);
}
FileUtils.forceMkdir(gameDir);
// Set game dir to test folder and call regular entry point
List<String> argsList = new ArrayList<>(Arrays.asList(args));
argsList.add("--gameDir");
argsList.add(gameDir.getCanonicalPath());
GradleStart.main(argsList.toArray(new String[argsList.size()]));
}
}

View File

@@ -0,0 +1,342 @@
package com.replaymod.core;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiOverlay;
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.GuiTexturedButton;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.utils.Consumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
import static com.replaymod.core.utils.Utils.addCallback;
public abstract class AbstractTask implements Task {
public static Task create(Consumer<AbstractTask> init) {
return new AbstractTask() {
@Override
protected void init() {
init.consume(this);
}
};
}
public static final Minecraft mc = Minecraft.getMinecraft();
public final ReplayMod core = ReplayMod.instance;
public SettableFuture<Void> future;
@Override
public ListenableFuture<Void> execute() {
future = SettableFuture.create();
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
addCallback(future, success -> {
FMLCommonHandler.instance().bus().unregister(this);
MinecraftForge.EVENT_BUS.unregister(this);
}, error -> {});
init();
return future;
}
protected void init() {}
protected void runLater(Runnable runnable) {
core.runLater(() -> {
try {
runnable.run();
} catch (Throwable t) {
future.setException(t);
}
});
}
public void expectGuiClosed(Runnable onClosed) {
expectGuiClosed0(10, onClosed);
}
public void expectGuiClosed(int timeout, Runnable onClosed) {
expectGuiClosed0(timeout, onClosed);
}
private void expectGuiClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
final net.minecraft.client.gui.GuiScreen currentScreen = mc.currentScreen;
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (currentScreen != mc.currentScreen) {
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
framesPassed++;
} else {
Object gui = (gui = GuiScreen.from(currentScreen)) == null ? currentScreen : gui;
Exception e = new TimeoutException("Timeout while waiting for " + gui + " to be closed.");
e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
future.setException(e);
}
}
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
public void expectPopupClosed(Runnable onClosed) {
expectPopupClosed0(10, onClosed);
}
public void expectPopupClosed(int timeout, Runnable onClosed) {
expectPopupClosed0(timeout, onClosed);
}
private void expectPopupClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
AbstractGuiPopup popup = getPopup(mc.currentScreen);
if (popup == null) {
throw new IllegalStateException("No popup found.");
}
class EventHandler {
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (getPopup(mc.currentScreen) != popup) {
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
framesPassed++;
} else {
Exception e = new TimeoutException("Timeout while waiting for " + popup + " to be closed.");
e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
future.setException(e);
}
}
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
private AbstractGuiPopup getPopup(net.minecraft.client.gui.GuiScreen minecraft) {
GuiContainer<?> container = GuiOverlay.from(minecraft);
if (container == null) {
container = GuiScreen.from(minecraft);
}
if (container != null) {
while (container.getContainer() != null) {
container = container.getContainer();
}
GuiElement popup = Iterables.getLast(container.getChildren());
if (popup instanceof AbstractGuiPopup) {
return (AbstractGuiPopup) popup;
}
}
return null;
}
public <T> void expectGui(Class<T> guiClass, Consumer<T> onOpen) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
net.minecraft.client.gui.GuiScreen currentScreen;
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (currentScreen != mc.currentScreen) {
currentScreen = mc.currentScreen;
framesPassed = 0;
}
if (framesPassed < 10) {
framesPassed++;
return;
}
FMLCommonHandler.instance().bus().unregister(this);
Object foundGui = null;
if (AbstractGuiScreen.class.isAssignableFrom(guiClass)) {
AbstractGuiScreen guiScreen = GuiScreen.from(currentScreen);
if (guiClass.isInstance(guiScreen)) {
onOpen.consume(guiClass.cast(guiScreen));
return;
}
foundGui = guiScreen;
} else if (AbstractGuiOverlay.class.isAssignableFrom(guiClass)) {
AbstractGuiOverlay guiScreen = GuiOverlay.from(currentScreen);
if (guiClass.isInstance(guiScreen)) {
onOpen.consume(guiClass.cast(guiScreen));
return;
}
foundGui = guiScreen;
} else if (AbstractGuiPopup.class.isAssignableFrom(guiClass)) {
AbstractGuiPopup popup = getPopup(currentScreen);
if (guiClass.isInstance(popup)) {
onOpen.consume(guiClass.cast(popup));
return;
}
} else {
if (guiClass.isInstance(currentScreen)) {
onOpen.consume(guiClass.cast(currentScreen));
return;
}
}
class UnexpectedGuiException extends Exception {
UnexpectedGuiException(Object foundGui) {
super("Expected instance of " + guiClass + " but found " + foundGui);
setStackTrace(Arrays.copyOfRange(stackTrace, 2, stackTrace.length));
}
}
future.setException(new UnexpectedGuiException(foundGui == null ? currentScreen : foundGui));
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
private void clickNow(int x, int y) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("mouseClicked", int.class, int.class, int.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, x, y, 0);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
public void click(int x, int y) {
runLater(() -> {
LOGGER.info("Clicking at {}/{}", x, y);
clickNow(x, y);
});
}
private void dragNow(int x, int y) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("mouseClickMove", int.class, int.class, int.class, long.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, x, y, 0, 0);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
public void drag(int x, int y) {
runLater(() -> {
LOGGER.info("Dragging to {}/{}", x, y);
dragNow(x, y);
});
}
public void click(GuiButton button) {
runLater(() -> {
if (!button.isEnabled()) {
future.setException(new IllegalStateException("Button is disabled: " + button.getLabel()));
return;
}
LOGGER.info("Clicking button {}", button.getLabel());
button.onClick();
});
}
public void click(GuiTexturedButton button) {
runLater(() -> {
if (!button.isEnabled()) {
future.setException(new IllegalStateException("Button is disabled: " + button.getTexture()));
return;
}
LOGGER.info("Clicking textured button {}", button.getTexture());
button.onClick();
});
}
public void click(String buttonText) {
runLater(() -> {
LOGGER.info("Clicking button {}", buttonText);
try {
Field field = net.minecraft.client.gui.GuiScreen.class.getDeclaredField("buttonList");
field.setAccessible(true);
@SuppressWarnings("unchecked")
List<net.minecraft.client.gui.GuiButton> buttonList = (List)
field.get(mc.currentScreen);
net.minecraft.client.gui.GuiButton button = null;
for (net.minecraft.client.gui.GuiButton guiButton : buttonList) {
if (guiButton.displayString.equals(buttonText)) {
button = guiButton;
}
}
if (button == null) {
future.setException(new NoSuchElementException("No button with label: " + buttonText));
return;
}
clickNow(button.xPosition + 5, button.yPosition + 5);
} catch (IllegalAccessException | NoSuchFieldException e) {
future.setException(e);
}
});
}
public void type(String string) {
for (char c : string.toCharArray()) {
press(c, Keyboard.getKeyIndex(String.valueOf(c).toUpperCase()));
}
}
public void press(int keyCode) {
String keyName = Keyboard.getKeyName(keyCode);
char character = keyName.length() == 1 ? keyName.charAt(0) : '\0';
press(character, keyCode);
}
public void press(char character, int keyCode) {
runLater(() -> {
LOGGER.info("Pressing key {}", Keyboard.getKeyName(keyCode));
if (mc.currentScreen == null || mc.currentScreen.allowUserInput) {
KeyBinding.onTick(keyCode);
FMLCommonHandler.instance().fireKeyInput();
}
if (mc.currentScreen != null) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("keyTyped", char.class, int.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, character, keyCode);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
});
}
}

View File

@@ -0,0 +1,41 @@
package com.replaymod.core;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
import static com.replaymod.core.utils.Utils.addCallback;
public class CompositeTask implements Task {
private SettableFuture<Void> future;
protected final Task[] children;
public CompositeTask(Task[] children) {
this.children = children;
}
@Override
public ListenableFuture<Void> execute() {
future = SettableFuture.create();
executeChild(0);
return future;
}
private void executeChild(int childIndex) {
if (future.isDone()) return;
if (childIndex < children.length) {
ReplayMod.instance.runLater(() -> {
try {
Task task = children[childIndex];
LOGGER.info("Running task {}", task);
ListenableFuture<Void> childFuture = task.execute();
addCallback(childFuture, done -> executeChild(childIndex + 1), err -> future.setException(err));
} catch (Throwable t) {
future.setException(t);
}
});
} else {
future.set(null);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.replaymod.core;
import com.replaymod.core.regression.RegressionTest60;
import com.replaymod.core.regression.RegressionTest61;
import com.replaymod.core.regression.RegressionTest62;
import com.replaymod.extra.DownloadOpenEye;
import com.replaymod.online.SkipLogin;
import com.replaymod.recording.CreateSPWorld;
import com.replaymod.recording.ExitSPWorld;
import com.replaymod.replay.ExitReplay;
import com.replaymod.replay.LoadReplay;
import com.replaymod.replay.OpenReplayViewer;
import net.minecraft.client.Minecraft;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import static com.replaymod.core.AbstractTask.mc;
import static com.replaymod.core.ReplayModIntegrationTest.MOD_ID;
import static com.replaymod.core.utils.Utils.addCallback;
/**
* Helper mod that initiates the integration tests.
*/
@Mod(modid = MOD_ID)
public class ReplayModIntegrationTest {
public static final String MOD_ID = "replaymod-integration-test";
public static Logger LOGGER;
@Mod.EventHandler
public void init(FMLPreInitializationEvent event) {
LOGGER = event.getModLog();
// Make sure the game window doesn't have to remain in focus during the test
mc.gameSettings.pauseOnLostFocus = false;
runTasks(
new SkipLogin(),
new DownloadOpenEye(),
new CreateSPWorld(),
new Wait(5000),
new ExitSPWorld(),
new OpenReplayViewer(),
new LoadReplay(),
new RegressionTest60(),
new RegressionTest61(),
new RegressionTest62(),
// new AbstractTask() {}, // Uncomment to not exit on success (useful for writing more tests)
new ExitReplay()
);
}
private void runTasks(Task... tests) {
addCallback(new CompositeTask(tests).execute(), success -> {
if (!Minecraft.getMinecraft().hasCrashed) {
LOGGER.info("===================================================");
LOGGER.info("= ALL TESTS PASSED =");
LOGGER.info("===================================================");
FMLCommonHandler.instance().exitJava(0, false);
}
}, error -> {
LOGGER.error("Failed task:", error);
LOGGER.error("===================================================");
LOGGER.error("= TEST FAILED =");
LOGGER.error("===================================================");
FMLCommonHandler.instance().exitJava(1, false);
});
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.core;
import com.google.common.util.concurrent.ListenableFuture;
public interface Task {
ListenableFuture<Void> execute();
static Task click(int x, int y) {
return AbstractTask.create(task -> {
task.click(x, y);
task.runLater(() -> task.future.set(null));
});
}
static Task drag(int x, int y) {
return AbstractTask.create(task -> {
task.drag(x, y);
task.runLater(() -> task.future.set(null));
});
}
static Task pressKey(int keyCode) {
return AbstractTask.create(task -> {
task.press(keyCode);
task.runLater(() -> task.future.set(null));
});
}
static Task pressKey(char character, int keyCode) {
return AbstractTask.create(task -> {
task.press(character, keyCode);
task.runLater(() -> task.future.set(null));
});
}
}

View File

@@ -0,0 +1,9 @@
package com.replaymod.core;
public class Utils {
public static void times(int x, Runnable runnable) {
for (int i = 0; i < x; i++) {
runnable.run();
}
}
}

View File

@@ -0,0 +1,21 @@
package com.replaymod.core;
public class Wait extends AbstractTask {
private final int duration;
public Wait(int duration) {
this.duration = duration;
}
@Override
protected void init() {
new Thread(() -> {
try {
Thread.sleep(duration);
runLater(() -> future.set(null));
} catch (InterruptedException e) {
runLater(() -> future.setException(e));
}
}).start();
}
}

View File

@@ -0,0 +1,59 @@
package com.replaymod.core.regression;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.SpectatePlayer;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
import com.replaymod.simplepathing.gui.GuiEditKeyframe;
import org.lwjgl.input.Keyboard;
import static com.replaymod.core.AbstractTask.mc;
import static com.replaymod.core.Utils.times;
/**
* Regression test: #60 Crash in path preview when two spectator keyframes are closer than 50ms
*/
public class RegressionTest60 extends CompositeTask {
public RegressionTest60() {
super(new Task[]{
new SpectatePlayer(),
OverlayGui.whileOpened(
// Place first spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Place second spectator keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Move second keyframe to 20ms on the keyframe timeline
AbstractTask.create(task -> {
// Double click keyframe
task.click(130, 50);
task.click(130, 50);
task.expectGui(GuiEditKeyframe.Spectator.class, gui -> {
// Set ms field to 20
task.click(mc.currentScreen.width / 2 + 80, mc.currentScreen.height / 2);
times(4, () -> task.press(Keyboard.KEY_BACK));
task.type("20");
// Set other fields to 0
times(2, () -> {
task.press(Keyboard.KEY_TAB);
times(4, () -> task.press(Keyboard.KEY_BACK));
task.press(Keyboard.KEY_0);
});
task.click(gui.saveButton);
task.expectPopupClosed(() -> task.future.set(null));
});
}),
// Place second spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton()
),
// Stop spectating player
new SpectatePlayer.End(),
// Enable path preview
Task.pressKey(Keyboard.KEY_H),
new GuiPathingTasks.ClearKeyframeTimeline(),
Task.pressKey(Keyboard.KEY_H),
});
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.core.regression;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
import com.replaymod.simplepathing.gui.GuiEditKeyframe;
/**
* Regression test: #61 NPE when saving in edit keyframe gui of last keyframe
*/
public class RegressionTest61 extends CompositeTask {
public RegressionTest61() {
super(new Task[]{
OverlayGui.whileOpened(
// Place keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
AbstractTask.create(task -> {
// Double click keyframe
task.click(130, 50);
task.click(130, 50);
task.expectGui(GuiEditKeyframe.Position.class, gui -> {
task.click(gui.saveButton);
task.expectPopupClosed(() -> task.future.set(null));
});
}),
// Place second spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton()
),
new GuiPathingTasks.ClearKeyframeTimeline(),
});
}
}

View File

@@ -0,0 +1,28 @@
package com.replaymod.core.regression;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
/**
* Regression test: #62 Swapping the only two existing keyframes causes NPE
*/
public class RegressionTest62 extends CompositeTask {
public RegressionTest62() {
super(new Task[]{
OverlayGui.whileOpened(
// Place first keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Place second keyframe
Task.click(150, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Move first keyframe past second keyframe
Task.click(130,50),
Task.drag(170,50)
),
new GuiPathingTasks.ClearKeyframeTimeline()
});
}
}

View File

@@ -0,0 +1,25 @@
package com.replaymod.extra;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.OpenEyeExtra;
import net.minecraftforge.fml.common.Loader;
import java.io.File;
import java.nio.file.NoSuchFileException;
public class DownloadOpenEye extends AbstractTask {
@Override
protected void init() {
expectGui(OpenEyeExtra.OfferGui.class, offerGui -> {
click(offerGui.yesButton);
expectGuiClosed(20 * 1000, () -> {
File targetFile = new File(mc.mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
if (!targetFile.exists()) {
future.setException(new NoSuchFileException(targetFile.getAbsolutePath()));
} else {
future.set(null);
}
});
});
}
}

View File

@@ -0,0 +1,14 @@
package com.replaymod.online;
import com.replaymod.core.AbstractTask;
import com.replaymod.online.gui.GuiLoginPrompt;
public class SkipLogin extends AbstractTask {
@Override
protected void init() {
expectGui(GuiLoginPrompt.class, gui -> {
click(gui.cancelButton);
expectGuiClosed(() -> future.set(null));
});
}
}

View File

@@ -0,0 +1,32 @@
package com.replaymod.recording;
import com.replaymod.core.AbstractTask;
import net.minecraft.client.gui.GuiCreateWorld;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiSelectWorld;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
public class CreateSPWorld extends AbstractTask {
@Override
protected void init() {
expectGui(GuiMainMenu.class, mainMenu -> {
click("Singleplayer");
expectGui(GuiSelectWorld.class, selectWorld -> {
click("Create New World");
expectGui(GuiCreateWorld.class, createWorld -> {
click("Create New World");
class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
});
});
});
}
}

View File

@@ -0,0 +1,36 @@
package com.replaymod.recording;
import com.replaymod.core.AbstractTask;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import java.io.IOException;
public class ExitSPWorld extends AbstractTask {
@Override
protected void init() {
mc.displayInGameMenu();
expectGui(GuiIngameMenu.class, ingameMenu -> {
click("Save and Quit to Title");
expectGui(GuiMainMenu.class, mainMenu -> new Thread(() -> {
try {
while (true) {
String[] dirs = core.getReplayFolder().list(DirectoryFileFilter.DIRECTORY);
if (dirs == null) {
future.setException(new NullPointerException("dirs is null"));
return;
}
if (dirs.length == 0) {
runLater(() -> future.set(null));
return;
}
Thread.sleep(10);
}
} catch (IOException | InterruptedException e) {
future.setException(e);
}
}).start());
});
}
}

View File

@@ -0,0 +1,16 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
public class ExitReplay extends AbstractTask {
@Override
protected void init() {
mc.displayInGameMenu();
expectGui(GuiIngameMenu.class, ingameMenu -> {
click("Exit Replay");
expectGui(GuiMainMenu.class, mainMenu -> future.set(null));
});
}
}

View File

@@ -0,0 +1,32 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import de.johni0702.minecraft.gui.function.Clickable;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.util.Point;
import org.lwjgl.util.ReadableDimension;
public class LoadReplay extends AbstractTask {
@Override
protected void init() {
expectGui(GuiReplayViewer.class, replayViewer -> runLater(() -> {
ReadableDimension size = replayViewer.getMaxSize();
// Select first entry
replayViewer.forEach(Clickable.class).mouseClick(new Point(size.getWidth() / 2, 40), 0);
// Load first replay
click(replayViewer.loadButton);
class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
}));
}
}

View File

@@ -0,0 +1,15 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import net.minecraft.client.gui.GuiMainMenu;
public class OpenReplayViewer extends AbstractTask {
@Override
protected void init() {
expectGui(GuiMainMenu.class, mainMenu -> {
click("Replay Viewer");
expectGui(GuiReplayViewer.class, replayViewer -> future.set(null));
});
}
}

View File

@@ -0,0 +1,42 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.playeroverview.PlayerOverviewGui;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.util.concurrent.TimeoutException;
public class SpectatePlayer extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_B);
expectGui(PlayerOverviewGui.class, overview -> {
click(overview.getMaxSize().getWidth() / 2 - 60, 60);
press(Keyboard.KEY_ESCAPE);
expectGuiClosed(() -> future.set(null));
});
}
public static class End extends AbstractTask {
private int timeout;
@Override
protected void init() {
runLater(() -> press(Keyboard.KEY_LSHIFT));
}
@SubscribeEvent
public void onTick(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (timeout++ > 20) {
future.setException(new TimeoutException("Camera hasn't stopped spectating."));
return;
}
if (mc.getRenderViewEntity() == mc.thePlayer) {
future.set(null);
}
}
}
}

View File

@@ -0,0 +1,33 @@
package com.replaymod.replay.overlay;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import org.lwjgl.input.Keyboard;
public class OverlayGui {
public static Task whileOpened(Task...tasks) {
Task[] nTasks = new Task[tasks.length + 2];
System.arraycopy(tasks, 0, nTasks, 1, tasks.length);
nTasks[0] = new Open();
nTasks[tasks.length + 1] = new Close();
return new CompositeTask(nTasks);
}
public static class Open extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_T);
expectGui(GuiReplayOverlay.class, done -> future.set(null));
}
}
public static class Close extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_ESCAPE);
expectGuiClosed(() -> future.set(null));
}
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.simplepathing;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import com.replaymod.simplepathing.gui.GuiPathing;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import org.lwjgl.input.Keyboard;
public abstract class GuiPathingTasks extends AbstractTask {
@Override
protected void init() {
expectGui(GuiReplayOverlay.class, ign -> init0(ReplayModSimplePathing.instance.getGuiPathing()));
}
protected abstract void init0(GuiPathing guiPathing);
public static class ClickPositionKeyframeButton extends GuiPathingTasks {
@Override
protected void init0(GuiPathing guiPathing) {
click(guiPathing.positionKeyframeButton);
runLater(() -> future.set(null));
}
}
public static class ClearKeyframeTimeline extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_C);
expectGui(GuiYesNoPopup.class, popup -> {
click(popup.getYesButton());
expectGuiClosed(() -> future.set(null));
});
}
}
}

View File

@@ -3,6 +3,7 @@ package com.replaymod.core.gui;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.io.ReplayInputStream; import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.replay.ZipReplayFile;
@@ -45,15 +46,19 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
ReplayMetaData metaData = replayFile.getMetaData(); ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) { if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration // Try to restore replay duration
try (ReplayInputStream in = replayFile.getPacketData()) { // We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData();
ReplayOutputStream out = replayFile.writePacketData()) {
PacketData last = null; PacketData last = null;
while ((last = in.readPacket()) != null) { while ((last = in.readPacket()) != null) {
metaData.setDuration((int) last.getTime()); metaData.setDuration((int) last.getTime());
out.write(last);
} }
replayFile.writeMetaData(metaData);
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
} }
// Write back the actual duration
replayFile.writeMetaData(metaData);
} }
replayFile.save(); replayFile.save();
replayFile.close(); replayFile.close();

View File

@@ -2,6 +2,7 @@ package com.replaymod.core.utils;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiScrollable; import de.johni0702.minecraft.gui.container.AbstractGuiScrollable;
@@ -25,14 +26,28 @@ import org.lwjgl.input.Keyboard;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import static net.minecraft.client.Minecraft.getMinecraft; import static net.minecraft.client.Minecraft.getMinecraft;
@@ -51,6 +66,42 @@ public class Utils {
DEFAULT_THUMBNAIL = thumbnail; DEFAULT_THUMBNAIL = thumbnail;
} }
/**
* Neither the root certificate of LetsEncrypt nor the root that cross-signed it is included in the default
* Java keystore prior to 8u101.
* Therefore whenever a connection to the replaymod.com site is made, this SSLContext has to be used instead.
* It has been constructed to include the necessary root certificates.
* @see #SSL_SOCKET_FACTORY
*/
public static final SSLContext SSL_CONTEXT;
/**
* @see #SSL_CONTEXT
*/
public static final SSLSocketFactory SSL_SOCKET_FACTORY;
static {
// Largely from https://community.letsencrypt.org/t/134/37
try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("1", certificate);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustManagerFactory.getTrustManagers(), null);
SSL_CONTEXT = ctx;
SSL_SOCKET_FACTORY = ctx.getSocketFactory();
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public static String convertSecondsToShortString(int seconds) { public static String convertSecondsToShortString(int seconds) {
int hours = seconds/(60*60); int hours = seconds/(60*60);
int min = seconds/60 - hours*60; int min = seconds/60 - hours*60;
@@ -101,6 +152,20 @@ public class Utils {
return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL);
} }
public static <T> void addCallback(ListenableFuture<T> future, Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
Futures.addCallback(future, new FutureCallback<T>() {
@Override
public void onSuccess(@Nullable T result) {
onSuccess.accept(result);
}
@Override
public void onFailure(@Nonnull Throwable t) {
onFailure.accept(t);
}
});
}
public static GuiInfoPopup error(Logger logger, GuiContainer container, CrashReport crashReport, Runnable onClose) { public static GuiInfoPopup error(Logger logger, GuiContainer container, CrashReport crashReport, Runnable onClose) {
// Convert crash report to string // Convert crash report to string
String crashReportStr = crashReport.getCompleteReport(); String crashReportStr = crashReport.getCompleteReport();

View File

@@ -1,5 +1,6 @@
package com.replaymod.editor.gui; package com.replaymod.editor.gui;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@@ -34,6 +35,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
@@ -57,6 +59,9 @@ public class GuiReplayEditor extends GuiScreen {
public GuiButton currentTabButton; public GuiButton currentTabButton;
public GuiPanel currentTabPanel; public GuiPanel currentTabPanel;
public final GuiLabel warningLabel = new GuiLabel(this).setColor(Colors.RED)
.setI18nText("replaymod.gui.editor.disclaimer");
public final GuiPanel tabButtons = new GuiPanel(this).setLayout(new GridLayout().setSpacingX(5)); public final GuiPanel tabButtons = new GuiPanel(this).setLayout(new GridLayout().setSpacingX(5));
public final List<GuiPanel> tabPanels = new ArrayList<>(); public final List<GuiPanel> tabPanels = new ArrayList<>();
@@ -82,15 +87,18 @@ public class GuiReplayEditor extends GuiScreen {
// Move all inactive panels aside // Move all inactive panels aside
tabPanels.forEach(e -> pos(e, Integer.MIN_VALUE, Integer.MIN_VALUE)); tabPanels.forEach(e -> pos(e, Integer.MIN_VALUE, Integer.MIN_VALUE));
pos(warningLabel, 10, 22);
size(warningLabel, width - 20, 10);
pos(tabButtons, 10, y(warningLabel) + height(warningLabel) + 2);
size(tabButtons, width - 20, 20);
pos(buttonPanel, width - 10 - width(buttonPanel), height - 10 - height(buttonPanel)); pos(buttonPanel, width - 10 - width(buttonPanel), height - 10 - height(buttonPanel));
if (currentTabPanel != null) { if (currentTabPanel != null) {
pos(currentTabPanel, 10, 50); pos(currentTabPanel, 10, y(tabButtons) + height(tabButtons) + 10);
size(currentTabPanel, width - 20, y(buttonPanel) - 10 - y(currentTabPanel)); size(currentTabPanel, width - 20, y(buttonPanel) - 10 - y(currentTabPanel));
} }
pos(tabButtons, 10, 20);
size(tabButtons, width - 20, 20);
} }
}); });
} }
@@ -119,10 +127,21 @@ public class GuiReplayEditor extends GuiScreen {
public void save(File inputFile, PacketStream.FilterInfo...filters) { public void save(File inputFile, PacketStream.FilterInfo...filters) {
save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> { save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> {
Studio studio = new ReplayStudio(); Studio studio = new ReplayStudio();
try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); File tmpDir = null;
ReplayOutputStream out = outputReplay.writePacketData()) { try {
// The input replay file MUST be closed before saving the output file File actualOutputFile = outputFile;
try (ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); if (outputFile.getCanonicalPath().equals(inputFile.getCanonicalPath())) {
// Input and output files are identical. Due to the way the ZipReplayFile stores its temporary
// data, the same replay file must not be opened twice for writing (tmp files will be deleted once
// either one is closed or, on Windows, will throw an exception when deleted).
tmpDir = Files.createTempDir();
outputFile = new File(tmpDir, "replay.mcpr");
LOGGER.debug("Output file is identical to input file, using temporary output file {} instead",
outputFile);
}
try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile);
ReplayOutputStream out = outputReplay.writePacketData();
ReplayFile inputReplay = new ZipReplayFile(studio, inputFile);
ReplayInputStream in = inputReplay.getPacketData()) { ReplayInputStream in = inputReplay.getPacketData()) {
ReplayMetaData metaData = inputReplay.getMetaData(); ReplayMetaData metaData = inputReplay.getMetaData();
PacketStream stream = studio.createReplayStream(in, true); PacketStream stream = studio.createReplayStream(in, true);
@@ -154,11 +173,21 @@ public class GuiReplayEditor extends GuiScreen {
// Update duration of new replay // Update duration of new replay
metaData.setDuration((int) lastTimestamp); metaData.setDuration((int) lastTimestamp);
outputReplay.writeMetaData(metaData); outputReplay.writeMetaData(metaData);
out.close();
outputReplay.save();
}
if (outputFile != actualOutputFile) {
LOGGER.debug("Moving temporary output file {} to {}");
FileUtils.forceDelete(actualOutputFile);
FileUtils.moveFile(outputFile, actualOutputFile);
} }
out.close();
outputReplay.save();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally {
if (tmpDir != null && !FileUtils.deleteQuietly(tmpDir)) {
LOGGER.warn("Failed to delete temporary directory {}", tmpDir);
}
} }
}); });
} }

View File

@@ -39,6 +39,10 @@ import static com.replaymod.editor.ReplayModEditor.LOGGER;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
public class GuiTrimPanel extends GuiPanel { public class GuiTrimPanel extends GuiPanel {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setMaxLength(2).setSize(20, 20).setValidateOnFocusChange(true);
}
// Special value indicating no replay files were found // Special value indicating no replay files were found
private static final File NO_REPLAY = new File("."); private static final File NO_REPLAY = new File(".");
// Special value for the initial "Select Marker Keyframe" entry // Special value for the initial "Select Marker Keyframe" entry
@@ -50,16 +54,16 @@ public class GuiTrimPanel extends GuiPanel {
.setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay())
.setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName())); .setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName()));
public final GuiNumberField startHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField startHour = newGuiNumberField();
public final GuiNumberField startMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField startMin = newGuiNumberField();
public final GuiNumberField startSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField startSec = newGuiNumberField();
public final GuiNumberField startMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiNumberField startMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> startMarker = new GuiDropdownMenu<>(); public final GuiDropdownMenu<Marker> startMarker = new GuiDropdownMenu<>();
public final GuiNumberField endHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField endHour = newGuiNumberField();
public final GuiNumberField endMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField endMin = newGuiNumberField();
public final GuiNumberField endSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); public final GuiNumberField endSec = newGuiNumberField();
public final GuiNumberField endMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiNumberField endMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> endMarker = new GuiDropdownMenu<>(); public final GuiDropdownMenu<Marker> endMarker = new GuiDropdownMenu<>();
public final GuiPanel timePanel = new GuiPanel(this) public final GuiPanel timePanel = new GuiPanel(this)

View File

@@ -1,102 +1,106 @@
package com.replaymod.extras; package com.replaymod.extras;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.online.ReplayModOnline;
import com.replaymod.online.api.ApiClient;
import com.replaymod.online.api.ApiException;
import lombok.RequiredArgsConstructor;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.data.IMetadataSection; import net.minecraft.client.resources.data.IMetadataSection;
import net.minecraft.client.resources.data.IMetadataSerializer; import net.minecraft.client.resources.data.IMetadataSerializer;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.io.IOUtils;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.ConnectException; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.replaymod.extras.ReplayModExtras.LOGGER;
public class LocalizationExtra implements Extra { public class LocalizationExtra implements Extra {
private ReplayModOnline module; private static final String ZIP_FILE_URL = "https://github.com/ReplayMod/Translations/archive/master.zip";
private static final String LANG_PREFIX = "Translations-master/";
@Override @Override
public void register(ReplayMod mod) throws Exception { public void register(ReplayMod mod) throws Exception {
this.module = ReplayModOnline.instance;
final Minecraft mc = mod.getMinecraft(); final Minecraft mc = mod.getMinecraft();
Thread localizedResourcePackLoader = new Thread(new Runnable() { if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
@Override return;
public void run() { }
try { Thread localizedResourcePackLoader = new Thread(() -> {
try {
// Download zip of lang files
LOGGER.debug("Downloading languages from {}", ZIP_FILE_URL);
Map<String, byte[]> languages = new HashMap<>();
try (InputStream urlIn = new URL(ZIP_FILE_URL).openStream();
ZipInputStream in = new ZipInputStream(urlIn)) {
ZipEntry entry;
while ((entry = in.getNextEntry()) != null) {
String name = entry.getName();
if (!name.startsWith(LANG_PREFIX) || !name.endsWith(".lang")) {
continue;
}
name = name.substring(LANG_PREFIX.length());
languages.put(name, IOUtils.toByteArray(in));
LOGGER.debug("Added language file {}", name);
}
}
LOGGER.debug("Downloaded {} languages", languages.size());
// Add lang files as resource pack
mc.addScheduledTask(() -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<IResourcePack> defaultResourcePacks = mc.defaultResourcePacks; List<IResourcePack> defaultResourcePacks = mc.defaultResourcePacks;
defaultResourcePacks.add(new LocalizedResourcePack(module.getApiClient())); defaultResourcePacks.add(new LocalizedResourcePack(languages));
mc.addScheduledTask(new Runnable() { mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager());
@Override LOGGER.debug("Added language files to resource packs and reloaded LanguageManager");
public void run() { });
mc.refreshResources(); } catch (Throwable t) {
} LOGGER.error("Loading localized resource pack:", t);
});
} catch(Exception e) {
e.printStackTrace();
}
} }
}, "localizedResourcePackLoader"); }, "localizedResourcePackLoader");
localizedResourcePackLoader.setDaemon(true);
localizedResourcePackLoader.start(); localizedResourcePackLoader.start();
} }
@RequiredArgsConstructor
public static class LocalizedResourcePack implements IResourcePack { public static class LocalizedResourcePack implements IResourcePack {
private final ApiClient apiClient; private final Pattern LANG_PATTERN = Pattern.compile("^lang/([.+].lang)$");
private Map<String, String> availableLanguages = new HashMap<>(); private final Map<String, byte[]> languages;
private boolean websiteAvailable = true;
public LocalizedResourcePack(Map<String, byte[]> languages) {
this.languages = languages;
}
@Override @Override
public InputStream getInputStream(ResourceLocation loc) { public InputStream getInputStream(ResourceLocation loc) {
if(!loc.getResourcePath().endsWith(".lang")) return null; if (!"replaymod".equals(loc.getResourceDomain())) return null;
String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0]; Matcher matcher = LANG_PATTERN.matcher(loc.getResourcePath());
if(availableLanguages.containsKey(langcode)) return new ByteArrayInputStream(availableLanguages.get(langcode).getBytes(Charsets.UTF_8)); if (matcher.matches()) {
byte[] bytes = languages.get(matcher.group());
if (bytes != null) {
return new ByteArrayInputStream(bytes);
}
}
return null; return null;
} }
@Override @Override
public boolean resourceExists(ResourceLocation loc) { public boolean resourceExists(ResourceLocation loc) {
if(!(loc.getResourcePath().endsWith(".lang"))) return false; // Assumes that getInputStream returns a ByteArrayInputStream that doesn't need to be closed
String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0]; return getInputStream(loc) != null;
if(availableLanguages.containsKey(langcode)) return true;
if(!websiteAvailable) return false;
try {
if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
return false;
}
String lang = apiClient.getTranslation(langcode);
String prop = StringEscapeUtils.unescapeHtml4(lang);
availableLanguages.put(langcode, prop);
return true;
} catch (ApiException e) {
if (e.getError().getId() != 16) { // This language has not been translated
e.printStackTrace();
}
} catch(ConnectException ce) {
websiteAvailable = false;
ce.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
return false;
} }
@Override @Override
public Set getResourceDomains() { public Set getResourceDomains() {
return ImmutableSet.of("minecraft", "replaymod"); return ImmutableSet.of("replaymod");
} }
@Override @Override

View File

@@ -17,6 +17,7 @@ import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.Loader;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.net.URL; import java.net.URL;
@@ -24,8 +25,10 @@ import java.nio.channels.Channels;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class OpenEyeExtra implements Extra { public class OpenEyeExtra implements Extra {
private static final String DOWNLOAD_URL = "http://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION;
private static final Setting<Boolean> ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true); private static final Setting<Boolean> ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true);
private ReplayMod mod; private ReplayMod mod;
@@ -40,7 +43,7 @@ public class OpenEyeExtra implements Extra {
} }
} }
private class OfferGui extends AbstractGuiScreen<OfferGui> { public class OfferGui extends AbstractGuiScreen<OfferGui> {
public final GuiScreen parent; public final GuiScreen parent;
public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3)) public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3))
.addElements(new VerticalLayout.Data(0.5), .addElements(new VerticalLayout.Data(0.5),
@@ -66,10 +69,12 @@ public class OpenEyeExtra implements Extra {
GuiPopup popup = new GuiPopup(OfferGui.this); GuiPopup popup = new GuiPopup(OfferGui.this);
new Thread(() -> { new Thread(() -> {
try { try {
File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar"); File targetFile = new File(mod.getMinecraft().mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
FileUtils.forceMkdir(targetFile.getParentFile()); FileUtils.forceMkdir(targetFile.getParentFile());
ReadableByteChannel in = Channels.newChannel(new URL(DOWNLOAD_URL).openStream()); HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
ReadableByteChannel in = Channels.newChannel(connection.getInputStream());
FileChannel out = new FileOutputStream(targetFile).getChannel(); FileChannel out = new FileOutputStream(targetFile).getChannel();
out.transferFrom(in, 0, Long.MAX_VALUE); out.transferFrom(in, 0, Long.MAX_VALUE);
} catch (Throwable e) { } catch (Throwable e) {
@@ -102,7 +107,7 @@ public class OpenEyeExtra implements Extra {
} }
} }
private static final class GuiPopup extends AbstractGuiPopup<GuiPopup> { public static final class GuiPopup extends AbstractGuiPopup<GuiPopup> {
GuiPopup(GuiContainer container) { GuiPopup(GuiContainer container) {
super(container); super(container);
popup.addElements(null, new GuiIndicator().setColor(Colors.BLACK)); popup.addElements(null, new GuiIndicator().setColor(Colors.BLACK));

View File

@@ -32,11 +32,11 @@ public class ReplayModExtras {
OpenEyeExtra.class OpenEyeExtra.class
); );
private Logger logger; public static Logger LOGGER;
@Mod.EventHandler @Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) { public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog(); LOGGER = event.getModLog();
} }
@Mod.EventHandler @Mod.EventHandler
@@ -46,7 +46,7 @@ public class ReplayModExtras {
Extra extra = cls.newInstance(); Extra extra = cls.newInstance();
extra.register(ReplayMod.instance); extra.register(ReplayMod.instance);
} catch (Throwable t) { } catch (Throwable t) {
logger.warn("Failed to load extra " + cls.getName() + ": ", t); LOGGER.warn("Failed to load extra " + cls.getName() + ": ", t);
} }
} }
} }

View File

@@ -11,7 +11,10 @@ import com.replaymod.replay.events.ReplayCloseEvent;
import com.replaymod.replay.events.ReplayOpenEvent; import com.replaymod.replay.events.ReplayOpenEvent;
import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderHandEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
@@ -56,6 +59,7 @@ public class PlayerOverview implements Extra {
}); });
FMLCommonHandler.instance().bus().register(this); FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
RenderManager renderManager = mod.getMinecraft().getRenderManager(); RenderManager renderManager = mod.getMinecraft().getRenderManager();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -92,6 +96,14 @@ public class PlayerOverview implements Extra {
hiddenPlayers.clear(); hiddenPlayers.clear();
} }
@SubscribeEvent
public void oRenderHand(RenderHandEvent event) {
Entity view = module.getCore().getMinecraft().getRenderViewEntity();
if (view != null && isHidden(view.getUniqueID())) {
event.setCanceled(true);
}
}
public boolean isSavingEnabled() { public boolean isSavingEnabled() {
return savingEnabled; return savingEnabled;
} }

View File

@@ -4,10 +4,15 @@ import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.*; import de.johni0702.minecraft.gui.container.GuiClickable;
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.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiCheckbox; import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiImage; import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiCheckbox; import de.johni0702.minecraft.gui.element.IGuiCheckbox;
import de.johni0702.minecraft.gui.function.Closeable; import de.johni0702.minecraft.gui.function.Closeable;
import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -36,6 +41,7 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
public final GuiVerticalList playersScrollable = new GuiVerticalList(contentPanel) public final GuiVerticalList playersScrollable = new GuiVerticalList(contentPanel)
.setDrawSlider(true).setDrawShadow(true); .setDrawSlider(true).setDrawShadow(true);
public final GuiCheckbox saveCheckbox = new GuiCheckbox(contentPanel) public final GuiCheckbox saveCheckbox = new GuiCheckbox(contentPanel)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.remembersettings.description"))
.setI18nLabel("replaymod.gui.playeroverview.remembersettings"); .setI18nLabel("replaymod.gui.playeroverview.remembersettings");
public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){ public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){
@Override @Override
@@ -43,14 +49,14 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(true); playersScrollable.forEach(IGuiCheckbox.class).setChecked(true);
} }
}.setLabel("").setChecked(true); }.setLabel("").setChecked(true).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.showall"));
public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){ public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){
@Override @Override
public void onClick() { public void onClick() {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(false); playersScrollable.forEach(IGuiCheckbox.class).setChecked(false);
} }
}.setLabel("").setChecked(false); }.setLabel("").setChecked(false).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.hideall"));
{ {
setBackground(Background.NONE); setBackground(Background.NONE);

View File

@@ -82,7 +82,7 @@ public class ReplayModOnline {
// Initial login prompt // Initial login prompt
if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) { if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) {
if (!isLoggedIn()) { if (!isLoggedIn()) {
new GuiLoginPrompt(apiClient, null, null, false).display(); core.runLater(() -> new GuiLoginPrompt(apiClient, GuiScreen.wrap(getMinecraft().currentScreen), null, false).display());
} }
} }
} }

View File

@@ -15,13 +15,19 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.net.ssl.HttpsURLConnection;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.BufferedInputStream;
import java.net.HttpURLConnection; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class ApiClient { public class ApiClient {
private static final Minecraft mc = Minecraft.getMinecraft(); private static final Minecraft mc = Minecraft.getMinecraft();
@@ -130,7 +136,11 @@ public class ApiClient {
QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail); QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail);
builder.put("id", file); builder.put("id", file);
URL url = new URL(builder.toString()); URL url = new URL(builder.toString());
return ImageIO.read(url); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
try (InputStream in = connection.getInputStream()) {
return ImageIO.read(in);
}
} }
private boolean cancelDownload = false; private boolean cancelDownload = false;
@@ -143,7 +153,8 @@ public class ApiClient {
builder.put("id", file); builder.put("id", file);
String url = builder.toString(); String url = builder.toString();
URL website = new URL(url); URL website = new URL(url);
HttpURLConnection con = (HttpURLConnection) website.openConnection(); HttpsURLConnection con = (HttpsURLConnection) website.openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
int fileSize = con.getContentLength(); int fileSize = con.getContentLength();

View File

@@ -7,12 +7,14 @@ import com.google.gson.JsonParser;
import com.replaymod.online.api.replay.holders.ApiError; import com.replaymod.online.api.replay.holders.ApiError;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Map; import java.util.Map;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class SimpleApiClient { public class SimpleApiClient {
private static final JsonParser jsonParser = new JsonParser(); private static final JsonParser jsonParser = new JsonParser();
@@ -72,10 +74,11 @@ public class SimpleApiClient {
// read response // read response
String responseContent = null; String responseContent = null;
InputStream is = null; InputStream is = null;
HttpURLConnection httpUrlConnection = null; HttpsURLConnection httpUrlConnection = null;
try { try {
URL url = new URL(urlString); URL url = new URL(urlString);
httpUrlConnection = (HttpURLConnection) url.openConnection(); httpUrlConnection = (HttpsURLConnection) url.openConnection();
httpUrlConnection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
httpUrlConnection.setRequestMethod("GET"); httpUrlConnection.setRequestMethod("GET");

View File

@@ -9,16 +9,18 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileUploader { public class FileUploader {
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
@@ -48,7 +50,8 @@ public class FileUploader {
postData += "&name=" + URLEncoder.encode(filename, "UTF-8"); postData += "&name=" + URLEncoder.encode(filename, "UTF-8");
String url = ReplayModApiMethods.upload_file + postData; String url = ReplayModApiMethods.upload_file + postData;
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); HttpsURLConnection con = (HttpsURLConnection) new URL(url).openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
con.setUseCaches(false); con.setUseCaches(false);
con.setDoOutput(true); con.setDoOutput(true);
con.setRequestMethod("POST"); con.setRequestMethod("POST");

View File

@@ -2,7 +2,7 @@ package com.replaymod.online.api.replay;
public class ReplayModApiMethods { public class ReplayModApiMethods {
public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; public static final String REPLAYMOD_BASE_URL = "https://ReplayMod.com/api/";
public static final String register = REPLAYMOD_BASE_URL+"register"; public static final String register = REPLAYMOD_BASE_URL+"register";
public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey";

View File

@@ -21,7 +21,7 @@ public class GuiLoginPrompt extends AbstractGuiScreen<GuiLoginPrompt> {
private GuiLabel noAccountLabel = new GuiLabel(this).setI18nText("replaymod.gui.login.noacc"); private GuiLabel noAccountLabel = new GuiLabel(this).setI18nText("replaymod.gui.login.noacc");
private GuiLabel statusLabel = new GuiLabel(this); private GuiLabel statusLabel = new GuiLabel(this);
private GuiButton loginButton = new GuiButton(this).setI18nLabel("replaymod.gui.login").setSize(150, 20).setEnabled(false); private GuiButton loginButton = new GuiButton(this).setI18nLabel("replaymod.gui.login").setSize(150, 20).setEnabled(false);
private GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20); public GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20);
private GuiButton registerButton = new GuiButton(this).setI18nLabel("replaymod.gui.register").setSize(150, 20); private GuiButton registerButton = new GuiButton(this).setI18nLabel("replaymod.gui.register").setSize(150, 20);
private GuiTextField username = new GuiTextField(this).setSize(145, 20).setMaxLength(16).setFocused(true); private GuiTextField username = new GuiTextField(this).setSize(145, 20).setMaxLength(16).setFocused(true);
private GuiPasswordField password = new GuiPasswordField(this).setSize(145, 20) private GuiPasswordField password = new GuiPasswordField(this).setSize(145, 20)

View File

@@ -28,6 +28,7 @@ import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiImage; import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiButton; import de.johni0702.minecraft.gui.element.IGuiButton;
import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList; import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList;
import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -109,6 +110,7 @@ public class GuiReplayCenter extends GuiScreen {
public void run() { public void run() {
GuiReplayEntry selected = list.getSelected(); GuiReplayEntry selected = list.getSelected();
replayButtonPanel.forEach(IGuiButton.class).setEnabled(selected != null); replayButtonPanel.forEach(IGuiButton.class).setEnabled(selected != null);
replayButtonPanel.forEach(IGuiButton.class).setTooltip(null);
if (selected != null) { if (selected != null) {
int replayId = selected.fileInfo.getId(); int replayId = selected.fileInfo.getId();
boolean favorited = favoritedReplays.contains(replayId); boolean favorited = favoritedReplays.contains(replayId);
@@ -123,10 +125,22 @@ public class GuiReplayCenter extends GuiScreen {
favoriteButton.setI18nLabel("replaymod.gui.center." + (favorited ? "unfavorite" : "favorite")); favoriteButton.setI18nLabel("replaymod.gui.center." + (favorited ? "unfavorite" : "favorite"));
// Only allow button usage for either unfavorite or favorite after they've actually downloaded it // Only allow button usage for either unfavorite or favorite after they've actually downloaded it
favoriteButton.setEnabled(favorited || selected.downloaded); favoriteButton.setEnabled(favorited || selected.downloaded);
if (favoriteButton.isEnabled()) {
favoriteButton.setTooltip(null);
} else {
favoriteButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
}
// Similar for like/dislike buttons // Similar for like/dislike buttons
likeButton.setEnabled(selected.downloaded); likeButton.setEnabled(selected.downloaded);
dislikeButton.setEnabled(selected.downloaded); dislikeButton.setEnabled(selected.downloaded);
if (likeButton.isEnabled()) {
likeButton.setTooltip(null);
dislikeButton.setTooltip(null);
} else {
likeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
dislikeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
}
likeButton.setI18nLabel("replaymod.gui." + (liked ? "removelike" : "like")); likeButton.setI18nLabel("replaymod.gui." + (liked ? "removelike" : "like"));
dislikeButton.setI18nLabel("replaymod.gui." + (disliked ? "removedislike" : "dislike")); dislikeButton.setI18nLabel("replaymod.gui." + (disliked ? "removedislike" : "dislike"));
} }

View File

@@ -17,7 +17,12 @@ import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiImage;
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.GuiDropdownMenu; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTextArea; import de.johni0702.minecraft.gui.element.advanced.GuiTextArea;
@@ -234,6 +239,8 @@ public class GuiUploadReplay extends GuiScreen {
}); });
validateInputs(); validateInputs();
name.onTextChanged(s -> validateInputs());
tags.onTextChanged(s -> validateInputs());
} }
public void validateInputs() { public void validateInputs() {

View File

@@ -4,6 +4,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
@@ -11,7 +12,11 @@ import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.*; import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.element.GuiTextField;
@@ -41,9 +46,16 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
timelines.put(selectedEntry.name, currentTimeline); GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
overwriteButton.setDisabled(); new GuiLabel().setI18nText("replaymod.gui.keyframerepo.overwrite").setColor(Colors.BLACK)
save(); ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
Utils.addCallback(popup.getFuture(), doIt -> {
if (doIt) {
timelines.put(selectedEntry.name, currentTimeline);
overwriteButton.setDisabled();
save();
}
}, Throwable::printStackTrace);
} }
}).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled(); }).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled();
public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() {

View File

@@ -4,8 +4,10 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Restrictions; import com.replaymod.core.utils.Restrictions;
import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.recording.handler.ConnectionEventHandler;
import com.replaymod.recording.packet.PacketListener; import com.replaymod.recording.packet.PacketListener;
import net.minecraftforge.fml.common.FMLCommonHandler; import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import net.minecraft.network.NetworkManager; import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
@@ -54,9 +56,12 @@ public class ReplayModRecording {
EventBus bus = FMLCommonHandler.instance().bus(); EventBus bus = FMLCommonHandler.instance().bus();
bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core));
NetworkRegistry.INSTANCE.newSimpleChannel(Restrictions.PLUGIN_CHANNEL); NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler());
} }
@ChannelHandler.Sharable
private static class RestrictionsChannelHandler extends ChannelDuplexHandler {}
public void initiateRecording(NetworkManager networkManager) { public void initiateRecording(NetworkManager networkManager) {
connectionEventHandler.onConnectedToServerEvent(networkManager); connectionEventHandler.onConnectedToServerEvent(networkManager);
} }

View File

@@ -26,6 +26,7 @@ import javax.annotation.Nonnull;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@@ -52,6 +53,9 @@ public class ResourcePackRecorder {
boolean doWrite = false; // Whether we are the first and have to write it boolean doWrite = false; // Whether we are the first and have to write it
synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically
Map<Integer, String> index = replayFile.getResourcePackIndex(); Map<Integer, String> index = replayFile.getResourcePackIndex();
if (index == null) {
index = new HashMap<>();
}
if (!index.containsValue(hash)) { if (!index.containsValue(hash)) {
// Hash is unknown, we have to write the resource pack ourselves // Hash is unknown, we have to write the resource pack ourselves
doWrite = true; doWrite = true;

View File

@@ -80,8 +80,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
} }
}).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values()); }).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values());
public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1); public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1); public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() { public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -93,7 +93,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2)) .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2))
.addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight); .addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight);
public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20); public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20).setValidateOnFocusChange(true);
public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>() public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>()
.setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps"); .setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps");

View File

@@ -209,6 +209,9 @@ public class ReplayHandler {
*/ */
public void spectateEntity(Entity e) { public void spectateEntity(Entity e) {
CameraEntity cameraEntity = getCameraEntity(); CameraEntity cameraEntity = getCameraEntity();
if (cameraEntity == null) {
return; // Cannot spectate if we have no camera
}
if (e == null || e == cameraEntity) { if (e == null || e == cameraEntity) {
spectating = null; spectating = null;
e = cameraEntity; e = cameraEntity;

View File

@@ -6,7 +6,11 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.ModCompat;
import com.replaymod.replay.camera.*; import com.replaymod.replay.camera.CameraController;
import com.replaymod.replay.camera.CameraControllerRegistry;
import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.camera.ClassicCameraController;
import com.replaymod.replay.camera.VanillaCameraController;
import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replay.gui.overlay.GuiMarkerTimeline;
import com.replaymod.replay.gui.screen.GuiModCompatWarning; import com.replaymod.replay.gui.screen.GuiModCompatWarning;
import com.replaymod.replay.handler.GuiHandler; import com.replaymod.replay.handler.GuiHandler;
@@ -99,6 +103,7 @@ public class ReplayModReplay {
@Override @Override
public void onSuccess(NoGuiScreenshot result) { public void onSuccess(NoGuiScreenshot result) {
try { try {
core.printInfoToChat("replaymod.chat.savingthumb");
replayHandler.getReplayFile().writeThumb(result.getImage()); replayHandler.getReplayFile().writeThumb(result.getImage());
core.printInfoToChat("replaymod.chat.savedthumb"); core.printInfoToChat("replaymod.chat.savedthumb");
} catch (IOException e) { } catch (IOException e) {
@@ -109,6 +114,7 @@ public class ReplayModReplay {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
t.printStackTrace(); t.printStackTrace();
core.printWarningToChat("replaymod.chat.failedthumb");
} }
}); });
} }

View File

@@ -252,6 +252,15 @@ public class CameraEntity extends EntityPlayerSP {
return false; return false;
} }
@Override
public boolean isInvisible() {
Entity view = mc.getRenderViewEntity();
if (view != this) {
return view.isInvisible();
}
return super.isInvisible();
}
@Override @Override
public ResourceLocation getLocationSkin() { public ResourceLocation getLocationSkin() {
Entity view = mc.getRenderViewEntity(); Entity view = mc.getRenderViewEntity();

View File

@@ -16,6 +16,10 @@ import org.lwjgl.input.Keyboard;
import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.ReadablePoint;
public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> implements Typeable { public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> implements Typeable {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true);
}
private final ReplayHandler replayHandler; private final ReplayHandler replayHandler;
private final Marker marker; private final Marker marker;
@@ -23,15 +27,15 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> imp
public final GuiTextField nameField = new GuiTextField().setSize(150, 20); public final GuiTextField nameField = new GuiTextField().setSize(150, 20);
// TODO: Replace with a min/sec/msec field // TODO: Replace with a min/sec/msec field
public final GuiNumberField timeField = new GuiNumberField().setSize(150, 20).setPrecision(0); public final GuiNumberField timeField = newGuiNumberField().setPrecision(0);
public final GuiNumberField xField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField xField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField yField = newGuiNumberField().setPrecision(10);
public final GuiNumberField zField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField zField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yawField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField yawField = newGuiNumberField().setPrecision(5);
public final GuiNumberField pitchField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField pitchField = newGuiNumberField().setPrecision(5);
public final GuiNumberField rollField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setPrecision(5);
public final GuiPanel inputs = GuiPanel.builder() public final GuiPanel inputs = GuiPanel.builder()
.layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3)) .layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3))

View File

@@ -18,7 +18,6 @@ import java.util.List;
public class GuiHandler { public class GuiHandler {
private static final int BUTTON_EXIT_SERVER = 1; private static final int BUTTON_EXIT_SERVER = 1;
private static final int BUTTON_RETURN_TO_GAME = 4;
private static final int BUTTON_ACHIEVEMENTS = 5; private static final int BUTTON_ACHIEVEMENTS = 5;
private static final int BUTTON_STATS = 6; private static final int BUTTON_STATS = 6;
private static final int BUTTON_OPEN_TO_LAN = 7; private static final int BUTTON_OPEN_TO_LAN = 7;
@@ -49,6 +48,7 @@ public class GuiHandler {
// Pause replay when menu is opened // Pause replay when menu is opened
mod.getReplayHandler().getReplaySender().setReplaySpeed(0); mod.getReplayHandler().getReplaySender().setReplaySpeed(0);
GuiButton achievements = null, stats = null, openToLan = null;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList; List<GuiButton> buttonList = event.buttonList;
for(GuiButton b : new ArrayList<>(buttonList)) { for(GuiButton b : new ArrayList<>(buttonList)) {
@@ -60,15 +60,39 @@ public class GuiHandler {
break; break;
// Remove "Achievements", "Stats" and "Open to LAN" buttons // Remove "Achievements", "Stats" and "Open to LAN" buttons
case BUTTON_ACHIEVEMENTS: case BUTTON_ACHIEVEMENTS:
buttonList.remove(achievements = b);
break;
case BUTTON_STATS: case BUTTON_STATS:
buttonList.remove(stats = b);
break;
case BUTTON_OPEN_TO_LAN: case BUTTON_OPEN_TO_LAN:
buttonList.remove(b); buttonList.remove(openToLan = b);
} break;
// Move all buttons except the "Return to game" button upwards
if (b.id != BUTTON_RETURN_TO_GAME) {
b.yPosition -= 48;
} }
} }
if (achievements != null && stats != null) {
moveAllButtonsDirectlyBelowUpwards(buttonList, achievements.yPosition,
achievements.xPosition, stats.xPosition + stats.width);
}
if (openToLan != null) {
moveAllButtonsDirectlyBelowUpwards(buttonList, openToLan.yPosition,
openToLan.xPosition, openToLan.xPosition + openToLan.width);
}
}
}
/**
* Moves all buttons that are within a rectangle below a certain y coordinate upwards by 24 units.
* @param buttons List of buttons
* @param belowY The Y limit
* @param xStart Left x limit of the rectangle
* @param xEnd Right x limit of the rectangle
*/
private void moveAllButtonsDirectlyBelowUpwards(List<GuiButton> buttons, int belowY, int xStart, int xEnd) {
for (GuiButton button : buttons) {
if (button.yPosition >= belowY && button.xPosition <= xEnd && button.xPosition + button.width >= xStart) {
button.yPosition -= 24;
}
} }
} }

View File

@@ -0,0 +1,54 @@
package com.replaymod.simplepathing;
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.function.Supplier;
@AllArgsConstructor
public enum InterpolatorType {
DEFAULT("default", null, null),
CATMULL_ROM("catmullrom", CatmullRomSplineInterpolator.class, () -> new CatmullRomSplineInterpolator(0.5)),
CUBIC("cubic", CubicSplineInterpolator.class, CubicSplineInterpolator::new),
LINEAR("linear", LinearInterpolator.class, LinearInterpolator::new);
@Getter
private String localizationKey;
@Getter
private Class<? extends Interpolator> interpolatorClass;
private Supplier<Interpolator> interpolatorConstructor;
public String getI18nName() {
return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey);
}
public String getI18nDescription() {
return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey);
}
public static InterpolatorType fromString(String string) {
for (InterpolatorType t : values()) {
if (t.getI18nName().equals(string)) return t;
}
return CATMULL_ROM; //the default
}
public static InterpolatorType fromClass(Class<? extends Interpolator> cls) {
for (InterpolatorType type : values()) {
if (cls.equals(type.getInterpolatorClass())) {
return type;
}
}
return DEFAULT;
}
public Interpolator newInstance() {
return interpolatorConstructor.get();
}
}

View File

@@ -1,38 +1,31 @@
package com.replaymod.simplepathing; package com.replaymod.simplepathing;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.core.events.SettingsChangedEvent;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayCloseEvent;
import com.replaymod.replay.events.ReplayOpenEvent; import com.replaymod.replay.events.ReplayOpenEvent;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.impl.TimelineImpl;
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.simplepathing.SPTimeline.SPPath;
import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.gui.GuiPathing;
import com.replaymod.simplepathing.preview.PathPreview; import com.replaymod.simplepathing.preview.PathPreview;
import lombok.Getter;
import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard;
import java.io.IOException;
@Mod(modid = ReplayModSimplePathing.MOD_ID, @Mod(modid = ReplayModSimplePathing.MOD_ID,
version = "@MOD_VERSION@", version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@", acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true) useMetadata = true)
public class ReplayModSimplePathing implements PathingRegistry { public class ReplayModSimplePathing {
public static final String MOD_ID = "replaymod-simplepathing"; public static final String MOD_ID = "replaymod-simplepathing";
@Mod.Instance(MOD_ID)
public static ReplayModSimplePathing instance;
private ReplayMod core; private ReplayMod core;
public static Logger LOGGER; public static Logger LOGGER;
@@ -50,88 +43,77 @@ public class ReplayModSimplePathing implements PathingRegistry {
PathPreview pathPreview = new PathPreview(this); PathPreview pathPreview = new PathPreview(this);
pathPreview.register(); pathPreview.register();
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, () -> {
if (guiPathing != null) guiPathing.keyframeRepoButtonPressed();
});
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> {
if (guiPathing != null) guiPathing.clearKeyframesButtonPressed();
});
core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> {
if (guiPathing != null) guiPathing.syncTimeButtonPressed();
});
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> {
if (guiPathing != null) guiPathing.deleteButtonPressed();
});
} }
@SubscribeEvent @SubscribeEvent
public void postReplayOpen(ReplayOpenEvent.Post event) { public void postReplayOpen(ReplayOpenEvent.Post event) {
clearCurrentTimeline();
guiPathing = new GuiPathing(core, this, event.getReplayHandler()); guiPathing = new GuiPathing(core, this, event.getReplayHandler());
} }
@SubscribeEvent @SubscribeEvent
public void onReplayClose(ReplayCloseEvent.Post event) { public void onReplayClose(ReplayCloseEvent.Post event) {
currentTimeline = null;
guiPathing = null; guiPathing = null;
currentTimeline = createTimeline(); selectedPath = null;
currentTimeline.createPath();
currentTimeline.createPath();
selectedKeyframe = null;
} }
@SubscribeEvent @SubscribeEvent
public void onSettingsChanged(SettingsChangedEvent event) { public void onSettingsChanged(SettingsChangedEvent event) {
if (event.getKey() == Setting.LINEAR_INTERPOLATION) { if (event.getKey() == Setting.DEFAULT_INTERPOLATION) {
if (currentTimeline != null && guiPathing != null) { if (currentTimeline != null && guiPathing != null) {
currentTimeline.applyChange(guiPathing.updateInterpolators()); updateDefaultInterpolatorType();
} }
} }
} }
private Timeline currentTimeline = createTimeline(); { currentTimeline.createPath(); currentTimeline.createPath(); } private SPTimeline currentTimeline;
private Keyframe selectedKeyframe;
public Keyframe getSelectedKeyframe() { @Getter
return selectedKeyframe; private SPPath selectedPath;
@Getter
private long selectedTime;
public boolean isSelected(Keyframe keyframe) {
return selectedPath != null && currentTimeline.getKeyframe(selectedPath, selectedTime) == keyframe;
} }
public void setSelectedKeyframe(Keyframe selected) { public void setSelected(SPPath path, long time) {
this.selectedKeyframe = selected; selectedPath = path;
selectedTime = time;
} }
public void setCurrentTimeline(Timeline currentTimeline) { public void setCurrentTimeline(SPTimeline newTimeline) {
if (this.currentTimeline != currentTimeline) { selectedPath = null;
selectedKeyframe = null; currentTimeline = newTimeline;
} updateDefaultInterpolatorType();
this.currentTimeline = currentTimeline;
} }
public Timeline getCurrentTimeline() { public void clearCurrentTimeline() {
setCurrentTimeline(new SPTimeline());
}
public SPTimeline getCurrentTimeline() {
return currentTimeline; return currentTimeline;
} }
@Override private void updateDefaultInterpolatorType() {
public Timeline createTimeline() { InterpolatorType newDefaultType =
Timeline timeline = new TimelineImpl(); InterpolatorType.fromString(core.getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION));
currentTimeline.setDefaultInterpolatorType(newDefaultType);
timeline.registerProperty(TimestampProperty.PROPERTY);
timeline.registerProperty(CameraProperties.POSITION);
timeline.registerProperty(CameraProperties.ROTATION);
timeline.registerProperty(SpectatorProperty.PROPERTY);
return timeline;
}
@Override
public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException {
if (interpolator instanceof LinearInterpolator) {
writer.value("linear");
} else if (interpolator instanceof CubicSplineInterpolator) {
writer.value("cubic-spline");
} else {
throw new IOException("Unknown interpolator type: " + interpolator);
}
}
@Override
public Interpolator deserializeInterpolator(JsonReader reader) throws IOException {
String type = reader.nextString();
switch (type) {
case "linear":
return new LinearInterpolator();
case "cubic-spline":
return new CubicSplineInterpolator();
default:
throw new IOException("Unknown interpolation type: " + type);
}
} }
public ReplayMod getCore() { public ReplayMod getCore() {

View File

@@ -0,0 +1,680 @@
package com.replaymod.simplepathing;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.change.AddKeyframe;
import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.change.CombinedChange;
import com.replaymod.replaystudio.pathing.change.SetInterpolator;
import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties;
import com.replaymod.replaystudio.pathing.impl.TimelineImpl;
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.property.Property;
import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.replaystudio.util.Location;
import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty;
import lombok.Getter;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ReportedException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Triple;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static com.replaymod.replaystudio.pathing.change.RemoveKeyframe.create;
import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER;
/**
* Simplified timeline abstraction used in the SimplePathing module.
*/
public class SPTimeline implements PathingRegistry {
public enum SPPath {
TIME,
POSITION,
}
@Getter
private final Timeline timeline;
@Getter
private final Path timePath;
@Getter
private final Path positionPath;
@Getter
private EntityPositionTracker entityTracker;
private InterpolatorType defaultInterpolatorType;
public SPTimeline() {
this(createInitialTimeline());
}
public SPTimeline(Timeline timeline) {
this.timeline = timeline;
this.timePath = timeline.getPaths().get(SPPath.TIME.ordinal());
this.positionPath = timeline.getPaths().get(SPPath.POSITION.ordinal());
}
public Path getPath(SPPath path) {
switch (path) {
case TIME:
return getTimePath();
case POSITION:
return getPositionPath();
}
throw new IllegalArgumentException("Unknown path " + path);
}
public Keyframe getKeyframe(SPPath path, long keyframe) {
return getPath(path).getKeyframe(keyframe);
}
public void setEntityTracker(EntityPositionTracker entityTracker) {
Preconditions.checkState(this.entityTracker == null, "Entity tracker already set");
this.entityTracker = entityTracker;
}
public void setDefaultInterpolatorType(InterpolatorType defaultInterpolatorType) {
Validate.isTrue(defaultInterpolatorType != InterpolatorType.DEFAULT, "Must not be DEFAULT");
this.defaultInterpolatorType = Validate.notNull(defaultInterpolatorType);
if (entityTracker != null) {
timeline.pushChange(updateInterpolators());
}
}
public Change setDefaultInterpolator(Interpolator interpolator) {
Preconditions.checkState(defaultInterpolatorType != null, "Default interpolator type not set.");
Validate.isInstanceOf(defaultInterpolatorType.getInterpolatorClass(), interpolator);
registerPositionInterpolatorProperties(interpolator);
Change change = CombinedChange.create(
positionPath.getSegments().stream()
// Ignore explicitly set segments
.filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent())
// Ignore spectator segments
.filter(s -> !isSpectatorSegment(s))
// Update interpolator for every remaining segment
// This will create a fragmented interpolator which is split by the updateInterpolators call
.map(s -> SetInterpolator.create(s, interpolator)).toArray(Change[]::new)
);
change.apply(timeline);
return CombinedChange.createFromApplied(change, updateInterpolators());
}
public boolean isTimeKeyframe(long time) {
return timePath.getKeyframe(time) != null;
}
public boolean isPositionKeyframe(long time) {
return positionPath.getKeyframe(time) != null;
}
public boolean isSpectatorKeyframe(long time) {
Keyframe keyframe = positionPath.getKeyframe(time);
return keyframe != null && keyframe.getValue(SpectatorProperty.PROPERTY).isPresent();
}
public void addPositionKeyframe(long time, double posX, double posY, double posZ,
float yaw, float pitch, float roll, int spectated) {
LOGGER.debug("Adding position keyframe at {} pos {}/{}/{} rot {}/{}/{} entId {}",
time, posX, posY, posZ, yaw, pitch, roll, spectated);
Path path = positionPath;
Preconditions.checkState(positionPath.getKeyframe(time) == null, "Keyframe already exists");
Change change = AddKeyframe.create(path, time);
change.apply(timeline);
Keyframe keyframe = path.getKeyframe(time);
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe);
builder.setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ));
builder.setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll));
if (spectated != -1) {
builder.setValue(SpectatorProperty.PROPERTY, spectated);
}
UpdateKeyframeProperties updateChange = builder.done();
updateChange.apply(timeline);
change = CombinedChange.createFromApplied(change, updateChange);
// If this new keyframe formed the first segment of the path
if (path.getSegments().size() == 1) {
// then create an initial interpolator of default type
PathSegment segment = path.getSegments().iterator().next();
Interpolator interpolator = createDefaultInterpolator();
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
setInterpolator.apply(timeline);
change = CombinedChange.createFromApplied(change, setInterpolator);
}
// Update interpolators for spectator keyframes
// while this is overkill, it is far simpler than updating differently for every possible case
change = CombinedChange.createFromApplied(change, updateInterpolators());
Change specPosUpdate = updateSpectatorPositions();
specPosUpdate.apply(timeline);
change = CombinedChange.createFromApplied(change, specPosUpdate);
timeline.pushChange(change);
}
public Change updatePositionKeyframe(long time, double posX, double posY, double posZ,
float yaw, float pitch, float roll) {
LOGGER.debug("Updating position keyframe at {} to pos {}/{}/{} rot {}/{}/{}",
time, posX, posY, posZ, yaw, pitch, roll);
Keyframe keyframe = positionPath.getKeyframe(time);
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
Preconditions.checkState(!keyframe.getValue(SpectatorProperty.PROPERTY).isPresent(), "Cannot update spectator keyframe");
Change change = UpdateKeyframeProperties.create(positionPath, keyframe)
.setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ))
.setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll))
.done();
change.apply(timeline);
return change;
}
public void removePositionKeyframe(long time) {
LOGGER.debug("Removing position keyframe at {}", time);
Path path = positionPath;
Keyframe keyframe = path.getKeyframe(time);
Preconditions.checkState(keyframe != null, "No keyframe at that time");
Change change = create(path, keyframe);
change.apply(timeline);
// Update interpolators for spectator keyframes
// while this is overkill, it is far simpler than updating differently for every possible case
change = CombinedChange.createFromApplied(change, updateInterpolators());
Change specPosUpdate = updateSpectatorPositions();
specPosUpdate.apply(timeline);
change = CombinedChange.createFromApplied(change, specPosUpdate);
timeline.pushChange(change);
}
public void addTimeKeyframe(long time, int replayTime) {
LOGGER.debug("Adding time keyframe at {} time {}", time, replayTime);
Path path = timePath;
Preconditions.checkState(path.getKeyframe(time) == null, "Keyframe already exists");
Change change = AddKeyframe.create(path, time);
change.apply(timeline);
Keyframe keyframe = path.getKeyframe(time);
UpdateKeyframeProperties updateChange = UpdateKeyframeProperties.create(path, keyframe)
.setValue(TimestampProperty.PROPERTY, replayTime)
.done();
updateChange.apply(timeline);
change = CombinedChange.createFromApplied(change, updateChange);
// If this new keyframe formed the first segment of the path
if (path.getSegments().size() == 1) {
// then create an initial interpolator
PathSegment segment = path.getSegments().iterator().next();
Interpolator interpolator = new LinearInterpolator();
interpolator.registerProperty(TimestampProperty.PROPERTY);
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
setInterpolator.apply(timeline);
change = CombinedChange.createFromApplied(change, setInterpolator);
}
Change specPosUpdate = updateSpectatorPositions();
specPosUpdate.apply(timeline);
change = CombinedChange.createFromApplied(change, specPosUpdate);
timeline.pushChange(change);
}
public Change updateTimeKeyframe(long time, int replayTime) {
LOGGER.debug("Updating time keyframe at {} to time {}", time, replayTime);
Keyframe keyframe = timePath.getKeyframe(time);
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
Change change = UpdateKeyframeProperties.create(timePath, keyframe)
.setValue(TimestampProperty.PROPERTY, replayTime)
.done();
change.apply(timeline);
return change;
}
public void removeTimeKeyframe(long time) {
LOGGER.debug("Removing time keyframe at {}", time);
Path path = timePath;
Keyframe keyframe = path.getKeyframe(time);
Preconditions.checkState(keyframe != null, "No keyframe at that time");
Change change = create(path, keyframe);
change.apply(timeline);
Change specPosUpdate = updateSpectatorPositions();
specPosUpdate.apply(timeline);
change = CombinedChange.createFromApplied(change, specPosUpdate);
timeline.pushChange(change);
}
public Change setInterpolatorToDefault(long time) {
LOGGER.debug("Setting interpolator of position keyframe at {} to the default", time);
Keyframe keyframe = positionPath.getKeyframe(time);
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
Change change = UpdateKeyframeProperties.create(positionPath, keyframe)
.removeProperty(ExplicitInterpolationProperty.PROPERTY)
.done();
change.apply(timeline);
return CombinedChange.createFromApplied(change, updateInterpolators());
}
public Change setInterpolator(long time, Interpolator interpolator) {
LOGGER.debug("Setting interpolator of position keyframe at {} to {}", time, interpolator);
Keyframe keyframe = positionPath.getKeyframe(time);
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
PathSegment segment = positionPath.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe)
.findFirst().orElseThrow(() -> new IllegalStateException("Keyframe has no following segment."));
registerPositionInterpolatorProperties(interpolator);
Change change = CombinedChange.create(
UpdateKeyframeProperties.create(positionPath, keyframe)
.setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL)
.done(),
SetInterpolator.create(segment, interpolator)
);
change.apply(timeline);
return CombinedChange.createFromApplied(change, updateInterpolators());
}
public Change moveKeyframe(SPPath spPath, long oldTime, long newTime) {
LOGGER.debug("Moving keyframe on {} from {} to {}", spPath, oldTime, newTime);
Path path = getPath(spPath);
Keyframe keyframe = path.getKeyframe(oldTime);
Preconditions.checkState(keyframe != null, "No keyframe at specified time");
// Interpolator of the first segment might be required later if it is the only one
Optional<Interpolator> firstInterpolator =
path.getSegments().stream().findFirst().map(PathSegment::getInterpolator);
// The interpolator that will be lost once we remove the old keyframe and has to be restored afterwards
Optional<Interpolator> lostInterpolator = path.getSegments().stream().filter(s -> {
// If this is the last keyframe,
if (Iterables.getLast(path.getKeyframes()) == keyframe) {
return s.getEndKeyframe() == keyframe; // the previous interpolator will be lost
} else { // otherwise
return s.getStartKeyframe() == keyframe; // the following interpolator will be lost
}
}).findFirst().map(PathSegment::getInterpolator);
// First remove the old keyframe
Change removeChange = create(path, keyframe);
removeChange.apply(timeline);
// and add a new one at the correct time
Change addChange = AddKeyframe.create(path, newTime);
addChange.apply(timeline);
// Then copy over all properties
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime));
for (Property property : keyframe.getProperties()) {
copyProperty(property, keyframe, builder);
}
Change propertyChange = builder.done();
propertyChange.apply(timeline);
Change restoreInterpolatorChange;
Keyframe newKf = path.getKeyframe(newTime);
if (Iterables.getLast(path.getKeyframes()) != newKf) { // Unless this is the last keyframe
// the interpolator of the following segment has been lost and needs to be restored
restoreInterpolatorChange = lostInterpolator.<Change>flatMap(interpolator ->
path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKf).findFirst().map(segment ->
SetInterpolator.create(segment, interpolator)
)
).orElseGet(CombinedChange::create);
} else { // If it is the last keyframe however,
// the interpolator of the previous segment has been lost and needs to be restored
restoreInterpolatorChange = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf)
.findFirst().flatMap(segment -> lostInterpolator.map(interpolator -> {
// additionally, if the interpolation of this keyframe was set to explicit, that property
// has to be transferred to the start keyframe of the new segment
if (newKf.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) {
return CombinedChange.create(
SetInterpolator.create(segment, interpolator),
UpdateKeyframeProperties.create(path, segment.getStartKeyframe())
.setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL).done()
);
} else {
return SetInterpolator.create(segment, interpolator);
}
})).orElseGet(CombinedChange::create);
}
restoreInterpolatorChange.apply(timeline);
// Finally update the interpolators
Change interpolatorUpdateChange;
if (spPath == SPPath.POSITION) {
// Position / Spectator keyframes need special handling
interpolatorUpdateChange = updateInterpolators();
} else {
// Time keyframes only need updating when only one segment of them exists
if (path.getSegments().size() == 1) {
assert firstInterpolator.isPresent() : "One segment should have existed before as well";
interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get());
} else {
interpolatorUpdateChange = CombinedChange.create(); // Noop change
}
interpolatorUpdateChange.apply(timeline);
}
// and update spectator positions
Change spectatorChange = updateSpectatorPositions();
spectatorChange.apply(timeline);
return CombinedChange.createFromApplied(removeChange, addChange, propertyChange,
restoreInterpolatorChange, interpolatorUpdateChange, spectatorChange);
}
// Helper method because generics cannot be defined on blocks
private <T> void copyProperty(Property<T> property, Keyframe from, UpdateKeyframeProperties.Builder to) {
from.getValue(property).ifPresent(value -> to.setValue(property, value));
}
private Change updateInterpolators() {
Collection<PathSegment> pathSegments = positionPath.getSegments();
// Contains updated pathsegment-interpolator mappings, may be changed multiple times over the course of this method
Map<PathSegment, Interpolator> updates = new HashMap<>();
// First, setup spectator interpolators
Interpolator interpolator = null;
// Iterate through all segments
for (PathSegment segment : pathSegments) {
if (isSpectatorSegment(segment)) {
// If the last segment was a spectator segment, then use its interpolator for this segment as well
if (interpolator == null) {
// otherwise create a new interpolator
interpolator = new LinearInterpolator();
interpolator.registerProperty(SpectatorProperty.PROPERTY);
}
// Now that we have an interpolator, set it for the current segment
updates.put(segment, interpolator);
} else {
// Not a spectator segment, make sure we don't re-use the last interpolator (it would not be continuous)
interpolator = null;
}
}
// Then, if required, replace old default interpolators with new ones
pathSegments.stream()
// Ignore explicitly set segments
.filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent())
// Ignore spectator segments
.filter(s -> !isSpectatorSegment(s))
// Ignore already correct segments, should ignore all if default hasn't changed
.filter(s -> !s.getInterpolator().getClass().equals(defaultInterpolatorType.getInterpolatorClass()))
// Finally, set the interpolators
// This will create one interpolator per segment, they will be merged later
.forEach(segment -> updates.put(segment, createDefaultInterpolator()));
// All interpolators should now be set appropriately but may still be fragmented.
// Cleaning that up is a two step process
// Firstly, all interpolators that are not continuous need to be split up
Interpolator lastInterpolator = null;
Set<Interpolator> used = Collections.newSetFromMap(new IdentityHashMap<>());
for (PathSegment segment : pathSegments) {
if (isSpectatorSegment(segment)) {
lastInterpolator = null;
continue; // Chain broken by spectator segment
}
Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator());
if (lastInterpolator == currentInterpolator) {
continue; // All fine, interpolator is still continuous
}
// New interpolator, make sure it hasn't been used before
if (!used.add(interpolator)) {
// It has been used before, we need to clone it and use the clone instead
// This will create a new interpolator for each segment which will be merged later
currentInterpolator = cloneInterpolator(currentInterpolator);
updates.put(segment, currentInterpolator);
}
lastInterpolator = currentInterpolator;
}
// Secondly, all neighbouring interpolators that are equal need to be merged
lastInterpolator = null;
String lastInterpolatorSerialized = null;
for (PathSegment segment : pathSegments) {
if (isSpectatorSegment(segment)) {
lastInterpolator = null;
lastInterpolatorSerialized = null;
continue; // Spectator segments are continuous by construction
}
Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator());
String serialized = serializeInterpolator(currentInterpolator);
if (lastInterpolator != currentInterpolator && serialized.equals(lastInterpolatorSerialized)) {
// This interpolator is equal to the last one but not the same, needs merging
updates.put(segment, lastInterpolator);
continue;
}
// New interpolator, not related to the previous one, merged by definition
lastInterpolator = currentInterpolator;
lastInterpolatorSerialized = serialized;
}
Change change = CombinedChange.create(updates.entrySet().stream()
.map(e -> SetInterpolator.create(e.getKey(), e.getValue())).toArray(Change[]::new));
change.apply(timeline);
return change;
}
private boolean isSpectatorSegment(PathSegment segment) {
return segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()
&& segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent();
}
private Change updateSpectatorPositions() {
List<Change> changes = new ArrayList<>();
timePath.updateAll();
for (Keyframe keyframe : positionPath.getKeyframes()) {
Optional<Integer> spectator = keyframe.getValue(SpectatorProperty.PROPERTY);
if (spectator.isPresent()) {
Optional<Integer> time = timePath.getValue(TimestampProperty.PROPERTY, keyframe.getTime());
if (!time.isPresent()) {
continue; // No time keyframes set at this video time, cannot determine replay time
}
Location expected = entityTracker.getEntityPositionAtTimestamp(spectator.get(), time.get());
if (expected == null) {
continue; // We don't have any data on this entity for some reason
}
Triple<Double, Double, Double> pos = keyframe.getValue(CameraProperties.POSITION).orElse(Triple.of(0D, 0D, 0D));
Triple<Float, Float, Float> rot = keyframe.getValue(CameraProperties.ROTATION).orElse(Triple.of(0F, 0F, 0F));
Location actual = new Location(pos.getLeft(), pos.getMiddle(), pos.getRight(), rot.getLeft(), rot.getRight());
if (!expected.equals(actual)) {
changes.add(UpdateKeyframeProperties.create(positionPath, keyframe)
.setValue(CameraProperties.POSITION, Triple.of(expected.getX(), expected.getY(), expected.getZ()))
.setValue(CameraProperties.ROTATION, Triple.of(expected.getYaw(), expected.getPitch(), 0f)).done()
);
}
}
}
return CombinedChange.create(changes.toArray(new Change[changes.size()]));
}
private Interpolator createDefaultInterpolator() {
return registerPositionInterpolatorProperties(defaultInterpolatorType.newInstance());
}
private Interpolator registerPositionInterpolatorProperties(Interpolator interpolator) {
interpolator.registerProperty(CameraProperties.POSITION);
interpolator.registerProperty(CameraProperties.ROTATION);
return interpolator;
}
@Override
public Timeline createTimeline() {
return createTimelineStatic();
}
private static Timeline createInitialTimeline() {
Timeline timeline = createTimelineStatic();
timeline.createPath();
timeline.createPath();
return timeline;
}
private static Timeline createTimelineStatic() {
Timeline timeline = new TimelineImpl();
timeline.registerProperty(TimestampProperty.PROPERTY);
timeline.registerProperty(CameraProperties.POSITION);
timeline.registerProperty(CameraProperties.ROTATION);
timeline.registerProperty(SpectatorProperty.PROPERTY);
timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY);
return timeline;
}
@Override
public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException {
if (interpolator instanceof LinearInterpolator) {
writer.value("linear");
} else if (interpolator instanceof CubicSplineInterpolator) {
writer.value("cubic-spline");
} else if (interpolator instanceof CatmullRomSplineInterpolator) {
writer.beginObject();
writer.name("type").value("catmull-rom-spline");
writer.name("alpha").value(((CatmullRomSplineInterpolator) interpolator).getAlpha());
writer.endObject();
} else {
throw new IOException("Unknown interpolator type: " + interpolator);
}
}
@Override
public Interpolator deserializeInterpolator(JsonReader reader) throws IOException {
String type;
JsonObject args;
switch (reader.peek()) {
case STRING:
type = reader.nextString();
args = null;
break;
case BEGIN_OBJECT:
args = new JsonParser().parse(reader).getAsJsonObject();
type = args.get("type").getAsString();
break;
default:
throw new IOException("Unexpected token: " + reader.peek());
}
switch (type) {
case "linear":
return new LinearInterpolator();
case "cubic-spline":
return new CubicSplineInterpolator();
case "catmull-rom-spline":
if (args == null || !args.has("alpha")) {
throw new IOException("Missing alpha value for catmull-rom-spline.");
}
return new CatmullRomSplineInterpolator(args.get("alpha").getAsDouble());
default:
throw new IOException("Unknown interpolation type: " + type);
}
}
/**
* Clones an interpolator by de- and re-serializing it.
* @param interpolator The interpolator to clone
* @return The cloned interpolator
*/
private Interpolator cloneInterpolator(Interpolator interpolator) {
Interpolator cloned = deserializeInterpolator(serializeInterpolator(interpolator));
interpolator.getKeyframeProperties().forEach(cloned::registerProperty);
return cloned;
}
/**
* Serializes the specific interpolator to String.
* Does <b>not</b> serialize the registered keyframe properties.
* @param interpolator The interpolator to serialize.
* @return The serialized interpolator
*/
private String serializeInterpolator(Interpolator interpolator) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JsonWriter jsonWriter = new JsonWriter(new PrintWriter(baos));
try {
jsonWriter.beginArray();
serializeInterpolator(jsonWriter, interpolator);
jsonWriter.endArray();
jsonWriter.flush();
} catch (IOException e) {
CrashReport crash = CrashReport.makeCrashReport(e, "Serializing interpolator");
CrashReportCategory category = crash.makeCategory("Serializing interpolator");
category.addCrashSectionCallable("Interpolator", interpolator::toString);
throw new ReportedException(crash);
}
return baos.toString();
}
private Interpolator deserializeInterpolator(String json) {
JsonReader jsonReader = new JsonReader(new StringReader(json));
try {
jsonReader.beginArray();
return deserializeInterpolator(jsonReader);
} catch (IOException e) {
CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator");
CrashReportCategory category = crash.makeCategory("De-serializing interpolator");
category.addCrashSection("Interpolator", json);
throw new ReportedException(crash);
}
}
}

View File

@@ -2,9 +2,25 @@ package com.replaymod.simplepathing;
import com.replaymod.core.SettingsRegistry; import com.replaymod.core.SettingsRegistry;
import java.util.Arrays;
import java.util.stream.Collectors;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> { 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> PATH_PREVIEW = make("pathpreview", "pathpreview", true);
public static final Setting<Boolean> LINEAR_INTERPOLATION = make("linear_interpolation", "linearinterpolation", false); public static final SettingsRegistry.MultipleChoiceSettingKeys<String> DEFAULT_INTERPOLATION;
static {
String format = "replaymod.gui.editkeyframe.interpolator.%s.name";
DEFAULT_INTERPOLATION = new SettingsRegistry.MultipleChoiceSettingKeys<>(
"simplepathing", "interpolator", "replaymod.gui.settings.interpolator",
String.format(format, InterpolatorType.fromString("invalid returns default").getLocalizationKey())
);
DEFAULT_INTERPOLATION.setChoices(
Arrays.stream(InterpolatorType.values()).filter(i -> i != InterpolatorType.DEFAULT)
.map(i -> String.format(format, i.getLocalizationKey()))
.collect(Collectors.toList())
);
}
private static <T> Setting<T> make(String key, String displayName, T defaultValue) { private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>(key, displayName, defaultValue); return new Setting<>(key, displayName, defaultValue);

View File

@@ -5,14 +5,27 @@ import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.change.CombinedChange; import com.replaymod.replaystudio.pathing.change.CombinedChange;
import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.simplepathing.InterpolatorType;
import com.replaymod.simplepathing.SPTimeline;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import com.replaymod.simplepathing.Setting;
import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty;
import de.johni0702.minecraft.gui.container.AbstractGuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiNumberField; import de.johni0702.minecraft.gui.element.GuiNumberField;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiClickable;
import de.johni0702.minecraft.gui.element.IGuiLabel; import de.johni0702.minecraft.gui.element.IGuiLabel;
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.function.Typeable;
import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout;
@@ -20,10 +33,16 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.Consumer; import de.johni0702.minecraft.gui.utils.Consumer;
import org.apache.commons.lang3.tuple.Triple; import lombok.Getter;
import net.minecraft.client.resources.I18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.ReadablePoint;
import java.util.Map;
import java.util.Optional;
import static de.johni0702.minecraft.gui.utils.Utils.link; import static de.johni0702.minecraft.gui.utils.Utils.link;
public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends AbstractGuiPopup<T> implements Typeable { public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends AbstractGuiPopup<T> implements Typeable {
@@ -31,6 +50,11 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true); return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true);
} }
protected static final Logger logger = LogManager.getLogger();
protected final GuiPathing guiPathing;
protected final long time;
protected final Keyframe keyframe; protected final Keyframe keyframe;
protected final Path path; protected final Path path;
@@ -66,12 +90,13 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
.addElements(new VerticalLayout.Data(0.5, false), title, inputs, timePanel, buttons); .addElements(new VerticalLayout.Data(0.5, false), title, inputs, timePanel, buttons);
} }
public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) { public GuiEditKeyframe(GuiPathing gui, SPPath path, long time, String type) {
super(ReplayModReplay.instance.getReplayHandler().getOverlay()); super(ReplayModReplay.instance.getReplayHandler().getOverlay());
this.keyframe = keyframe; this.guiPathing = gui;
this.path = path; this.time = time;
this.path = gui.getMod().getCurrentTimeline().getPath(path);
this.keyframe = this.path.getKeyframe(time);
long time = keyframe.getTime();
Consumer<String> updateSaveButtonState = s -> saveButton.setEnabled(canSave()); Consumer<String> updateSaveButtonState = s -> saveButton.setEnabled(canSave());
timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState); timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState);
timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState); timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState);
@@ -81,10 +106,14 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
saveButton.onClick(() -> { saveButton.onClick(() -> {
Change change = save(); Change change = save();
long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger(); long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger();
if (newTime != keyframe.getTime()) { if (newTime != time) {
change = CombinedChange.createFromApplied(change, gui.moveKeyframe(path, keyframe, newTime)); change = CombinedChange.createFromApplied(change,
gui.getMod().getCurrentTimeline().moveKeyframe(path, time, newTime));
if (gui.getMod().getSelectedPath() == path && gui.getMod().getSelectedTime() == time) {
gui.getMod().setSelected(path, newTime);
}
} }
path.getTimeline().pushChange(change); gui.getMod().getCurrentTimeline().getTimeline().pushChange(change);
close(); close();
}); });
} }
@@ -114,7 +143,7 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
protected abstract Change save(); protected abstract Change save();
public static class Spectator extends GuiEditKeyframe<Spectator> { public static class Spectator extends GuiEditKeyframe<Spectator> {
public Spectator(GuiPathing gui, Path path, Keyframe keyframe) { public Spectator(GuiPathing gui, SPPath path, long keyframe) {
super(gui, path, keyframe, "spec"); super(gui, path, keyframe, "spec");
link(timeMinField, timeSecField, timeMSecField); link(timeMinField, timeSecField, timeMSecField);
@@ -147,10 +176,10 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
timestampMSecField, new GuiLabel().setI18nText("replaymod.gui.milliseconds")); timestampMSecField, new GuiLabel().setI18nText("replaymod.gui.milliseconds"));
} }
public Time(GuiPathing gui, Path path, Keyframe keyframe) { public Time(GuiPathing gui, SPPath path, long keyframe) {
super(gui, path, keyframe, "time"); super(gui, path, keyframe, "time");
keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> { this.keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> {
timestampMinField.setValue(time / 1000 / 60); timestampMinField.setValue(time / 1000 / 60);
timestampSecField.setValue(time / 1000 % 60); timestampSecField.setValue(time / 1000 % 60);
timestampMSecField.setValue(time % 1000); timestampMSecField.setValue(time % 1000);
@@ -165,11 +194,7 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
protected Change save() { protected Change save() {
int time = (timestampMinField.getInteger() * 60 + timestampSecField.getInteger()) * 1000 int time = (timestampMinField.getInteger() * 60 + timestampSecField.getInteger()) * 1000
+ timestampMSecField.getInteger(); + timestampMSecField.getInteger();
Change change = UpdateKeyframeProperties.create(path, keyframe) return guiPathing.getMod().getCurrentTimeline().updateTimeKeyframe(keyframe.getTime(), time);
.setValue(TimestampProperty.PROPERTY, time)
.done();
change.apply(path.getTimeline());
return change;
} }
@Override @Override
@@ -187,8 +212,11 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5); public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5);
public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5);
public final InterpolationPanel interpolationPanel = new InterpolationPanel();
{ {
inputs.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) GuiPanel positionInputs = new GuiPanel()
.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5))
.addElements(new GridLayout.Data(1, 0.5), .addElements(new GridLayout.Data(1, 0.5),
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.xpos"), xField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.xpos"), xField,
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camyaw"), yawField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camyaw"), yawField,
@@ -196,17 +224,20 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.campitch"), pitchField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.campitch"), pitchField,
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.zpos"), zField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.zpos"), zField,
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camroll"), rollField); new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camroll"), rollField);
inputs.setLayout(new VerticalLayout().setSpacing(10)).addElements(new VerticalLayout.Data(0.5, false),
positionInputs, interpolationPanel);
} }
public Position(GuiPathing gui, Path path, Keyframe keyframe) { public Position(GuiPathing gui, SPPath path, long keyframe) {
super(gui, path, keyframe, "pos"); super(gui, path, keyframe, "pos");
keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> { this.keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> {
xField.setValue(pos.getLeft()); xField.setValue(pos.getLeft());
yField.setValue(pos.getMiddle()); yField.setValue(pos.getMiddle());
zField.setValue(pos.getRight()); zField.setValue(pos.getRight());
}); });
keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> { this.keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> {
yawField.setValue(rot.getLeft()); yawField.setValue(rot.getLeft());
pitchField.setValue(rot.getMiddle()); pitchField.setValue(rot.getMiddle());
rollField.setValue(rot.getRight()); rollField.setValue(rot.getRight());
@@ -219,17 +250,181 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
@Override @Override
protected Change save() { protected Change save() {
Change change = UpdateKeyframeProperties.create(path, keyframe) SPTimeline timeline = guiPathing.getMod().getCurrentTimeline();
.setValue(CameraProperties.POSITION, Triple.of(xField.getDouble(), yField.getDouble(), zField.getDouble())) Change positionChange = timeline.updatePositionKeyframe(time,
.setValue(CameraProperties.ROTATION, Triple.of(yawField.getFloat(), pitchField.getFloat(), rollField.getFloat())) xField.getDouble(), yField.getDouble(), zField.getDouble(),
.done(); yawField.getFloat(), pitchField.getFloat(), rollField.getFloat()
change.apply(path.getTimeline()); );
return change; if (interpolationPanel.getSettingsPanel() == null) {
// The last keyframe doesn't have interpolator settings because there is no segment following it
return positionChange;
}
Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator();
if (interpolationPanel.getInterpolatorType() == InterpolatorType.DEFAULT) {
return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time),
timeline.setDefaultInterpolator(interpolator));
} else {
return CombinedChange.createFromApplied(positionChange, timeline.setInterpolator(time, interpolator));
}
} }
@Override @Override
protected Position getThis() { protected Position getThis() {
return this; return this;
} }
public class InterpolationPanel extends AbstractGuiContainer<InterpolationPanel> {
@Getter
private SettingsPanel settingsPanel;
private GuiDropdownMenu<InterpolatorType> dropdown;
public InterpolationPanel() {
setLayout(new VerticalLayout());
dropdown = new GuiDropdownMenu<InterpolatorType>()
.setToString(s -> I18n.format(s.getI18nName()))
.setValues(InterpolatorType.values()).setHeight(20)
.onSelection(i -> setSettingsPanel(dropdown.getSelectedValue()));
// set hover tooltips
for (Map.Entry<InterpolatorType, IGuiClickable> e : dropdown.getDropdownEntries().entrySet()) {
e.getValue().setTooltip(new GuiTooltip().setI18nText(e.getKey().getI18nDescription()));
}
GuiPanel dropdownPanel = new GuiPanel()
.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(3).setSpacingY(5))
.addElements(new GridLayout.Data(1, 0.5),
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.interpolator"), dropdown);
addElements(new VerticalLayout.Data(0.5, false), dropdownPanel);
Optional<PathSegment> segment = path.getSegments().stream()
.filter(s -> s.getStartKeyframe() == keyframe).findFirst();
if (segment.isPresent()) {
Interpolator interpolator = segment.get().getInterpolator();
InterpolatorType type = InterpolatorType.fromClass(interpolator.getClass());
if (keyframe.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) {
dropdown.setSelected(type); // trigger the callback once to display settings panel
} else {
setSettingsPanel(InterpolatorType.DEFAULT);
type = InterpolatorType.DEFAULT;
}
if (getInterpolatorTypeNoDefault(type).getInterpolatorClass().isInstance(interpolator)) {
//noinspection unchecked
settingsPanel.loadSettings(interpolator);
}
} else {
// Disable dropdown if this is the last keyframe
dropdown.setDisabled();
}
}
public void setSettingsPanel(InterpolatorType type) {
removeElement(this.settingsPanel);
switch (getInterpolatorTypeNoDefault(type)) {
case CATMULL_ROM:
settingsPanel = new CatmullRomSettingsPanel();
break;
case CUBIC:
settingsPanel = new CubicSettingsPanel();
break;
case LINEAR:
settingsPanel = new LinearSettingsPanel();
break;
}
addElements(new GridLayout.Data(0.5, 0.5), settingsPanel);
}
protected InterpolatorType getInterpolatorTypeNoDefault(InterpolatorType interpolatorType) {
if (interpolatorType == InterpolatorType.DEFAULT || interpolatorType == null) {
InterpolatorType defaultType = InterpolatorType.fromString(
guiPathing.getMod().getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION));
return defaultType;
}
return interpolatorType;
}
public InterpolatorType getInterpolatorType() {
return dropdown.getSelectedValue();
}
@Override
protected InterpolationPanel getThis() {
return this;
}
public abstract class SettingsPanel<I extends Interpolator, T extends SettingsPanel<I, T>> extends AbstractGuiContainer<T> {
public abstract void loadSettings(I interpolator);
public abstract I createInterpolator();
}
public class CatmullRomSettingsPanel extends SettingsPanel<CatmullRomSplineInterpolator, CatmullRomSettingsPanel> {
public final GuiLabel alphaLabel = new GuiLabel().setColor(Colors.BLACK)
.setI18nText("replaymod.gui.editkeyframe.interpolator.catmullrom.alpha");
public final GuiNumberField alphaField = new GuiNumberField().setSize(100, 20).setPrecision(5)
.setMinValue(0).setValidateOnFocusChange(true);
{
setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER));
addElements(new HorizontalLayout.Data(0.5), alphaLabel, alphaField);
}
@Override
public void loadSettings(CatmullRomSplineInterpolator interpolator) {
alphaField.setValue(interpolator.getAlpha());
}
@Override
public CatmullRomSplineInterpolator createInterpolator() {
return new CatmullRomSplineInterpolator(alphaField.getDouble());
}
@Override
protected CatmullRomSettingsPanel getThis() {
return this;
}
}
public class CubicSettingsPanel extends SettingsPanel<CubicSplineInterpolator, CubicSettingsPanel> {
@Override
public void loadSettings(CubicSplineInterpolator interpolator) {
}
@Override
public CubicSplineInterpolator createInterpolator() {
return new CubicSplineInterpolator();
}
@Override
protected CubicSettingsPanel getThis() {
return this;
}
}
public class LinearSettingsPanel extends SettingsPanel<LinearInterpolator, LinearSettingsPanel> {
@Override
public void loadSettings(LinearInterpolator interpolator) {
}
@Override
public LinearInterpolator createInterpolator() {
return new LinearInterpolator();
}
@Override
protected LinearSettingsPanel getThis() {
return this;
}
}
}
} }
} }

View File

@@ -1,7 +1,5 @@
package com.replaymod.simplepathing.gui; package com.replaymod.simplepathing.gui;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.SpectatorProperty;
@@ -11,9 +9,10 @@ import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment; import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.replaystudio.pathing.property.Property;
import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.ReplayModSimplePathing;
import com.replaymod.simplepathing.SPTimeline;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline;
import de.johni0702.minecraft.gui.function.Draggable; import de.johni0702.minecraft.gui.function.Draggable;
@@ -23,7 +22,6 @@ import org.lwjgl.util.Point;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.ReadablePoint;
import javax.annotation.Nullable;
import java.util.Comparator; import java.util.Comparator;
import java.util.Optional; import java.util.Optional;
@@ -37,14 +35,14 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
private final GuiPathing gui; private final GuiPathing gui;
/** /**
* The keyframe that was last clicked on using the left mouse button. * The keyframe (time on timeline) that was last clicked on using the left mouse button.
*/ */
private Keyframe lastClickedKeyframe; private long lastClickedKeyframe;
/** /**
* Id of the path of {@link #lastClickedKeyframe}. * Path of {@link #lastClickedKeyframe}.
*/ */
private int lastClickedPath; private SPPath lastClickedPath;
/** /**
* The time at which {@link #lastClickedKeyframe} was updated. * The time at which {@link #lastClickedKeyframe} was updated.
@@ -89,18 +87,13 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
renderer.bindTexture(ReplayMod.TEXTURE); renderer.bindTexture(ReplayMod.TEXTURE);
for (Keyframe keyframe : Iterables.concat(Iterables.transform(mod.getCurrentTimeline().getPaths(), new Function<Path, Iterable<Keyframe>>() { SPTimeline timeline = mod.getCurrentTimeline();
@Nullable
@Override timeline.getTimeline().getPaths().stream().flatMap(path -> path.getKeyframes().stream()).forEach(keyframe -> {
public Iterable<Keyframe> apply(@Nullable Path input) {
assert input != null;
return input.getKeyframes();
}
}))) {
if (keyframe.getTime() >= startTime && keyframe.getTime() <= endTime) { if (keyframe.getTime() >= startTime && keyframe.getTime() <= endTime) {
double relativeTime = keyframe.getTime() - startTime; double relativeTime = keyframe.getTime() - startTime;
int positonX = BORDER_LEFT + (int) (relativeTime / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2; int positonX = BORDER_LEFT + (int) (relativeTime / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2;
int u = KEYFRAME_TEXTURE_X + (mod.getSelectedKeyframe() == keyframe ? KEYFRAME_SIZE : 0); int u = KEYFRAME_TEXTURE_X + (mod.isSelected(keyframe) ? KEYFRAME_SIZE : 0);
int v = KEYFRAME_TEXTURE_Y; int v = KEYFRAME_TEXTURE_Y;
if (keyframe.getValue(CameraProperties.POSITION).isPresent()) { if (keyframe.getValue(CameraProperties.POSITION).isPresent()) {
if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) { if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) {
@@ -113,10 +106,10 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
} }
} }
} });
// Draw colored quads on spectator path segments // Draw colored quads on spectator path segments
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.POSITION_PATH).getSegments()) { for (PathSegment segment : timeline.getPositionPath().getSegments()) {
if (segment.getInterpolator() == null if (segment.getInterpolator() == null
|| !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) { || !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) {
continue; // Not a spectator segment continue; // Not a spectator segment
@@ -125,7 +118,7 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
} }
// Draw red quads on time path segments that would require time going backwards // Draw red quads on time path segments that would require time going backwards
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.TIME_PATH).getSegments()) { for (PathSegment segment : timeline.getTimePath().getSegments()) {
long startTimestamp = segment.getStartKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); long startTimestamp = segment.getStartKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
long endTimestamp = segment.getEndKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); long endTimestamp = segment.getEndKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
if (endTimestamp >= startTimestamp) { if (endTimestamp >= startTimestamp) {
@@ -162,29 +155,28 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
* @param position The raw position * @param position The raw position
* @return Pair of path id and keyframe or null when no keyframe was clicked * @return Pair of path id and keyframe or null when no keyframe was clicked
*/ */
private Pair<Integer, Keyframe> getKeyframe(ReadablePoint position) { private Pair<SPPath, Long> getKeyframe(ReadablePoint position) {
int time = getTimeAt(position.getX(), position.getY()); int time = getTimeAt(position.getX(), position.getY());
if (time != -1) { if (time != -1) {
Point mouse = new Point(position); Point mouse = new Point(position);
getContainer().convertFor(this, mouse); getContainer().convertFor(this, mouse);
int mouseY = mouse.getY(); int mouseY = mouse.getY();
if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) { if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) {
Timeline timeline = gui.getMod().getCurrentTimeline(); SPPath path;
int path;
if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) { if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) {
// Position keyframe // Position keyframe
path = GuiPathing.POSITION_PATH; path = SPPath.POSITION;
} else { } else {
// Time keyframe // Time keyframe
path = GuiPathing.TIME_PATH; path = SPPath.TIME;
} }
int visibleTime = (int) (getZoom() * getLength()); int visibleTime = (int) (getZoom() * getLength());
int tolerance = visibleTime * KEYFRAME_SIZE / (size.getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2; int tolerance = visibleTime * KEYFRAME_SIZE / (getLastSize().getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2;
Optional<Keyframe> keyframe = timeline.getPaths().get(path).getKeyframes().stream() Optional<Keyframe> keyframe = gui.getMod().getCurrentTimeline().getPath(path).getKeyframes().stream()
.filter(k -> Math.abs(k.getTime() - time) <= tolerance) .filter(k -> Math.abs(k.getTime() - time) <= tolerance)
.sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time))) .sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time)))
.findFirst(); .findFirst();
return Pair.of(path, keyframe.orElse(null)); return Pair.of(path, keyframe.map(Keyframe::getTime).orElse(null));
} }
} }
return Pair.of(null, null); return Pair.of(null, null);
@@ -193,30 +185,31 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
@Override @Override
public boolean mouseClick(ReadablePoint position, int button) { public boolean mouseClick(ReadablePoint position, int button) {
int time = getTimeAt(position.getX(), position.getY()); int time = getTimeAt(position.getX(), position.getY());
Pair<Integer, Keyframe> pathKeyframePair = getKeyframe(position); Pair<SPPath, Long> pathKeyframePair = getKeyframe(position);
if (pathKeyframePair.getRight() != null) { if (pathKeyframePair.getRight() != null) {
SPPath path = pathKeyframePair.getLeft();
// Clicked on keyframe // Clicked on keyframe
Keyframe keyframe = pathKeyframePair.getRight(); long keyframeTime = pathKeyframePair.getRight();
if (button == 0) { // Left click if (button == 0) { // Left click
long now = Minecraft.getSystemTime(); long now = Minecraft.getSystemTime();
if (lastClickedKeyframe == keyframe) { if (lastClickedKeyframe == keyframeTime) {
// Clicked the same keyframe again, potentially a double click // Clicked the same keyframe again, potentially a double click
if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) { if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) {
// Yup, double click, open the edit keyframe gui // Yup, double click, open the edit keyframe gui
Path path = gui.getMod().getCurrentTimeline().getPaths().get(pathKeyframePair.getLeft()); gui.openEditKeyframePopup(path, keyframeTime);
gui.openEditKeyframePopup(path, keyframe);
return true; return true;
} }
} }
// Not a double click, just update the click time and selection // Not a double click, just update the click time and selection
lastClickedTime = now; lastClickedTime = now;
lastClickedKeyframe = keyframe; lastClickedKeyframe = keyframeTime;
lastClickedPath = pathKeyframePair.getLeft(); lastClickedPath = path;
gui.getMod().setSelectedKeyframe(lastClickedKeyframe); gui.getMod().setSelected(lastClickedPath, lastClickedKeyframe);
// We might be dragging // We might be dragging
draggingStartX = position.getX(); draggingStartX = position.getX();
dragging = true; dragging = true;
} else if (button == 1) { // Right click } else if (button == 1) { // Right click
Keyframe keyframe = gui.getMod().getCurrentTimeline().getKeyframe(path, keyframeTime);
for (Property property : keyframe.getProperties()) { for (Property property : keyframe.getProperties()) {
applyPropertyToGame(property, keyframe); applyPropertyToGame(property, keyframe);
} }
@@ -226,12 +219,11 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
// Clicked on timeline but not on any keyframe // Clicked on timeline but not on any keyframe
if (button == 0) { // Left click if (button == 0) { // Left click
setCursorPosition(time); setCursorPosition(time);
gui.getMod().setSelectedKeyframe(null); gui.getMod().setSelected(null, 0);
} else if (button == 1) { // Right click } else if (button == 1) { // Right click
if (pathKeyframePair.getLeft() != null) { if (pathKeyframePair.getLeft() != null) {
// Apply the value of the clicked path at the clicked position // Apply the value of the clicked path at the clicked position
Timeline timeline = gui.getMod().getCurrentTimeline(); Path path = gui.getMod().getCurrentTimeline().getPath(pathKeyframePair.getLeft());
Path path = timeline.getPaths().get(pathKeyframePair.getLeft());
path.getKeyframes().stream().flatMap(k -> k.getProperties().stream()).distinct().forEach( path.getKeyframes().stream().flatMap(k -> k.getProperties().stream()).distinct().forEach(
p -> applyPropertyToGame(p, path, time)); p -> applyPropertyToGame(p, path, time));
} }
@@ -280,12 +272,13 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
} }
} }
if (actuallyDragging) { if (actuallyDragging) {
if (!gui.ensureEntityTracker(() -> mouseDrag(position, button, timeSinceLastCall))) return true;
// Threshold passed // Threshold passed
Path path = gui.getMod().getCurrentTimeline().getPaths().get(lastClickedPath); SPTimeline timeline = gui.getMod().getCurrentTimeline();
Point mouse = new Point(position); Point mouse = new Point(position);
getContainer().convertFor(this, mouse); getContainer().convertFor(this, mouse);
int mouseX = mouse.getX(); int mouseX = mouse.getX();
int width = size.getWidth(); int width = getLastSize().getWidth();
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
double segmentLength = getLength() * getZoom(); double segmentLength = getLength() * getZoom();
double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth; double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth;
@@ -295,24 +288,21 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
} }
// If there already is a keyframe at the target time, then increase the time by one until there is none // If there already is a keyframe at the target time, then increase the time by one until there is none
while (path.getKeyframe(newTime) != null) { while (timeline.getKeyframe(lastClickedPath, newTime) != null) {
newTime++; newTime++;
} }
// First undo any previous changes // First undo any previous changes
if (draggingChange != null) { if (draggingChange != null) {
draggingChange.undo(gui.getMod().getCurrentTimeline()); draggingChange.undo(timeline.getTimeline());
} }
// Move keyframe to new position and // Move keyframe to new position and
// store change for later undoing / pushing to history // store change for later undoing / pushing to history
draggingChange = gui.moveKeyframe(path, lastClickedKeyframe, newTime); draggingChange = timeline.moveKeyframe(lastClickedPath, lastClickedKeyframe, newTime);
// Selected keyframe has been replaced // Selected keyframe has been replaced
gui.getMod().setSelectedKeyframe(path.getKeyframe(newTime)); gui.getMod().setSelected(lastClickedPath, newTime);
// Path has been changed
path.updateAll();
} }
return true; return true;
} }
@@ -321,7 +311,7 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
public boolean mouseRelease(ReadablePoint position, int button) { public boolean mouseRelease(ReadablePoint position, int button) {
if (dragging) { if (dragging) {
if (actuallyDragging) { if (actuallyDragging) {
gui.getMod().getCurrentTimeline().pushChange(draggingChange); gui.getMod().getCurrentTimeline().getTimeline().pushChange(draggingChange);
draggingChange = null; draggingChange = null;
actuallyDragging = false; actuallyDragging = false;
} }

View File

@@ -1,10 +1,12 @@
package com.replaymod.simplepathing.gui; package com.replaymod.simplepathing.gui;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.pathing.gui.GuiKeyframeRepository; import com.replaymod.pathing.gui.GuiKeyframeRepository;
import com.replaymod.pathing.player.RealtimeTimelinePlayer; import com.replaymod.pathing.player.RealtimeTimelinePlayer;
import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.CameraProperties;
@@ -14,25 +16,26 @@ import com.replaymod.render.gui.GuiRenderSettings;
import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import com.replaymod.replaystudio.pathing.change.*;
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.property.Property;
import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization;
import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.replaystudio.util.Location;
import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.ReplayModSimplePathing;
import com.replaymod.simplepathing.Setting; import com.replaymod.simplepathing.SPTimeline;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.element.AbstractGuiClickable;
import de.johni0702.minecraft.gui.element.AbstractGuiElement;
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.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime;
import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -43,9 +46,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import net.minecraft.entity.Entity;
import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.Loader;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
@@ -57,10 +58,7 @@ import org.lwjgl.util.WritablePoint;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.error; import static com.replaymod.core.utils.Utils.error;
@@ -71,9 +69,6 @@ import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER;
* Gui plug-in to the GuiReplayOverlay for simple pathing. * Gui plug-in to the GuiReplayOverlay for simple pathing.
*/ */
public class GuiPathing { public class GuiPathing {
public static final int TIME_PATH = 0;
public static final int POSITION_PATH = 1;
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
public final GuiTexturedButton playPauseButton = new GuiTexturedButton() { public final GuiTexturedButton playPauseButton = new GuiTexturedButton() {
@@ -99,10 +94,11 @@ public class GuiPathing {
if (!preparePathsForPlayback()) return; if (!preparePathsForPlayback()) return;
// Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue // Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue
Timeline timeline = mod.getCurrentTimeline(); SPTimeline spTimeline = mod.getCurrentTimeline();
Timeline timeline;
try { try {
TimelineSerialization serialization = new TimelineSerialization(mod, null); TimelineSerialization serialization = new TimelineSerialization(spTimeline, null);
String serialized = serialization.serialize(Collections.singletonMap("", timeline)); String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline()));
timeline = serialization.deserialize(serialized).get(""); timeline = serialization.deserialize(serialized).get("");
} catch (Throwable t) { } catch (Throwable t) {
error(LOGGER, replayHandler.getOverlay(), CrashReport.makeCrashReport(t, "Cloning timeline"), () -> {}); error(LOGGER, replayHandler.getOverlay(), CrashReport.makeCrashReport(t, "Cloning timeline"), () -> {});
@@ -261,6 +257,7 @@ public class GuiPathing {
} }
}; };
private final ReplayMod core;
private final ReplayModSimplePathing mod; private final ReplayModSimplePathing mod;
private final ReplayHandler replayHandler; private final ReplayHandler replayHandler;
private final RealtimeTimelinePlayer player; private final RealtimeTimelinePlayer player;
@@ -270,6 +267,7 @@ public class GuiPathing {
private SettableFuture<Void> entityTrackerFuture; private SettableFuture<Void> entityTrackerFuture;
public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) { public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) {
this.core = core;
this.mod = mod; this.mod = mod;
this.replayHandler = replayHandler; this.replayHandler = replayHandler;
this.player = new RealtimeTimelinePlayer(replayHandler); this.player = new RealtimeTimelinePlayer(replayHandler);
@@ -296,27 +294,34 @@ public class GuiPathing {
if (player.isActive()) { if (player.isActive()) {
player.getFuture().cancel(false); player.getFuture().cancel(false);
} else { } else {
Timeline timeline = mod.getCurrentTimeline(); Path timePath = mod.getCurrentTimeline().getTimePath();
Path timePath = timeline.getPaths().get(TIME_PATH);
if (!preparePathsForPlayback()) return; if (!preparePathsForPlayback()) return;
timePath.setActive(!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)); timePath.setActive(!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT));
// Start from cursor time unless the control key is pressed (then start from beginning) // 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(); int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition();
ListenableFuture<Void> future = player.start(timeline, startTime); ListenableFuture<Void> future = player.start(mod.getCurrentTimeline().getTimeline(), startTime);
overlay.setCloseable(false); overlay.setCloseable(false);
overlay.setMouseVisible(true); overlay.setMouseVisible(true);
core.printInfoToChat("replaymod.chat.pathstarted");
Futures.addCallback(future, new FutureCallback<Void>() { Futures.addCallback(future, new FutureCallback<Void>() {
@Override @Override
public void onSuccess(@Nullable Void result) { public void onSuccess(@Nullable Void result) {
if (future.isCancelled()) {
core.printInfoToChat("replaymod.chat.pathinterrupted");
} else {
core.printInfoToChat("replaymod.chat.pathfinished");
}
overlay.setCloseable(true); overlay.setCloseable(true);
timePath.setActive(true);
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
t.printStackTrace(); t.printStackTrace();
overlay.setCloseable(true); overlay.setCloseable(true);
timePath.setActive(true);
} }
}); });
} }
@@ -326,26 +331,28 @@ public class GuiPathing {
positionKeyframeButton.setTexturePosH(new ReadablePoint() { positionKeyframeButton.setTexturePosH(new ReadablePoint() {
@Override @Override
public int getX() { public int getX() {
Keyframe keyframe = mod.getSelectedKeyframe(); SPPath keyframePath = mod.getSelectedPath();
if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) { long keyframeTime = mod.getSelectedTime();
if (keyframePath != SPPath.POSITION) {
// No keyframe or wrong path // No keyframe or wrong path
keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition()); keyframeTime = timeline.getCursorPosition();
keyframePath = mod.getCurrentTimeline().isPositionKeyframe(keyframeTime) ? SPPath.POSITION : null;
} }
if (keyframe == null) { if (keyframePath != SPPath.POSITION) {
return replayHandler.isCameraView() ? 0 : 40; return replayHandler.isCameraView() ? 0 : 40;
} else { } else {
return keyframe.getValue(SpectatorProperty.PROPERTY).isPresent() ? 40 : 0; return mod.getCurrentTimeline().isSpectatorKeyframe(keyframeTime) ? 40 : 0;
} }
} }
@Override @Override
public int getY() { public int getY() {
Keyframe keyframe = mod.getSelectedKeyframe(); SPPath keyframePath = mod.getSelectedPath();
if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) { if (keyframePath != SPPath.POSITION) {
// No keyframe selected but there might be one at exactly the position of the cursor // No keyframe selected but there might be one at exactly the position of the cursor
keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition()); keyframePath = mod.getCurrentTimeline().isPositionKeyframe(timeline.getCursorPosition()) ? SPPath.POSITION : null;
} }
return keyframe != null && keyframe.getValue(CameraProperties.POSITION).isPresent() ? 60 : 40; return keyframePath == SPPath.POSITION ? 60 : 40;
} }
@Override @Override
@@ -355,7 +362,7 @@ public class GuiPathing {
}).onClick(new Runnable() { }).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
updateKeyframe(false); updateKeyframe(SPPath.POSITION);
} }
}); });
@@ -367,12 +374,12 @@ public class GuiPathing {
@Override @Override
public int getY() { public int getY() {
Keyframe keyframe = mod.getSelectedKeyframe(); SPPath keyframePath = mod.getSelectedPath();
if (keyframe == null || !keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) { if (keyframePath != SPPath.TIME) {
// No keyframe selected but there might be one at exactly the position of the cursor // No keyframe selected but there might be one at exactly the position of the cursor
keyframe = mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframe(timeline.getCursorPosition()); keyframePath = mod.getCurrentTimeline().isTimeKeyframe(timeline.getCursorPosition()) ? SPPath.TIME : null;
} }
return keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ? 100 : 80; return keyframePath == SPPath.TIME ? 100 : 80;
} }
@Override @Override
@@ -382,7 +389,7 @@ public class GuiPathing {
}).onClick(new Runnable() { }).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
updateKeyframe(true); updateKeyframe(SPPath.TIME);
} }
}); });
@@ -396,94 +403,90 @@ public class GuiPathing {
} }
}); });
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, new Runnable() { startLoadingEntityTracker();
@Override }
public void run() {
if (!overlay.isVisible()) {
return;
}
try {
GuiKeyframeRepository gui = new GuiKeyframeRepository(
mod, replayHandler.getReplayFile(), mod.getCurrentTimeline());
Futures.addCallback(gui.getFuture(), new FutureCallback<Timeline>() {
@Override
public void onSuccess(Timeline result) {
if (result != null) {
mod.setCurrentTimeline(result);
}
}
@Override public void keyframeRepoButtonPressed() {
public void onFailure(Throwable t) { try {
t.printStackTrace(); GuiKeyframeRepository gui = new GuiKeyframeRepository(
core.printWarningToChat("Error loading timeline: " + t.getMessage()); mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline());
} Futures.addCallback(gui.getFuture(), new FutureCallback<Timeline>() {
});
gui.display();
} catch (IOException e) {
e.printStackTrace();
core.printWarningToChat("Error loading timeline: " + e.getMessage());
}
}
});
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> {
GuiYesNoPopup popup = GuiYesNoPopup.open(overlay,
new GuiLabel().setI18nText("replaymod.gui.clearcallback.title").setColor(Colors.BLACK)
).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
Futures.addCallback(popup.getFuture(), new FutureCallback<Boolean>() {
@Override @Override
public void onSuccess(Boolean delete) { public void onSuccess(Timeline result) {
if (delete) { if (result != null) {
Timeline timeline = mod.createTimeline(); mod.setCurrentTimeline(new SPTimeline(result));
timeline.createPath();
timeline.createPath();
mod.setCurrentTimeline(timeline);
} }
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
t.printStackTrace(); t.printStackTrace();
core.printWarningToChat("Error loading timeline: " + t.getMessage());
} }
}); });
}); gui.display();
} catch (IOException e) {
e.printStackTrace();
core.printWarningToChat("Error loading timeline: " + e.getMessage());
}
}
core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> { public void clearKeyframesButtonPressed() {
// Current replay time GuiYesNoPopup popup = GuiYesNoPopup.open(replayHandler.getOverlay(),
int time = replayHandler.getReplaySender().currentTimeStamp(); new GuiLabel().setI18nText("replaymod.gui.clearcallback.title").setColor(Colors.BLACK)
// Position of the cursor ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
int cursor = timeline.getCursorPosition(); Futures.addCallback(popup.getFuture(), new FutureCallback<Boolean>() {
// Get the last time keyframe before the cursor @Override
mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframes().stream() public void onSuccess(Boolean delete) {
.filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> { if (delete) {
// Cursor position at the keyframe mod.clearCurrentTimeline();
int keyframeCursor = (int) keyframe.getTime(); if (entityTracker != null) {
// Replay time at the keyframe mod.getCurrentTimeline().setEntityTracker(entityTracker);
// This is a keyframe from the time path, so it _should_ always have a time property }
int keyframeTime = keyframe.getValue(TimestampProperty.PROPERTY).get(); }
// Replay time passed
int timePassed = time - keyframeTime;
// Speed (set to 1 when shift is held)
double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : overlay.getSpeedSliderValue();
// Cursor time passed
int cursorPassed = (int) (timePassed / speed);
// Move cursor to new position
timeline.setCursorPosition(keyframeCursor + cursorPassed);
// Deselect keyframe to allow the user to add a new one right away
mod.setSelectedKeyframe(null);
});
});
core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> {
if (!overlay.isVisible()) {
return;
} }
if (mod.getSelectedKeyframe() != null) {
updateKeyframe(mod.getSelectedKeyframe().getValue(TimestampProperty.PROPERTY).isPresent()); @Override
public void onFailure(Throwable t) {
t.printStackTrace();
} }
}); });
}
public void syncTimeButtonPressed() {
// Current replay time
int time = replayHandler.getReplaySender().currentTimeStamp();
// Position of the cursor
int cursor = timeline.getCursorPosition();
// Get the last time keyframe before the cursor
mod.getCurrentTimeline().getTimePath().getKeyframes().stream()
.filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> {
// Cursor position at the keyframe
int keyframeCursor = (int) keyframe.getTime();
// Replay time at the keyframe
// This is a keyframe from the time path, so it _should_ always have a time property
int keyframeTime = keyframe.getValue(TimestampProperty.PROPERTY).get();
// Replay time passed
int timePassed = time - keyframeTime;
// Speed (set to 1 when shift is held)
double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : replayHandler.getOverlay().getSpeedSliderValue();
// Cursor time passed
int cursorPassed = (int) (timePassed / speed);
// Move cursor to new position
timeline.setCursorPosition(keyframeCursor + cursorPassed);
// Deselect keyframe to allow the user to add a new one right away
mod.setSelected(null, 0);
});
}
public void deleteButtonPressed() {
if (mod.getSelectedPath() != null) {
updateKeyframe(mod.getSelectedPath());
}
}
private void startLoadingEntityTracker() {
Preconditions.checkState(entityTrackerFuture == null);
// Start loading entity tracker // Start loading entity tracker
entityTrackerFuture = SettableFuture.create(); entityTrackerFuture = SettableFuture.create();
new Thread(() -> { new Thread(() -> {
@@ -496,12 +499,13 @@ public class GuiPathing {
} }
}); });
logger.info("Loaded entity tracker in " + (System.currentTimeMillis() - start) + "ms"); logger.info("Loaded entity tracker in " + (System.currentTimeMillis() - start) + "ms");
} catch (IOException e) { } catch (Throwable e) {
logger.error("Loading entity tracker:", e); logger.error("Loading entity tracker:", e);
mod.getCore().runLater(() -> { mod.getCore().runLater(() -> {
mod.getCore().printWarningToChat("Error loading entity tracker: %s", e.getLocalizedMessage()); mod.getCore().printWarningToChat("Error loading entity tracker: %s", e.getLocalizedMessage());
entityTrackerFuture.setException(e); entityTrackerFuture.setException(e);
}); });
return;
} }
entityTracker = tracker; entityTracker = tracker;
mod.getCore().runLater(() -> { mod.getCore().runLater(() -> {
@@ -511,13 +515,12 @@ public class GuiPathing {
} }
private boolean preparePathsForPlayback() { private boolean preparePathsForPlayback() {
Timeline timeline = mod.getCurrentTimeline(); SPTimeline timeline = mod.getCurrentTimeline();
timeline.getPaths().get(TIME_PATH).updateAll(); timeline.getTimeline().getPaths().forEach(Path::updateAll);
timeline.getPaths().get(POSITION_PATH).updateAll();
// Make sure time keyframes's values are monotonically increasing // Make sure time keyframes's values are monotonically increasing
int lastTime = 0; int lastTime = 0;
for (Keyframe keyframe : timeline.getPaths().get(TIME_PATH).getKeyframes()) { for (Keyframe keyframe : timeline.getTimePath().getKeyframes()) {
int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
if (time < lastTime) { if (time < lastTime) {
// We are going backwards in time // We are going backwards in time
@@ -531,8 +534,8 @@ public class GuiPathing {
} }
// Make sure there are at least two position- and two time-keyframes // Make sure there are at least two position- and two time-keyframes
if (timeline.getPaths().get(POSITION_PATH).getSegments().isEmpty() if (timeline.getPositionPath().getSegments().isEmpty()
|| timeline.getPaths().get(TIME_PATH).getSegments().isEmpty()) { || timeline.getTimePath().getSegments().isEmpty()) {
GuiInfoPopup.open(replayHandler.getOverlay(), "replaymod.chat.morekeyframes"); GuiInfoPopup.open(replayHandler.getOverlay(), "replaymod.chat.morekeyframes");
return false; return false;
} }
@@ -544,204 +547,85 @@ public class GuiPathing {
scrollbar.setZoom(scrollbar.getZoom() * factor); scrollbar.setZoom(scrollbar.getZoom() * factor);
} }
/** public boolean ensureEntityTracker(Runnable withDelayedTracker) {
* Called when either one of the property buttons is pressed.
* @param isTime {@code true} for the time property button, {@code false} for the place property button
*/
private void updateKeyframe(final boolean isTime) {
if (entityTracker == null) { if (entityTracker == null) {
LOGGER.debug("Entity tracker not yet loaded, delaying...");
LoadEntityTrackerPopup popup = new LoadEntityTrackerPopup(replayHandler.getOverlay()); LoadEntityTrackerPopup popup = new LoadEntityTrackerPopup(replayHandler.getOverlay());
entityTrackerLoadingProgress = p -> popup.progressBar.setProgress(p.floatValue()); entityTrackerLoadingProgress = p -> popup.progressBar.setProgress(p.floatValue());
Futures.addCallback(entityTrackerFuture, new FutureCallback<Void>() { Futures.addCallback(entityTrackerFuture, new FutureCallback<Void>() {
@Override @Override
public void onSuccess(@Nullable Void result) { public void onSuccess(@Nullable Void result) {
popup.close(); popup.close();
updateKeyframe(isTime); if (mod.getCurrentTimeline().getEntityTracker() == null) {
mod.getCurrentTimeline().setEntityTracker(entityTracker);
}
withDelayedTracker.run();
} }
@Override @Override
public void onFailure(@Nonnull Throwable t) { public void onFailure(@Nonnull Throwable t) {
popup.close(); String message = "Failed to load entity tracker, pathing will be unavailable.";
GuiReplayOverlay overlay = replayHandler.getOverlay();
Utils.error(LOGGER, overlay, CrashReport.makeCrashReport(t, message), popup::close);
} }
}); });
return; return false;
} }
if (mod.getCurrentTimeline().getEntityTracker() == null) {
mod.getCurrentTimeline().setEntityTracker(entityTracker);
}
return true;
}
/**
* 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
*/
private void updateKeyframe(SPPath path) {
LOGGER.debug("Updating keyframe on path {}" + path);
if (!ensureEntityTracker(() -> updateKeyframe(path))) return;
int time = timeline.getCursorPosition(); int time = timeline.getCursorPosition();
Timeline timeline = mod.getCurrentTimeline(); SPTimeline timeline = mod.getCurrentTimeline();
Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH);
Keyframe keyframe = mod.getSelectedKeyframe(); switch (path) {
if (keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ^ isTime) { case TIME:
// Keyframe is on the wrong timeline if (mod.getSelectedPath() == path) {
keyframe = null; LOGGER.debug("Selected keyframe is time keyframe -> removing keyframe");
} timeline.removeTimeKeyframe(mod.getSelectedTime());
if (keyframe == null) { mod.setSelected(null, 0);
// No keyframe selected but there may still be one at this exact time } else if (timeline.isTimeKeyframe(time)) {
keyframe = path.getKeyframe(time); LOGGER.debug("Keyframe at cursor position is time keyframe -> removing keyframe");
} timeline.removeTimeKeyframe(time);
Change change; mod.setSelected(null, 0);
if (keyframe == null) { } else {
change = AddKeyframe.create(path, time); LOGGER.debug("No time keyframe found -> adding new keyframe");
change.apply(timeline); timeline.addTimeKeyframe(time, replayHandler.getReplaySender().currentTimeStamp());
keyframe = path.getKeyframe(time); mod.setSelected(path, time);
} else {
change = RemoveKeyframe.create(path, keyframe);
change.apply(timeline);
keyframe = null;
}
if (keyframe != null) {
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe);
if (isTime) {
builder.setValue(TimestampProperty.PROPERTY, replayHandler.getReplaySender().currentTimeStamp());
} else {
CameraEntity camera = replayHandler.getCameraEntity();
builder.setValue(CameraProperties.POSITION, Triple.of(camera.posX, camera.posY, camera.posZ));
builder.setValue(CameraProperties.ROTATION, Triple.of(camera.rotationYaw, camera.rotationPitch, camera.roll));
if (!replayHandler.isCameraView()) {
Entity spectated = replayHandler.getOverlay().getMinecraft().getRenderViewEntity();
builder.setValue(SpectatorProperty.PROPERTY, spectated.getEntityId());
} }
} break;
UpdateKeyframeProperties updateChange = builder.done(); case POSITION:
updateChange.apply(timeline); if (mod.getSelectedPath() == path) {
change = CombinedChange.createFromApplied(change, updateChange); LOGGER.debug("Selected keyframe is position keyframe -> removing keyframe");
timeline.removePositionKeyframe(mod.getSelectedTime());
// If this new keyframe formed the first segment of the time path mod.setSelected(null, 0);
if (isTime && path.getSegments().size() == 1) { } else if (timeline.isPositionKeyframe(time)) {
PathSegment segment = path.getSegments().iterator().next(); LOGGER.debug("Keyframe at cursor position is position keyframe -> removing keyframe");
Interpolator interpolator = new LinearInterpolator(); timeline.removePositionKeyframe(time);
interpolator.registerProperty(TimestampProperty.PROPERTY); mod.setSelected(null, 0);
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); } else {
setInterpolator.apply(timeline); LOGGER.debug("No position keyframe found -> adding new keyframe");
change = CombinedChange.createFromApplied(change, setInterpolator); CameraEntity camera = replayHandler.getCameraEntity();
} int spectatedId = -1;
} if (!replayHandler.isCameraView()) {
spectatedId = replayHandler.getOverlay().getMinecraft().getRenderViewEntity().getEntityId();
// Update interpolators for spectator keyframes }
// while this is overkill, it is far simpler than updating differently for every possible case timeline.addPositionKeyframe(time, camera.posX, camera.posY, camera.posZ,
if (!isTime) { camera.rotationYaw, camera.rotationPitch, camera.roll, spectatedId);
Change interpolators = updateInterpolators(); mod.setSelected(path, time);
interpolators.apply(timeline);
change = CombinedChange.createFromApplied(change, interpolators);
}
Change specPosUpdate = updateSpectatorPositions();
specPosUpdate.apply(timeline);
change = CombinedChange.createFromApplied(change, specPosUpdate);
timeline.pushChange(change);
mod.setSelectedKeyframe(keyframe);
}
public Change updateInterpolators() {
boolean linearInterpolation = mod.getCore().getSettingsRegistry().get(Setting.LINEAR_INTERPOLATION);
List<Change> changes = new ArrayList<>();
Interpolator interpolator = null;
boolean isSpectatorInterpolator = false;
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments()) {
if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()
&& segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) {
// Spectator segment
if (!isSpectatorInterpolator) {
isSpectatorInterpolator = true;
interpolator = new LinearInterpolator();
interpolator.registerProperty(SpectatorProperty.PROPERTY);
} }
changes.add(SetInterpolator.create(segment, interpolator)); break;
} else {
// Normal segment
if (isSpectatorInterpolator || interpolator == null) {
isSpectatorInterpolator = false;
interpolator = linearInterpolation ? new LinearInterpolator() : new CubicSplineInterpolator();
interpolator.registerProperty(CameraProperties.POSITION);
interpolator.registerProperty(CameraProperties.ROTATION);
}
changes.add(SetInterpolator.create(segment, interpolator));
}
} }
return CombinedChange.create(changes.toArray(new Change[changes.size()]));
}
public Change updateSpectatorPositions() {
List<Change> changes = new ArrayList<>();
Path positionPath = mod.getCurrentTimeline().getPaths().get(POSITION_PATH);
Path timePath = mod.getCurrentTimeline().getPaths().get(TIME_PATH);
timePath.updateAll();
for (Keyframe keyframe : positionPath.getKeyframes()) {
Optional<Integer> spectator = keyframe.getValue(SpectatorProperty.PROPERTY);
if (spectator.isPresent()) {
Optional<Integer> time = timePath.getValue(TimestampProperty.PROPERTY, keyframe.getTime());
if (!time.isPresent()) {
continue; // No time keyframes set at this video time, cannot determine replay time
}
Location expected = entityTracker.getEntityPositionAtTimestamp(spectator.get(), time.get());
if (expected == null) {
continue; // We don't have any data on this entity for some reason
}
Triple<Double, Double, Double> pos = keyframe.getValue(CameraProperties.POSITION).orElse(Triple.of(0D, 0D, 0D));
Triple<Float, Float, Float> rot = keyframe.getValue(CameraProperties.ROTATION).orElse(Triple.of(0F, 0F, 0F));
Location actual = new Location(pos.getLeft(), pos.getMiddle(), pos.getRight(), rot.getLeft(), rot.getRight());
if (!expected.equals(actual)) {
changes.add(UpdateKeyframeProperties.create(positionPath, keyframe)
.setValue(CameraProperties.POSITION, Triple.of(expected.getX(), expected.getY(), expected.getZ()))
.setValue(CameraProperties.ROTATION, Triple.of(expected.getYaw(), expected.getPitch(), 0f)).done()
);
}
}
}
return CombinedChange.create(changes.toArray(new Change[changes.size()]));
}
public Change moveKeyframe(Path path, Keyframe keyframe, long newTime) {
Timeline timeline = mod.getCurrentTimeline();
// Interpolator might be required later (only if path is the time path)
Optional<Interpolator> interpolator =
path.getSegments().stream().findFirst().map(PathSegment::getInterpolator);
// First remove the old keyframe
Change removeChange = RemoveKeyframe.create(path, keyframe);
removeChange.apply(timeline);
// and add a new one at the correct time
Change addChange = AddKeyframe.create(path, newTime);
addChange.apply(timeline);
path.getKeyframe(newTime);
// Then copy over all properties
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime));
for (Property property : keyframe.getProperties()) {
copyProperty(property, keyframe, builder);
}
Change propertyChange = builder.done();
propertyChange.apply(timeline);
// Finally set the interpolators
Change interpolatorChange;
if (path.getTimeline().getPaths().indexOf(path) == GuiPathing.POSITION_PATH) {
// Position / Spectator keyframes need special handling
interpolatorChange = updateInterpolators();
} else {
// Time keyframes only need updating when only one segment of them exists
if (path.getSegments().size() == 1) {
interpolatorChange = SetInterpolator.create(path.getSegments().iterator().next(), interpolator.get());
} else {
interpolatorChange = CombinedChange.create(); // Noop change
}
}
interpolatorChange.apply(timeline);
// and update spectator positions
Change spectatorChange = updateSpectatorPositions();
spectatorChange.apply(timeline);
return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, interpolatorChange, spectatorChange);
}
// Helper method because generics cannot be defined on blocks
private <T> void copyProperty(Property<T> property, Keyframe from, UpdateKeyframeProperties.Builder to) {
from.getValue(property).ifPresent(value -> to.setValue(property, value));
} }
public ReplayModSimplePathing getMod() { public ReplayModSimplePathing getMod() {
@@ -752,13 +636,15 @@ public class GuiPathing {
return entityTracker; return entityTracker;
} }
public void openEditKeyframePopup(Path path, Keyframe keyframe) { public void openEditKeyframePopup(SPPath path, long time) {
if (!ensureEntityTracker(() -> openEditKeyframePopup(path, time))) return;
Keyframe keyframe = mod.getCurrentTimeline().getKeyframe(path, time);
if (keyframe.getProperties().contains(SpectatorProperty.PROPERTY)) { if (keyframe.getProperties().contains(SpectatorProperty.PROPERTY)) {
new GuiEditKeyframe.Spectator(this, path, keyframe).open(); new GuiEditKeyframe.Spectator(this, path, keyframe.getTime()).open();
} else if (keyframe.getProperties().contains(CameraProperties.POSITION)) { } else if (keyframe.getProperties().contains(CameraProperties.POSITION)) {
new GuiEditKeyframe.Position(this, path, keyframe).open(); new GuiEditKeyframe.Position(this, path, keyframe.getTime()).open();
} else { } else {
new GuiEditKeyframe.Time(this, path, keyframe).open(); new GuiEditKeyframe.Time(this, path, keyframe.getTime()).open();
} }
} }

View File

@@ -9,10 +9,10 @@ import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment; import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.replaystudio.util.Location; import com.replaymod.replaystudio.util.Location;
import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.ReplayModSimplePathing;
import com.replaymod.simplepathing.SPTimeline;
import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.gui.GuiPathing;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.GlStateManager;
@@ -68,11 +68,11 @@ public class PathPreviewRenderer {
if (guiPathing == null) return; if (guiPathing == null) return;
EntityPositionTracker entityTracker = guiPathing.getEntityTracker(); EntityPositionTracker entityTracker = guiPathing.getEntityTracker();
Timeline timeline = mod.getCurrentTimeline(); SPTimeline timeline = mod.getCurrentTimeline();
if (timeline == null) return; if (timeline == null) return;
Path path = timeline.getPaths().get(GuiPathing.POSITION_PATH); Path path = timeline.getPositionPath();
if (path.getKeyframes().isEmpty()) return; if (path.getKeyframes().isEmpty()) return;
Path timePath = timeline.getPaths().get(GuiPathing.TIME_PATH); Path timePath = timeline.getTimePath();
path.update(); path.update();
@@ -97,8 +97,8 @@ public class PathPreviewRenderer {
if (spectator && entityTracker == null) { if (spectator && entityTracker == null) {
continue; // Cannot render spectator positions when entity tracker is not yet loaded continue; // Cannot render spectator positions when entity tracker is not yet loaded
} }
// Spectator segments have 20 lines per second whereas normal segments have a fixed 100 // Spectator segments have 20 lines per second (at least 10) whereas normal segments have a fixed 100
long steps = spectator ? diff / 50 : 100; long steps = spectator ? Math.max(diff / 50, 10) : 100;
Triple<Double, Double, Double> prevPos = null; Triple<Double, Double, Double> prevPos = null;
for (int i = 0; i <= steps; i++) { for (int i = 0; i <= steps; i++) {
long time = start.getTime() + diff * i / steps; long time = start.getTime() + diff * i / steps;
@@ -240,7 +240,7 @@ public class PathPreviewRenderer {
float posY = 0f; float posY = 0f;
float size = 10f / ReplayMod.TEXTURE_SIZE; float size = 10f / ReplayMod.TEXTURE_SIZE;
if (keyframe == mod.getSelectedKeyframe()) { if (mod.isSelected(keyframe)) {
posY += size; posY += size;
} }

View File

@@ -0,0 +1,44 @@
package com.replaymod.simplepathing.properties;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.replaymod.replaystudio.pathing.property.AbstractProperty;
import com.replaymod.replaystudio.pathing.property.PropertyPart;
import lombok.NonNull;
import org.apache.commons.lang3.ObjectUtils;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
/**
* Property indicating that the next path segment's interpolator is fixed
*/
public class ExplicitInterpolationProperty extends AbstractProperty<Object> {
public static final ExplicitInterpolationProperty PROPERTY = new ExplicitInterpolationProperty();
private ExplicitInterpolationProperty() {
super("interpolationFixed", "<internal>", null, new Object());
}
@Override
public Collection<PropertyPart<Object>> getParts() {
return Collections.emptyList();
}
@Override
public void applyToGame(Object value, @NonNull Object replayHandler) {
// dummy property, do nothing
}
@Override
public void toJson(JsonWriter writer, Object value) throws IOException {
writer.nullValue();
}
@Override
public Object fromJson(JsonReader reader) throws IOException {
reader.nextNull();
return ObjectUtils.NULL;
}
}

View File

@@ -1,526 +0,0 @@
replaymod.title=Replay Mod
#Website API translations
replaymod.api.parammissing=Required parameter missing: %1$s
replaymod.api.invalidvalue=Invalid value passed for parameter %1$s - Possible values: %2$s
replaymod.api.invalidlogin=Invalid login data provided
replaymod.api.invalidauthkey=Invalid authentication key provided
replaymod.api.notenoughspace=Not enough space left to upload this file. Consider purchasing Premium on ReplayMod.com
replaymod.api.invalidfile=This file is invalid
replaymod.api.fileduplicate=This file has already been uploaded
replaymod.api.filenoexist=This file does not exist
replaymod.api.nopermissions=You don't have permission to access this file
replaymod.api.alreadyrated=You already rated this file
replaymod.api.usernoexist=This user does not exist
replaymod.api.invalidcategory=Invalid file category
replaymod.api.nofilesuploaded=No files uploaded
replaymod.api.toomanyvalues=Too many values passed for %1$s
replaymod.api.noparameters=At least one parameter required. Parameters: %1$s
replaymod.api.invalidfilename=File name must be between 5 and 30 characters long
replaymod.api.unknownlang=This language has not been translated
replaymod.api.zippingerror=An error occured while zipping the language files
replaymod.api.usernameexists=This username is already taken
replaymod.api.mailexists=This Email address is already linked to an account
replaymod.api.invalidmail=Invalid Email Address
replaymod.api.nopermissions=You don't have permission to do this
replaymod.api.authfailed=Authentication to the Minecraft Session Servers failed
replaymod.api.mcuserexists=A ReplayMod account is already associated with this Minecraft account
replaymod.api.filetoobig=The uploaded file is too big
replaymod.api.usernametoolong=The username is too long
replaymod.api.descriptiontoolong=The description is too long
replaymod.api.passwordlength=The password has to be between 5 and 1024 characters long
replaymod.api.suspended=Your account has been suspended
#All of the chat messages
replaymod.chat.recordingstarted=Recording started
replaymod.chat.recordingfailed=Failed to start recording
replaymod.chat.savingthumb=Saving Thumbnail...
replaymod.chat.savedthumb=Thumbnail has been successfully saved
replaymod.chat.failedthumb=Thumbnail could not be saved
replaymod.chat.addedmarker=Event Marker has been added
#Chat messages displayed in Replay Viewer
replaymod.chat.morekeyframes=At least 2 position keyframes and 2 time keyframes required
replaymod.chat.negativetime=Camera Paths can't play time backwards. Please consult your nearest time lord for assistance.
replaymod.chat.pathstarted=Camera Path started
replaymod.chat.pathfinished=Camera Path finished
replaymod.chat.pathinterrupted=Camera Path canceled
#Replay Categories
replaymod.category=Category
replaymod.category.survival=Survival
replaymod.category.minigame=Minigame
replaymod.category.build=Build
replaymod.category.misc=Miscellaneous
#Common GUI-related strings
replaymod.gui.replay=Replay
replaymod.gui.login=Login
replaymod.gui.logout=Logout
replaymod.gui.cancel=Cancel
replaymod.gui.username=Username
replaymod.gui.password=Password
replaymod.gui.back=Back
replaymod.gui.duration=Duration
replaymod.gui.load=Load
replaymod.gui.download=Download
replaymod.gui.like=Like
replaymod.gui.dislike=Dislike
replaymod.gui.removelike=Remove Like
replaymod.gui.removedislike=Remove Dislike
replaymod.gui.save=Save
replaymod.gui.upload=Upload
replaymod.gui.rename=Rename
replaymod.gui.remove=Remove
replaymod.gui.add=Add
replaymod.gui.start=Start
replaymod.gui.end=End
replaymod.gui.delete=Delete
replaymod.gui.recording=Recording
replaymod.gui.speed=Speed
replaymod.gui.register=Register
replaymod.gui.mail=Email Address
replaymod.gui.loading=Loading
replaymod.gui.pleasewait=Please wait
replaymod.gui.render=Render
replaymod.gui.iphidden=Server IP Hidden
replaymod.gui.overwrite=Overwrite
replaymod.gui.saveas=Save as ...
replaymod.gui.done=Done
replaymod.gui.close=Close
replaymod.gui.notagain=Don't show again
replaymod.gui.renderdonetitle=Video rendered
replaymod.gui.openfolder=Open Video Folder
replaymod.gui.youtubeupload=Upload to YouTube
replaymod.gui.renderdone1=Your video was successfully rendered.
replaymod.gui.renderdone2=How would you like to proceed?
replaymod.gui.videotitle=Title
replaymod.gui.videodescription=Description
replaymod.gui.videotags=Tags,Tags,Tags
replaymod.gui.videothumbnail=Video Thumbnail
replaymod.gui.videovisibility.private=Private
replaymod.gui.videovisibility.unlisted=Unlisted
replaymod.gui.videovisibility.public=Public
replaymod.gui.ytuploadprogress.auth=[1/4] Authorization
replaymod.gui.ytuploadprogress.prepare_video=[2/4] Preparing video: %%d%%%%
replaymod.gui.ytuploadprogress.upload=[3/4] Uploading: %%d%%%%
replaymod.gui.ytuploadprogress.cleanup=[4/4] Cleanup
replaymod.gui.ytuploadprogress.done=Done: %s
replaymod.gui.titleempty=Title cannot be empty
replaymod.gui.videothumbnailtoolarge=Thumbnail size exceeds 2MB
replaymod.gui.videothumbnailformat=Thumbnail has to be either JPEG or PNG format
replaymod.gui.original=Original
replaymod.gui.modified=Modified
replaymod.gui.outdated=There is a newer Replay Mod version available. Please download it from replaymod.com
replaymod.gui.hours=h
replaymod.gui.minutes=min
replaymod.gui.seconds=sec
replaymod.gui.milliseconds=ms
replaymod.gui.pitch=Pitch
replaymod.gui.yaw=Yaw
replaymod.gui.roll=Roll
replaymod.gui.camera=Camera
replaymod.gui.position=Position
replaymod.gui.unknownerror=An unknown error occured
replaymod.gui.exit=Exit Replay
replaymod.gui.java=Java 1.7 or newer required
replaymod.gui.morereplays=At least one Replay required
replaymod.gui.loggedin=LOGGED IN
replaymod.gui.loggedout=LOGGED OUT
replaymod.gui.mainmenu=Main Menu
replaymod.gui.settings=Settings
replaymod.gui.keyframerepo.delete=Are you sure you want to delete these keyframes?
replaymod.gui.restorereplay1=It seems like Minecraft has not quit normally.
replaymod.gui.restorereplay2=The Replay "%1$s" was not saved correctly.
replaymod.gui.restorereplay3=Do you wish to recover it?
replaymod.gui.loadentitytracker=Loading entity positions: %%d%%%%
#Only change these if it's neccessary
replaymod.gui.replayviewer=Replay Viewer
replaymod.gui.replaycenter=Replay Center
replaymod.gui.replaysettings=Replay Settings
replaymod.gui.replayeditor=Replay Editor
#Login GUI
replaymod.gui.login.title=Login to ReplayMod.com
replaymod.gui.login.logging=Logging in...
replaymod.gui.login.incorrect=Incorrect username or password
replaymod.gui.login.connectionerror=Could not connect to ReplayMod.com
replaymod.gui.login.skip=Skip
replaymod.gui.register.title=Register on ReplayMod.com
replaymod.gui.register.confirmpw=Confirm Password
replaymod.gui.register.disclaimer=By registering an account, you agree to the Replay Mod's Terms of Service: https://replaymod.com/legal/terms
replaymod.gui.register.error.nomatch=Passwords don't match
replaymod.gui.register.error.shortusername=Username has to be at least 5 characters long
replaymod.gui.register.error.invalidname=Username may only contain letters and numbers
replaymod.gui.register.error.shortpw=Password has to be at least 5 characters long
replaymod.gui.register.error.longpw=Password has to be at most 1024 characters long
replaymod.gui.register.error.authfailed=Could not authenticate your Minecraft account
#Replay Viewer GUI
replaymod.gui.viewer.rename.title=Rename Replay
replaymod.gui.viewer.rename.name=Replay Name
replaymod.gui.viewer.replayfolder=Open Replay Folder...
replaymod.gui.viewer.noauth=Log in to upload a Replay
replaymod.gui.viewer.alreadyuploaded=This Replay has already been uploaded
replaymod.gui.viewer.delete.linea=Are you sure you want to delete this replay?
replaymod.gui.viewer.delete.failed1=Your OS did not allow us to move the replay. There is
replaymod.gui.viewer.delete.failed2=nothing we can do about this. You have to do it yourself.
replaymod.gui.viewer.delete.lineb='%1$s' will be lost forever! (a long time!)
replaymod.gui.viewer.download.title=Downloading Replay File...
replaymod.gui.viewer.download.message=Please wait while "%1$s" is being downloaded.
replaymod.gui.viewer.chooser.title=Choose Replay File
replaymod.gui.viewer.chooser.message=You have a modified version of "%1$s" on your computer. Which Replay do you want to load?
replaymod.gui.login.noacc=Don't have an account yet?
#Replay Center GUI
replaymod.gui.center.logoutcallback=Do you really want to log out?
replaymod.gui.center.top.recent=Recent
replaymod.gui.center.top.best=Best
replaymod.gui.center.top.downloaded=Downloaded
replaymod.gui.center.top.favorited=Favorited
replaymod.gui.center.top.search=Search
replaymod.gui.center.search.filters=Search Filters
replaymod.gui.center.search.gametype=Gametype
replaymod.gui.center.search.order=Sort by
replaymod.gui.center.search.order.best=Best
replaymod.gui.center.search.order.recent=Recent
replaymod.gui.center.search.name=Replay Name
replaymod.gui.center.search.server=Server IP (Multiplayer only)
replaymod.gui.center.search.category=Filter by Category
replaymod.gui.center.search.version=Filter by Version
replaymod.gui.center.downloadrequired=Download the Replay to rate or favorite it
replaymod.gui.center.favorite=Favorite
replaymod.gui.center.unfavorite=Unfavorite
replaymod.gui.center.favorites=Favorites
replaymod.gui.center.author=by %1$s%2$s
#Upload GUI
replaymod.gui.upload.title=Upload File
replaymod.gui.upload.start=Start Upload
replaymod.gui.upload.cancel=Cancel Upload
replaymod.gui.upload.tagshint=Tags separated by comma
replaymod.gui.upload.namehint=Replay Title
replaymod.gui.upload.descriptionhint=Description
replaymod.gui.upload.duration=Duration: %02dm%02ds
replaymod.gui.upload.uploading=Uploading...
replaymod.gui.upload.success=File has been successfully uploaded
replaymod.gui.upload.canceled=Upload has been canceled
replaymod.gui.upload.hideip2=Hide Server IP (%s)
replaymod.gui.upload.nothumbnail=Missing Thumbnail. Please create a Thumbnail in the Replay using the %1$s key.
replaymod.gui.upload.tryagain=Try again?
replaymod.gui.upload.error.name.length=The Replay Name has to be between 5 and 30 characters long
replaymod.gui.upload.error.name=The Replay Name may not contain special characters
replaymod.gui.upload.error.tags=Tags may only contain letters and are separated using a comma
#Replay Editor
replaymod.gui.editor.replayfile=Replay File
replaymod.gui.editor.savemode.override=Replace Source File
replaymod.gui.editor.savemode.newfile=Save to new File
replaymod.gui.editor.trim.title=Trim Replay
replaymod.gui.editor.connect.title=Connect Replays
replaymod.gui.editor.modify.title=Modify Replay
replaymod.gui.editor.trim.description=Removes the beginning and end of a Replay File, only keeping the Replay between the given timestamps
replaymod.gui.editor.connect.description=Connects multiple Replays in the specified order
replaymod.gui.editor.modify.description=Provides several filters to modify Replay Files
replaymod.gui.editor.trim.marker=Select Event Marker
replaymod.gui.editor.progress.title=Editing Replay File...
replaymod.gui.editor.progress.pleasewait=Please wait while the Replay is being edited.
replaymod.gui.editor.progress.status.initializing=Initializing
replaymod.gui.editor.progress.status.writing.raw=Rewriting Replay...
replaymod.gui.editor.progress.status.writing.final=Writing File to disk...
replaymod.gui.editor.progress.status.finished=Finished Editing!
replaymod.gui.editor.disclaimer=The Replay Editor is an experimental feature and may contain bugs.
#Cancel Replay GUI
replaymod.gui.cancelrender.title=Cancel Rendering
replaymod.gui.cancelrender.message=Are you sure that you want to cancel the current rendering process?
#Saving Replay GUI
replaymod.gui.replaysaving.title=Saving Replay File...
replaymod.gui.replaysaving.message=Please wait while your recent Replay is being saved.
replaymod.gui.replaymodified.message=Replay was modified. Would you like to save the changes?
replaymod.gui.replaymodified.yes=Save Modified Replay
replaymod.gui.replaymodified.no=Discard Changes
replaymod.gui.replaymodified.warning1=A Replay named "%1$s" already exists.
replaymod.gui.replaymodified.warning2=Are you sure you want to replace it?
#Player Overview GUI
replaymod.gui.playeroverview.visible=Visible
replaymod.gui.playeroverview.hideall=Hide all
replaymod.gui.playeroverview.showall=Show all
replaymod.gui.playeroverview.spectate=Spectate Player
replaymod.gui.playeroverview.remembersettings=Remember Hidden Players
replaymod.gui.playeroverview.remembersettings.description=Saves Player Visibility in Replay File
#Replay Mod Settings GUI
replaymod.gui.settings.title=Replay Mod Settings
replaymod.gui.settings.interpolation.linear=Linear
replaymod.gui.settings.interpolation.cubic=Cubic
replaymod.gui.settings.bitrate=Video Bitrate
replaymod.gui.settings.framerate=Video Framerate
replaymod.gui.settings.notifications=Enable Notifications
replaymod.gui.settings.recordserver=Record Server
replaymod.gui.settings.recordsingleplayer=Record Singleplayer
replaymod.gui.settings.indicator=Recording Indicator
replaymod.gui.settings.lighting=Ambient Lighting
replaymod.gui.settings.forcechunks=Force Render Chunks
replaymod.gui.settings.resources=Server Resource Packs
replaymod.gui.settings.interpolation=Path Interpolation
replaymod.gui.settings.linearinterpolation=Linear Path Interpolation
replaymod.gui.settings.pathpreview=Show Path Preview
replaymod.gui.settings.keyframecleancallback=Clear Confirmation
replaymod.gui.settings.renderinvisible=Render invisible Entities
replaymod.gui.settings.camera=Camera
replaymod.gui.settings.showchat=Show Chat
replaymod.gui.settings.warning.linea=WARNING: Recording settings will be
replaymod.gui.settings.warning.lineb=applied the next time you join a world.
#Replay Mod Keybindings
replaymod.input.lighting=Toggle Lighting
replaymod.input.thumbnail=Capture Thumbnail
replaymod.input.playeroverview=Player Overview
replaymod.input.clearkeyframes=Clear Keyframes
replaymod.input.synctimeline=Synchronize Timeline
replaymod.input.keyframerepository=Open Keyframe Presets
replaymod.input.rollclockwise=Roll Clockwise
replaymod.input.rollcounterclockwise=Roll Counterclockwise
replaymod.input.resettilt=Reset Camera Tilt
replaymod.input.playpause=Play/Pause Replay
replaymod.input.marker=Add Event Marker
replaymod.input.pathpreview=Toggle Path Preview
replaymod.input.assetmanager=Asset Manager
replaymod.input.objectmanager=Object Manager
replaymod.input.interpolation=Toggle Interpolation
replaymod.input.settings=ReplayMod Settings
# CameraControllers
replaymod.camera.classic=Classic
replaymod.camera.vanilla=Vanilla-ish
#Keyframe Presets GUI
replaymod.gui.keyframerepository.title=Keyframe Repository
replaymod.gui.keyframerepository.presets=Keyframe Presets
replaymod.gui.keyframerepository.positionkeyframes=Position Keyframes
replaymod.gui.keyframerepository.timekeyframes=Time Keyframes
replaymod.gui.keyframerepository.savecurrent=Save current Path
replaymod.gui.keyframerepository.noentries=No Presets available
replaymod.gui.keyframerepository.preset.defaultname=New Keyframe Preset
replaymod.gui.keyframerepository.duplicate=This Preset already exists
#Edit Keyframe GUI
replaymod.gui.editkeyframe.title.pos=Edit Position Keyframe
replaymod.gui.editkeyframe.title.time=Edit Time Keyframe
replaymod.gui.editkeyframe.title.marker=Edit Event Marker
replaymod.gui.editkeyframe.title.spec=Edit Spectator Keyframe
replaymod.gui.editkeyframe.xpos=X Position
replaymod.gui.editkeyframe.ypos=Y Position
replaymod.gui.editkeyframe.zpos=Z Position
replaymod.gui.editkeyframe.campitch=Camera Pitch
replaymod.gui.editkeyframe.camyaw=Camera Yaw
replaymod.gui.editkeyframe.camroll=Camera Roll
replaymod.gui.editkeyframe.timelineposition=Timeline Position
replaymod.gui.editkeyframe.markername=Event Marker Name
replaymod.gui.editkeyframe.timestamp=Time Index
replaymod.gui.editkeyframe.spec.method=Spectating Method
replaymod.gui.editkeyframe.spec.method.firstperson=First Person
replaymod.gui.editkeyframe.spec.method.shoulder=Shoulder Camera
replaymod.gui.editkeyframe.spec.method.shoulder.distance=Camera Distance
replaymod.gui.editkeyframe.spec.method.shoulder.pitch=Pitch Offset
replaymod.gui.editkeyframe.spec.method.shoulder.yaw=Rotation Angle
replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness
#Render Settings GUI
replaymod.gui.rendersettings.title=Rendering Options
replaymod.gui.rendersettings.renderer=Rendering Method
replaymod.gui.rendersettings.renderer.default=Default Rendering
replaymod.gui.rendersettings.renderer.tiled=Tiled Rendering
replaymod.gui.rendersettings.renderer.stereoscopic=Stereoscopic Rendering
replaymod.gui.rendersettings.renderer.cubic=Cubic Rendering
replaymod.gui.rendersettings.renderer.equirectangular=Equirectangular Rendering
replaymod.gui.rendersettings.renderer.ods=ODS Rendering
replaymod.gui.rendersettings.renderer.default.description=Renders the video in the specified resolution. Fastest Rendering Option.
replaymod.gui.rendersettings.renderer.stereoscopic.description=Renders the video as a stereoscopic (side-by-side) 3D movie, usable by different 3D technologies. The image for one eye is half the width of the video
replaymod.gui.rendersettings.renderer.cubic.description=Renders the video with a 360 degree panoramic view, using Cubic Projection. This is usable by several 360 degree video players (and the Oculus Rift), for example VR Player.
replaymod.gui.rendersettings.renderer.equirectangular.description=Renders the video with a 360 degree panoramic view, using Equirectangular Projection. This is usable by YouTube's new 360 degree video function, and several video players (and the Oculus Rift), for example VR Player.
replaymod.gui.rendersettings.renderer.ods.description=Renders the video with a stereoscopic 3D 360 degree panoramic view, also called VR Video. This is usable by YouTube's new VR video function.
replaymod.gui.rendersettings.customresolution=Video Resolution
replaymod.gui.rendersettings.customresolution.warning.cubic=Cubic Rendering requires an aspect ratio of 4:3
replaymod.gui.rendersettings.customresolution.warning.equirectangular=Equirectangular Rendering requires an aspect ratio of 2:1
replaymod.gui.rendersettings.customresolution.warning.yuv420=For this preset, the width and height values need to be even numbers
replaymod.gui.rendersettings.customresolution.warning.ods=ODS Rendering requires an aspect ratio of 1:1
replaymod.gui.rendersettings.resolution=Video Resolution
replaymod.gui.rendersettings.interpolation=Path Interpolation
replaymod.gui.rendersettings.forcechunks=Force Render Chunks
replaymod.gui.rendersettings.framerate=Video Framerate
replaymod.gui.rendersettings.quality=Video Quality
replaymod.gui.rendersettings.chromakey=Chroma Keying
replaymod.gui.rendersettings.skycolor=Sky Color
replaymod.gui.rendersettings.nametags=Render Name Tags
replaymod.gui.rendersettings.outputfile=Output File
replaymod.gui.rendersettings.360metadata=Inject 360° Metadata
replaymod.gui.rendersettings.360metadata.error=360° Metadata can only be added to Equirectangular videos in MP4 format
replaymod.gui.rendersettings.command=Command
replaymod.gui.rendersettings.arguments=Command Line Arguments
replaymod.gui.rendersettings.ffmpeg.description=If you are an advanced user, you can customize the command line parameters used to export the video. For more information, check http://replaymod.com/docs
replaymod.gui.rendersettings.stabilizecamera=Stabilize Camera
replaymod.gui.rendersettings.exportyoutube=Export for YouTube
replaymod.gui.rendersettings.video=Video Settings
replaymod.gui.rendersettings.advanced=Advanced Settings
replaymod.gui.rendersettings.commandline=Command Line Settings
replaymod.gui.rendersettings.presets=Encoding Presets
replaymod.gui.rendersettings.presets.mp4.custom=MP4 - Custom Bitrate
replaymod.gui.rendersettings.presets.mp4.high=MP4 - High Quality
replaymod.gui.rendersettings.presets.mp4.default=MP4 - Default Quality
replaymod.gui.rendersettings.presets.mp4.potato=MP4 - Potato Quality
replaymod.gui.rendersettings.presets.webm.custom=WEBM - Custom Bitrate
replaymod.gui.rendersettings.presets.mkv.lossless=MKV - Lossless
replaymod.gui.rendersettings.presets.png=PNG Sequence
replaymod.gui.rendersettings.antialiasing=Anti-Aliasing
replaymod.gui.rendersettings.antialiasing.none=None
replaymod.gui.rendersettings.antialiasing.x2=2x
replaymod.gui.rendersettings.antialiasing.x4=4x
replaymod.gui.rendersettings.antialiasing.x8=8x
#Render Queue GUI
replaymod.gui.renderqueue.title=Render Queue
replaymod.gui.renderqueue.open=Open Queue
replaymod.gui.renderqueue.jobname=Job Name:
replaymod.gui.renderqueue.add=Add current configuration
#Rendering GUI
replaymod.gui.rendering.title=Rendering Video
replaymod.gui.rendering.pause=Pause Rendering
replaymod.gui.rendering.resume=Resume Rendering
replaymod.gui.rendering.cancel=Cancel Rendering
replaymod.gui.rendering.cancel.callback=Are you sure?
replaymod.gui.rendering.preview=Show Preview (Performance might suffer)
replaymod.gui.rendering.progress=Frames rendered: %1$d / %2$d
replaymod.gui.rendering.timetaken=Render Time
replaymod.gui.rendering.timeleft=Time Left
#Render Errors
replaymod.gui.rendering.error.title=Rendering Failed
replaymod.gui.rendering.error.optifine=Please uninstall Optifine before rendering.
replaymod.gui.rendering.error.message=To render a video, you need to have ffmpeg installed. http://ffmpeg.org
#Ingame Menu
replaymod.gui.ingame.menu.addposkeyframe=Add Position Keyframe
replaymod.gui.ingame.menu.removeposkeyframe=Remove Position Keyframe
replaymod.gui.ingame.menu.addspeckeyframe=Add Spectator Keyframe
replaymod.gui.ingame.menu.removespeckeyframe=Remove Spectator Keyframe
replaymod.gui.ingame.menu.addtimekeyframe=Add Time Keyframe
replaymod.gui.ingame.menu.removetimekeyframe=Remove Time Keyframe
replaymod.gui.ingame.menu.pause=Pause Replay
replaymod.gui.ingame.menu.unpause=Unpause Replay
replaymod.gui.ingame.menu.renderpath=Render Camera Path
replaymod.gui.ingame.menu.playpath=Play Camera Path from Cursor Position
replaymod.gui.ingame.menu.playpathfromstart=Play Camera Path from Start
replaymod.gui.ingame.menu.pausepath=Pause Camera Path
replaymod.gui.ingame.menu.zoomin=Zoom in
replaymod.gui.ingame.menu.zoomout=Zoom out
replaymod.gui.ingame.unnamedmarker=Unnamed Event Marker
#Clear Keyframe Callback
replaymod.gui.clearcallback.title=Clear all Keyframes?
replaymod.gui.clearcallback.message=This Callback can be disabled in the Replay Settings.
#Asset Manager Gui
replaymod.gui.assets.title=Asset Manager
replaymod.gui.assets.filechooser=Asset File
replaymod.gui.assets.defaultname=New Asset
replaymod.gui.assets.emptylist=No Assets available
replaymod.gui.assets.namehint=Asset Name
replaymod.gui.assets.changefile=Change Asset File
replaymod.gui.assets.noselection=No Asset selected
#Object Manager Gui
replaymod.gui.objects.properties.anchor=Anchor Point
replaymod.gui.objects.properties.position=Position
replaymod.gui.objects.properties.scale=Scale
replaymod.gui.objects.properties.orientation=Orientation
replaymod.gui.objects.properties.opacity=Opacity
replaymod.gui.objects.properties.name=Object Name
replaymod.gui.objects.empty=No Objects added
replaymod.gui.objects.defaultname=New Object
replaymod.gui.objects=Custom Objects
#Errors
replaymod.error.unknownrestriction1=This replay cannot be played with your current version.
replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown.
replaymod.error.negativetime1=Some of your time keyframes are out of order.
replaymod.error.negativetime2=Going backwards in time is not supported.
replaymod.error.negativetime3=The invalid parts are marked in red.
#Replay Mod Incompatibility Warning
replaymod.gui.modwarning.title=Incompatibility detected
replaymod.gui.modwarning.message1=A possible incompatibility between your current
replaymod.gui.modwarning.message2=Minecraft version and this Replay has been detected.
replaymod.gui.modwarning.message3=Loading this Replay may result in errors or a crash.
replaymod.gui.modwarning.name=Mod Name:
replaymod.gui.modwarning.id=Mod Id:
replaymod.gui.modwarning.missing=Missing Mods
replaymod.gui.modwarning.version=Different Mod Versions
replaymod.gui.modwarning.version.expected=Expected
replaymod.gui.modwarning.version.found=Found
#OpenEye
replaymod.gui.offeropeneye1=Do you want to install OpenEye?
replaymod.gui.offeropeneye2=OpenEye is a mod that collects anonymous information
replaymod.gui.offeropeneye3=about the mods being run and any crashes that happen.
replaymod.gui.offeropeneye4=
replaymod.gui.offeropeneye5=OpenEye can be disabled at any time and is completely optional.
replaymod.gui.offeropeneye6=More information is available at https://openeye.openmods.info/faq
replaymod.gui.offeropeneye7=Clicking Yes will download OpenEye and place it in your mods folder.
replaymod.gui.offeropeneye8=It will then be active the next time you start Minecraft.

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----

View File

@@ -0,0 +1,647 @@
package com.replaymod.simplepathing;
import com.google.common.collect.Iterables;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.replaystudio.util.Location;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.logging.log4j.LogManager;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class SPTimelineTest {
private SPTimeline impl;
private Timeline timeline;
@BeforeClass
public static void setupLogger() {
ReplayModSimplePathing.LOGGER = LogManager.getLogger();
}
@Before
public void setup() {
impl = new SPTimeline();
impl.setDefaultInterpolatorType(InterpolatorType.CUBIC);
impl.setEntityTracker(new EntityPositionTrackerMock());
timeline = impl.getTimeline();
}
@Test
public void testGetPath() {
assertSame(impl.getTimeline().getPaths().get(0), impl.getTimePath());
assertSame(impl.getTimeline().getPaths().get(0), impl.getPath(SPPath.TIME));
assertSame(impl.getTimeline().getPaths().get(1), impl.getPositionPath());
assertSame(impl.getTimeline().getPaths().get(1), impl.getPath(SPPath.POSITION));
}
@Test
public void testGetKeyframe() {
assertSame(timeline.getPaths().get(0).insert(123), impl.getKeyframe(SPPath.TIME, 123));
assertSame(timeline.getPaths().get(1).insert(456), impl.getKeyframe(SPPath.POSITION, 456));
}
@Test
public void testIsTimeKeyframe() {
impl.getTimePath().insert(123);
impl.getPositionPath().insert(456);
impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
assertTrue(impl.isTimeKeyframe(123));
assertFalse(impl.isTimeKeyframe(456));
assertFalse(impl.isTimeKeyframe(789));
assertFalse(impl.isTimeKeyframe(42));
}
@Test
public void testIsPositionKeyframe() {
impl.getTimePath().insert(123);
impl.getPositionPath().insert(456);
impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
assertFalse(impl.isPositionKeyframe(123));
assertTrue(impl.isPositionKeyframe(456));
assertFalse(impl.isPositionKeyframe(789));
assertTrue(impl.isSpectatorKeyframe(42));
}
@Test
public void testIsSpectatorKeyframe() {
impl.getTimePath().insert(123);
impl.getPositionPath().insert(456);
impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
assertFalse(impl.isSpectatorKeyframe(123));
assertFalse(impl.isSpectatorKeyframe(456));
assertFalse(impl.isSpectatorKeyframe(789));
assertTrue(impl.isSpectatorKeyframe(42));
}
@Test
public void testAddPositionKeyframe() {
assertNull(timeline.peekUndoStack());
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
assertNotNull(timeline.peekUndoStack());
Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
assertNotNull(keyframe);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
timeline.undoLastChange();
assertNull(timeline.peekUndoStack());
assertNull(impl.getKeyframe(SPPath.POSITION, 0));
}
@Test
public void testAddPositionKeyframeSpectator() {
assertNull(timeline.peekUndoStack());
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7);
assertNotNull(timeline.peekUndoStack());
Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
assertNotNull(keyframe);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
assertEquals(keyframe.getValue(SpectatorProperty.PROPERTY), Optional.of(7));
timeline.undoLastChange();
assertNull(timeline.peekUndoStack());
assertNull(impl.getKeyframe(SPPath.POSITION, 0));
}
@Test
public void testPositionKeyframeSpectatorPosition() {
Keyframe keyframe;
impl.addTimeKeyframe(0, 0);
impl.addTimeKeyframe(9, 9);
impl.addPositionKeyframe(4, 1, 2, 3, 4, 5, 6, 1);
keyframe = impl.getKeyframe(SPPath.POSITION, 4);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
impl.addPositionKeyframe(6, 1, 2, 3, 4, 5, 6, 42);
keyframe = impl.getKeyframe(SPPath.POSITION, 6);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(12d, 0d, 0d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f)));
impl.addPositionKeyframe(8, 1, 2, 3, 4, 5, 6, 42);
keyframe = impl.getKeyframe(SPPath.POSITION, 8);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(16d, 0d, 0d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f)));
impl.addPositionKeyframe(10, 1, 2, 3, 4, 5, 6, 42);
keyframe = impl.getKeyframe(SPPath.POSITION, 10);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
}
@Test(expected = IllegalStateException.class)
public void testAddPositionKeyframeDuplicate() {
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
}
@Test
public void testUpdatePositionKeyframe() {
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
Change change = impl.updatePositionKeyframe(0, 7, 8, 9, 10, 11, 12);
assertNotNull(change);
Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(7d, 8d, 9d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(10f, 11f, 12f)));
assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
change.undo(timeline);
keyframe = impl.getKeyframe(SPPath.POSITION, 0);
assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
}
@Test(expected = IllegalStateException.class)
public void testUpdatePositionKeyframeNoKeyframe() {
impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6);
}
@Test(expected = IllegalStateException.class)
public void testUpdatePositionKeyframeSpectatorKeyframe() {
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7);
impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6);
}
@Test
public void testRemovePositionKeyframe() {
impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
impl.removePositionKeyframe(0);
assertNull(impl.getKeyframe(SPPath.POSITION, 0));
}
@Test(expected = IllegalStateException.class)
public void testRemovePositionKeyframeNoKeyframe() {
impl.removePositionKeyframe(0);
}
@Test
public void testAddTimeKeyframe() {
assertNull(timeline.peekUndoStack());
impl.addTimeKeyframe(0, 1);
assertNotNull(timeline.peekUndoStack());
Keyframe keyframe = impl.getKeyframe(SPPath.TIME, 0);
assertNotNull(keyframe);
assertEquals(keyframe.getValue(TimestampProperty.PROPERTY), Optional.of(1));
timeline.undoLastChange();
assertNull(timeline.peekUndoStack());
assertNull(impl.getKeyframe(SPPath.TIME, 0));
}
@Test(expected = IllegalStateException.class)
public void testAddTimeKeyframeDuplicate() {
impl.addTimeKeyframe(0, 1);
impl.addTimeKeyframe(0, 1);
}
@Test
public void testUpdateTimeKeyframe() {
impl.addTimeKeyframe(0, 1);
Change change = impl.updateTimeKeyframe(0, 2);
assertNotNull(change);
assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(2));
change.undo(timeline);
assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(1));
}
@Test(expected = IllegalStateException.class)
public void testUpdateTimeKeyframeNoKeyframe() {
impl.updateTimeKeyframe(0, 1);
}
@Test
public void testRemoveTimeKeyframe() {
impl.addTimeKeyframe(0, 1);
impl.removeTimeKeyframe(0);
assertNull(impl.getKeyframe(SPPath.TIME, 0));
}
@Test(expected = IllegalStateException.class)
public void testRemoveTimeKeyframeNoKeyframe() {
impl.removeTimeKeyframe(0);
}
/**
* Checks all interpolators to make sure that they are continuous and not fragmented.
*/
private void assertValidInterpolators(SPPath path, int expectedNumberOfInterpolators) {
String str = prettyPrintInterpolators(impl, path);
List<Interpolator> interpolators = impl.getPath(path).getSegments().stream().map(PathSegment::getInterpolator)
.distinct().collect(Collectors.toList());
assertFalse("Not all interpolators set: " + str, interpolators.contains(null));
assertEquals("Mismatched interpolator number: " + str, expectedNumberOfInterpolators, interpolators.size());
int i = 0;
for (Interpolator interpolator : interpolators) {
Keyframe lastKeyframe = null;
for (PathSegment segment : interpolator.getSegments().stream()
.sorted(Comparator.comparing(s -> s.getStartKeyframe().getTime())).collect(Collectors.toList())) {
String message = "Fragmentation in interpolator " + i + ": " + str;
assertTrue(message, lastKeyframe == null || segment.getStartKeyframe() == lastKeyframe);
lastKeyframe = segment.getEndKeyframe();
}
i++;
}
}
@Test
public void testAddPositionKeyframeAppend() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
addPosition(3, 1);
addPosition(4, 1);
}
@Test
public void testAddPositionKeyframePrepend() {
addPosition(4, 0);
addPosition(3, 1);
addPosition(2, 1);
addPosition(1, 1);
addPosition(0, 1);
}
@Test
public void testAddPositionKeyframeMixed() {
addPosition(2, 0);
addPosition(4, 1);
addPosition(3, 1);
addPosition(0, 1);
addPosition(1, 1);
}
@Test
public void testAddPositionSpectatorKeyframe() {
addPosition(0, 0);
addSpectator(1, 1);
addSpectator(2, 2);
addPosition(4, 3);
addPosition(7, 3);
addPosition(8, 3);
addSpectator(3, 3);
addSpectator(5, 3);
addSpectator(6, 5);
}
@Test
public void testRemovePositionKeyframeFromStart() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
addPosition(3, 1);
removePosition(0, 1);
removePosition(1, 1);
removePosition(2, 0);
removePosition(3, 0);
}
@Test
public void testRemovePositionKeyframeFromEnd() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
addPosition(3, 1);
removePosition(3, 1);
removePosition(2, 1);
removePosition(1, 0);
removePosition(0, 0);
}
@Test
public void testRemovePositionKeyframeFromMiddle() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
addPosition(3, 1);
removePosition(1, 1);
removePosition(2, 1);
removePosition(0, 0);
removePosition(3, 0);
}
@Test
public void testRemovePositionSpectatorKeyframe() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
addSpectator(3, 1);
addSpectator(4, 2);
addSpectator(5, 2);
addPosition(6, 3);
addPosition(7, 3);
addSpectator(8, 3);
addSpectator(9, 4);
removePosition(4, 4);
removePosition(9, 3);
removePosition(6, 3);
removePosition(7, 2);
removePosition(0, 2);
removePosition(1, 2);
removePosition(3, 2);
removePosition(2, 1);
removePosition(5, 0);
removePosition(8, 0);
}
@Test
public void testAddTimeKeyframeAppend() {
addTime(0, 0);
addTime(1, 1);
addTime(2, 1);
addTime(3, 1);
addTime(4, 1);
}
@Test
public void testAddTimeKeyframePrepend() {
addTime(4, 0);
addTime(3, 1);
addTime(2, 1);
addTime(1, 1);
addTime(0, 1);
}
@Test
public void testAddTimeKeyframeMixed() {
addTime(2, 0);
addTime(4, 1);
addTime(3, 1);
addTime(0, 1);
addTime(1, 1);
}
@Test
public void testRemoveTimeKeyframeFromStart() {
addTime(0, 0);
addTime(1, 1);
addTime(2, 1);
addTime(3, 1);
removeTime(0, 1);
removeTime(1, 1);
removeTime(2, 0);
removeTime(3, 0);
}
@Test
public void testRemoveTimeKeyframeFromEnd() {
addTime(0, 0);
addTime(1, 1);
addTime(2, 1);
addTime(3, 1);
removeTime(3, 1);
removeTime(2, 1);
removeTime(1, 0);
removeTime(0, 0);
}
@Test
public void testRemoveTimeKeyframeFromMiddle() {
addTime(0, 0);
addTime(1, 1);
addTime(2, 1);
addTime(3, 1);
removeTime(1, 1);
removeTime(2, 1);
removeTime(0, 0);
removeTime(3, 0);
}
@Test
public void testSetInterpolator() {
addPosition(0, 0);
addPosition(1, 1);
addPosition(2, 1);
setInterpolator(1, new LinearInterpolator(), 2);
assertIsLinear(1);
Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), 1).getInterpolator();
assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.POSITION));
assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.ROTATION));
addPosition(3, 3);
addPosition(4, 3);
addPosition(5, 3);
setInterpolator(3, new LinearInterpolator(), 5);
assertIsLinear(3);
removePosition(2, 3);
removePosition(3, 3);
removePosition(0, 2);
removePosition(1, 1);
removePosition(4, 0);
removePosition(5, 0);
}
private void assertIsLinear(int index) {
assertTrue("Expected segment " + index + " to have linear interpolator: " + prettyPrintInterpolators(impl, SPPath.POSITION),
Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator() instanceof LinearInterpolator);
}
@Test(expected = IllegalStateException.class)
public void testSetInterpolatorNoKeyframe() {
addTime(0, 0);
impl.setInterpolator(0, new LinearInterpolator());
}
@Test(expected = IllegalStateException.class)
public void testSetInterpolatorToDefaultNoKeyframe() {
addTime(0, 0);
impl.setInterpolatorToDefault(0);
}
@Test
public void testMoveKeyframeSimple() {
addPosition(0, 0);
addPosition(1, 1);
setInterpolator(0, new LinearInterpolator(), 1);
assertIsLinear(0);
impl.moveKeyframe(SPPath.POSITION, 0, 2);
assertValidInterpolators(SPPath.POSITION, 1);
assertIsLinear(0);
impl.moveKeyframe(SPPath.POSITION, 2, 0);
assertValidInterpolators(SPPath.POSITION, 1);
assertIsLinear(0);
addPosition(2, 1);
impl.moveKeyframe(SPPath.POSITION, 0, 3);
assertValidInterpolators(SPPath.POSITION, 1);
assertIsLinear(0);
assertIsLinear(1);
}
@Test
public void testMoveKeyframe() {
addPosition(1, 0);
addPosition(3, 1);
addPosition(5, 1);
addPosition(7, 1);
addSpectator(9, 1);
addSpectator(11, 2);
addSpectator(13, 2);
addPosition(15, 3);
addPosition(17, 3);
addPosition(19, 3);
setInterpolator(3, new LinearInterpolator(), 5);
assertIsLinear(1);
setInterpolator(5, new LinearInterpolator(), 5);
assertIsLinear(2);
// [P 1] C0 [P 3] L1 [P 5] L1 [P 7] C2 [S 9] L3 [S 11] L3 [S 13] C4 [P 15] C4 [P 17] C4 [P 19]
impl.moveKeyframe(SPPath.POSITION, 5, 0);
assertValidInterpolators(SPPath.POSITION, 6);
assertIsLinear(0);
impl.moveKeyframe(SPPath.POSITION, 0, 5);
assertValidInterpolators(SPPath.POSITION, 5);
assertIsLinear(2);
impl.moveKeyframe(SPPath.POSITION, 3, 12);
assertValidInterpolators(SPPath.POSITION, 7);
assertIsLinear(5);
impl.moveKeyframe(SPPath.POSITION, 12, 3);
assertValidInterpolators(SPPath.POSITION, 5);
assertIsLinear(1);
impl.moveKeyframe(SPPath.POSITION, 7, 14);
assertValidInterpolators(SPPath.POSITION, 4);
impl.moveKeyframe(SPPath.POSITION, 14, 20);
assertValidInterpolators(SPPath.POSITION, 4);
}
@Test(expected = IllegalArgumentException.class)
public void testSetDefaultInterpolatorDifferentType() {
addPosition(0, 0);
addPosition(1, 1);
impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42));
}
@Test
public void testSetDefaultInterpolatorSameType() {
impl.setDefaultInterpolatorType(InterpolatorType.CATMULL_ROM);
addPosition(0, 0);
addSpectator(1, 1);
addSpectator(2, 2);
addPosition(3, 3);
addPosition(4, 3);
impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42));
assertValidInterpolators(SPPath.POSITION, 3);
assertIsCatmullRom(0, 42);
assertIsCatmullRom(2, 42);
assertIsCatmullRom(3, 42);
}
private void assertIsCatmullRom(int index, double alpha) {
String str = prettyPrintInterpolators(impl, SPPath.POSITION);
Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator();
assertTrue("Expected segment " + index + " to be catmull rom interpolator: " + str,
interpolator instanceof CatmullRomSplineInterpolator);
assertTrue("Expected interpolator of segment segment " + index + " to have alpha " + alpha + ": " + str,
((CatmullRomSplineInterpolator) interpolator).getAlpha() == 42);
}
private void addPosition(int time, int expectedNumberOfInterpolators) {
impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, -1);
assertNotNull(impl.getKeyframe(SPPath.POSITION, time));
assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
}
private void addSpectator(int time, int expectedNumberOfInterpolators) {
impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, 42);
assertNotNull(impl.getKeyframe(SPPath.POSITION, time));
assertTrue(impl.isSpectatorKeyframe(time));
assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
}
private void addTime(int time, int expectedNumberOfInterpolators) {
impl.addTimeKeyframe(time, 1);
assertNotNull(impl.getKeyframe(SPPath.TIME, time));
assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators);
}
private void removePosition(int time, int expectedNumberOfInterpolators) {
impl.removePositionKeyframe(time);
assertNull(impl.getKeyframe(SPPath.POSITION, time));
assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
}
private void removeTime(int time, int expectedNumberOfInterpolators) {
impl.removeTimeKeyframe(time);
assertNull(impl.getKeyframe(SPPath.TIME, time));
assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators);
}
private void setInterpolator(int time, Interpolator interpolator, int expectedNumberOfInterpolators) {
impl.setInterpolator(time, interpolator);
assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
}
private void assertPositionState(String expected) {
assertEquals(expected, prettyPrintInterpolators(impl, SPPath.POSITION));
}
// Tracks only entity 42 which always is at x=time*2, y,z=0
private class EntityPositionTrackerMock extends EntityPositionTracker {
public EntityPositionTrackerMock() {
super(null);
}
@Override
public Location getEntityPositionAtTimestamp(int entityID, long timestamp) {
if (entityID == 42) {
return new Location(timestamp * 2, 0 ,0, 0, 0);
} else {
return null;
}
}
}
public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath) {
return prettyPrintInterpolators(timeline, spPath, Collections.emptyMap());
}
public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath, Map<PathSegment, Interpolator> updates) {
Map<Interpolator, String> interpolatorIdMap = new IdentityHashMap<>();
Path path = timeline.getPath(spPath);
StringBuilder sb = new StringBuilder();
for (Keyframe keyframe : path.getKeyframes()) {
if (spPath == SPPath.TIME) {
sb.append("[T ").append(keyframe.getTime()).append("]");
} else if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) {
sb.append("[S ").append(keyframe.getTime()).append("]");
} else {
sb.append("[P ").append(keyframe.getTime()).append("]");
}
path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().ifPresent(segment -> {
Interpolator interpolator = updates.getOrDefault(segment, segment.getInterpolator());
String id = interpolatorIdMap.computeIfAbsent(interpolator, i ->
(i instanceof LinearInterpolator ? "L" : "C") + interpolatorIdMap.size());
sb.append(' ').append(id).append(' ');
});
}
return sb.toString();
}
}