Add first person spectator keyframes to simplepathing
This commit is contained in:
Submodule ReplayStudio updated: ef307b4678...4cb8cad6c8
@@ -50,7 +50,9 @@ public class CameraProperties extends AbstractPropertyGroup {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyToGame(Triple<Double, Double, Double> value, @NonNull Object replayHandler) {
|
public void applyToGame(Triple<Double, Double, Double> value, @NonNull Object replayHandler) {
|
||||||
((ReplayHandler) replayHandler).getCameraEntity().setCameraPosition(value.getLeft(), value.getMiddle(), value.getRight());
|
ReplayHandler handler = ((ReplayHandler) replayHandler);
|
||||||
|
handler.spectateCamera();
|
||||||
|
handler.getCameraEntity().setCameraPosition(value.getLeft(), value.getMiddle(), value.getRight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,7 +88,9 @@ public class CameraProperties extends AbstractPropertyGroup {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyToGame(Triple<Float, Float, Float> value, @NonNull Object replayHandler) {
|
public void applyToGame(Triple<Float, Float, Float> value, @NonNull Object replayHandler) {
|
||||||
((ReplayHandler) replayHandler).getCameraEntity().setCameraRotation(value.getLeft(), value.getMiddle(), value.getRight());
|
ReplayHandler handler = ((ReplayHandler) replayHandler);
|
||||||
|
handler.spectateCamera();
|
||||||
|
handler.getCameraEntity().setCameraRotation(value.getLeft(), value.getMiddle(), value.getRight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.replaymod.pathing.properties;
|
||||||
|
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import com.replaymod.replay.ReplayHandler;
|
||||||
|
import com.replaymod.replaystudio.pathing.property.AbstractProperty;
|
||||||
|
import com.replaymod.replaystudio.pathing.property.PropertyPart;
|
||||||
|
import com.replaymod.replaystudio.pathing.property.PropertyParts;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import net.minecraft.entity.Entity;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for the camera spectating an entity.
|
||||||
|
*/
|
||||||
|
public class SpectatorProperty extends AbstractProperty<Integer> {
|
||||||
|
public static final SpectatorProperty PROPERTY = new SpectatorProperty();
|
||||||
|
public final PropertyPart<Integer> ENTITY_ID = new PropertyParts.ForInteger(this, false);
|
||||||
|
private SpectatorProperty() {
|
||||||
|
super("spectate", "replaymod.gui.playeroverview.spectate", null, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<PropertyPart<Integer>> getParts() {
|
||||||
|
return Collections.singletonList(ENTITY_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyToGame(Integer value, @NonNull Object replayHandler) {
|
||||||
|
ReplayHandler handler = ((ReplayHandler) replayHandler);
|
||||||
|
World world = handler.getCameraEntity().getEntityWorld();
|
||||||
|
// Lookup entity by id, returns null if an entity with the id does not exists
|
||||||
|
Entity target = world.getEntityByID(value);
|
||||||
|
// Spectate entity, when called with null, returns to camera
|
||||||
|
handler.spectateEntity(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toJson(JsonWriter writer, Integer value) throws IOException {
|
||||||
|
writer.value(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer fromJson(JsonReader reader) throws IOException {
|
||||||
|
return reader.nextInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.google.gson.stream.JsonReader;
|
|||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
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.TimestampProperty;
|
import com.replaymod.pathing.properties.TimestampProperty;
|
||||||
import com.replaymod.replay.events.ReplayOpenEvent;
|
import com.replaymod.replay.events.ReplayOpenEvent;
|
||||||
import com.replaymod.replaystudio.pathing.PathingRegistry;
|
import com.replaymod.replaystudio.pathing.PathingRegistry;
|
||||||
@@ -79,6 +80,7 @@ public class ReplayModSimplePathing implements PathingRegistry {
|
|||||||
timeline.registerProperty(TimestampProperty.PROPERTY);
|
timeline.registerProperty(TimestampProperty.PROPERTY);
|
||||||
timeline.registerProperty(CameraProperties.POSITION);
|
timeline.registerProperty(CameraProperties.POSITION);
|
||||||
timeline.registerProperty(CameraProperties.ROTATION);
|
timeline.registerProperty(CameraProperties.ROTATION);
|
||||||
|
timeline.registerProperty(SpectatorProperty.PROPERTY);
|
||||||
|
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
@@ -107,4 +109,8 @@ public class ReplayModSimplePathing implements PathingRegistry {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReplayMod getCore() {
|
||||||
|
return core;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import com.google.common.base.Function;
|
|||||||
import com.google.common.collect.Iterables;
|
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.TimestampProperty;
|
import com.replaymod.pathing.properties.TimestampProperty;
|
||||||
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.ReplayModSimplePathing;
|
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
||||||
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;
|
||||||
@@ -53,6 +55,9 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
|||||||
? KEYFRAME_SIZE : 0);
|
? 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()) {
|
||||||
|
v += 2 * KEYFRAME_SIZE;
|
||||||
|
}
|
||||||
renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
|
renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
|
||||||
}
|
}
|
||||||
if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) {
|
if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) {
|
||||||
@@ -62,6 +67,27 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw colored quads on spectator path segments
|
||||||
|
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.POSITION_PATH).getSegments()) {
|
||||||
|
if (segment.getInterpolator() == null
|
||||||
|
|| !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) {
|
||||||
|
continue; // Not a spectator segment
|
||||||
|
}
|
||||||
|
long startFrameTime = segment.getStartKeyframe().getTime();
|
||||||
|
long endFrameTime = segment.getEndKeyframe().getTime();
|
||||||
|
if (startFrameTime >= endTime || endFrameTime <= startTime) {
|
||||||
|
continue; // Segment out of display range
|
||||||
|
}
|
||||||
|
|
||||||
|
double relativeStart = startFrameTime - startTime;
|
||||||
|
double relativeEnd = endFrameTime - startTime;
|
||||||
|
int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1);
|
||||||
|
int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2);
|
||||||
|
if (startX < endX) {
|
||||||
|
renderer.drawRect(startX + 1, BORDER_TOP + 1, endX - startX - 2, KEYFRAME_SIZE - 2, 0xFF0088FF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.drawTimelineCursor(renderer, size);
|
super.drawTimelineCursor(renderer, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.replaymod.core.ReplayMod;
|
|||||||
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;
|
||||||
|
import com.replaymod.pathing.properties.SpectatorProperty;
|
||||||
import com.replaymod.pathing.properties.TimestampProperty;
|
import com.replaymod.pathing.properties.TimestampProperty;
|
||||||
import com.replaymod.render.gui.GuiRenderSettings;
|
import com.replaymod.render.gui.GuiRenderSettings;
|
||||||
import com.replaymod.replay.ReplayHandler;
|
import com.replaymod.replay.ReplayHandler;
|
||||||
@@ -19,18 +20,26 @@ 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.path.Timeline;
|
||||||
|
import com.replaymod.replaystudio.util.EntityPositionTracker;
|
||||||
|
import com.replaymod.replaystudio.util.Location;
|
||||||
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
||||||
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.GuiPanel;
|
import de.johni0702.minecraft.gui.container.GuiPanel;
|
||||||
import de.johni0702.minecraft.gui.element.*;
|
import de.johni0702.minecraft.gui.element.*;
|
||||||
|
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.element.advanced.IGuiTimeline;
|
import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline;
|
||||||
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
||||||
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
||||||
import de.johni0702.minecraft.gui.layout.VerticalLayout;
|
import de.johni0702.minecraft.gui.layout.VerticalLayout;
|
||||||
|
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
|
||||||
|
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.commons.lang3.tuple.Triple;
|
||||||
|
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.Dimension;
|
import org.lwjgl.util.Dimension;
|
||||||
import org.lwjgl.util.ReadableDimension;
|
import org.lwjgl.util.ReadableDimension;
|
||||||
@@ -38,6 +47,9 @@ import org.lwjgl.util.ReadablePoint;
|
|||||||
import org.lwjgl.util.WritablePoint;
|
import org.lwjgl.util.WritablePoint;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +59,8 @@ public class GuiPathing {
|
|||||||
public static final int TIME_PATH = 0;
|
public static final int TIME_PATH = 0;
|
||||||
public static final int POSITION_PATH = 1;
|
public static final int POSITION_PATH = 1;
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20)
|
public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20)
|
||||||
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE);
|
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE);
|
||||||
|
|
||||||
@@ -174,6 +188,8 @@ public class GuiPathing {
|
|||||||
private final ReplayHandler replayHandler;
|
private final ReplayHandler replayHandler;
|
||||||
private final RealtimeTimelinePlayer player;
|
private final RealtimeTimelinePlayer player;
|
||||||
|
|
||||||
|
private EntityPositionTracker entityTracker;
|
||||||
|
|
||||||
public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) {
|
public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) {
|
||||||
this.mod = mod;
|
this.mod = mod;
|
||||||
this.replayHandler = replayHandler;
|
this.replayHandler = replayHandler;
|
||||||
@@ -315,6 +331,11 @@ public class GuiPathing {
|
|||||||
* @param isTime {@code true} for the time property button, {@code false} for the place property button
|
* @param isTime {@code true} for the time property button, {@code false} for the place property button
|
||||||
*/
|
*/
|
||||||
private void updateKeyframe(final boolean isTime) {
|
private void updateKeyframe(final boolean isTime) {
|
||||||
|
if (entityTracker == null) {
|
||||||
|
loadEntityTracker(() -> updateKeyframe(isTime));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int time = timeline.getCursorPosition();
|
int time = timeline.getCursorPosition();
|
||||||
Timeline timeline = mod.getCurrentTimeline();
|
Timeline timeline = mod.getCurrentTimeline();
|
||||||
Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH);
|
Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH);
|
||||||
@@ -339,30 +360,63 @@ public class GuiPathing {
|
|||||||
CameraEntity camera = replayHandler.getCameraEntity();
|
CameraEntity camera = replayHandler.getCameraEntity();
|
||||||
builder.setValue(CameraProperties.POSITION, Triple.of(camera.posX, camera.posY, camera.posZ));
|
builder.setValue(CameraProperties.POSITION, Triple.of(camera.posX, camera.posY, camera.posZ));
|
||||||
builder.setValue(CameraProperties.ROTATION, Triple.of(camera.rotationYaw, camera.rotationPitch, camera.roll));
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UpdateKeyframeProperties updateChange = builder.done();
|
UpdateKeyframeProperties updateChange = builder.done();
|
||||||
updateChange.apply(timeline);
|
updateChange.apply(timeline);
|
||||||
// If this new keyframe formed the first segment, then create and set the appropriate interpolator
|
change = CombinedChange.createFromApplied(change, updateChange);
|
||||||
if (path.getSegments().size() == 1) {
|
|
||||||
|
// If this new keyframe formed the first segment of the time path
|
||||||
|
if (isTime && path.getSegments().size() == 1) {
|
||||||
PathSegment segment = path.getSegments().iterator().next();
|
PathSegment segment = path.getSegments().iterator().next();
|
||||||
Interpolator interpolator;
|
Interpolator interpolator = new LinearInterpolator();
|
||||||
if (isTime) {
|
|
||||||
interpolator = new LinearInterpolator();
|
|
||||||
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
||||||
|
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
|
||||||
|
if (!isTime) {
|
||||||
|
Interpolator interpolator = null;
|
||||||
|
boolean isSpectatorInterpolator = false;
|
||||||
|
for (PathSegment segment : 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);
|
||||||
|
}
|
||||||
|
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
||||||
|
setInterpolator.apply(timeline);
|
||||||
|
change = CombinedChange.createFromApplied(change, setInterpolator);
|
||||||
} else {
|
} else {
|
||||||
|
// Normal segment
|
||||||
|
if (isSpectatorInterpolator || interpolator == null) {
|
||||||
|
isSpectatorInterpolator = false;
|
||||||
interpolator = new CubicSplineInterpolator();
|
interpolator = new CubicSplineInterpolator();
|
||||||
interpolator.registerProperty(CameraProperties.POSITION);
|
interpolator.registerProperty(CameraProperties.POSITION);
|
||||||
interpolator.registerProperty(CameraProperties.ROTATION);
|
interpolator.registerProperty(CameraProperties.ROTATION);
|
||||||
}
|
}
|
||||||
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
||||||
setInterpolator.apply(timeline);
|
setInterpolator.apply(timeline);
|
||||||
timeline.pushChange(CombinedChange.createFromApplied(change, updateChange, setInterpolator));
|
change = CombinedChange.createFromApplied(change, setInterpolator);
|
||||||
} else {
|
|
||||||
timeline.pushChange(CombinedChange.createFromApplied(change, updateChange));
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Change specPosUpdate = updateSpectatorPositions();
|
||||||
|
specPosUpdate.apply(timeline);
|
||||||
|
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
||||||
|
|
||||||
timeline.pushChange(change);
|
timeline.pushChange(change);
|
||||||
}
|
|
||||||
|
|
||||||
if (isTime) {
|
if (isTime) {
|
||||||
mod.setSelectedTimeKeyframe(keyframe);
|
mod.setSelectedTimeKeyframe(keyframe);
|
||||||
@@ -371,7 +425,78 @@ public class GuiPathing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadEntityTracker(Runnable runnable) {
|
||||||
|
LoadEntityTrackerPopup popup = new LoadEntityTrackerPopup(replayHandler.getOverlay());
|
||||||
|
new Thread(() -> {
|
||||||
|
EntityPositionTracker tracker = new EntityPositionTracker(replayHandler.getReplayFile());
|
||||||
|
try {
|
||||||
|
tracker.load(c -> popup.progressBar.setProgress((float)(double) c));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Loading entity tracker:", e);
|
||||||
|
mod.getCore().runLater(() -> {
|
||||||
|
mod.getCore().printWarningToChat("Error loading entity tracker: %s", e.getLocalizedMessage());
|
||||||
|
popup.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entityTracker = tracker;
|
||||||
|
mod.getCore().runLater(() -> {
|
||||||
|
popup.close();
|
||||||
|
runnable.run();
|
||||||
|
});
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public ReplayModSimplePathing getMod() {
|
public ReplayModSimplePathing getMod() {
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class LoadEntityTrackerPopup extends AbstractGuiPopup<LoadEntityTrackerPopup> {
|
||||||
|
private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20)
|
||||||
|
.setI18nLabel("replaymod.gui.loadentitytracker");
|
||||||
|
|
||||||
|
public LoadEntityTrackerPopup(GuiContainer container) {
|
||||||
|
super(container);
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected LoadEntityTrackerPopup getThis() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
package eu.crushedpixel.replaymod.preparation;
|
|
||||||
|
|
||||||
import com.replaymod.replaystudio.PacketData;
|
|
||||||
import com.replaymod.replaystudio.io.ReplayInputStream;
|
|
||||||
import com.replaymod.replaystudio.replay.ZipReplayFile;
|
|
||||||
import com.replaymod.replaystudio.studio.ReplayStudio;
|
|
||||||
import com.replaymod.replaystudio.util.PacketUtils;
|
|
||||||
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.spacehq.mc.protocol.data.game.values.entity.player.PositionElement;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.ServerEntityMovementPacket;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.ServerEntityPositionPacket;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.ServerEntityPositionRotationPacket;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.ServerEntityTeleportPacket;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.player.ServerPlayerPositionRotationPacket;
|
|
||||||
import org.spacehq.mc.protocol.packet.ingame.server.entity.spawn.*;
|
|
||||||
import org.spacehq.packetlib.packet.Packet;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An EntityPositionTracker knows every Entity's position at any timestamp for a single Replay File.
|
|
||||||
* To do so, it once reads the whole Replay File and stores all Packets that set or change an Entity's position into Memory.<br>
|
|
||||||
* <br>
|
|
||||||
* While this significantly increases the Replay loading time, it's the only way to:<br>
|
|
||||||
* 1) Properly preview the Camera Path when an Entity is spectated<br>
|
|
||||||
* 2) Calculate a smooth Path from an Entity's Shoulder Cam perspective
|
|
||||||
*/
|
|
||||||
public class EntityPositionTracker {
|
|
||||||
|
|
||||||
private static final List<Class> MOVEMENT_PACKETS = new ArrayList<Class>() {
|
|
||||||
{
|
|
||||||
add(ServerPlayerPositionRotationPacket.class);
|
|
||||||
add(ServerSpawnPlayerPacket.class);
|
|
||||||
add(ServerSpawnObjectPacket.class);
|
|
||||||
add(ServerSpawnMobPacket.class);
|
|
||||||
add(ServerSpawnPaintingPacket.class);
|
|
||||||
add(ServerSpawnExpOrbPacket.class);
|
|
||||||
add(ServerEntityMovementPacket.class);
|
|
||||||
add(ServerEntityPositionRotationPacket.class);
|
|
||||||
add(ServerEntityPositionPacket.class);
|
|
||||||
add(ServerEntityTeleportPacket.class);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final File replayFile;
|
|
||||||
private boolean loaded = false;
|
|
||||||
|
|
||||||
private Map<Integer, List<Pair<Integer, AdvancedPosition>>> entityPositions;
|
|
||||||
|
|
||||||
public EntityPositionTracker(File replayFile) {
|
|
||||||
this.replayFile = replayFile;
|
|
||||||
this.entityPositions = new HashMap<Integer, List<Pair<Integer, AdvancedPosition>>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void load() throws IOException {
|
|
||||||
ReplayStudio studio = new ReplayStudio();
|
|
||||||
studio.setWrappingEnabled(false);
|
|
||||||
|
|
||||||
ZipReplayFile zrf = new ZipReplayFile(studio, replayFile);
|
|
||||||
ReplayInputStream rin = zrf.getPacketData();
|
|
||||||
|
|
||||||
PacketData packetData;
|
|
||||||
while((packetData = rin.readPacket()) != null) {
|
|
||||||
Packet packet = packetData.getPacket();
|
|
||||||
|
|
||||||
if(!MOVEMENT_PACKETS.contains(packet.getClass())) continue;
|
|
||||||
|
|
||||||
Integer entityID = PacketUtils.getEntityId(packet);
|
|
||||||
if(entityID == null) continue;
|
|
||||||
|
|
||||||
AdvancedPosition oldPosition = new AdvancedPosition(0, 0, 0, 0, 0);
|
|
||||||
|
|
||||||
List<Pair<Integer, AdvancedPosition>> positions = entityPositions.get(entityID);
|
|
||||||
|
|
||||||
if(positions == null) positions = new ArrayList<Pair<Integer, AdvancedPosition>>();
|
|
||||||
if(!positions.isEmpty()) {
|
|
||||||
oldPosition = positions.get(positions.size() - 1).getRight();
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedPosition newPosition = getNewPosition(oldPosition, packet);
|
|
||||||
|
|
||||||
if(newPosition != null) positions.add(Pair.of((int) packetData.getTime(), newPosition));
|
|
||||||
entityPositions.put(entityID, positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param entityID The Entity's ID
|
|
||||||
* @param timestamp The timestamp
|
|
||||||
* @return The specified Entity's position at the given timestamp, or null if the entity doesn't exist at that timestamp
|
|
||||||
* @throws IllegalStateException if the EntityPositionTracker's load() method wasn't called yet
|
|
||||||
*/
|
|
||||||
public AdvancedPosition getEntityPositionAtTimestamp(int entityID, int timestamp) {
|
|
||||||
if(!loaded) throw new IllegalStateException("The EntityPositionTracker hasn't loaded the Replay File yet");
|
|
||||||
|
|
||||||
List<Pair<Integer, AdvancedPosition>> positions = entityPositions.get(entityID);
|
|
||||||
if(positions == null || positions.isEmpty()) return null;
|
|
||||||
|
|
||||||
boolean exists = false;
|
|
||||||
|
|
||||||
for(Pair<Integer, AdvancedPosition> pair : positions) {
|
|
||||||
if(pair.getKey() <= timestamp) exists = true;
|
|
||||||
if(pair.getKey() >= timestamp) {
|
|
||||||
if(exists) return pair.getValue().copy();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//@johni0702 - you may consider adding a similar method to the ReplayStudio PacketUtils
|
|
||||||
private AdvancedPosition getNewPosition(AdvancedPosition previous, Packet packet) {
|
|
||||||
if(packet instanceof ServerPlayerPositionRotationPacket) {
|
|
||||||
ServerPlayerPositionRotationPacket ppl = (ServerPlayerPositionRotationPacket)packet;
|
|
||||||
|
|
||||||
double x = previous.getX() + ppl.getX();
|
|
||||||
double y = previous.getY() + ppl.getY();
|
|
||||||
double z = previous.getZ() + ppl.getZ();
|
|
||||||
double yaw = previous.getYaw() + ppl.getYaw();
|
|
||||||
double pitch = previous.getPitch() + ppl.getPitch();
|
|
||||||
|
|
||||||
for(PositionElement relative : ppl.getRelativeElements()) {
|
|
||||||
if(relative == PositionElement.X) {
|
|
||||||
x -= previous.getX();
|
|
||||||
} else if(relative == PositionElement.Y) {
|
|
||||||
y -= previous.getY();
|
|
||||||
} else if(relative == PositionElement.Z) {
|
|
||||||
z -= previous.getZ();
|
|
||||||
} else if(relative == PositionElement.YAW) {
|
|
||||||
yaw -= previous.getYaw();
|
|
||||||
} else if(relative == PositionElement.PITCH) {
|
|
||||||
pitch -= previous.getPitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AdvancedPosition(x, y, z, pitch, yaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerSpawnPlayerPacket) {
|
|
||||||
ServerSpawnPlayerPacket spp = (ServerSpawnPlayerPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getX(), spp.getY(), spp.getZ(), spp.getPitch(), spp.getYaw());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerSpawnObjectPacket) {
|
|
||||||
ServerSpawnObjectPacket spp = (ServerSpawnObjectPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getX(), spp.getY(), spp.getZ(), spp.getPitch(), spp.getYaw());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerSpawnExpOrbPacket) {
|
|
||||||
ServerSpawnExpOrbPacket spp = (ServerSpawnExpOrbPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getX(), spp.getY(), spp.getZ(), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerSpawnMobPacket) {
|
|
||||||
ServerSpawnMobPacket spp = (ServerSpawnMobPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getX(), spp.getY(), spp.getZ(), spp.getPitch(), spp.getYaw());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerSpawnPaintingPacket) {
|
|
||||||
ServerSpawnPaintingPacket spp = (ServerSpawnPaintingPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getPosition().getX(), spp.getPosition().getY(), spp.getPosition().getZ(), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerEntityMovementPacket) {
|
|
||||||
ServerEntityMovementPacket ppl = (ServerEntityMovementPacket)packet;
|
|
||||||
//why is the rot/pos variable of ServerEntityMovementPacket not accessible?
|
|
||||||
boolean rot = ppl instanceof ServerEntityPositionRotationPacket;
|
|
||||||
|
|
||||||
double x = previous.getX() + ppl.getMovementX();
|
|
||||||
double y = previous.getY() + ppl.getMovementY();
|
|
||||||
double z = previous.getZ() + ppl.getMovementZ();
|
|
||||||
|
|
||||||
double pitch = previous.getPitch();
|
|
||||||
double yaw = previous.getYaw();
|
|
||||||
|
|
||||||
if(rot) {
|
|
||||||
pitch = ppl.getPitch();
|
|
||||||
yaw = ppl.getYaw();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AdvancedPosition(x, y, z, pitch, yaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packet instanceof ServerEntityTeleportPacket) {
|
|
||||||
ServerEntityTeleportPacket spp = (ServerEntityTeleportPacket)packet;
|
|
||||||
return new AdvancedPosition(spp.getX(), spp.getY(), spp.getZ(), spp.getPitch(), spp.getYaw());
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -125,6 +125,8 @@ replaymod.gui.restorereplay1=It seems like Minecraft has not quit normally.
|
|||||||
replaymod.gui.restorereplay2=The Replay "%1$s" was not saved correctly.
|
replaymod.gui.restorereplay2=The Replay "%1$s" was not saved correctly.
|
||||||
replaymod.gui.restorereplay3=Do you wish to recover it?
|
replaymod.gui.restorereplay3=Do you wish to recover it?
|
||||||
|
|
||||||
|
replaymod.gui.loadentitytracker=Loading entity positions: %%d%%%%
|
||||||
|
|
||||||
#Only change these if it's neccessary
|
#Only change these if it's neccessary
|
||||||
replaymod.gui.replayviewer=Replay Viewer
|
replaymod.gui.replayviewer=Replay Viewer
|
||||||
replaymod.gui.replaycenter=Replay Center
|
replaymod.gui.replaycenter=Replay Center
|
||||||
|
|||||||
Reference in New Issue
Block a user